This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch feature/WW-5455-jasperreports7
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 1b02decc721c2e243a2d9f60b173cbee7dd161a1
Author: Lukasz Lenart <lukaszlen...@apache.org>
AuthorDate: Sun Nov 10 12:11:46 2024 +0100

    WW-5455 Defines a new plugin to support Jasper Reports 7
---
 plugins/jasperreports7/README.md                   |   6 +
 plugins/{ => jasperreports7}/pom.xml               |  88 ++--
 .../views/jasperreports7/CompileReport.java        |  48 ++
 .../jasperreports7/JasperReportConstants.java      |  56 +++
 .../views/jasperreports7/JasperReports7Result.java | 525 +++++++++++++++++++++
 .../views/jasperreports7/ValueStackDataSource.java | 145 ++++++
 .../views/jasperreports7/ValueStackShadowMap.java  |  79 ++++
 .../jasperreports7/src/main/resources/LICENSE.txt  | 174 +++++++
 .../jasperreports7/src/main/resources/NOTICE.txt   |   5 +
 .../struts2/views/jasperreports7/package.html      |  21 +
 .../src/main/resources/struts-plugin.xml           |  34 ++
 plugins/jasperreports7/src/site/site.xml           |  56 +++
 .../jasperreports7/JasperReports7ResultTest.java   | 369 +++++++++++++++
 .../struts2/views/jasperreports7/simple.jrxml      |  42 ++
 plugins/pom.xml                                    |   1 +
 15 files changed, 1596 insertions(+), 53 deletions(-)

diff --git a/plugins/jasperreports7/README.md b/plugins/jasperreports7/README.md
new file mode 100644
index 000000000..0dc43af31
--- /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/pom.xml b/plugins/jasperreports7/pom.xml
similarity index 50%
copy from plugins/pom.xml
copy to plugins/jasperreports7/pom.xml
index f5447a2e9..40c57f820 100644
--- a/plugins/pom.xml
+++ b/plugins/jasperreports7/pom.xml
@@ -23,78 +23,60 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.apache.struts</groupId>
-        <artifactId>struts2-parent</artifactId>
+        <artifactId>struts2-plugins</artifactId>
         <version>7.0.0-M11-SNAPSHOT</version>
     </parent>
 
-    <artifactId>struts2-plugins</artifactId>
-    <packaging>pom</packaging>
-    <name>Struts 2 Plugins</name>
+    <artifactId>struts2-jasperreports7-plugin</artifactId>
+    <packaging>jar</packaging>
+    <name>Struts 2 Jasper Reports 7 Plugin [EXPERIMENTAL]</name>
 
-    <modules>
-        <module>async</module>
-        <module>bean-validation</module>
-        <module>cdi</module>
-        <module>config-browser</module>
-        <module>convention</module>
-        <module>jasperreports</module>
-        <module>javatemplates</module>
-        <module>jfreechart</module>
-        <module>json</module>
-        <module>junit</module>
-        <module>rest</module>
-        <module>spring</module>
-        <module>testng</module>
-        <module>tiles</module>
-        <module>velocity</module>
-        <module>xslt</module>
-    </modules>
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <jasperreports7.version>7.0.1</jasperreports7.version>
+    </properties>
 
     <dependencies>
-
         <dependency>
-            <groupId>org.apache.struts</groupId>
-            <artifactId>struts2-core</artifactId>
-            <scope>provided</scope>
+            <groupId>net.sf.jasperreports</groupId>
+            <artifactId>jasperreports</artifactId>
+            <version>${jasperreports7.version}</version>
+            <exclusions>
+                <!-- not necessary to compile and it force dependency 
convergence issues -->
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-databind</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-annotations</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
-
-        <!-- Test dependencies -->
         <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <scope>test</scope>
+            <groupId>net.sf.jasperreports</groupId>
+            <artifactId>jasperreports-pdf</artifactId>
+            <version>${jasperreports7.version}</version>
+            <optional>true</optional>
         </dependency>
         <dependency>
-            <groupId>org.assertj</groupId>
-            <artifactId>assertj-core</artifactId>
-            <scope>test</scope>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
+            <groupId>org.apache.struts</groupId>
+            <artifactId>struts2-junit-plugin</artifactId>
             <scope>test</scope>
         </dependency>
