diff --git a/plugins/jasperreports7/README.md b/plugins/jasperreports7/README.md new file mode 100644 index 0000000000..0dc43af31b --- /dev/null +++ b/plugins/jasperreports7/README.md @@ -0,0 +1,6 @@ +# Jasper Reports plugin +This plugin allows to use Jasper reports as a one of the result types. +You will find more details in [documentation](https://struts.apache.org/plugins/jasperreports/). + +## Installation +Just drop this plugin JAR into `WEB-INF/lib` folder or add it as a Maven dependency. diff --git a/plugins/jasperreports7/pom.xml b/plugins/jasperreports7/pom.xml new file mode 100644 index 0000000000..40c57f820c --- /dev/null +++ b/plugins/jasperreports7/pom.xml @@ -0,0 +1,82 @@ + + + + 4.0.0 + + org.apache.struts + struts2-plugins + 7.0.0-M11-SNAPSHOT + + + struts2-jasperreports7-plugin + jar + Struts 2 Jasper Reports 7 Plugin [EXPERIMENTAL] + + + UTF-8 + 7.0.1 + + + + + net.sf.jasperreports + jasperreports + ${jasperreports7.version} + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + + + net.sf.jasperreports + jasperreports-pdf + ${jasperreports7.version} + true + + + com.fasterxml.jackson.core + jackson-databind + + + org.apache.struts + struts2-junit-plugin + test + + + org.springframework + spring-web + test + + + org.easymock + easymock + test + + + diff --git a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/JasperReportConstants.java b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/JasperReportConstants.java new file mode 100644 index 0000000000..14a9d10217 --- /dev/null +++ b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/JasperReportConstants.java @@ -0,0 +1,56 @@ +/* + * 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.jasperreports7; + + +/** + * JasperReportConstants + */ +public interface JasperReportConstants { + + /** + * PDF format constant + */ + String FORMAT_PDF = "PDF"; + + /** + * XML format constant + */ + String FORMAT_XML = "XML"; + + /** + * HTML format constant + */ + String FORMAT_HTML = "HTML"; + + /** + * XLS format constant + */ + String FORMAT_XLSX = "XLSX"; + + /** + * CSV format constant + */ + String FORMAT_CSV = "CSV"; + + /** + * RTF format constant + */ + String FORMAT_RTF = "RTF"; +} diff --git a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/JasperReports7Result.java b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/JasperReports7Result.java new file mode 100644 index 0000000000..24739a5635 --- /dev/null +++ b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/JasperReports7Result.java @@ -0,0 +1,525 @@ +/* + * 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.jasperreports7; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import net.sf.jasperreports.engine.JRException; +import net.sf.jasperreports.engine.JRParameter; +import net.sf.jasperreports.engine.JasperFillManager; +import net.sf.jasperreports.engine.JasperPrint; +import net.sf.jasperreports.engine.JasperReport; +import net.sf.jasperreports.engine.export.HtmlExporter; +import net.sf.jasperreports.engine.export.HtmlResourceHandler; +import net.sf.jasperreports.engine.export.JRCsvExporter; +import net.sf.jasperreports.engine.export.JRRtfExporter; +import net.sf.jasperreports.engine.export.JRXmlExporter; +import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter; +import net.sf.jasperreports.engine.util.JRLoader; +import net.sf.jasperreports.export.Exporter; +import net.sf.jasperreports.export.OutputStreamExporterOutput; +import net.sf.jasperreports.export.SimpleCsvExporterConfiguration; +import net.sf.jasperreports.export.SimpleExporterInput; +import net.sf.jasperreports.export.SimpleHtmlExporterOutput; +import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput; +import net.sf.jasperreports.export.SimpleWriterExporterOutput; +import net.sf.jasperreports.export.SimpleXmlExporterOutput; +import net.sf.jasperreports.export.WriterExporterOutput; +import net.sf.jasperreports.export.XmlExporterOutput; +import net.sf.jasperreports.pdf.JRPdfExporter; +import net.sf.jasperreports.web.util.WebHtmlResourceHandler; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.inject.Inject; +import org.apache.struts2.result.StrutsResultSupport; +import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker; +import org.apache.struts2.util.ValueStack; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.sql.Connection; +import java.util.Map; +import java.util.TimeZone; + +/** + * + *

