Package org.mapfish.print.processor.jasper

Source Code of org.mapfish.print.processor.jasper.TableProcessor$Output

/*
* 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.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import net.sf.jasperreports.engine.JRBand;
import net.sf.jasperreports.engine.JRElement;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRField;
import net.sf.jasperreports.engine.JRStyle;
import net.sf.jasperreports.engine.data.JRMapCollectionDataSource;
import net.sf.jasperreports.engine.design.JRDesignBand;
import net.sf.jasperreports.engine.design.JRDesignElement;
import net.sf.jasperreports.engine.design.JRDesignExpression;
import net.sf.jasperreports.engine.design.JRDesignField;
import net.sf.jasperreports.engine.design.JRDesignImage;
import net.sf.jasperreports.engine.design.JRDesignSection;
import net.sf.jasperreports.engine.design.JRDesignTextField;
import net.sf.jasperreports.engine.design.JasperDesign;
import net.sf.jasperreports.engine.type.HorizontalAlignEnum;
import net.sf.jasperreports.engine.type.ScaleImageEnum;
import net.sf.jasperreports.engine.type.StretchTypeEnum;
import net.sf.jasperreports.engine.xml.JRXmlLoader;
import net.sf.jasperreports.engine.xml.JRXmlWriter;
import org.mapfish.print.Constants;
import org.mapfish.print.PrintException;
import org.mapfish.print.attribute.TableAttribute.TableAttributeValue;
import org.mapfish.print.config.Configuration;
import org.mapfish.print.config.ConfigurationException;
import org.mapfish.print.config.Template;
import org.mapfish.print.http.MfClientHttpRequestFactory;
import org.mapfish.print.processor.AbstractProcessor;
import org.mapfish.print.processor.InternalValue;
import org.mapfish.print.wrapper.PArray;
import org.springframework.beans.factory.annotation.Autowired;

import java.awt.image.RenderedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
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 static org.mapfish.print.processor.jasper.JasperReportBuilder.JASPER_REPORT_COMPILED_FILE_EXT;
import static org.mapfish.print.processor.jasper.JasperReportBuilder.JASPER_REPORT_XML_FILE_EXT;

/**
* A processor for generating a table.
*
* @author Jesse
* @author sbrunner
*/
public final class TableProcessor extends AbstractProcessor<TableProcessor.Input, TableProcessor.Output> {

    private static final int SPACE_BETWEEN_COLS = 0;
    private static final int DEFAULT_MAX_COLUMNS = 9;
    private Map<String, TableColumnConverter<?>> columnConverterMap = Maps.newHashMap();
    private List<TableColumnConverter<?>> converters = Lists.newArrayList();
    private boolean dynamic = false;
    private String jasperTemplate = null;
    private String firstHeaderStyle;
    private String lastHeaderStyle;
    private String headerStyle;
    private String firstDetailStyle;
    private String lastDetailStyle;
    private String detailStyle;
    private int maxColumns = DEFAULT_MAX_COLUMNS;
    private Set<String> excludeColumns = Sets.newHashSet();

    @Autowired
    private JasperReportBuilder jasperReportBuilder;

    /**
     * Constructor.
     */
    protected TableProcessor() {
        super(Output.class);
    }

    /**
     * The path to the jasper template that contains the template for the sub-report.  If dynamic is false then the template
     * will be used without any changes.  It will simply be compiled and used as is.
     * <p>
     * If dynamic is true then the template will be used to obtain the column styles and the size of the subreport.  The
     * actual field and column definitions will be dynamically generated from the table data that is provided.
     * This is required if dynamic is true.
     * </p>
     * This may be null if dynamic is false.  If it is null then the main template will likely use the generated
     * table datasource directly as its datasource for use in its detail section and the table will be directly in the main template's
     * detail section.  Or a later processor may use the table's datasource in someway.
     *
     * @param jasperTemplate the template to use for rendering the table.
     */
    public void setJasperTemplate(final String jasperTemplate) {
        this.jasperTemplate = jasperTemplate;
    }

