de.iteratec.iteraplan.presentation.responsegenerators.GraphicsResponseGenerator.java Source code

Java tutorial

Introduction

Here is the source code for de.iteratec.iteraplan.presentation.responsegenerators.GraphicsResponseGenerator.java

Source

/*
 * iteraplan is an IT Governance web application developed by iteratec, GmbH
 * Copyright (C) 2004 - 2014 iteratec, GmbH
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License version 3 as published by
 * the Free Software Foundation with the addition of the following permission
 * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
 * WORK IN WHICH THE COPYRIGHT IS OWNED BY ITERATEC, ITERATEC DISCLAIMS THE
 * WARRANTY OF NON INFRINGEMENT  OF THIRD PARTY RIGHTS.
 *
 * This program 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 Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301 USA.
 *
 * You can contact iteratec GmbH headquarters at Inselkammerstr. 4
 * 82008 Munich - Unterhaching, Germany, or at email address info@iteratec.de.
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License version 3.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License
 * version 3, these Appropriate Legal Notices must retain the display of the
 * "iteraplan" logo. If the display of the logo is not reasonably
 * feasible for technical reasons, the Appropriate Legal Notices must display
 * the words "Powered by iteraplan".
 */
package de.iteratec.iteraplan.presentation.responsegenerators;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.io.IOUtils;

import de.iteratec.iteraplan.common.Constants;
import de.iteratec.iteraplan.common.Logger;
import de.iteratec.iteraplan.common.error.IteraplanErrorMessages;
import de.iteratec.iteraplan.common.error.IteraplanTechnicalException;
import de.iteratec.iteraplan.model.queries.ReportType;
import de.iteratec.iteraplan.presentation.dialog.GraphicalReporting.GraphicExportBean;
import de.iteratec.svg.model.SvgExportException;

/**
 * A class to summarize the common methods of all the graphics response generators.
 */
public abstract class GraphicsResponseGenerator {

    private static final Logger LOGGER = Logger.getIteraplanLogger(GraphicsResponseGenerator.class);

    private static final String ERROR_UNKNOWN = "Unknown";
    private static final String ERROR_UNSUPPORTED = "Unsupported";
    private static final String TEMPFILE_PREFIX = "iteraplanDiagram";
    private static final String FILE_SUFFIX = ".tmp";

    /**
     * Models two types of contents, i.e. either attachment or inline.
     * 
     * @author est
     */
    public static enum Content {

        ATTACH("attachment; "), INLINE("inline; "), UNKNOWN("");

        private String typeOfContent;
        private static final String END_LINE = "; ";

        private Content(String typeOfContent) {
            this.typeOfContent = typeOfContent;
        }

        public String getValue() {
            return this.typeOfContent;
        }

        public static Content fromValue(String value) {

            String normalizedValue = value;

            /* normalize content string */
            if (!value.endsWith(END_LINE)) {
                normalizedValue += END_LINE;
            }

            for (Content content : Content.values()) {
                if (content.getValue().equals(normalizedValue)) {
                    return content;
                }
            }
            return UNKNOWN;
        }

    }

    /**
     * TODO Should this really be called ResultFormat? ResultFormat seems to be used for different
     * stuff at other locations, i.e. {@link de.iteratec.iteraplan.businesslogic.reports.query.options.TabularReporting.TabularOptionsBean#getResultFormat}
     */
    public static enum ResultFormat {
        VISIO("Visio", Constants.REPORTS_EXPORT_GRAPHICAL_VISIO, VisioResponseGenerator.class), SVG("Svg",
                Constants.REPORTS_EXPORT_GRAPHICAL_SVG,
                SVGResponseGenerator.class), PDF("Pdf", Constants.REPORTS_EXPORT_GRAPHICAL_PDF,
                        PDFResponseGenerator.class), PNG("Png", Constants.REPORTS_EXPORT_GRAPHICAL_PNG,
                                PNGResponseGenerator.class), JPEG("Jpeg", Constants.REPORTS_EXPORT_GRAPHICAL_JPEG,
                                        JPEGResponseGenerator.class), UNKNOWN("", "", null);

        private String format;
        private String graphicFormat;
        private Class<? extends GraphicsResponseGenerator> clazz;

        private ResultFormat(String format, String graphicFormat,
                Class<? extends GraphicsResponseGenerator> clazz) {
            this.format = format;
            this.graphicFormat = graphicFormat;
            this.clazz = clazz;
        }

        /**
         * @return the simple string representation of the result format (i.e. Png)
         */
        public String getFormat() {
            return this.format;
        }

        /**
         * @return one of the result formats defined in the Constants (i.e.
         *         Constants.REPORTS_EXPORT_GRAPHICAL_PNG) or empty string if unknown
         */
        public String getGraphicFormat() {
            return this.graphicFormat;
        }

        public Class<? extends GraphicsResponseGenerator> getGeneratorClass() {
            return this.clazz;
        }

