docs: @SuppressWarnings("unchecked")
      public ProcessorDependencyGraph build(
              final List<? extends Processor> processors,
              final Map<String, Class<?>> attributes) {
          ProcessorDependencyGraph graph = new ProcessorDependencyGraph();
  
          final Map<String, ProcessorGraphNode<Object, Object>> provideByProcessor =
                  new HashMap<>();
          final Map<String, Class<?>> outputTypes = new HashMap<>(attributes);
  
          // Add internal values
          outputTypes.put(Values.VALUES_KEY, Values.class);
          outputTypes.put(Values.TASK_DIRECTORY_KEY, File.class);
          outputTypes.put(Values.CLIENT_HTTP_REQUEST_FACTORY_KEY, MfClientHttpRequestFactoryProvider.class);
          outputTypes.put(Values.TEMPLATE_KEY, Template.class);
          outputTypes.put(Values.PDF_CONFIG_KEY, PDFConfig.class);
          outputTypes.put(Values.SUBREPORT_DIR_KEY, String.class);
          outputTypes.put(Values.OUTPUT_FORMAT_KEY, String.class);
          outputTypes.put(Values.JOB_ID_KEY, String.class);
          outputTypes.put(MapPrinterServlet.JSON_REQUEST_HEADERS, HttpRequestHeadersAttribute.Value.class);
          outputTypes.put(Values.LOCALE_KEY, Locale.class);
  
          for (Processor processor: processors) {
              final ProcessorGraphNode<Object, Object> node =
                      new ProcessorGraphNode<Object, Object>(processor, this.metricRegistry);
  
              final Set<InputValue> inputs = getInputs(node.getProcessor());
              boolean isRoot = true;
              // check input/output value dependencies
              for (InputValue input: inputs) {
                  if (input.name.equals(Values.VALUES_KEY)) {
                      if (processor instanceof CustomDependencies) {
                          for (String name: ((CustomDependencies) processor).getDependencies()) {
                              final Class<?> outputType = outputTypes.get(name);
                              if (outputType == null) {
                                  throw new IllegalArgumentException(String.format(
                                          "The Processor '%s' has no value for the dynamic input '%s'.",
                                          processor, name));
                              }
                              final ProcessorGraphNode<Object, Object> processorSolution =
                                      provideByProcessor.get(name);
                              if (processorSolution != null) {
                                  processorSolution.addDependency(node);
                                  isRoot = false;
                              }
                          }
                      } else {
                          for (ProcessorGraphNode<Object, Object> processorSolution:
                                  provideByProcessor.values()) {
                              processorSolution.addDependency(node);
                              isRoot = false;
                          }
                      }
                  } else {
                      final Class<?> outputType = outputTypes.get(input.name);
                      if (outputType != null) {
                          final Class<?> inputType = input.type;
                          final ProcessorGraphNode<Object, Object> processorSolution =
                                  provideByProcessor.get(input.name);
                          if (inputType.isAssignableFrom(outputType)) {
                              if (processorSolution != null) {
                                  processorSolution.addDependency(node);
                                  isRoot = false;
                              }
                          } else {
                              if (processorSolution != null) {
                                  throw new IllegalArgumentException(String.format(
                                          "Type conflict: Processor '%s' provides an output with name '%s' " +
                                                  "and of type '%s', while processor '%s' expects an input " +
                                                  "of that name with type '%s'! Please rename one of the " +
                                                  "attributes in the mappings of the processors.",
                                          processorSolution.getName(), input.name, outputType, node.getName(),
                                          inputType));
                              } else {
                                  throw new IllegalArgumentException(String.format(
                                          "Type conflict: the attribute '%s' of type '%s', while processor " +
                                                  "'%s' expects an input of that name with type '%s'!",
                                          input.name, outputType, node.getName(), inputType));
                              }
                          }
                      } else {
                          if (input.field.getAnnotation(HasDefaultValue.class) == null) {
                              throw new IllegalArgumentException(String.format(
                                      "The Processor '%s' has no value for the input '%s'.",
                                      processor, input.name));
                          }
                      }
                  }
              }
              if (isRoot) {
                  graph.addRoot(node);
              }
  
              for (OutputValue value: getOutputValues(node.getProcessor())) {
                  String outputName = value.name;
                  if (outputTypes.containsKey(outputName)) {
                      // there is already an output with the same name
                      if (value.canBeRenamed) {
                          // if this is just a debug output, we can simply rename it
                          outputName = outputName + "_" + UUID.randomUUID().toString();
                      } else {
                          ProcessorGraphNode<Object, Object> provider = provideByProcessor.get(outputName);
                          if (provider == null) {
                              throw new IllegalArgumentException(String.format(
                                      "Processors '%s' provide the output '%s' who is already declared as " +
                                              "an attribute.  You have to rename one of the outputs and the " +
                                              "corresponding input so that there is no ambiguity with " +
                                              "regards to the input a processor consumes.",
                                      processor, outputName));
                          } else {
                              throw new IllegalArgumentException(String.format(
                                      "Multiple processors provide the same output mapping: '%s' and " +
                                              "'%s' both provide: '%s'.  You have to rename one of the " +
                                              "outputs and the corresponding input so that there is no " +
                                              "ambiguity with regards to the input a processor consumes.",
                                      processor, provider, outputName));
                          }
                      }
                  }
  
                  provideByProcessor.put(outputName, node);
                  outputTypes.put(outputName, value.type);
              }
  
              // check input/output value dependencies
              for (InputValue input: inputs) {
                  if (input.field.getAnnotation(InputOutputValue.class) != null) {
                      provideByProcessor.put(input.name, node);
                  }
              }
          }
  
          final Collection<? extends Processor> missingProcessors = CollectionUtils.subtract(
                  processors, graph.getAllProcessors());
          final StringBuilder missingProcessorsName = new StringBuilder();
          for (Processor p: missingProcessors) {
              missingProcessorsName.append("\n- ");
              missingProcessorsName.append(p.toString());
          }
          Assert.isTrue(
                  missingProcessors.isEmpty(),
                  "The processor graph:\n" + graph + "\ndoes not contain all the processors, missing:" +
                          missingProcessorsName);
  
          return graph;
      }