    /**
     * If true then the Jasper Report Template will be generated dynamically based on the columns in the table attribute.
     * <p>By default this is false because width is required if it is true</p>
     *
     * @param dynamic indicate if the template should be dynamically generated for each print request.
     */
    public void setDynamic(final boolean dynamic) {
        this.dynamic = dynamic;
    }

    /**
     * Set strategies for converting the textual representation of each column to some other object (image, other text, etc...).
     * <p/>
     * Note: The type returned by the column converter must match the type in the jasper template.
     *
     * @param columnConverters Map from column name -> {@link TableColumnConverter}
     */
    public void setColumns(final Map<String, TableColumnConverter<?>> columnConverters) {
        this.columnConverterMap = columnConverters;
    }

    /**
     * Set strategies for converting the textual representation of each cell to some other object (image, other text, etc...).
     * <p/>
     * This is similar to the converters specified for a particular column. The difference is that these converters are applied
     * to every cell of the table (except for the cells of those columns that are assigned a specific converter).
     * @param converters A list of {@link TableColumnConverter}s.
     */
    public void setConverters(final List<TableColumnConverter<?>> converters) {
        this.converters = converters;
    }

    /**
     * The id of the style to apply to the first column in the table header.  This is optional.
     * <p>
     *     The style must be a style element in the jasperTemplate.
     * </p>
     * @param firstHeaderStyle a ref to a style in the japserTemplate
     */
    public void setFirstHeaderStyle(final String firstHeaderStyle) {
        this.firstHeaderStyle = firstHeaderStyle;
    }

    /**
     * The id of the style to apply to the last column in the table header.  This is optional.
     * <p>
     *     The style must be a style element in the jasperTemplate.
     * </p>
     * @param lastHeaderStyle a ref to a style in the japserTemplate
     */
    public void setLastHeaderStyle(final String lastHeaderStyle) {
        this.lastHeaderStyle = lastHeaderStyle;
    }
    /**
     * The id of the style to apply to the all columns in the table header except first and last columns.  This value is will be
     * used as a default if either firstHeaderStyle or lastHeaderStyle is not defined.  This is required if dynamic is true
     * and is not permitted if dynamic is false.
     * <p>
     *     The style must be a style element in the jasperTemplate.
     * </p>
     * @param headerStyle a ref to a style in the japserTemplate
     */
    public void setHeaderStyle(final String headerStyle) {
        this.headerStyle = headerStyle;
    }
    /**
     * The id of the style to apply to the first column in the table detail section.  This is optional.
     * <p>
     *     The style must be a style element in the jasperTemplate.
     * </p>
     * @param firstDetailStyle a ref to a style in the jasperTemplate
     */
    public void setFirstDetailStyle(final String firstDetailStyle) {
        this.firstDetailStyle = firstDetailStyle;
    }
    /**
     * The id of the style to apply to the last column in the table detail section.  This is optional.
     * <p>
     *     The style must be a style element in the jasperTemplate.
     * </p>
     * @param lastDetailStyle a ref to a style in the jasperTemplate
     */
    public void setLastDetailStyle(final String lastDetailStyle) {
        this.lastDetailStyle = lastDetailStyle;
    }
    /**
     * The id of the style to apply to the all columns in the table detail section except first and last columns.  This value is will be
     * used as a default if either firstDetailStyle or lastDetailStyle is not defined.  This is required if dynamic is true
     * and is not permitted if dynamic is false.
     * <p>
     *     The style must be a style element in the jasperTemplate.
     * </p>
     * @param detailStyle a ref to a style in the japserTemplate
     */
    public void setDetailStyle(final String detailStyle) {
        this.detailStyle = detailStyle;
    }

    /**
     * The maximum number of columns to allow.
     * @param maxColumns maximum number of columns to allow.
     */
    public void setMaxColumns(final int maxColumns) {
        this.maxColumns = maxColumns;
    }

    /**
     * A set of column names to exclude from the table.
     *
     * @param excludeColumns a set of names of the columns to exclude from the table.
     */
    public void setExcludeColumns(final Set<String> excludeColumns) {
        this.excludeColumns = excludeColumns;
    }

