package org.mapfish.print.processor.jasper; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import jsr166y.ForkJoinTask; import net.sf.jasperreports.engine.JRDataSource; import net.sf.jasperreports.engine.JREmptyDataSource; import net.sf.jasperreports.engine.JRException; import net.sf.jasperreports.engine.data.JRMapCollectionDataSource; import org.json.JSONException; import org.json.JSONObject; import org.mapfish.print.attribute.Attribute; import org.mapfish.print.attribute.DataSourceAttribute; import org.mapfish.print.config.Configuration; import org.mapfish.print.config.ConfigurationException; import org.mapfish.print.config.Template; import org.mapfish.print.output.Values; import org.mapfish.print.parser.MapfishParser; import org.mapfish.print.processor.AbstractProcessor; import org.mapfish.print.processor.CustomDependencies; import org.mapfish.print.processor.Processor; import org.mapfish.print.processor.ProcessorDependencyGraph; import org.mapfish.print.processor.ProcessorDependencyGraphFactory; import org.mapfish.print.processor.RequireAttributes; import org.mapfish.print.wrapper.json.PJsonObject; import org.springframework.beans.factory.annotation.Autowired; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.PostConstruct; import static org.mapfish.print.attribute.DataSourceAttribute.DataSourceAttributeValue; /** * <p>A processor that will process a * {@link org.mapfish.print.attribute.DataSourceAttribute.DataSourceAttributeValue} and construct a single * Jasper DataSource from the input values in the * {@link org.mapfish.print.attribute.DataSourceAttribute.DataSourceAttributeValue} input object.</p> * * <p>The {@link org.mapfish.print.attribute.DataSourceAttribute.DataSourceAttributeValue} has an array of * maps, each map in the array equates to a row in the Jasper DataSource.</p> * * <p>The DataSourceProcessor can be configured with processors which will be used * to transform each map in the input array before constructing the final DataSource row.</p> * * <p>For example, each map in the array could be * {@link org.mapfish.print.attribute.map.MapAttribute.MapAttributeValues} and the DataSourceProcessor * could be configured with !createMap processor. In this scenario each element in the array would be * transformed by the !createMap processor and thus each row of the resulting DataSource will contain the * map subreport created by the !createMap processor.</p> * * <p>An additional point to remember is that (as with the normal execution) in addition to the output of * the processors, the attributes in the input map will also be columns in the row. This means that the * jasper report that makes use of the resulting DataSource will have access to both the results of the * processor as well as the input values (unless overwritten by the processor output).</p> * * <p>If the reportKey is defined (and reportTemplate) then a the reportTemplate jrxml file will be * compiled (as required by all jrxml files) and an additional column will be added to each row [reportKey] * : [compiled reportTemplate File]</p> * * <p>If reportKey is defined the reportTemplate must also be defined (and vice-versa).</p> * * <p>See also: <a href="attributes.html#!datasource">!datasource</a> attribute</p> * [[examples=verboseExample,datasource_dynamic_tables,datasource_many_dynamictables_legend, * datasource_multiple_maps,customDynamicReport,report]] */ public final class DataSourceProcessor extends AbstractProcessor<DataSourceProcessor.Input, DataSourceProcessor.Output> implements RequireAttributes, CustomDependencies { private Map<String, Attribute> internalAttributes = Maps.newHashMap(); private Map<String, Attribute> allAttributes = Maps.newHashMap(); @Autowired private ProcessorDependencyGraphFactory processorGraphFactory; private ProcessorDependencyGraph processorGraph; private List<Processor> processors; private List<String> copyAttributes = Lists.newArrayList(); @Autowired private MapfishParser parser; @Autowired private JasperReportBuilder jasperReportBuilder; private String reportTemplate; private String reportKey; /** * Constructor. */ public DataSourceProcessor() { super(Output.class); } @PostConstruct private void init() { // default to no processors this.processorGraph = this.processorGraphFactory.build(Collections.<Processor>emptyList(), Collections.<String, Class<?>>emptyMap()); } /** * The path to the report template used to render each row of the data. This is only required if a * subreport needs to be compiled and is referenced in the containing report's detail section. * <p> * The path should be relative to the configuration directory * </p> * @param reportTemplate the path to the report template. */ public void setReportTemplate(final String reportTemplate) { this.reportTemplate = reportTemplate; } /** * The key/name to use when putting the path to the compiled subreport in each row of the datasource. * This is required if {@link #reportTemplate} has been set. The path to the compiled * subreport will be added to each row in the datasource with this value as the key. This allows the * containing report to reference the subreport in each row. * * @param reportKey the key/name to use when putting the path to the compiled subreport in each row of * the datasource. */ public void setReportKey(final String reportKey) { this.reportKey = reportKey; } /** * All the processors that will executed for each value retrieved from the * {@link org.mapfish.print.output.Values} object with the datasource name. All output values from the * processor graph will be the datasource values. * <p></p> * <p> * Each value retrieved from values with the datasource name will be the input of the processor graph * and all the output values for that execution will be the values of a single row in the datasource. * The Jasper template can use any of the values in its detail band. * </p> * * @param processors the processors which will be ran to create the datasource */ public void setProcessors(final List<Processor> processors) { this.processors = processors; } /** * All the attributes needed either by the processors for each datasource row or by the jasper template. * * @param attributes the attributes. */ public void setAttributes(final Map<String, Attribute> attributes) { this.internalAttributes = attributes; this.allAttributes.putAll(attributes); } /** * The attributes that will be copied from the previous level. * * @param copyAttributes the attributes name */ public void setCopyAttributes(final List<String> copyAttributes) { this.copyAttributes = copyAttributes; } @Nonnull @Override public Collection<String> getDependencies() { return this.copyAttributes; } /** * All the sub-level attributes. * * @param name the attribute name. * @param attribute the attribute. */ public void setAttribute(final String name, final Attribute attribute) { if (name.equals("datasource")) { this.allAttributes.putAll(((DataSourceAttribute) attribute).getAttributes()); } else if (this.copyAttributes.contains(name)) { this.allAttributes.put(name, attribute); } } @Nullable @Override public Input createInputParameter() { return new Input(); } @Nullable @Override public Output execute(final Input input, final ExecutionContext context) throws Exception { JRDataSource jrDataSource = processInput(input); if (jrDataSource == null) { jrDataSource = new JREmptyDataSource(); } return new Output(jrDataSource); } private JRDataSource processInput(@Nonnull final Input input) throws JSONException, JRException { List<Values> dataSourceValues = Lists.newArrayList(); for (Map<String, Object> o : input.datasource.attributesValues) { // copy only the required values Values rowValues = new Values(input.values); for (String attributeName: this.copyAttributes) { rowValues.put(attributeName, input.values.getObject(attributeName, Object.class)); } for (Map.Entry<String, Object> entry : o.entrySet()) { rowValues.put(entry.getKey(), entry.getValue()); } dataSourceValues.add(rowValues); } List<ForkJoinTask<Values>> futures = Lists.newArrayList(); if (!dataSourceValues.isEmpty()) { for (Values dataSourceValue : dataSourceValues) { addAttributes(input.template, dataSourceValue); final ForkJoinTask<Values> taskFuture = this.processorGraph.createTask(dataSourceValue).fork(); futures.add(taskFuture); } final File reportFile; if (this.reportTemplate != null) { final Configuration configuration = input.template.getConfiguration(); final File file = new File(configuration.getDirectory(), this.reportTemplate); reportFile = this.jasperReportBuilder.compileJasperReport(configuration, file); } else { reportFile = null; } List<Map<String, ?>> rows = new ArrayList<Map<String, ?>>(); for (ForkJoinTask<Values> future : futures) { final Values rowData = future.join(); if (reportFile != null) { rowData.put(this.reportKey, reportFile.getAbsolutePath()); } rows.add(rowData.asMap()); } return new JRMapCollectionDataSource(rows); } return null; } private void addAttributes(@Nonnull final Template template, @Nonnull final Values dataSourceValue) throws JSONException { dataSourceValue.populateFromAttributes(template, this.parser, this.internalAttributes, new PJsonObject(new JSONObject(), "DataSourceProcessorAttributes")); } @Override protected void extraValidation( final List<Throwable> validationErrors, final Configuration configuration) { if (this.reportTemplate != null && this.reportKey == null || this.reportTemplate == null && this.reportKey != null) { validationErrors.add(new ConfigurationException("'reportKey' and 'reportTemplate' must ither " + "both be null or both be non-null. reportKey: " + this.reportKey + " reportTemplate: " + this.reportTemplate)); } for (Attribute attribute : this.internalAttributes.values()) { attribute.validate(validationErrors, configuration); } ProcessorDependencyGraphFactory.fillProcessorAttributes(this.processors, this.allAttributes); for (Processor processor : this.processors) { processor.validate(validationErrors, configuration); } final Map<String, Class<?>> attcls = new HashMap<String, Class<?>>(); for (String attributeName: this.allAttributes.keySet()) { attcls.put(attributeName, this.allAttributes.get(attributeName).getValueType()); } try { this.processorGraph = this.processorGraphFactory.build(this.processors, attcls); } catch (IllegalArgumentException e) { validationErrors.add(e); } if (this.processorGraph == null) { validationErrors.add(new ConfigurationException( "There are no child processors for this processor")); } else { final Set<Processor<?, ?>> allProcessors = this.processorGraph.getAllProcessors(); for (Processor<?, ?> processor : allProcessors) { processor.validate(validationErrors, configuration); } } } /** * Contains the datasource input. */ public static final class Input { /** * The values object with all values. This is required in order to run sub-processor graph */ public Template template; /** * The values object with all values. This is required in order to run sub-processor graph */ public Values values; /** * The data that will be processed by this processor in order to create a Jasper DataSource object. */ public DataSourceAttributeValue datasource; } /** * Contains the datasource output. */ public static final class Output { /** * The datasource to be assigned to a report or sub-report detail/table section. */ public final JRDataSource jrDataSource; /** * Constructor for setting the table data. * * @param datasource the table data */ public Output(@Nonnull final JRDataSource datasource) { this.jrDataSource = datasource; } } }