org.libreplan.web.print.CutyPrint.java Source code

Java tutorial

Introduction

Here is the source code for org.libreplan.web.print.CutyPrint.java

Source

/*
 * This file is part of LibrePlan
 *
 * Copyright (C) 2009-2010 Fundacin para o Fomento da Calidade Industrial e
 *                         Desenvolvemento Tecnolxico de Galicia
 * Copyright (C) 2010-2011 Igalia, S.L.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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 Affero 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/>.
 */

package org.libreplan.web.print;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicLong;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.UriBuilder;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.libreplan.business.orders.entities.Order;
import org.libreplan.web.common.entrypoints.EntryPointsHandler;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.zkoss.ganttz.Planner;
import org.zkoss.ganttz.servlets.CallbackServlet;
import org.zkoss.ganttz.servlets.CallbackServlet.IServletRequestHandler;
import org.zkoss.util.Locales;
import org.zkoss.zk.ui.Executions;

public class CutyPrint {

    private static final Log LOG = LogFactory.getLog(CutyPrint.class);

    private static final String CUTYCAPT_COMMAND = "cutycapt";

    private static final String INDEX_ZUL = "/planner/index.zul";

    private static final String PX_IMPORTANT = "px !important; } \n";

    /**  Estimated maximum execution time (ms) */
    private static final int CAPTURE_DELAY = 10000;

    /**
     * Default width in pixels of the task name text field for depth level 1.
     * Got from .listdetails .depth_1 input.task_title { width: 121px; } at src/main/webapp/planner/css/ganttzk.css
     */
    private static final int BASE_TASK_NAME_PIXELS = 121;

    private static int TASK_HEIGHT = 25;

    private static class CutyCaptParameters {

        private static final AtomicLong counter = new AtomicLong();

        private final HttpServletRequest request = (HttpServletRequest) Executions.getCurrent().getNativeRequest();

        private final ServletContext context = request.getSession().getServletContext();

        private final String forwardURL;

        private final Map<String, String> entryPointsMap;

        private final Map<String, String> printParameters;

        private final Planner planner;

        private final boolean containersExpandedByDefault;

        private final int minWidthForTaskNameColumn;

        private final String generatedSnapshotServerPath;

        private final int recentUniqueToken = (int) (counter.getAndIncrement() % 1000);

        public CutyCaptParameters(final String forwardURL, final Map<String, String> entryPointsMap,
                Map<String, String> printParameters, Planner planner) {

            this.forwardURL = forwardURL;
            this.entryPointsMap = (entryPointsMap != null) ? entryPointsMap : Collections.emptyMap();

            this.printParameters = (printParameters != null) ? printParameters : Collections.emptyMap();

            this.planner = planner;

            containersExpandedByDefault = Planner
                    .guessContainersExpandedByDefaultGivenPrintParameters(printParameters);
            minWidthForTaskNameColumn = planner.calculateMinimumWidthForTaskNameColumn(containersExpandedByDefault);
            generatedSnapshotServerPath = buildCaptureDestination(printParameters.get("extension"));
        }

        String getGeneratedSnapshotServerPath() {
            return generatedSnapshotServerPath;
        }

        private String buildCaptureDestination(String extension) {
            String newExtension = extension;

            if (StringUtils.isEmpty(newExtension)) {
                newExtension = ".pdf";
            }

            return String.format("/print/%tY%<tm%<td%<tH%<tM%<tS-%s%s", new Date(), recentUniqueToken,
                    newExtension);
        }

        /**
         * An unique recent display number for Xvfb.
         * It's not truly unique across all the life of a LibrePlan application, but it's in the last period of time.
         *
         * @return the display number to use by Xvfb
         */
        public int getXvfbDisplayNumber() {
            // avoid display 0
            return recentUniqueToken + 1;
        }

        void fillParameters(ProcessBuilder c) {
            Map<String, String> parameters = buildParameters();
            for (Entry<String, String> each : parameters.entrySet()) {
                c.command().add(String.format("--%s=%s", each.getKey(), each.getValue()));
            }
        }

        private Map<String, String> buildParameters() {
            Map<String, String> result = new HashMap<>();

            result.put("url", buildSnapshotURLParam());

            int width = buildMinWidthParam();
            result.put("min-width", Integer.toString(width));

            result.put("min-height", Integer.toString(buildMinHeightParam()));
            result.put("delay", Integer.toString(CAPTURE_DELAY));
            result.put("user-style-path", buildCustomCSSParam(width));
            result.put("out", buildPathToOutputFileParam());
            result.put("header", String.format("Accept-Language:%s", Locales.getCurrent().getLanguage()));

            return result;
        }