    @Override
    public Input createInputParameter() {
        return new Input();
    }

    @Override
    public Output execute(final Input values, final ExecutionContext context) throws Exception {
        final TableAttributeValue jsonTable = values.table;
        final Collection<Map<String, ?>> table = new ArrayList<Map<String, ?>>();

        final String[] columnNames = jsonTable.columns;

        // this map needs to be linked so it keeps order
        Map<String, Class<?>> columns = Maps.newLinkedHashMap();
        final PArray[] jsonData = jsonTable.data;
        for (final PArray jsonRow : jsonData) {
            checkCancelState(context);
            final Map<String, Object> row = new HashMap<String, Object>();
            for (int j = 0; j < jsonRow.size(); j++) {
                final String columnName = columnNames[j];
                Object rowValue = jsonRow.get(j);
                TableColumnConverter<?> converter = this.columnConverterMap.get(columnName);
                if (converter != null) {
                    rowValue = converter.resolve(values.clientHttpRequestFactory, (String) rowValue);
                } else {
                    rowValue = tryConvert(values.clientHttpRequestFactory, rowValue);
                }
                if (columns.size() < this.maxColumns && !this.excludeColumns.contains(columnName)) {
                    Class<?> columnDef = columns.get(columnName);
                    if (columnDef == null) {
                        Class<?> rowValueClass = null;
                        if (rowValue != null) {
                            rowValueClass = rowValue.getClass();
                        }
                        columns.put(columnName, rowValueClass);
                    }
                }
                row.put(columnName, rowValue);
            }
            table.add(row);
        }

        String subreport = null;
        if (this.dynamic) {
            subreport = generateSubReport(values, columns);
        }
        final JRMapCollectionDataSource dataSource = new JRMapCollectionDataSource(table);
        return new Output(dataSource, table.size(), subreport);
    }

    /**
     * If converters are set on a table, this function tests if these can convert
     * a cell value. The first converter, which claims that it can convert,
     * will be used to do the conversion.
     */
    private Object tryConvert(final MfClientHttpRequestFactory clientHttpRequestFactory,
            final Object rowValue) throws URISyntaxException, IOException {
        if (this.converters.isEmpty()) {
            return rowValue;
        }

        String value = String.valueOf(rowValue);
        for (TableColumnConverter<?> converter : this.converters) {
            if (converter.canConvert(value)) {
                return converter.resolve(clientHttpRequestFactory, value);
            }
        }

        return rowValue;
    }

