/*
* This file is part of LibrePlan
*
* Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e
* Desenvolvemento Tecnolóxico 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);
}
}
}