        private String buildSnapshotURLParam() {
            IServletRequestHandler snapshotRequestHandler = executeOnOriginalContext(new IServletRequestHandler() {

                @Override
                public void handle(HttpServletRequest request, HttpServletResponse response)
                        throws ServletException, IOException {

                    EntryPointsHandler.setupEntryPointsForThisRequest(request, entryPointsMap);

                    // Pending to forward and process additional parameters as show labels, resources, zoom or expand all
                    request.getRequestDispatcher(forwardURL).forward(request, response);
                }
            });

            String pageToSnapshot = CallbackServlet.registerAndCreateURLFor(request, snapshotRequestHandler);

            return createCaptureURL(pageToSnapshot);
        }

        private String createCaptureURL(String capturePath) {
            String hostName = resolveLocalHost();
            String uri = String.format("%s://%s:%s", request.getScheme(), hostName, request.getLocalPort());
            UriBuilder result = UriBuilder.fromUri(uri).path(capturePath);

            for (Entry<String, String> entry : printParameters.entrySet()) {
                result = result.queryParam(entry.getKey(), entry.getValue());
            }

            return result.build().toASCIIString();
        }

        private String resolveLocalHost() {
            try {
                return InetAddress.getByName(request.getLocalName()).getHostName();
            } catch (UnknownHostException e) {
                throw new RuntimeException(e);
            }
        }

        private int buildMinWidthParam() {
            return planner != null && planner.getTimeTracker() != null
                    ? planner.getTimeTracker().getHorizontalSize() + calculateTaskDetailsWidth()
                    : 0;
        }

        private int calculateTaskDetailsWidth() {
            int TASKDETAILS_BASE_WIDTH = 310;
            return TASKDETAILS_BASE_WIDTH + Math.max(0, minWidthForTaskNameColumn - BASE_TASK_NAME_PIXELS);
        }

        private int buildMinHeightParam() {
            int PRINT_VERTICAL_SPACING = 160;
            return (containersExpandedByDefault ? planner.getAllTasksNumber() : planner.getTaskNumber())
                    * TASK_HEIGHT + PRINT_VERTICAL_SPACING;
        }

        private String buildCustomCSSParam(int plannerWidth) {
            // Calculate application path and destination file relative route
            String absolutePath = context.getRealPath("/");
            cssLinesToAppend(plannerWidth);

            return createCSSFile(absolutePath + "/planner/css/print.css", cssLinesToAppend(plannerWidth));
        }

        private static String createCSSFile(String sourceFile, String cssLinesToAppend) {
            File destination;
            try {
                destination = File.createTempFile("print", ".css");
                FileUtils.copyFile(new File(sourceFile), destination);
            } catch (IOException e) {
                LOG.error("Can't create a temporal file for storing the CSS files", e);
                return sourceFile;
            }

            FileWriter appendToFile = null;
            try {
                appendToFile = new FileWriter(destination, true);
                appendToFile.write(cssLinesToAppend);
                appendToFile.flush();
            } catch (IOException e) {
                LOG.error("Can't append to the created file " + destination, e);
            } finally {
                try {
                    if (appendToFile != null) {
                        appendToFile.close();
                    }
                } catch (IOException e) {
                    LOG.warn("error closing fileWriter", e);
                }
            }

            return destination.getAbsolutePath();
        }

        private String cssLinesToAppend(int width) {
            String includeCSSLines = " body { width: " + width + "px; } \n";
            if ("all".equals(printParameters.get("labels"))) {
                includeCSSLines += " .task-labels { display: inline !important;} \n ";
            }

            if ("all".equals(printParameters.get("resources"))) {
                includeCSSLines += " .task-resources { display: inline !important;} \n";
            }

            includeCSSLines += heightCSS();
            includeCSSLines += widthForTaskNamesColumnCSS();

            return includeCSSLines;
        }

        private String heightCSS() {
            int tasksNumber = containersExpandedByDefault ? planner.getAllTasksNumber() : planner.getTaskNumber();
            int PRINT_VERTICAL_PADDING = 50;
            int height = (tasksNumber * TASK_HEIGHT) + PRINT_VERTICAL_PADDING;
            String heightCSS = "";
            heightCSS += " body div#scroll_container { height: " + height + PX_IMPORTANT; /* 1110 */
            heightCSS += " body div#timetracker { height: " + (height + 20) + PX_IMPORTANT;
            heightCSS += " body div.plannerlayout { height: " + (height + 80) + PX_IMPORTANT;
            heightCSS += " body div.main-layout { height: " + (height + 90) + PX_IMPORTANT;

            return heightCSS;
        }