    private String generateSubReport(
            final Input input,
            final Map<String, Class<?>> columns) throws JRException, ClassNotFoundException, IOException {
        byte[] bytes = input.template.getConfiguration().loadFile(this.jasperTemplate);
        final JasperDesign templateDesign = JRXmlLoader.load(new ByteArrayInputStream(bytes));
        int headerHeight = templateDesign.getColumnHeader().getHeight();
        final JRDesignSection detailSection = (JRDesignSection) templateDesign.getDetailSection();
        int detailHeight = detailSection.getBands()[0].getHeight();

        final JRElement sampleHeaderEl = templateDesign.getColumnHeader().getElements()[0];
        int headerPosX = sampleHeaderEl.getX();
        int headerPosY = sampleHeaderEl.getY();
        final JRElement sampleDetailEl = detailSection.getBands()[0].getElements()[0];
        int detailPosX = sampleDetailEl.getX();
        int detailPosY = sampleDetailEl.getY();
        clearFields(templateDesign);
        removeDetailBand(templateDesign);
        JRDesignBand headerBand = new JRDesignBand();
        headerBand.setHeight(headerHeight);
        templateDesign.setColumnHeader(headerBand);

        JRDesignBand detailBand = new JRDesignBand();
        detailBand.setHeight(detailHeight);
        detailSection.addBand(detailBand);

        final int columnWidth;
        final int numColumns = columns.size();
        if (columns.isEmpty()) {
            columnWidth = templateDesign.getPageWidth();
        } else {
            columnWidth = (templateDesign.getPageWidth() - (SPACE_BETWEEN_COLS * (numColumns - 1))) / numColumns;
        }

        int i = 0;
        for (Map.Entry<String, Class<?>> entry : columns.entrySet()) {
            i++;

            JRStyle columnDetailStyle;
            JRStyle columnHeaderStyle;
            if (i == 1) {
                columnDetailStyle = getStyle(templateDesign, this.firstDetailStyle, this.detailStyle);
                columnHeaderStyle = getStyle(templateDesign, this.firstHeaderStyle, this.headerStyle);
            } else if (i == numColumns) {
                columnDetailStyle = getStyle(templateDesign, this.lastDetailStyle, this.detailStyle);
                columnHeaderStyle = getStyle(templateDesign, this.lastHeaderStyle, this.headerStyle);
            } else {
                columnDetailStyle = templateDesign.getStylesMap().get(this.detailStyle);
                columnHeaderStyle = templateDesign.getStylesMap().get(this.headerStyle);
            }
            String columnName = entry.getKey();
            Class<?> valueClass = String.class;
            if (entry.getValue() != null) {
                valueClass = entry.getValue();
            }
            // Create a Column Field
            JRDesignField field = new JRDesignField();
            field.setName(columnName);
            if (this.converters.isEmpty()) {
                // if there are no cell converters, the type for all cells in a column should be the same.
                // so we can set a specific type. otherwise we have to set a generic type (Object.class).
                field.setValueClass(valueClass);
            } else {
                field.setValueClass(Object.class);
            }
            templateDesign.addField(field);

            // Add a Header Field to the headerBand
            JRDesignTextField colHeaderField = new JRDesignTextField();
            colHeaderField.setX(headerPosX);
            colHeaderField.setY(headerPosY);
            colHeaderField.setWidth(columnWidth);
            colHeaderField.setHeight(headerHeight);
            colHeaderField.setHorizontalAlignment(HorizontalAlignEnum.LEFT);
            colHeaderField.setStyle(columnHeaderStyle);
            colHeaderField.setStretchWithOverflow(true);
            colHeaderField.setStretchType(StretchTypeEnum.RELATIVE_TO_TALLEST_OBJECT);

            JRDesignExpression headerExpression = new JRDesignExpression();
            headerExpression.setText('"' + columnName + '"');
            colHeaderField.setExpression(headerExpression);
            headerBand.addElement(colHeaderField);

            // Add fields to the detailBand
            if (this.converters.isEmpty()) {
                // if no converters are used, create a field depending on the type
                JRDesignElement designElement;
                if (RenderedImage.class.isAssignableFrom(valueClass)) {
                    designElement = createImageElement(templateDesign, columnName);
                    addElement(detailBand, designElement, detailPosX, detailPosY,
                            columnWidth, detailHeight, columnDetailStyle);
                } else {
                    JRDesignTextField textField = createTextField(columnName);
                    addElement(detailBand, textField, detailPosX, detailPosY,
                            columnWidth, detailHeight, columnDetailStyle);
                }
            } else {
                // image element
                JRDesignElement imageElement = createImageElement(templateDesign, columnName);
                // condition: use this element for images
                JRDesignExpression printWhenExpression = new JRDesignExpression();
                printWhenExpression.setText("new Boolean($F{" + columnName + "}.getClass().equals(java.awt.image.BufferedImage.class))");
                imageElement.setPrintWhenExpression(printWhenExpression);

                addElement(detailBand, imageElement, detailPosX, detailPosY,
                        columnWidth, detailHeight, columnDetailStyle);

                // text field element
                JRDesignTextField textField = createTextField(columnName);
                // condition: use this element for non-images
                printWhenExpression = new JRDesignExpression();
                printWhenExpression.setText("new Boolean(!$F{" + columnName + "}.getClass().equals(java.awt.image.BufferedImage.class))");
                textField.setPrintWhenExpression(printWhenExpression);

                addElement(detailBand, textField, detailPosX, detailPosY,
                        columnWidth, detailHeight, columnDetailStyle);
            }

            headerPosX = headerPosX + columnWidth + SPACE_BETWEEN_COLS;
            detailPosX = detailPosX + columnWidth + SPACE_BETWEEN_COLS;
        }

        final File jrxmlFile = File.createTempFile("table-", JASPER_REPORT_XML_FILE_EXT, input.tempTaskDirectory);
        JRXmlWriter.writeReport(templateDesign, jrxmlFile.getAbsolutePath(), Constants.DEFAULT_ENCODING);

        final File buildFile = File.createTempFile("table-", JASPER_REPORT_COMPILED_FILE_EXT, input.tempTaskDirectory);
        if (!buildFile.delete()) {
            throw new PrintException("Unable to delete the build file: " + buildFile);
        }
        return this.jasperReportBuilder.compileJasperReport(buildFile, jrxmlFile).getAbsolutePath();
    }

