package org.mapfish.print.processor; import com.codahale.metrics.MetricRegistry; import com.google.common.base.Objects; import com.google.common.collect.BiMap; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.vividsolutions.jts.util.Assert; import org.apache.commons.collections.CollectionUtils; import org.mapfish.print.attribute.Attribute; import org.mapfish.print.attribute.HttpRequestHeadersAttribute; import org.mapfish.print.config.PDFConfig; import org.mapfish.print.config.Template; import org.mapfish.print.output.Values; import org.mapfish.print.parser.HasDefaultValue; import org.mapfish.print.parser.ParserUtils; import org.mapfish.print.processor.http.MfClientHttpRequestFactoryProvider; import org.mapfish.print.servlet.MapPrinterServlet; import org.springframework.beans.factory.annotation.Autowired; import java.io.File; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import static org.mapfish.print.parser.ParserUtils.getAllAttributes; /** * Class for constructing {@link org.mapfish.print.processor.ProcessorDependencyGraph} instances. * <p></p> */ public final class ProcessorDependencyGraphFactory { @Autowired private MetricRegistry metricRegistry; /** * External dependencies between processor types. */ @Autowired(required = false) private List<ProcessorDependency> dependencies = Lists.newArrayList(); /** * Sets the external dependencies between processors. Usually configured in * <code>mapfish-spring-processors.xml</code> * * @param dependencies the dependencies */ public void setDependencies(final List<ProcessorDependency> dependencies) { this.dependencies = dependencies; } /** * Create a {@link ProcessorDependencyGraph}. * * @param processors the processors that will be part of the graph * @param attributes the list of attributes name * @return a {@link org.mapfish.print.processor.ProcessorDependencyGraph} constructed from the passed in processors */ @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<String, ProcessorGraphNode<Object, Object>>(); final Map<String, Class<?>> outputTypes = new HashMap<String, Class<?>>(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(MapPrinterServlet.JSON_REQUEST_HEADERS, HttpRequestHeadersAttribute.Value.class); final List<ProcessorGraphNode<Object, Object>> nodes = new ArrayList<ProcessorGraphNode<Object, Object>>(processors.size()); 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(input.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 (String name : provideByProcessor.keySet()) { final ProcessorGraphNode<Object, Object> processorSolution = provideByProcessor.get(name); 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); } nodes.add(node); // 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; } private static Set<InputValue> getInputs(final Processor<?, ?> processor) { final BiMap<String, String> inputMapper = processor.getInputMapperBiMap(); final Set<InputValue> inputs = Sets.newHashSet(); final Object inputParameter = processor.createInputParameter(); if (inputParameter != null) { verifyAllMappingsMatchParameter(inputMapper.values(), inputParameter.getClass(), "One or more of the input mapping values of '" + processor + "' do not match an input" + " parameter. The bad mappings are"); final Collection<Field> allProperties = getAllAttributes(inputParameter.getClass()); for (Field field : allProperties) { String name = ProcessorUtils.getInputValueName(processor.getInputPrefix(), inputMapper, field.getName()); inputs.add(new InputValue(name, field)); } } return inputs; } private static Collection<OutputValue> getOutputValues(final Processor<?, ?> processor) { final Map<String, String> outputMapper = processor.getOutputMapperBiMap(); final Set<OutputValue> values = Sets.newHashSet(); final Set<String> mappings = outputMapper.keySet(); final Class<?> paramType = processor.getOutputType(); verifyAllMappingsMatchParameter(mappings, paramType, "One or more of the output mapping keys of '" + processor + "' do not match an output parameter. The bad mappings are: "); final Collection<Field> allProperties = getAllAttributes(paramType); for (Field field : allProperties) { // if the field is annotated with @DebugValue, it can be renamed automatically in a // mapping in case of a conflict. final boolean canBeRenamed = field.getAnnotation(InternalValue.class) != null; String name = ProcessorUtils.getOutputValueName(processor.getOutputPrefix(), outputMapper, field); values.add(new OutputValue(name, canBeRenamed, field)); } return values; } /** * Fill the attributes in the processor. * @see RequireAttributes * @see ProvideAttributes * * @param processors The processors * @param initialAttributes The attributes */ public static void fillProcessorAttributes( final List<Processor> processors, final Map<String, Attribute> initialAttributes) { Map<String, Attribute> currentAttributes = new HashMap<String, Attribute>(initialAttributes); for (Processor processor : processors) { if (processor instanceof RequireAttributes) { for (ProcessorDependencyGraphFactory.InputValue inputValue : ProcessorDependencyGraphFactory.getInputs(processor)) { if (inputValue.type == Values.class) { if (processor instanceof CustomDependencies) { for (String attributeName : ((CustomDependencies) processor).getDependencies()) { Attribute attribute = currentAttributes.get(attributeName); if (attribute == null) { throw new IllegalArgumentException(String.format("The Processor '%s' " + "has no value for the dynamic input '%s'.", processor, attributeName)); } ((RequireAttributes) processor).setAttribute( attributeName, currentAttributes.get(attributeName)); } } else { for (String attributeName : currentAttributes.keySet()) { ((RequireAttributes) processor).setAttribute( attributeName, currentAttributes.get(attributeName)); } } } else { try { ((RequireAttributes) processor).setAttribute( inputValue.internalName, currentAttributes.get(inputValue.name)); } catch (ClassCastException e) { throw new IllegalArgumentException(String.format("The processor '%s' requires " + "the attribute '%s' (%s) but he has the wrong type:\n%s", processor, inputValue.name, inputValue.internalName, e.getMessage()), e); } } } } if (processor instanceof ProvideAttributes) { Map<String, Attribute> newAttributes = ((ProvideAttributes) processor).getAttributes(); for (ProcessorDependencyGraphFactory.OutputValue ouputValue : ProcessorDependencyGraphFactory.getOutputValues(processor)) { currentAttributes.put( ouputValue.name, newAttributes.get(ouputValue.internalName)); } } } } private static void verifyAllMappingsMatchParameter( final Set<String> mappings, final Class<?> paramType, final String errorMessagePrefix) { final Set<String> attributeNames = ParserUtils.getAllAttributeNames(paramType); StringBuilder errors = new StringBuilder(); for (String mapping : mappings) { if (!attributeNames.contains(mapping)) { errors.append("\n * ").append(mapping); } } Assert.isTrue(0 == errors.length(), errorMessagePrefix + errors + listOptions(attributeNames) + "\n"); } private static String listOptions(final Set<String> attributeNames) { StringBuilder msg = new StringBuilder("\n\nThe possible parameter names are:"); for (String attributeName : attributeNames) { msg.append("\n * ").append(attributeName); } return msg.toString(); } private static class InputValue { public final String name; public final String internalName; public final Class<?> type; public final Field field; public InputValue(final String name, final Field field) { this.name = name; this.internalName = field.getName(); this.type = field.getType(); this.field = field; } @Override public int hashCode() { return Objects.hashCode(this.name); } @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (this.getClass() != obj.getClass()) { return false; } return Objects.equal(this.name, ((InputValue) obj).name); } @Override public String toString() { return "InputValue{" + "name='" + this.name + "', " + "type=" + this.type.getSimpleName() + '}'; } } private static final class OutputValue extends InputValue { public final boolean canBeRenamed; private OutputValue( final String name, final boolean canBeRenamed, final Field field) { super(name, field); this.canBeRenamed = canBeRenamed; } } }