| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.struts2.views.jasperreports; |
| |
| import com.opensymphony.xwork2.ActionInvocation; |
| import com.opensymphony.xwork2.inject.Inject; |
| import com.opensymphony.xwork2.security.NotExcludedAcceptedPatternsChecker; |
| import com.opensymphony.xwork2.util.ValueStack; |
| |
| import net.sf.jasperreports.engine.*; |
| import net.sf.jasperreports.engine.export.*; |
| import net.sf.jasperreports.engine.util.JRLoader; |
| |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.struts2.ServletActionContext; |
| import org.apache.struts2.result.StrutsResultSupport; |
| |
| import javax.servlet.ServletContext; |
| import javax.servlet.ServletException; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.sql.Connection; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.TimeZone; |
| |
| /** |
| * <!-- START SNIPPET: description --> |
| * <p> |
| * Generates a JasperReports report using the specified format or PDF if no |
| * format is specified. |
| * </p> |
| * <!-- END SNIPPET: description --> |
| * <p> |
| * <b>This result type takes the following parameters:</b> |
| * </p> |
| * <!-- START SNIPPET: params --> |
| * |
| * <ul> |
| * |
| * <li><b>location (default)</b> - the location where the compiled jasper report |
| * definition is (foo.jasper), relative from current URL.</li> |
| * <li><b>dataSource (required)</b> - the EL expression used to retrieve the |
| * datasource from the value stack (usually a List).</li> |
| * <li><b>parse</b> - true by default. If set to false, the location param will |
| * not be parsed for EL expressions.</li> |
| * <li><b>format</b> - the format in which the report should be generated. Valid |
| * values can be found in {@link JasperReportConstants}. If no format is |
| * specified, PDF will be used.</li> |
| * <li><b>contentDisposition</b> - disposition (defaults to "inline", values are |
| * typically <i>filename="document.pdf"</i>).</li> |
| * <li><b>documentName</b> - name of the document (will generate the http header |
| * <code>Content-disposition = X; filename=X.[format]</code>).</li> |
| * <li><b>delimiter</b> - the delimiter used when generating CSV reports. By |
| * default, the character used is ",".</li> |
| * <li><b>imageServletUrl</b> - name of the url that, when prefixed with the |
| * context page, can return report images.</li> |
| * <li> |
| * <b>reportParameters</b> - (2.1.2+) OGNL expression used to retrieve a map of |
| * report parameters from the value stack. The parameters may be accessed |
| * in the report via the usual JR mechanism and might include data not |
| * part of the dataSource, such as the user name of the report creator, etc. |
| * </li> |
| * <li> |
| * <b>exportParameters</b> - (2.1.2+) OGNL expression used to retrieve a map of |
| * JR exporter parameters from the value stack. The export parameters are |
| * used to customize the JR export. For example, a PDF export might enable |
| * encryption and set the user password to a string known to the report creator. |
| * </li> |
| * <li> |
| * <b>connection</b> - (2.1.7+) JDBC Connection which can be passed to the |
| * report instead of dataSource |
| * </li> |
| * <li><b>wrapField</b> - (2.3.18+) defines if fields should warp with ValueStackDataSource |
| * see https://issues.apache.org/jira/browse/WW-3698 for more details |
| * </li> |
| * </ul> |
| * <p> |
| * This result follows the same rules from {@link StrutsResultSupport}. |
| * Specifically, all parameters will be parsed if the "parse" parameter |
| * is not set to false. |
| * </p> |
| * <!-- END SNIPPET: params --> |
| * <p><b>Example:</b></p> |
| * <pre> |
| * <!-- START SNIPPET: example1 --> |
| * <result name="success" type="jasper"> |
| * <param name="location">foo.jasper</param> |
| * <param name="dataSource">mySource</param> |
| * <param name="format">CSV</param> |
| * </result> |
| * <!-- END SNIPPET: example1 --> |
| * </pre> |
| * |
| * or for pdf |
| * |
| * <pre> |
| * <!-- START SNIPPET: example2 --> |
| * <result name="success" type="jasper"> |
| * <param name="location">foo.jasper</param> |
| * <param name="dataSource">mySource</param> |
| * </result> |
| * <!-- END SNIPPET: example2 --> |
| * </pre> |
| */ |
| public class JasperReportsResult extends StrutsResultSupport implements JasperReportConstants { |
| |
| private static final long serialVersionUID = -2523174799621182907L; |
| |
| private final static Logger LOG = LogManager.getLogger(JasperReportsResult.class); |
| |
| protected String dataSource; |
| private String parsedDataSource; |
| protected String format; |
| protected String documentName; |
| protected String contentDisposition; |
| protected String delimiter; |
| protected String imageServletUrl = "/images/"; |
| protected String timeZone; |
| protected boolean wrapField = true; |
| |
| /** |
| * Connection which can be passed to the report |
| * instead od dataSource. |
| */ |
| protected String connection; |
| |
| /** |
| * Names a report parameters map stack value, allowing |
| * additional report parameters from the action. |
| */ |
| protected String reportParameters; |
| private String parsedReportParameters; |
| |
| /** |
| * Names an exporter parameters map stack value, |
| * allowing the use of custom export parameters. |
| */ |
| protected String exportParameters; |
| private String parsedExportParameters; |
| |
| private NotExcludedAcceptedPatternsChecker notExcludedAcceptedPatterns; |
| |
| /** |
| * Default ctor. |
| */ |
| public JasperReportsResult() { |
| super(); |
| } |
| |
| /** |
| * Default ctor with location. |
| * |
| * @param location Result location. |
| */ |
| public JasperReportsResult(String location) { |
| super(location); |
| } |
| |
| @Inject |
| public void setNotExcludedAcceptedPatterns(NotExcludedAcceptedPatternsChecker notExcludedAcceptedPatterns) { |
| this.notExcludedAcceptedPatterns = notExcludedAcceptedPatterns; |
| } |
| |
| public String getImageServletUrl() { |
| return imageServletUrl; |
| } |
| |
| public void setImageServletUrl(final String imageServletUrl) { |
| this.imageServletUrl = imageServletUrl; |
| } |
| |
| public void setDataSource(String dataSource) { |
| this.dataSource = dataSource; |
| } |
| |
| public void setFormat(String format) { |
| this.format = format; |
| } |
| |
| public void setDocumentName(String documentName) { |
| this.documentName = documentName; |
| } |
| |
| public void setContentDisposition(String contentDisposition) { |
| this.contentDisposition = contentDisposition; |
| } |
| |
| public void setDelimiter(String delimiter) { |
| this.delimiter = delimiter; |
| } |
| |
| /** |
| * set time zone id |
| * |
| * @param timeZone |
| */ |
| public void setTimeZone(final String timeZone) { |
| this.timeZone = timeZone; |
| } |
| |
| public void setWrapField(boolean wrapField) { |
| this.wrapField = wrapField; |
| } |
| |
| public String getReportParameters() { |
| return reportParameters; |
| } |
| |
| public void setReportParameters(String reportParameters) { |
| this.reportParameters = reportParameters; |
| } |
| |
| public String getExportParameters() { |
| return exportParameters; |
| } |
| |
| public void setExportParameters(String exportParameters) { |
| this.exportParameters = exportParameters; |
| } |
| |
| public String getConnection() { |
| return connection; |
| } |
| |
| public void setConnection(String connection) { |
| this.connection = connection; |
| } |
| |
| protected void doExecute(String finalLocation, ActionInvocation invocation) throws Exception { |
| // Will throw a runtime exception if no "datasource" property. TODO Best place for that is...? |
| initializeProperties(invocation); |
| |
| LOG.debug("Creating JasperReport for dataSource = {}, format = {}", dataSource, format); |
| |
| HttpServletRequest request = (HttpServletRequest) invocation.getInvocationContext().get(ServletActionContext.HTTP_REQUEST); |
| HttpServletResponse response = (HttpServletResponse) invocation.getInvocationContext().get(ServletActionContext.HTTP_RESPONSE); |
| |
| // Handle IE special case: it sends a "contype" request first. |
| // TODO Set content type to config settings? |
| if ("contype".equals(request.getHeader("User-Agent"))) { |
| try (OutputStream outputStream = response.getOutputStream()) { |
| response.setContentType("application/pdf"); |
| response.setContentLength(0); |
| } catch (IOException e) { |
| LOG.error("Error writing report output", e); |
| throw new ServletException(e.getMessage(), e); |
| } |
| return; |
| } |
| |
| // Construct the data source for the report. |
| ValueStack stack = invocation.getStack(); |
| ValueStackDataSource stackDataSource = null; |
| |
| Connection conn = (Connection) stack.findValue(connection); |
| if (conn == null) { |
| boolean evaluated = parsedDataSource != null && !parsedDataSource.equals(dataSource); |
| boolean reevaluate = !evaluated || isAcceptableExpression(parsedDataSource); |
| if (reevaluate) { |
| stackDataSource = new ValueStackDataSource(stack, parsedDataSource, wrapField); |
| } else { |
| throw new ServletException(String.format("Error building dataSource for excluded or not accepted [%s]", |
| parsedDataSource)); |
| } |
| } |
| |
| if ("https".equalsIgnoreCase(request.getScheme())) { |
| // set the the HTTP Header to work around IE SSL weirdness |
| response.setHeader("CACHE-CONTROL", "PRIVATE"); |
| response.setHeader("Cache-Control", "maxage=3600"); |
| response.setHeader("Pragma", "public"); |
| response.setHeader("Accept-Ranges", "none"); |
| } |
| |
| // Determine the directory that the report file is in and set the reportDirectory parameter |
| // For WW 2.1.7: |
| // ServletContext servletContext = ((ServletConfig) invocation.getInvocationContext().get(ServletActionContext.SERVLET_CONFIG)).getServletContext(); |
| ServletContext servletContext = (ServletContext) invocation.getInvocationContext().get(ServletActionContext.SERVLET_CONTEXT); |
| String systemId = servletContext.getRealPath(finalLocation); |
| Map parameters = new ValueStackShadowMap(stack); |
| File directory = new File(systemId.substring(0, systemId.lastIndexOf(File.separator))); |
| parameters.put("reportDirectory", directory); |
| parameters.put(JRParameter.REPORT_LOCALE, invocation.getInvocationContext().getLocale()); |
| |
| // put timezone in jasper report parameter |
| if (timeZone != null) { |
| timeZone = conditionalParse(timeZone, invocation); |
| final TimeZone tz = TimeZone.getTimeZone(timeZone); |
| if (tz != null) { |
| // put the report time zone |
| parameters.put(JRParameter.REPORT_TIME_ZONE, tz); |
| } |
| } |
| |
| // Add any report parameters from action to param map. |
| boolean evaluated = parsedReportParameters != null && !parsedReportParameters.equals(reportParameters); |
| boolean reevaluate = !evaluated || isAcceptableExpression(parsedReportParameters); |
| Map reportParams = reevaluate ? (Map) stack.findValue(parsedReportParameters) : null; |
| if (reportParams != null) { |
| LOG.debug("Found report parameters; adding to parameters..."); |
| parameters.putAll(reportParams); |
| } |
| |
| ByteArrayOutputStream output; |
| JasperPrint jasperPrint; |
| |
| // Fill the report and produce a print object |
| try { |
| JasperReport jasperReport = (JasperReport) JRLoader.loadObject(new File(systemId)); |
| if (conn == null) { |
| jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, stackDataSource); |
| } |
| else { |
| jasperPrint = JasperFillManager.fillReport(jasperReport, parameters, conn); |
| } |
| } catch (JRException e) { |
| LOG.error("Error building report for uri {}", systemId, e); |
| throw new ServletException(e.getMessage(), e); |
| } |
| |
| // Export the print object to the desired output format |
| try { |
| if (contentDisposition != null || documentName != null) { |
| final StringBuffer tmp = new StringBuffer(); |
| tmp.append((contentDisposition == null) ? "inline" : contentDisposition); |
| |
| if (documentName != null) { |
| tmp.append("; filename="); |
| tmp.append(documentName); |
| tmp.append("."); |
| tmp.append(format.toLowerCase()); |
| } |
| |
| response.setHeader("Content-disposition", tmp.toString()); |
| } |
| |
| JRExporter exporter; |
| |
| if (format.equals(FORMAT_PDF)) { |
| response.setContentType("application/pdf"); |
| exporter = new JRPdfExporter(); |
| } else if (format.equals(FORMAT_CSV)) { |
| response.setContentType("text/csv"); |
| exporter = new JRCsvExporter(); |
| } else if (format.equals(FORMAT_HTML)) { |
| response.setContentType("text/html"); |
| |
| // IMAGES_MAPS seems to be only supported as "backward compatible" from JasperReports 1.1.0 |
| |
| Map imagesMap = new HashMap(); |
| request.getSession(true).setAttribute("IMAGES_MAP", imagesMap); |
| |
| exporter = new JRHtmlExporter(); |
| exporter.setParameter(JRHtmlExporterParameter.IMAGES_MAP, imagesMap); |
| exporter.setParameter(JRHtmlExporterParameter.IMAGES_URI, request.getContextPath() + imageServletUrl); |
| |
| // Needed to support chart images: |
| exporter.setParameter(JRExporterParameter.JASPER_PRINT, jasperPrint); |
| request.getSession().setAttribute("net.sf.jasperreports.j2ee.jasper_print", jasperPrint); |
| } else if (format.equals(FORMAT_XLS)) { |
| response.setContentType("application/vnd.ms-excel"); |
| exporter = new JRXlsExporter(); |
| } else if (format.equals(FORMAT_XML)) { |
| response.setContentType("text/xml"); |
| exporter = new JRXmlExporter(); |
| } else if (format.equals(FORMAT_RTF)) { |
| response.setContentType("application/rtf"); |
| exporter = new JRRtfExporter(); |
| } else { |
| throw new ServletException("Unknown report format: " + format); |
| } |
| |
| evaluated = parsedExportParameters != null && !parsedExportParameters.equals(exportParameters); |
| reevaluate = !evaluated || isAcceptableExpression(parsedExportParameters); |
| Map exportParams = reevaluate ? (Map) stack.findValue(parsedExportParameters) : null; |
| if (exportParams != null) { |
| LOG.debug("Found export parameters; adding to exporter parameters..."); |
| exporter.getParameters().putAll(exportParams); |
| } |
| |
| output = exportReportToBytes(jasperPrint, exporter); |
| } catch (JRException e) { |
| LOG.error("Error producing {} report for uri {}", format, systemId, e); |
| throw new ServletException(e.getMessage(), e); |
| } finally { |
| try { |
| if (conn != null) { |
| // avoid NPE if connection was not used for the report |
| conn.close(); |
| } |
| } catch (Exception e) { |
| LOG.warn("Could not close db connection properly", e); |
| } |
| } |
| |
| response.setContentLength(output.size()); |
| // Will throw ServletException on IOException. |
| writeReport(response, output); |
| } |
| |
| /** |
| * Writes report bytes to response output stream. |
| * |
| * @param response Current response. |
| * @param output Report bytes to write. |
| * @throws ServletException on stream IOException. |
| */ |
| private void writeReport(HttpServletResponse response, ByteArrayOutputStream output) throws ServletException { |
| try (OutputStream outputStream = response.getOutputStream()) { |
| output.writeTo(outputStream); |
| outputStream.flush(); |
| } catch (IOException e) { |
| LOG.error("Error writing report output", e); |
| throw new ServletException(e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Sets up result properties, parsing etc. |
| * |
| * @param invocation Current invocation. |
| * @throws Exception on initialization error. |
| */ |
| private void initializeProperties(ActionInvocation invocation) throws Exception { |
| if (dataSource == null && connection == null) { |
| String message = "No dataSource specified..."; |
| LOG.error(message); |
| throw new RuntimeException(message); |
| } |
| if (dataSource != null) { |
| parsedDataSource = conditionalParse(dataSource, invocation); |
| } |
| |
| format = conditionalParse(format, invocation); |
| if (StringUtils.isEmpty(format)) { |
| format = FORMAT_PDF; |
| } |
| |
| if (contentDisposition != null) { |
| contentDisposition = conditionalParse(contentDisposition, invocation); |
| } |
| |
| if (documentName != null) { |
| documentName = conditionalParse(documentName, invocation); |
| } |
| |
| parsedReportParameters = conditionalParse(reportParameters, invocation); |
| parsedExportParameters = conditionalParse(exportParameters, invocation); |
| } |
| |
| /** |
| * Run a Jasper report to CSV format and put the results in a byte array |
| * |
| * @param jasperPrint The Print object to render as CSV |
| * @param exporter The exporter to use to export the report |
| * @return A CSV formatted report |
| * @throws net.sf.jasperreports.engine.JRException |
| * If there is a problem running the report |
| */ |
| private ByteArrayOutputStream exportReportToBytes(JasperPrint jasperPrint, JRExporter exporter) throws JRException { |
| ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| |
| exporter.setParameter(JRExporterParameter.JASPER_PRINT, jasperPrint); |
| exporter.setParameter(JRExporterParameter.OUTPUT_STREAM, baos); |
| if (delimiter != null) { |
| exporter.setParameter(JRCsvExporterParameter.FIELD_DELIMITER, delimiter); |
| } |
| |
| exporter.exportReport(); |
| return baos; |
| } |
| |
| /** |
| * Checks if expression doesn't contain vulnerable code |
| * |
| * @param expression of result |
| * @return true|false |
| * @since 2.5.27 |
| */ |
| protected boolean isAcceptableExpression(String expression) { |
| NotExcludedAcceptedPatternsChecker.IsAllowed isAllowed = notExcludedAcceptedPatterns.isAllowed(expression); |
| if (isAllowed.isAllowed()) { |
| return true; |
| } |
| |
| LOG.warn("Expression [{}] isn't allowed by pattern [{}]! See Accepted / Excluded patterns at\n" + |
| "https://struts.apache.org/security/", expression, isAllowed.getAllowedPattern()); |
| |
| return false; |
| } |
| } |