        public static ResultFormat fromString(String value) {
            for (ResultFormat format : ResultFormat.values()) {
                if (format.getFormat().equalsIgnoreCase(value)) {
                    return format;
                }
            }
            return UNKNOWN;
        }

    }

    /**
     * Models the type of graphical report.
     * 
     * @author est
     */
    public static enum GraphicalReport {
        PORTFOLIO("PortfolioDiagram", ReportType.PORTFOLIO.getValue()),

        LANDSCAPE("LandscapeDiagram", ReportType.LANDSCAPE.getValue()),

        MASTERPLAN("MasterplanDiagram", ReportType.MASTERPLAN.getValue()),

        CLUSTER("ClusterDiagram", ReportType.CLUSTER.getValue()),

        INFORMATIONFLOW("InformationFlowDiagram", ReportType.INFORMATIONFLOW.getValue()),

        COMPOSITE("CompositeDiagram", ReportType.COMPOSITE.getValue()),

        PIE("PieDiagram", ReportType.PIE.getValue()),

        BAR("BarDiagram", ReportType.BAR.getValue()),

        LINE("LineDiagram", ReportType.LINE.getValue()),

        VBBCLUSTER("NestingClusterDiagram", ReportType.VBBCLUSTER.getValue()),

        TIMELINE("TimelineDiagram", ReportType.TIMELINE.getValue()),

        MATRIX("MatrixDiagram", ReportType.MATRIX.getValue()),

        NEIGHBORHOOD("NeighborhoodDiagram", ReportType.NEIGHBORHOOD.getValue()),

        UNKNOWN("", "");

        private String typeOfDiagram;
        private String val;

        private static final String DIAGRAM = "Diagram";

        private GraphicalReport(String typeOfDiagram, String val) {
            this.typeOfDiagram = typeOfDiagram;
            this.val = val;
        }

        public String getType() {
            return this.typeOfDiagram;
        }

        public String getVal() {
            return this.val;
        }

        public static GraphicalReport fromTypeOrValueString(String typeOrValue) {
            GraphicalReport grep = fromValueString(typeOrValue);
            if (grep == UNKNOWN) {
                grep = fromTypeString(typeOrValue);
            }
            return grep;
        }

        public static GraphicalReport fromValueString(String value) {
            for (GraphicalReport grep : GraphicalReport.values()) {
                if (grep.getVal().equals(value)) {
                    return grep;
                }
            }
            return UNKNOWN;
        }

        public static GraphicalReport fromTypeString(String savedQueryType) {

            String normalizedType = savedQueryType;

            /* normalize diagram type string */
            if (!savedQueryType.endsWith(DIAGRAM)) {
                normalizedType += DIAGRAM;
            }
            for (GraphicalReport grep : GraphicalReport.values()) {
                if (grep.getType().equals(normalizedType)) {
                    return grep;
                }
            }
            return UNKNOWN;
        }
    }

    protected GraphicsResponseGenerator() {
        super();
    }

    public abstract GraphicsResponseGenerator getInstance();

    /**
     * Extracts the output type from the screen attribute. For details on the screen attribute format
     * see method getScript.
     * 
     * @param screen
     *          the screen specification
     * @return the output type specification or an empty string if no output type specification is
     *         found
     */
    public String getOutputType(String screen) {
        int colonPos = screen.indexOf(':');

        if (colonPos != -1) {
            return screen.substring(colonPos + 1);
        }
        return "";
    }

    /**
     * Transforms a report file into an appropriate HTTP response. This method is designed to work
     * independent from the actual graphics format. This detail is delegated to the abstract method
     * {@link #writeGraphics(GraphicExportBean, OutputStream)}. Child class response generators
     * must implement that method according to the format that they are responsible for. Additionally,
     * child class response generators must implement the abstract methods {@link #getContentType()}
     * and getContentDisposition(String,Content).
     * 
     * This implementation first writes the diagram into a temporary file and then writes that file's
     * contents to the response. This detour is necessary to find out the length of the download and
     * set the Content-Length response header.
     * 
     * @param response
     *          HTTP Response where the report file shall be written to.
     * @param graphicsBean
     *          contains the generated diagram, regardless of the specific file format
     * @param reportType
     *          Specifies which diagram type is downloaded. Influences the suggested download filename
     * @param content
     *          Enum to influence the download's Content-Disposition
     */
    public void generateResponse(HttpServletResponse response, GraphicExportBean graphicsBean,
            GraphicalReport reportType, Content content) {
        File tempFile = null;
        try {
            // Create a new temporary file
            tempFile = File.createTempFile(TEMPFILE_PREFIX + reportType.getType(), FILE_SUFFIX);

            FileOutputStream tempFileOStream = null;
            try {
                // now write the temporary file
                tempFileOStream = new FileOutputStream(tempFile);
                writeGraphics(graphicsBean, tempFileOStream);
                tempFileOStream.flush();
            } finally {
                IOUtils.closeQuietly(tempFileOStream);
            }

            prepareResponse(response, reportType, content, tempFile.length());
            ServletOutputStream servletOutputStream = response.getOutputStream();

            FileInputStream tempFileInStream = null;
            try {
                // read temp file back in and write it to Servlet response
                tempFileInStream = new FileInputStream(tempFile);
                IOUtils.copy(tempFileInStream, servletOutputStream);
                servletOutputStream.flush();

            } finally {
                IOUtils.closeQuietly(tempFileInStream);
            }

        } catch (Exception e) {
            if (hasBeenAborted(e)) {
                LOGGER.info(
                        "The download of the graphics document has been canceled by the user or has been aborted due to a network error.");
            } else {
                throw new IteraplanTechnicalException(IteraplanErrorMessages.GRAPHIC_GENERATION_FAILED, e);
            }

        } finally {
            // delete temporary file
            if (tempFile != null && !tempFile.delete()) {
                LOGGER.error("Couldn't delete temporary file {0}", tempFile.toString());
            }
        }
    }