        private String widthForTaskNamesColumnCSS() {
            String css = "/* ------ Make the area for task names wider ------ */\n";
            css += "th.z-tree-col {width: 76px !important;}\n";
            css += "th.tree-text {width: " + (34 + minWidthForTaskNameColumn) + "px !important;}\n";
            css += ".taskdetailsContainer, .z-west-body, .z-tree-header, .z-tree-body {";
            css += "width: " + (176 + minWidthForTaskNameColumn) + "px !important;}\n";

            return css;
        }

        private String buildPathToOutputFileParam() {
            return context.getRealPath(generatedSnapshotServerPath);
        }

        private static IServletRequestHandler executeOnOriginalContext(final IServletRequestHandler original) {
            final SecurityContext originalContext = SecurityContextHolder.getContext();
            final Locale current = Locales.getCurrent();

            return new IServletRequestHandler() {
                @Override
                public void handle(HttpServletRequest request, HttpServletResponse response)
                        throws ServletException, IOException {

                    Locales.setThreadLocal(current);
                    SecurityContextHolder.setContext(originalContext);
                    original.handle(request, response);
                }
            };
        }

    }

    public static void print(Order order) {
        print(INDEX_ZUL, entryPointForShowingOrder(order), Collections.emptyMap());
    }

    public static void print(Order order, Map<String, String> parameters) {
        print(INDEX_ZUL, entryPointForShowingOrder(order), parameters);
    }

    public static void print(Order order, Map<String, String> parameters, Planner planner) {
        print(INDEX_ZUL, entryPointForShowingOrder(order), parameters, planner);
    }

    public static void print() {
        print(INDEX_ZUL, Collections.emptyMap(), Collections.emptyMap());
    }

    public static void print(Map<String, String> parameters) {
        print(INDEX_ZUL, Collections.emptyMap(), parameters);
    }

    public static void print(Map<String, String> parameters, Planner planner) {
        print(INDEX_ZUL, Collections.emptyMap(), parameters, planner);
    }

    private static Map<String, String> entryPointForShowingOrder(Order order) {
        final Map<String, String> result = new HashMap<>();
        result.put("order", order.getCode() + "");
        return result;
    }

    public static void print(final String forwardURL, final Map<String, String> entryPointsMap,
            Map<String, String> parameters) {
        print(forwardURL, entryPointsMap, parameters, null);
    }

    public static void print(final String forwardURL, final Map<String, String> entryPointsMap,
            Map<String, String> parameters, Planner planner) {

        CutyCaptParameters params = new CutyCaptParameters(forwardURL, entryPointsMap, parameters, planner);
        String generatedSnapshotServerPath = takeSnapshot(params);

        openInAnotherTab(generatedSnapshotServerPath);
    }

    private static void openInAnotherTab(String producedPrintFilePath) {
        Executions.getCurrent().sendRedirect(producedPrintFilePath, "_blank");
    }

    /**
     * It blocks until the snapshot is ready.
     * It invokes cutycapt program in order to take a snapshot from a specified url.
     *
     * @return the path in the web application to access via a HTTP GET to the
     *         generated snapshot.
     */
    private static String takeSnapshot(CutyCaptParameters params) {

        ProcessBuilder capture = new ProcessBuilder(CUTYCAPT_COMMAND);
        params.fillParameters(capture);
        String generatedSnapshotServerPath = params.getGeneratedSnapshotServerPath();

        Process printProcess = null;
        Process serverProcess = null;
        try {
            LOG.info("calling printing: " + capture.command());

            // If there is a not real X server environment then use Xvfb
            if (StringUtils.isEmpty(System.getenv("DISPLAY"))) {
                ProcessBuilder s = new ProcessBuilder("Xvfb", ":" + params.getXvfbDisplayNumber());
                serverProcess = s.start();
                capture.environment().put("DISPLAY", ":" + params.getXvfbDisplayNumber() + ".0");
            }

            printProcess = capture.start();
            printProcess.waitFor();

            // Once the printProcess finishes, the print snapshot is available
            return generatedSnapshotServerPath;
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            LOG.error("error invoking command", e);
            throw new RuntimeException(e);
        } finally {
            if (printProcess != null) {
                destroy(printProcess);
            }
            if (serverProcess != null) {
                destroy(serverProcess);
            }
        }
    }

    private static void destroy(Process process) {
        try {
            process.destroy();
        } catch (Exception e) {
            LOG.error("error stoping process " + process, e);
        }
    }

}