    private JRDesignTextField createTextField(final String columnName) {
        JRDesignTextField textField = new JRDesignTextField();
        textField.setHorizontalAlignment(HorizontalAlignEnum.LEFT);
        JRDesignExpression expression = new JRDesignExpression();
        expression.setText("$F{" + columnName + "}");
        textField.setExpression(expression);
        textField.setStretchWithOverflow(true);
        return textField;
    }

    private JRDesignElement createImageElement(final JasperDesign templateDesign,
            final String columnName) {
        JRDesignImage designImage = new JRDesignImage(templateDesign);
        designImage.setScaleImage(ScaleImageEnum.RETAIN_SHAPE);
        designImage.setHorizontalAlignment(HorizontalAlignEnum.LEFT);
        JRDesignExpression expression = new JRDesignExpression();
        expression.setText("$F{" + columnName + "}");
        designImage.setExpression(expression);
        return designImage;
    }

    private void addElement(final JRDesignBand detailBand,
            final JRDesignElement designElement, final int detailPosX, final int detailPosY,
            final int columnWidth, final int detailHeight, final JRStyle columnDetailStyle) {
        designElement.setStretchType(StretchTypeEnum.RELATIVE_TO_TALLEST_OBJECT);
        designElement.setX(detailPosX);
        designElement.setY(detailPosY);
        designElement.setWidth(columnWidth);
        designElement.setHeight(detailHeight);
        designElement.setStyle(columnDetailStyle);
        detailBand.addElement(designElement);
    }

    private void removeDetailBand(final JasperDesign templateDesign) {
        final JRDesignSection detailSection = (JRDesignSection) templateDesign.getDetailSection();
        final List<JRBand> bandsList = Lists.newArrayList(detailSection.getBandsList());
        for (JRBand jrBand : bandsList) {
            detailSection.removeBand(jrBand);
        }
    }

    private void clearFields(final JasperDesign templateDesign) {
        final List<JRField> fieldsList = Lists.newArrayList(templateDesign.getFieldsList());
        for (JRField jrField : fieldsList) {
            templateDesign.removeField(jrField);
        }
    }

    private JRStyle  getStyle(final JasperDesign templateDesign,
                           final String specificStyle,
                           final String defaultStyle) {
        JRStyle columnDetailStyle;
        if (specificStyle != null) {
            columnDetailStyle = templateDesign.getStylesMap().get(specificStyle);
        } else {
            columnDetailStyle = templateDesign.getStylesMap().get(defaultStyle);
        }
        return columnDetailStyle;
    }