    private void prepareResponse(HttpServletResponse response, GraphicalReport reportType, Content content,
            long fileLength) {
        response.setContentType(getContentType());
        response.setHeader("Content-disposition", getContentDisposition(reportType, content));

        // ITERAPLAN-1732: add the length of the file to the header of the response to make IE happy
        response.addHeader("Content-Length", Long.toString(fileLength));
    }

    protected final String getContentDisposition(GraphicalReport reportType, Content content) {
        StringBuilder resultBuilder = new StringBuilder();
        resultBuilder.append(content.getValue());

        LOGGER.info("Looking up content disposition for {0} output type '{1}'", getGraphicType(), reportType);

        if (!GraphicalReport.UNKNOWN.equals(reportType)) {
            if (savedQueryTypeSupported(reportType)) {
                resultBuilder.append("filename=iteraplan");
                resultBuilder.append(reportType.getType());
                resultBuilder.append(getFileType());
            } // unsupported graphic for report type (currently visio export for cluster diagram)
            else {
                throwError(ERROR_UNSUPPORTED, reportType);
            }
        } // unknown report type
        else {
            throwError(ERROR_UNKNOWN, reportType);
        }

        String result = resultBuilder.toString();
        LOGGER.info("Returning '{0}'", result);

        return result;
    }

    protected abstract void writeGraphics(GraphicExportBean graphicsBean, OutputStream outputStream)
            throws IOException, SvgExportException;

    protected abstract boolean savedQueryTypeSupported(GraphicalReport grep);

    protected abstract String getContentType();

    protected abstract String getFileType();

    /**
     * @return the standard filename that should be proposed for a specific kind of graphical export.
     */
    protected String getFileNameProposition() {
        return "iteraplan" + getGraphicType() + "Export" + getFileType();
    }

    /**
     * @return The type of graphic, e.g. Visio, JPEG, PDF, etc. as String. This is derived from the
     *         concrete class's name
     */
    protected String getGraphicType() {
        return this.getClass().getName().replace("ResponseGenerator", "");
    }

    private void throwError(String typeOfError, GraphicalReport reportType) {
        LOGGER.error("{0} {1} output type: {2}", typeOfError, getGraphicType(), reportType);

        throw new IteraplanTechnicalException(IteraplanErrorMessages.GRAPHIC_GENERATION_FAILED);

    }

    /**
     * Checks whether the generation of the HTTP response containing the SVG document has been aborted
     * by the user. This method makes the assumption that if the root cause of the given IOException
     * is an instance of TransformerException the operation has been canceled. Of course this
     * assumption might hide other reasons for why this kind of exception might have been thrown, but
     * it's the main reason.
     * <p>
     * There's a problem that makes it cumbersome to reliably detect if the generation of the response
     * has been canceled: The actual exception type at the bottom of the stack of Throwables that make
     * up the given IOException is a java.net.SocketException which in turn is probably wrapped by app
     * server specific exception types. These are then wrapped by exceptions of the various frameworks
     * that are used to write the SVG document to the HTTP response. For example, on Tomcat the stack
     * (from top to bottom) looks like the following:
     * <ul>
     * <li>java.io.IOException</li>
     * <li>javax.xml.transform.TransformerException</li>
     * <li>org.xml.sax.SAXException</li>
     * <li>org.apache.catalina.connector.ClientAbortException</
     * <li>java.net.SocketException</li>
     * </ul>
     * Therefore the above assumption is made for brevity.
     * 
     * @param ex
     *          The IOException that might indicate that the operation has been cancelled.
     * @return {@code true}, if the operation has probably been cancelled, {@code false} otherwise.
     */
    protected boolean hasBeenAborted(Exception ex) {
        Throwable t = ex;
        while ((t.getCause() != null) && !t.getCause().equals(t)) {
            if ((t.getCause() instanceof javax.net.ssl.SSLException)
                    || (t.getCause() instanceof javax.xml.transform.TransformerException)
                    || (t.getCause() instanceof java.net.SocketException)) {
                return true;
            }
            t = t.getCause();
        }
        return false;
    }
}