/**
   * Create a {@link ProcessorDependencyGraph}.
   *
   * @param processors the processors that will be part of the graph
   * @return a {@link org.mapfish.print.processor.ProcessorDependencyGraph} constructed from the
   *     passed in processors
   */
  @SuppressWarnings("unchecked")
  public ProcessorDependencyGraph build(final List<? extends Processor> processors) {
    ProcessorDependencyGraph graph = new ProcessorDependencyGraph();

    final Map<String, ProcessorGraphNode<Object, Object>> provideBy =
        new HashMap<String, ProcessorGraphNode<Object, Object>>();
    final Map<String, Class<?>> outputTypes = new HashMap<String, Class<?>>();
    final List<ProcessorGraphNode<Object, Object>> nodes =
        new ArrayList<ProcessorGraphNode<Object, Object>>(processors.size());

    for (Processor<Object, Object> processor : processors) {
      final ProcessorGraphNode<Object, Object> node =
          new ProcessorGraphNode<Object, Object>(processor, this.metricRegistry);
      for (OutputValue value : getOutputValues(node)) {
        String outputName = value.getName();
        if (provideBy.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 {
            throw new IllegalArgumentException(
                "Multiple processors provide the same output mapping: '"
                    + processor
                    + "' and '"
                    + provideBy.get(outputName)
                    + "' both provide: '"
                    + outputName
                    + "'.  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.");
          }
        }

        provideBy.put(outputName, node);
        outputTypes.put(outputName, value.getType());
      }
      nodes.add(node);
    }

    ArrayList<ProcessorDependency> allDependencies = Lists.newArrayList(this.dependencies);
    for (ProcessorGraphNode<Object, Object> node : nodes) {
      if (node.getProcessor() instanceof CustomDependencies) {
        CustomDependencies custom = (CustomDependencies) node.getProcessor();
        allDependencies.addAll(custom.createDependencies(nodes));
      }
    }
    final SetMultimap<ProcessorGraphNode<Object, Object>, InputValue> inputsForNodes =
        cacheInputsForNodes(nodes);
    for (ProcessorGraphNode<Object, Object> node : nodes) {
      final Set<InputValue> inputs = inputsForNodes.get(node);

      // check explicit, external dependencies between nodes
      checkExternalDependencies(allDependencies, node, nodes);

      // check input/output value dependencies
      for (InputValue input : inputs) {
        final ProcessorGraphNode<Object, Object> solution = provideBy.get(input.getName());
        if (solution != null && solution != node) {
          // check that the provided output has the same type
          final Class<?> inputType = input.getType();
          final Class<?> outputType = outputTypes.get(input.getName());
          if (inputType.isAssignableFrom(outputType)) {
            solution.addDependency(node);
          } else {
            throw new IllegalArgumentException(
                "Type conflict: Processor '"
                    + solution.getName()
                    + "' provides an output with name '"
                    + input.getName()
                    + "' and of type '"
                    + outputType
                    + " ', while "
                    + "processor '"
                    + node.getName()
                    + "' expects an input of that name with type '"
                    + inputType
                    + "'! Please rename one of the attributes in the mappings of the processors.");
          }
        }
      }
    }

    // once all dependencies are discovered, select the root nodes
    for (ProcessorGraphNode<Object, Object> node : nodes) {
      // a root node is a node that has no requirements (that is no other node
      // should be executed before the node) and that has only external inputs
      if (!node.hasRequirements()
          && hasNoneOrOnlyExternalInput(node, inputsForNodes.get(node), provideBy)) {
        graph.addRoot(node);
      }
    }

    Assert.isTrue(
        graph.getAllProcessors().containsAll(processors),
        "'" + graph + "' does not contain all the processors: " + processors);

    return graph;
  }