-
         <dependency>
-            <groupId>org.apache.logging.log4j</groupId>
-            <artifactId>log4j-core</artifactId>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
             <scope>test</scope>
         </dependency>
-
-        <dependency>
-            <groupId>jakarta.servlet.jsp</groupId>
-            <artifactId>jakarta.servlet.jsp-api</artifactId>
-            <scope>provided</scope>
-        </dependency>
         <dependency>
-            <groupId>jakarta.servlet</groupId>
-            <artifactId>jakarta.servlet-api</artifactId>
-            <scope>provided</scope>
+            <groupId>org.easymock</groupId>
+            <artifactId>easymock</artifactId>
+            <scope>test</scope>
         </dependency>
-
     </dependencies>
-
-    <properties>
-       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-    </properties>
 </project>
diff --git 
a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/CompileReport.java
 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/CompileReport.java
new file mode 100644
index 000000000..755abd0b2
--- /dev/null
+++ 
b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/CompileReport.java
@@ -0,0 +1,48 @@
+/*
+ * 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.JasperCompileManager;
+
+/**
+ * Ported to Struts:
+ *
+ */
+public class CompileReport {
+
+    public static void main(String[] args) {
+        if (args.length < 1) {
+            System.out.println("Please supply the name of the report(s) source 
to compile.");
+            System.exit(-1);
+        }
+
+        try {
+            for (int i = 0; i < args.length; i++) {
+                System.out.println("JasperReports Compiling: " + args[i]);
+                JasperCompileManager.compileReportToFile(args[i]);
+            }
+        } catch (JRException e) {
+            e.printStackTrace();
+            System.exit(-1);
+        }
+
+        System.exit(0);
+    }
+}
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 000000000..14a9d1021
--- /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;
+
+
+/**
+ * <code>JasperReportConstants</code>
+ */
+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 000000000..24739a563
--- /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;
+
+/**
+ * <!-- 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>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 <a href="https://issues.apache.org/jira/browse/WW-3698";>WW-3698</a> 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 -->
+ * &lt;result name="success" type="jasper"&gt;
+ *   &lt;param name="location"&gt;foo.jasper&lt;/param&gt;
+ *   &lt;param name="dataSource"&gt;mySource&lt;/param&gt;
+ *   &lt;param name="format"&gt;CSV&lt;/param&gt;
+ * &lt;/result&gt;
+ * <!-- END SNIPPET: example1 -->
+ * </pre>
+ * <p>
+ * or for pdf
+ *
+ * <pre>
+ * <!-- START SNIPPET: example2 -->
+ * &lt;result name="success" type="jasper"&gt;
+ *   &lt;param name="location"&gt;foo.jasper&lt;/param&gt;
+ *   &lt;param name="dataSource"&gt;mySource&lt;/param&gt;
+ * &lt;/result&gt;
+ * <!-- END SNIPPET: example2 -->
+ * </pre>
+ */
+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<String, Object> 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<String, Object> reportParams = reevaluate ? (Map<String, Object>) 
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 000000000..577e03b7b
--- /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 <code>null</code>.
+     * @return an <code>Object</code> containing the field value or a new
+     * <code>ValueStackDataSource</code> 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 <code>true</code> if there are more elements to iterate over and
+     * <code>false</code> 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 000000000..32b8440a5
--- /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<String, Object> {
+
+    /**
+     * 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 <tt>true</tt>, if contains key, <tt>false</tt> 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 000000000..dd5b3a58a
--- /dev/null
+++ b/plugins/jasperreports7/src/main/resources/LICENSE.txt
@@ -0,0 +1,174 @@
+                                 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.
diff --git a/plugins/jasperreports7/src/main/resources/NOTICE.txt 
b/plugins/jasperreports7/src/main/resources/NOTICE.txt
new file mode 100644
index 000000000..bfba90c29
--- /dev/null
+++ b/plugins/jasperreports7/src/main/resources/NOTICE.txt
@@ -0,0 +1,5 @@
+Apache Struts
+Copyright 2000-2011 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 000000000..8ea790db9
--- /dev/null
+++ 
b/plugins/jasperreports7/src/main/resources/org/apache/struts2/views/jasperreports7/package.html
@@ -0,0 +1,21 @@
+<!--
+/*
+ * 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.
+ */
+-->
+<body>Classes for views using Jasper Reports.</body>
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 000000000..3c218d5ec
--- /dev/null
+++ b/plugins/jasperreports7/src/main/resources/struts-plugin.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+/*
+ * 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.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+       "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
+       "https://struts.apache.org/dtds/struts-6.0.dtd";>
+
+<struts>
+    <package name="jasperreports-default" extends="struts-default">
+
+       <result-types>
+               <result-type name="jasperReport7" 
class="org.apache.struts2.views.jasperreports7.JasperReports7Result"/>
+       </result-types>
+    </package>
+
+</struts>
diff --git a/plugins/jasperreports7/src/site/site.xml 
b/plugins/jasperreports7/src/site/site.xml
new file mode 100644
index 000000000..54fdcf4f4
--- /dev/null
+++ b/plugins/jasperreports7/src/site/site.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<project name="Apache Struts">
+    <skin>
+        <groupId>org.apache.maven.skins</groupId>
+        <artifactId>maven-fluido-skin</artifactId>
+        <version>${fluido-skin.version}</version>
+    </skin>
+    <bannerLeft>
+        <name>Apache Software Foundation</name>
+        <src>http://www.apache.org/images/asf-logo.gif</src>
+        <href>http://www.apache.org/</href>
+    </bannerLeft>
+    <bannerRight>
+        <name>Apache Struts</name>
+        <src>http://struts.apache.org/img/struts-logo.svg</src>
+        <href>http://struts.apache.org/</href>
+    </bannerRight>
+    <publishDate position="left"/>
+    <version position="right"/>
+    <body>
+        <links>
+            <item name="Apache" href="http://www.apache.org/"/>
+            <item name="Struts" href="http://struts.apache.org/"/>
+        </links>
+
+        <menu ref="parent"/>
+        <menu ref="reports"/>
+
+        <footer>
+            <![CDATA[<div class="row span12">
+            Apache Struts, Struts, Apache, the Apache feather logo, and the 
Apache Struts project
+            logos are trademarks of The Apache Software Foundation.
+            </div>]]>
+        </footer>
+    </body>
+</project>
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 000000000..f11446e37
--- /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<Map<String, String>> 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<Map<String, String>> 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<String, String> 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<String, String> 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<Map<String, String>> JR_MAP_ARRAY_DATA_SOURCE = 
Stream.<Map<String, String>>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<String> patterns) {
+
+        }
+
+        @Override
+        public Set<Pattern> 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<String> patterns) {
+
+        }
+
+        @Override
+        public Set<Pattern> 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 000000000..efbb643ba
--- /dev/null
+++ 
b/plugins/jasperreports7/src/test/resources/org/apache/struts2/views/jasperreports7/simple.jrxml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+<jasperReport name="SimpleReport" language="java" pageWidth="842" 
pageHeight="595" orientation="Landscape" columnWidth="802"
+              uuid="3de5a1ab-15fd-4dec-bc88-dc18453bf715">
+    <parameter name="title"/>
+    <field name="firstName"/>
+    <field name="lastName"/>
+
+    <title height="40">
+        <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>
+    </title>
+    <detail>
+        <band height="16">
+            <element kind="textField" x="0" y="0" width="100" height="16"
+                     uuid="ef0654b5-f925-42ed-a611-bc5b3db61c43">
+                <expression>"Hello " + <![CDATA[$F{firstName}]]> + " " + 
<![CDATA[$F{lastName}]]> + "!"</expression>
+            </element>
+        </band>
+    </detail>
+</jasperReport>
\ No newline at end of file
diff --git a/plugins/pom.xml b/plugins/pom.xml
index f5447a2e9..4df6066fd 100644
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -38,6 +38,7 @@
         <module>config-browser</module>
         <module>convention</module>
         <module>jasperreports</module>
+        <module>jasperreports7</module>
         <module>javatemplates</module>
         <module>jfreechart</module>
         <module>json</module>

Reply via email to