Package org.mapfish.print.processor.jasper

Source Code of org.mapfish.print.processor.jasper.MergeDataSourceProcessor

/*
* Copyright (C) 2014  Camptocamp
*
* This file is part of MapFish Print
*
* MapFish Print is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* MapFish Print is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with MapFish Print.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.mapfish.print.processor.jasper;

import com.google.common.annotations.Beta;
import com.google.common.collect.BiMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.vividsolutions.jts.util.Assert;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.data.JRMapCollectionDataSource;
import net.sf.jasperreports.engine.design.JRDesignField;
import org.mapfish.print.config.Configuration;
import org.mapfish.print.config.ConfigurationException;
import org.mapfish.print.config.ConfigurationObject;
import org.mapfish.print.output.Values;
import org.mapfish.print.parser.ParserUtils;
import org.mapfish.print.processor.AbstractProcessor;
import org.mapfish.print.processor.CustomDependencies;
import org.mapfish.print.processor.InternalValue;
import org.mapfish.print.processor.Processor;
import org.mapfish.print.processor.ProcessorDependency;
import org.mapfish.print.processor.ProcessorGraphNode;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
* This processor combines DataSources and individual processor outputs (or attribute values) into a single DataSource which
* can be used in a jasper report's detail section.
* <p>
* An example use case is where we might have zero or many of tables and zero or many legends.  You can configure the
* template with a detail section that contains a subreport, the name of which is a field in the DataSources and the DataSources
* for the sub-template another field.  Then you can merge the legend and the tables into a single DataSources.  This way the
* report will nicely expand depending on if you have a legend and how many tables you have in your report.
* </p>
*
* @author Jesse on 9/6/2014.
*/
@Beta
public final class MergeDataSourceProcessor extends AbstractProcessor<MergeDataSourceProcessor.In, MergeDataSourceProcessor.Out>
        implements CustomDependencies {
    private List<Source> sources = Lists.newArrayList();

    /**
     * Constructor.
     */
    protected MergeDataSourceProcessor() {
        super(Out.class);
    }

    /**
     * The <em>source</em> to add to the merged DataSource.  Each <em>source</em> indicates if it should be treated
     * as a datasource or as a single item to add to the merged DataSource.  If the source indicates that it is a
     * {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor.SourceType#DATASOURCE} the object
     * each row in the datasource will be used to form a row in the merged DataSource.  If the source type is
     * {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor.SourceType#SINGLE} the object will be a single row
     * even if it is in fact a DataSource.
     *
     * @param sources the source objects to merge
     */
    public void setSources(final List<Source> sources) {
        this.sources = sources;
    }

    @Override
    protected void extraValidation(final List<Throwable> validationErrors, final Configuration config) {
        if (this.sources == null || this.sources.isEmpty()) {
            validationErrors.add(new ConfigurationException(getClass().getSimpleName() + " needs to have at minimum a single source. " +
                                                            "Although logically it should have more"));
            return;
        }

        for (int i = 0; i < this.sources.size(); i++) {
            Source source = this.sources.get(i);

            if (source.type == null) {
                validationErrors.add(new ConfigurationException(
                        "The " + indexString(i) + " source in " + getClass().getSimpleName() + " needs to " +
                        "have a 'type' parameter defined."));
            } else {
                source.type.validate(i, validationErrors, source);
            }
        }
    }

    @Nullable
    @Override
    public In createInputParameter() {
        return new In();
    }

    @Nullable
    @Override
    public Out execute(final In values, final ExecutionContext context) throws Exception {
        List<Map<String, ?>> rows = Lists.newArrayList();

        for (Source source : this.sources) {
            source.type.add(rows, values.values, source);
        }

        JRDataSource mergedDataSource = new JRMapCollectionDataSource(rows);
        return new Out(mergedDataSource);
    }

    @Nonnull
    @Override
    public List<ProcessorDependency> createDependencies(@Nonnull final List<ProcessorGraphNode<Object, Object>> nodes) {
        HashSet<String> sourceKeys = Sets.newHashSet();
        for (Source source : this.sources) {
            source.type.addValuesKeys(source, sourceKeys);
        }

        final ArrayList<ProcessorDependency> dependencies = Lists.newArrayList();
        for (ProcessorGraphNode<Object, Object> node : nodes) {
            if (node.getProcessor() == this) {
                continue;
            }
            final Processor<?, ?> processor = node.getProcessor();
            final Collection<Field> allAttributes = ParserUtils.getAllAttributes(processor.getOutputType());
            final BiMap<String, String> outputMapper = node.getOutputMapper();
            ProcessorDependency customDependency = null;
            for (Field allAttribute : allAttributes) {
                String attributeName = allAttribute.getName();
                final String mappedName = outputMapper.get(attributeName);
                if (mappedName != null) {
                    attributeName = mappedName;
                }
                if (sourceKeys.contains(attributeName)) {
                    if (customDependency == null) {
                        final Class<? extends Processor<?, ?>> processorClass = (Class<? extends Processor<?, ?>>) processor.getClass();
                        customDependency = new ProcessorDependency(processorClass, getClass(),
                                Collections.singleton(attributeName));
                        dependencies.add(customDependency);
                    } else {
                        customDependency.addCommonInput(attributeName);
                    }
                }
            }
        }
        return dependencies;
    }

    private static String indexString(final int i) {
        switch (i + 1) {
            case 1:
                return "1st";
            case 2:
                return "2nd";
            default:
                return (i + 1) + "th";
        }
    }

    /**
     * The input object for {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor}.
     */
    public static class In {
        /**
         * The values used to look up the values to merge together.
         */
        @InternalValue
        public Values values;
    }

    /**
     * The output object for {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor}.
     */
    public static class Out {
        /**
         * The resulting datasource.
         */
        public final JRDataSource mergedDataSource;

        /**
         * Constructor.
         *
         * @param mergedDataSource the merged datasource
         */
        public Out(final JRDataSource mergedDataSource) {
            this.mergedDataSource = mergedDataSource;
        }
    }

    /**
     * Describes the objects to used as sources for the merged DataSource.
     */
    public static final class Source implements ConfigurationObject {
        String key;
        SourceType type;
        Map<String, String> fields = Maps.newHashMap();

        /**
         * The key to use when looking for the object among the attributes and the processor output values.
         *
         * @param key the look up key
         */
        public void setKey(final String key) {
            this.key = key;
        }

        /**
         * The type of source.  See {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor.SourceType} for the options.
         *
         * @param type the type of source
         */
        public void setType(final SourceType type) {
            this.type = type;
        }

        /**
         * The names of each field in the DataSource.  See {@link org.mapfish.print.processor.jasper.MergeDataSourceProcessor.SourceType}
         * for instructions on how to declare the fields
         *
         * @param fields the field names
         */
        public void setFields(final Map<String, String> fields) {
            this.fields = fields;
        }

        static Source createSource(final String key, final SourceType type) {
            Source source = new Source();
            source.key = key;
            source.type = type;
            return source;
        }

        static Source createSource(final String key, final SourceType type, final Map<String, String> fields) {
            Source source = new Source();
            source.key = key;
            source.type = type;
            source.fields = fields;
            return source;
        }

        @Override
        public void validate(final List<Throwable> validationErrors, final Configuration config) {
            // validation is done in MergeDataSourceProcessor
        }
    }

    /**
     * An enumeration of the different <em>types</em> of source objects.  Essentially this describes how the source should be merged
     * into the final merged DataSource.
     */
    public enum SourceType {
        /**
         * Creates a single row from a set of values from the output and attribute objects.
         * <p>
         * In this case the key is not required, only the fields.  Each field key will be the
         * look up key to find the object from the set of processor output and attributes.  The field
         * value will be the column name for that value in the created row
         * </p>
         */
        SINGLE {
            @Override
            void add(final List<Map<String, ?>> rows, final Values values, final Source source) {

                Map<String, Object> row = Maps.newHashMap();
                for (Map.Entry<String, String> entry : source.fields.entrySet()) {
                    final Object object = values.getObject(entry.getKey(), Object.class);
                    row.put(entry.getValue(), object);
                }

                rows.add(row);
            }

            @Override
            void validate(final int rowIndex, final List<Throwable> validationErrors, final Source source) {
                if (source.key != null) {
                    validationErrors.add(new ConfigurationException(
                            "The 'key' property is not required for source with the type " + name() + ". The " + indexString(rowIndex) +
                            " source has a key property configured when it should not"));
                }
                if (source.fields.isEmpty()) {
                    validationErrors.add(new ConfigurationException(
                            "The " + indexString(rowIndex) + " source in " + getClass().getSimpleName() + " has an invalid 'fields' " +
                            "parameter defined. There should be at least most one field defined"));
                }
            }

            @Override
            public void addValuesKeys(final Source source, final HashSet<String> sourceKeys) {
                sourceKeys.addAll(source.fields.keySet());
            }
        },
        /**
         * Indicates that the object is a DataSource and each row in it should be expanded to be a row in the output table.
         * <p>
         * If the datasource does not exist or is null then this source will be skipped
         * </p>
         * <p>
         * The fields parameter of the source should contain all the fields to pull from the source DataSource.  Not all
         * Fields need to be declared.  For example if the source has 5 fields not all of them need to be in the resulting
         * merged datasource.
         * </p>
         */
        DATASOURCE {
            @Override
            void add(final List<Map<String, ?>> rows, final Values values, final Source source) throws JRException {
                JRDataSource dataSource = values.getObject(source.key, JRDataSource.class);
                Assert.isTrue(dataSource != null, "The Datasource object referenced by key: " + source.key + " does not exist.  Check" +
                                                  " that the key is correctly spelled in the config.yaml file.\n\t This is one of the" +
                                                  " sources for the !mergeDataSources.");

                JRDesignField jrField = new JRDesignField();

                while (dataSource.next()) {
                    Map<String, Object> row = Maps.newHashMap();
                    for (Map.Entry<String, String> field : source.fields.entrySet()) {
                        jrField.setName(field.getKey());
                        row.put(field.getValue(), dataSource.getFieldValue(jrField));
                    }
                    rows.add(row);
                }
            }

            @Override
            void validate(final int rowIndex, final List<Throwable> validationErrors, final Source source) {
                if (source.key.isEmpty()) {
                    validationErrors.add(new ConfigurationException(
                            "The " + indexString(rowIndex) + " source in " + MergeDataSourceProcessor.class.getSimpleName() +
                            " needs to have a 'key' parameter defined."));
                }
                if (source.fields.isEmpty()) {
                    validationErrors.add(new ConfigurationException(
                            "The " + indexString(rowIndex) + " source in " + MergeDataSourceProcessor.class.getSimpleName() +
                            " needs to have a 'fields' parameter defined."));
                }

            }

            @Override
            public void addValuesKeys(final Source source, final HashSet<String> sourceKeys) {
                sourceKeys.add(source.key);
            }
        };

        abstract void add(List<Map<String, ?>> rows, Values values, Source source) throws JRException;

        abstract void validate(int rowIndex, List<Throwable> validationErrors, Source source);

        abstract void addValuesKeys(Source source, HashSet<String> sourceKeys);
    }
}
TOP

Related Classes of org.mapfish.print.processor.jasper.MergeDataSourceProcessor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.