+ * Generates a JasperReports report using the specified format or PDF if no + * format is specified. + *

+ * + *

+ * This result type takes the following parameters: + *

+ * + * + * + *

+ * This result follows the same rules from {@link StrutsResultSupport}. + * Specifically, all parameters will be parsed if the "parse" parameter + * is not set to false. + *

+ * + *

Example:

+ *
+ * 
+ * <result name="success" type="jasper">
+ *   <param name="location">foo.jasper</param>
+ *   <param name="dataSource">mySource</param>
+ *   <param name="format">CSV</param>
+ * </result>
+ * 
+ * 
+ *

+ * or for pdf + * + *

+ * 
+ * <result name="success" type="jasper">
+ *   <param name="location">foo.jasper</param>
+ *   <param name="dataSource">mySource</param>
+ * </result>
+ * 
+ * 
+ */ +public class JasperReports7Result extends StrutsResultSupport implements JasperReportConstants { + + private static final Logger LOG = LogManager.getLogger(JasperReports7Result.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 can be passed to the report instead of dataSource. + */ + protected String connection; + + /** + * Names a report parameters map stack value, allowing additional report parameters from the action. + */ + protected String reportParameters; + private String parsedReportParameters; + + /** + * Parameters validator, excludes not accepted params + */ + private NotExcludedAcceptedPatternsChecker notExcludedAcceptedPatterns; + + public JasperReports7Result() { + super(); + } + + @Inject + public void setNotExcludedAcceptedPatterns(NotExcludedAcceptedPatternsChecker notExcludedAcceptedPatterns) { + this.notExcludedAcceptedPatterns = notExcludedAcceptedPatterns; + } + + 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 = invocation.getInvocationContext().getServletRequest(); + HttpServletResponse response = invocation.getInvocationContext().getServletResponse(); + + // 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 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"); + } + + ServletContext servletContext = invocation.getInvocationContext().getServletContext(); + 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); + } + + 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); + } + + LOG.debug("Export the print object to the desired output format: {}", format); + try { + if (contentDisposition != null || documentName != null) { + final StringBuilder tmp = new StringBuilder(); + 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()); + } + + Exporter exporter = switch (format) { + case FORMAT_PDF -> createPdfExporter(response, jasperPrint); + case FORMAT_CSV -> createCsvExporter(response, jasperPrint); + case FORMAT_HTML -> createHtmlExporter(request, response, jasperPrint); + case FORMAT_XLSX -> createXlsExporter(response, jasperPrint); + case FORMAT_XML -> createXmlExporter(response, jasperPrint); + case FORMAT_RTF -> createRtfExporter(response, jasperPrint); + default -> throw new ServletException("Unknown report format: " + format); + }; + + LOG.debug("Exporting report: {} as: {} and flushing response stream", jasperPrint.getName(), format); + exporter.exportReport(); + + response.getOutputStream().flush(); + } 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); + } + } + } + + protected JRPdfExporter createPdfExporter(HttpServletResponse response, JasperPrint jasperPrint) throws ServletException { + response.setContentType("application/pdf"); + + JRPdfExporter exporter = new JRPdfExporter(); + + SimpleExporterInput input = new SimpleExporterInput(jasperPrint); + exporter.setExporterInput(input); + + try (OutputStream responseStream = response.getOutputStream()) { + OutputStreamExporterOutput exporterOutput = new SimpleOutputStreamExporterOutput(responseStream); + exporter.setExporterOutput(exporterOutput); + } catch (IOException e) { + LOG.error("Error writing report output", e); + throw new ServletException(e.getMessage(), e); + } + + return exporter; + } + + protected JRCsvExporter createCsvExporter(HttpServletResponse response, JasperPrint jasperPrint) throws ServletException { + response.setContentType("text/csv"); + JRCsvExporter exporter = new JRCsvExporter(); + + SimpleCsvExporterConfiguration config = new SimpleCsvExporterConfiguration(); + config.setFieldDelimiter(delimiter); + config.setRecordDelimiter(delimiter); + exporter.setConfiguration(config); + + SimpleExporterInput input = new SimpleExporterInput(jasperPrint); + exporter.setExporterInput(input); + + try (OutputStream responseStream = response.getOutputStream()) { + WriterExporterOutput exporterOutput = new SimpleWriterExporterOutput(responseStream); + exporter.setExporterOutput(exporterOutput); + } catch (IOException e) { + LOG.error("Error writing report output", e); + throw new ServletException(e.getMessage(), e); + } + + return exporter; + } + + protected HtmlExporter createHtmlExporter(HttpServletRequest request, HttpServletResponse response, JasperPrint jasperPrint) throws ServletException { + response.setContentType("text/html"); + HtmlExporter exporter = new HtmlExporter(); + + SimpleExporterInput input = new SimpleExporterInput(jasperPrint); + exporter.setExporterInput(input); + + try (OutputStream responseStream = response.getOutputStream()) { + SimpleHtmlExporterOutput exporterOutput = new SimpleHtmlExporterOutput(responseStream); + HtmlResourceHandler imageHandler = new WebHtmlResourceHandler(request.getContextPath() + imageServletUrl + "%s"); + exporterOutput.setImageHandler(imageHandler); + exporter.setExporterOutput(exporterOutput); + } catch (IOException e) { + LOG.error("Error writing report output", e); + throw new ServletException(e.getMessage(), e); + } + + return exporter; + } + + protected JRXlsxExporter createXlsExporter(HttpServletResponse response, JasperPrint jasperPrint) throws ServletException { + response.setContentType("application/vnd.ms-excel"); + + JRXlsxExporter exporter = new JRXlsxExporter(); + + SimpleExporterInput input = new SimpleExporterInput(jasperPrint); + exporter.setExporterInput(input); + + try (OutputStream responseStream = response.getOutputStream()) { + OutputStreamExporterOutput exporterOutput = new SimpleOutputStreamExporterOutput(responseStream); + exporter.setExporterOutput(exporterOutput); + } catch (IOException e) { + LOG.error("Error writing report output", e); + throw new ServletException(e.getMessage(), e); + } + + return exporter; + } + + protected JRXmlExporter createXmlExporter(HttpServletResponse response, JasperPrint jasperPrint) throws ServletException { + response.setContentType("text/xml"); + + JRXmlExporter exporter = new JRXmlExporter(); + + SimpleExporterInput input = new SimpleExporterInput(jasperPrint); + exporter.setExporterInput(input); + + try (OutputStream responseOutput = response.getOutputStream()) { + XmlExporterOutput exporterOutput = new SimpleXmlExporterOutput(responseOutput); + exporter.setExporterOutput(exporterOutput); + } catch (IOException e) { + LOG.error("Error writing report output using: {}", JRXmlExporter.class.getName(), e); + throw new ServletException(e.getMessage(), e); + } + + return exporter; + } + + protected JRRtfExporter createRtfExporter(HttpServletResponse response, JasperPrint jasperPrint) throws ServletException { + response.setContentType("application/rtf"); + + JRRtfExporter exporter = new JRRtfExporter(); + + SimpleExporterInput input = new SimpleExporterInput(jasperPrint); + exporter.setExporterInput(input); + + try (OutputStream responseStream = response.getOutputStream()) { + WriterExporterOutput exporterOutput = new SimpleWriterExporterOutput(responseStream); + exporter.setExporterOutput(exporterOutput); + } catch (IOException e) { + LOG.error("Error writing report output", e); + throw new ServletException(e.getMessage(), e); + } + + return exporter; + } + + /** + * Sets up result properties, parsing etc. + * + * @param invocation Current invocation. + */ + private void initializeProperties(ActionInvocation invocation) { + 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); + } + + /** + * Checks if expression doesn't contain vulnerable code + * + * @param expression of result + * @return true|false + * @since 6.0.0 + */ + 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; + } + + + /** + * SETTERS + **/ + + 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; + } + + public void setTimeZone(final String timeZone) { + this.timeZone = timeZone; + } + + public void setWrapField(boolean wrapField) { + this.wrapField = wrapField; + } + + public void setReportParameters(String reportParameters) { + this.reportParameters = reportParameters; + } + + public void setConnection(String connection) { + this.connection = connection; + } + +} diff --git a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/ValueStackDataSource.java b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/ValueStackDataSource.java new file mode 100644 index 0000000000..577e03b7b6 --- /dev/null +++ b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/ValueStackDataSource.java @@ -0,0 +1,145 @@ +/* + * 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.jasperreports7; + +import net.sf.jasperreports.engine.JRException; +import net.sf.jasperreports.engine.JRField; +import net.sf.jasperreports.engine.JRRewindableDataSource; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.util.MakeIterator; +import org.apache.struts2.util.ValueStack; + +import java.util.Iterator; + +/** + * Ported to Struts. + */ +public class ValueStackDataSource implements JRRewindableDataSource { + + private static final Logger LOG = LogManager.getLogger(ValueStackDataSource.class); + + private final ValueStack valueStack; + private final String dataSource; + private final boolean wrapField; + + private Iterator iterator; + + private boolean firstTimeThrough = true; + + /** + * Create a value stack data source on the given iterable property + * + * @param valueStack The value stack to base the data source on + * @param dataSourceParam The property to iterate over for the report + */ + public ValueStackDataSource(ValueStack valueStack, String dataSourceParam, boolean wrapField) { + this.valueStack = valueStack; + this.dataSource = dataSourceParam; + this.wrapField = wrapField; + + Object dataSourceValue = valueStack.findValue(dataSource); + + if (dataSourceValue != null) { + if (MakeIterator.isIterable(dataSourceValue)) { + iterator = MakeIterator.convert(dataSourceValue); + } else { + Object[] array = new Object[1]; + array[0] = dataSourceValue; + iterator = MakeIterator.convert(array); + } + } else { + LOG.warn("Data source value for data source: {} was null", dataSource); + } + } + + + /** + * Get the value of a given field + * + * @param field The field to get the value for. The expression language to get the value + * of the field is either taken from the description property or from the name of the field + * if the description is null. + * @return an Object containing the field value or a new + * ValueStackDataSource object if the field value evaluates to + * an object that can be iterated over. + */ + public Object getFieldValue(JRField field) { + String expression = field.getName(); + + Object value = valueStack.findValue(expression); + LOG.debug("Field [{}] = [{}]", field.getName(), value); + + if (!wrapField && MakeIterator.isIterable(value) && field.getValueClass().isInstance(value)) { + return value; + } else if (MakeIterator.isIterable(value)) { + // wrap value with ValueStackDataSource if not already wrapped + return new ValueStackDataSource(this.valueStack, expression, wrapField); + } else { + return value; + } + } + + /** + * Move to the first item. + */ + public void moveFirst() { + Object dataSourceValue = valueStack.findValue(dataSource); + if (dataSourceValue != null) { + if (MakeIterator.isIterable(dataSourceValue)) { + iterator = MakeIterator.convert(dataSourceValue); + } else { + Object[] array = new Object[1]; + array[0] = dataSourceValue; + iterator = MakeIterator.convert(array); + } + } else { + LOG.warn("Data source value for data source [{}] was null", dataSource); + } + } + + /** + * Is there any more data + * + * @return true if there are more elements to iterate over and + * false otherwise + * @throws JRException if there is a problem determining whether there + * is more data + */ + public boolean next() throws JRException { + if (firstTimeThrough) { + firstTimeThrough = false; + } else { + valueStack.pop(); + } + + if ((iterator != null) && (iterator.hasNext())) { + valueStack.push(iterator.next()); + if (LOG.isDebugEnabled()) { + LOG.debug("Pushed next value: {}", valueStack.findValue(".")); + } + + return true; + } else { + LOG.debug("No more values"); + + return false; + } + } +} diff --git a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/ValueStackShadowMap.java b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/ValueStackShadowMap.java new file mode 100644 index 0000000000..32b8440a58 --- /dev/null +++ b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/ValueStackShadowMap.java @@ -0,0 +1,79 @@ +/* + * 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.jasperreports7; + +import org.apache.struts2.util.ValueStack; + +import java.util.HashMap; + + +/** + * Ported to Struts: + */ +public class ValueStackShadowMap extends HashMap { + + /** + * valueStack reference + */ + transient ValueStack valueStack; + + /** + * Constructs an instance of ValueStackShadowMap. + * + * @param valueStack - the underlying valuestack + */ + public ValueStackShadowMap(ValueStack valueStack) { + this.valueStack = valueStack; + } + + + /** + * Implementation of containsKey(), overriding HashMap implementation. + * + * @param key - The key to check in HashMap and if not found to check on valueStack. + * @return true, if contains key, false otherwise. + * @see java.util.HashMap#containsKey + */ + public boolean containsKey(String key) { + boolean hasKey = super.containsKey(key); + + if (!hasKey && valueStack.findValue(key) != null) { + hasKey = true; + } + + return hasKey; + } + + /** + * Implementation of get(), overriding HashMap implementation. + * + * @param key - The key to get in HashMap and if not found there from the valueStack. + * @return value - The object from HashMap or if null, from the valueStack. + * @see java.util.HashMap#get + */ + public Object get(String key) { + Object value = super.get(key); + + if ((value == null)) { + value = valueStack.findValue(key); + } + + return value; + } +} diff --git a/plugins/jasperreports7/src/main/resources/LICENSE.txt b/plugins/jasperreports7/src/main/resources/LICENSE.txt new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/plugins/jasperreports7/src/main/resources/LICENSE.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/plugins/jasperreports7/src/main/resources/NOTICE.txt b/plugins/jasperreports7/src/main/resources/NOTICE.txt new file mode 100644 index 0000000000..1b4ebccdd0 --- /dev/null +++ b/plugins/jasperreports7/src/main/resources/NOTICE.txt @@ -0,0 +1,5 @@ +Apache Struts +Copyright 2000-2024 The Apache Software Foundation + +This product includes software developed by +The Apache Software Foundation (http://www.apache.org/). \ No newline at end of file diff --git a/plugins/jasperreports7/src/main/resources/org/apache/struts2/views/jasperreports7/package.html b/plugins/jasperreports7/src/main/resources/org/apache/struts2/views/jasperreports7/package.html new file mode 100644 index 0000000000..8ea790db98 --- /dev/null +++ b/plugins/jasperreports7/src/main/resources/org/apache/struts2/views/jasperreports7/package.html @@ -0,0 +1,21 @@ + +Classes for views using Jasper Reports. diff --git a/plugins/jasperreports7/src/main/resources/struts-plugin.xml b/plugins/jasperreports7/src/main/resources/struts-plugin.xml new file mode 100644 index 0000000000..3c218d5eca --- /dev/null +++ b/plugins/jasperreports7/src/main/resources/struts-plugin.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/plugins/jasperreports7/src/site/site.xml b/plugins/jasperreports7/src/site/site.xml new file mode 100644 index 0000000000..54fdcf4f47 --- /dev/null +++ b/plugins/jasperreports7/src/site/site.xml @@ -0,0 +1,56 @@ + + + + + org.apache.maven.skins + maven-fluido-skin + ${fluido-skin.version} + + + Apache Software Foundation + http://www.apache.org/images/asf-logo.gif + http://www.apache.org/ + + + Apache Struts + http://struts.apache.org/img/struts-logo.svg + http://struts.apache.org/ + + + + + + + + + + + + +
+ + Apache Struts, Struts, Apache, the Apache feather logo, and the Apache Struts project + logos are trademarks of The Apache Software Foundation. + ]]> +
+ + diff --git a/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReports7ResultTest.java b/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReports7ResultTest.java new file mode 100644 index 0000000000..f11446e37c --- /dev/null +++ b/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReports7ResultTest.java @@ -0,0 +1,369 @@ +/* + * 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.jasperreports7; + +import jakarta.servlet.ServletException; +import net.sf.jasperreports.engine.JasperCompileManager; +import org.apache.struts2.ActionContext; +import org.apache.struts2.junit.StrutsTestCase; +import org.apache.struts2.mock.MockActionInvocation; +import org.apache.struts2.security.NotExcludedAcceptedPatternsChecker; +import org.apache.struts2.util.ClassLoaderUtil; +import org.apache.struts2.util.ValueStack; + +import java.net.URL; +import java.sql.Connection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; + +public class JasperReports7ResultTest extends StrutsTestCase { + + private MockActionInvocation invocation; + private ValueStack stack; + private JasperReports7Result result; + + public void testConnClose() throws Exception { + // given + Connection connection = createMock(Connection.class); + final Boolean[] closed = {Boolean.FALSE}; + connection.close(); + expectLastCall().andAnswer(() -> { + closed[0] = true; + return null; + }); + replay(connection); + + stack.push(connection); + result.setConnection("top"); + assertFalse(closed[0]); + + // when + result.execute(this.invocation); + + // then + verify(connection); + assertTrue(closed[0]); + } + + public void testDataSourceNotAccepted() throws Exception { + // given + stack.push(new Object() { + public String getDatasourceName() { + return "getDatasource()"; + } + + public List> getDatasource() { + return JR_MAP_ARRAY_DATA_SOURCE; + } + }); + result.setDataSource("${datasourceName}"); + + try { + result.execute(this.invocation); + } catch (ServletException e) { + assertEquals("Error building dataSource for excluded or not accepted [getDatasource()]", + e.getMessage()); + } + + // verify that above test has really effect + result.setNotExcludedAcceptedPatterns(NO_EXCLUSION_ACCEPT_ALL_PATTERNS_CHECKER); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("text/xml"); + assertThat(response.getContentAsString()).contains("Hello Foo Bar!"); + } + + public void testDataSourceAccepted() throws Exception { + // given + stack.push(new Object() { + public String getDatasourceName() { + return "datasource"; + } + + public List> getDatasource() { + return JR_MAP_ARRAY_DATA_SOURCE; + } + }); + result.setDataSource("${datasourceName}"); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("text/xml"); + assertTrue(response.getContentAsString().contains("Hello Foo Bar!")); + } + + public void testDataSourceExpressionAccepted() throws Exception { + // given + result.setDataSource("{#{'firstName':'Qux', 'lastName':'Quux'}}"); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("text/xml"); + assertThat(response.getContentAsString()).contains("Hello Qux Quux!"); + } + + public void testReportParametersNotAccepted() throws Exception { + // given + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + + stack.push(new Object() { + public String getReportParametersName() { + return "getReportParameters()"; + } + + public Map getReportParameters() { + return new HashMap<>() {{ + put("title", "Baz"); + }}; + } + }); + + result.setReportParameters("${reportParametersName}"); + + // when + result.execute(this.invocation); + assertTrue(response.getContentAsString().contains("null Report")); + + // verify that above test has really effect + response.setCommitted(false); + response.reset(); + result.setNotExcludedAcceptedPatterns(NO_EXCLUSION_ACCEPT_ALL_PATTERNS_CHECKER); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("text/xml"); + assertTrue(response.getContentAsString().contains("Baz Report")); + } + + public void testReportParametersAccepted() throws Exception { + // given + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + + stack.push(new Object() { + public String getReportParametersName() { + return "reportParameters"; + } + + public Map getReportParameters() { + return new HashMap<>() {{ + put("title", "Baz"); + }}; + } + }); + + result.setReportParameters("${reportParametersName}"); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("text/xml"); + assertThat(response.getContentAsString()).contains("Baz Report"); + } + + public void testExportToXml() throws Exception { + // given + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + result.setReportParameters("#{'title':'Qux'}"); + result.setFormat(JasperReportConstants.FORMAT_XML); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("text/xml"); + assertThat(response.getContentAsString()).contains("Qux Report"); + } + + public void testExportToCsv() throws Exception { + // given + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + result.setReportParameters("#{'title':'Qux'}"); + result.setFormat(JasperReportConstants.FORMAT_CSV); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("text/csv"); + assertThat(response.getContentAsString()).contains("Qux Report"); + } + + public void testExportToRtf() throws Exception { + // given + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + result.setReportParameters("#{'title':'Qux'}"); + result.setFormat(JasperReportConstants.FORMAT_RTF); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("application/rtf"); + assertThat(response.getContentAsString()).contains("Qux Report"); + } + + public void testExportToPdf() throws Exception { + // given + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + result.setReportParameters("#{'title':'Qux'}"); + result.setFormat(JasperReportConstants.FORMAT_PDF); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("application/pdf"); + assertThat(response.getContentAsByteArray()).hasSizeGreaterThan(0); + } + + public void testExportToHtml() throws Exception { + // given + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + result.setReportParameters("#{'title':'Qux'}"); + result.setFormat(JasperReportConstants.FORMAT_HTML); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("text/html"); + assertThat(response.getContentAsString()).contains("Qux Report"); + } + + public void testExportToXlsx() throws Exception { + // given + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + result.setReportParameters("#{'title':'Qux'}"); + result.setFormat(JasperReportConstants.FORMAT_XLSX); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentType()).isEqualTo("application/vnd.ms-excel"); + assertThat(response.getContentAsByteArray()).hasSizeGreaterThan(0); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + this.request.setRequestURI("http://someuri"); + ActionContext context = ActionContext.getContext() + .withServletResponse(this.response) + .withServletRequest(this.request) + .withServletContext(this.servletContext); + this.stack = context.getValueStack(); + + this.invocation = new MockActionInvocation(); + this.invocation.setInvocationContext(context); + this.invocation.setStack(this.stack); + + result = new JasperReports7Result(); + container.inject(result); + URL url = ClassLoaderUtil.getResource("org/apache/struts2/views/jasperreports7/simple.jrxml", this.getClass()); + JasperCompileManager.compileReportToFile(url.getFile(), url.getFile() + ".jasper"); + result.setLocation("org/apache/struts2/views/jasperreports7/simple.jrxml.jasper"); + result.setFormat(JasperReportConstants.FORMAT_XML); + } + + private static final List> JR_MAP_ARRAY_DATA_SOURCE = Stream.>of( + new HashMap<>() {{ + put("firstName", "Foo"); + put("lastName", "Bar"); + }} + ).toList(); + + private static final NotExcludedAcceptedPatternsChecker NO_EXCLUSION_ACCEPT_ALL_PATTERNS_CHECKER + = new NotExcludedAcceptedPatternsChecker() { + @Override + public IsAllowed isAllowed(String value) { + return IsAllowed.yes("*"); + } + + @Override + public IsAccepted isAccepted(String value) { + return null; + } + + @Override + public void setAcceptedPatterns(String commaDelimitedPatterns) { + + } + + @Override + public void setAcceptedPatterns(String[] patterns) { + + } + + @Override + public void setAcceptedPatterns(Set patterns) { + + } + + @Override + public Set getAcceptedPatterns() { + return null; + } + + @Override + public IsExcluded isExcluded(String value) { + return null; + } + + @Override + public void setExcludedPatterns(String commaDelimitedPatterns) { + + } + + @Override + public void setExcludedPatterns(String[] patterns) { + + } + + @Override + public void setExcludedPatterns(Set patterns) { + + } + + @Override + public Set getExcludedPatterns() { + return null; + } + }; +} diff --git a/plugins/jasperreports7/src/test/resources/org/apache/struts2/views/jasperreports7/simple.jrxml b/plugins/jasperreports7/src/test/resources/org/apache/struts2/views/jasperreports7/simple.jrxml new file mode 100644 index 0000000000..efbb643ba0 --- /dev/null +++ b/plugins/jasperreports7/src/test/resources/org/apache/struts2/views/jasperreports7/simple.jrxml @@ -0,0 +1,42 @@ + + + + + + + + + <element kind="textField" x="0" y="10" width="515" height="30" fontSize="22.0" hTextAlign="Center" + uuid="26fc2f4f-de0f-411a-b386-e67caf96f441"> + <expression><![CDATA[$P{title}]]> + " Report"</expression> + </element> + + + + + "Hello " + + " " + + "!" + + + + \ No newline at end of file diff --git a/plugins/pom.xml b/plugins/pom.xml index f5447a2e9d..4df6066fd7 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -38,6 +38,7 @@ config-browser convention jasperreports + jasperreports7 javatemplates jfreechart json