    @Override
    protected void extraValidation(final List<Throwable> validationErrors, final Configuration configuration) {
        final boolean styleRefDeclared = this.firstHeaderStyle != null || this.lastHeaderStyle != null || this.headerStyle != null ||
                                         this.firstDetailStyle != null || this.lastDetailStyle != null || this.detailStyle != null;
        if (styleRefDeclared && this.jasperTemplate == null) {
            validationErrors.add(new ConfigurationException(
                    "if a style is declared a 'jasperTemplate' must also be declared (in !tableProcessor)."));
    }
        if (styleRefDeclared && !this.dynamic) {
            validationErrors.add(new ConfigurationException(
                    "if a style is declared dynamic must be true (in !tableProcessor)."));
    }
        if (this.dynamic) {
            if (this.jasperTemplate == null) {
                validationErrors.add(new ConfigurationException(
                        "'jasperTemplate' property must be declared if !tableProcessor is dynamic."));
    }
            if (this.headerStyle == null) {
                validationErrors.add(new ConfigurationException(
                        "'headerStyle' property must be declared if !tableProcessor is dynamic."));
            }
            if (this.detailStyle == null) {
                validationErrors.add(new ConfigurationException(
                        "'detailStyle' property must be declared if !tableProcessor is dynamic."));
        }

            try {
                byte[] bytes = configuration.loadFile(this.jasperTemplate);
                final JasperDesign templateDesign = JRXmlLoader.load(new ByteArrayInputStream(bytes));
                final Map<String, JRStyle> stylesMap = templateDesign.getStylesMap();
                if (templateDesign.getColumnHeader() == null) {
                    validationErrors.add(new ConfigurationException(
                            "JasperTemplate must have a column band defined for height and positioning information"));
                } else if (templateDesign.getColumnHeader().getElements().length == 0) {
                        validationErrors.add(new ConfigurationException(
                                "column header band must have at least one element defined for to height and positioning information"));
                }

                final JRDesignSection detailSection = (JRDesignSection) templateDesign.getDetailSection();
                if (detailSection.getBands().length == 0) {
                    validationErrors.add(new ConfigurationException(
                            "JasperTemplate must have a detail band defined for height and positioning information"));
                } else if (detailSection.getBands()[0].getElements().length == 0) {
                    validationErrors.add(new ConfigurationException(
                            "detail band must have at least one element defined for to height and positioning information"));
                }

                checkStyleExists(validationErrors, stylesMap, this.firstDetailStyle);
                checkStyleExists(validationErrors, stylesMap, this.detailStyle);
                checkStyleExists(validationErrors, stylesMap, this.lastDetailStyle);
                checkStyleExists(validationErrors, stylesMap, this.firstHeaderStyle);
                checkStyleExists(validationErrors, stylesMap, this.headerStyle);
                checkStyleExists(validationErrors, stylesMap, this.lastHeaderStyle);
            } catch (Throwable e) {
                validationErrors.add(e);
    }

        }
    }

    private void checkStyleExists(final List<Throwable> validationErrors,
                                  final Map<String, JRStyle> stylesMap,
                                  final String styleRef) {
        if (styleRef != null && !stylesMap.containsKey(styleRef)) {
            validationErrors.add(new ConfigurationException(
                    "No style with id: '" + styleRef + "' exists in " + this.jasperTemplate));
        }
    }

    /**
     * Input object for execute.
     */
    public static final class Input {
        /**
         * A factory for making http requests.  This is added to the values by the framework and therefore
         * does not need to be set in configuration
         */
        @InternalValue
        public MfClientHttpRequestFactory clientHttpRequestFactory;
        /**
         * The directory to write the generated table to (if dynamic).
         */
        @InternalValue
        public File tempTaskDirectory;
        /**
         * The template containing this table processor.
         */
        @InternalValue
        public Template template;
        /**
         * Data for constructing the table Datasource.
         */
        public TableAttributeValue table;
    }

    /**
     * The Output of the processor.
     */
    public static final class Output {
        /**
         * The table datasource.
         */
        public final JRMapCollectionDataSource table;

        /**
         * The number of rows in the table.
         */
        public final int numberOfTableRows;

        /**
         * The path to the generated sub-report.  If dynamic is false then this will be null.
         */
        public final String tableSubReport;

        private Output(final JRMapCollectionDataSource dataSource,
                       final int numberOfTableRows,
                       final String subReport) {
            this.table = dataSource;
            this.numberOfTableRows = numberOfTableRows;
            this.tableSubReport = subReport;
        }
    }

}
TOP

Related Classes of org.mapfish.print.processor.jasper.TableProcessor$Output

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.