org.springframework.web.servlet.view.jasperreports.AbstractJasperReportsView Maven / Gradle / Ivy
/*
* Copyright 2002-2004 the original author or authors.
*
* 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.
*/
package org.springframework.web.servlet.view.jasperreports;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRDataSourceProvider;
import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JRExporterParameter;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.design.JRBshCompiler;
import net.sf.jasperreports.engine.design.JRCompiler;
import net.sf.jasperreports.engine.design.JasperDesign;
import net.sf.jasperreports.engine.util.JRLoader;
import net.sf.jasperreports.engine.xml.JRXmlLoader;
import org.springframework.context.ApplicationContextException;
import org.springframework.core.io.Resource;
import org.springframework.ui.jasperreports.JasperReportsUtils;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.view.AbstractUrlBasedView;
/**
* Base class for all JasperReports views. Applies on-the-fly compilation
* of report designs as required and coordinates the rendering process.
* The resource path of the main report needs to be specified as url
.
*
* This class is responsible for getting report data from the model that has
* been provided to the view. The default implementation checks for a model object
* under the specified reportDataKey
first, then falls back to looking
* for a value of type JRDataSource
, java.util.Collection
,
* object array (in that order).
*
* Provides support for sub-reports through the subReportUrls
and
* subReportDataKeys
properties.
*
* When using sub-reports, the master report should be configured using the
* url
property and the sub-reports files should be configured using
* the subReportUrls
property. Each entry in the subReportUrls
* Map corresponds to an individual sub-report. The key of an entry must match up
* to a sub-report parameter in your report file of type
* net.sf.jasperreports.engine.JasperReport
,
* and the value of an entry must be the URL for the sub-report file.
*
* For sub-reports that require an instance of JRDataSource
, that is,
* they don't have a hard-coded query for data retrieval, you can include the
* appropriate data in your model as would with the data source for the parent report.
* However, you must provide a List of parameter names that need to be converted to
* JRDataSource
instances for the sub-report via the
* subReportDataKeys
property. When using JRDataSource
* instances for sub-reports, you must specify a value for the
* reportDataKey
property, indicating the data to use for the main report.
*
* Allows for exporter parameters to be configured declatively using the
* exporterParameters
property. This is a Map
typed
* property where the key of an entry corresponds to the fully-qualified name
* of the static field for the JRExporterParameter
and the value
* of an entry is the value you want to assign to the exporter parameter.
*
* Response headers can be controlled via the headers
property. Spring
* will attempt to set the correct value for the Content-Diposition
header
* so that reports render correctly in Internet Explorer. However, you can override this
* setting through the headers
property.
*
* @author Rob Harrop
* @author Juergen Hoeller
* @see #setUrl
* @see #getReportData
* @see #setSubReportDataKeys(String[])
* @see #setSubReportUrls(java.util.Properties)
* @see #setExporterParameters(java.util.Map)
* @see #setHeaders(java.util.Properties)
* @since 1.1.3
*/
public abstract class AbstractJasperReportsView extends AbstractUrlBasedView {
/**
* Constant that defines "Content-Disposition" header.
*/
protected static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
/**
* Stores the default Content-Disposition header. Used to make IE play nice.
*/
private static final String CONTENT_DISPOSITION_INLINE = "inline";
/**
* A String key used to lookup the JRDataSource
in the model.
*/
private String reportDataKey;
/**
* Stores the paths to any sub-report files used by this top-level report,
* along with the keys they are mapped to in the top-level report file.
*/
private Properties subReportUrls;
/**
* Stores the names of any data source objects that need to be converted to
* JRDataSource
instances and included in the report parameters
* to be passed on to a sub-report.
*/
private String[] subReportDataKeys;
/**
* The JasperReport
that is used to render the view.
*/
private JasperReport report;
/**
* Holds mappings between sub-report keys and JasperReport
objects.
*/
private Map subReports;
/**
* Stores the headers to written with each response
*/
private Properties headers;
/**
* Stores the String
keyed exporter parameters passed in by the user.
*/
private Map exporterParameters;
/**
* Set the name of the model attribute that represents the report data.
* If not specified, the model map will be searched for a matching value type.
*
A JRDataSource
will be taken as-is. For other types, conversion
* will apply: By default, a java.util.Collection
will be converted
* to JRBeanCollectionDataSource
, and an object array to
* JRBeanArrayDataSource
.
*
Note: If you pass in a Collection or object array in the model map
* for use as plain report parameter, rather than as report data to extract fields
* from, you need to specify the key for the actual report data to use, to avoid
* mis-detection of report data by type.
*
* @see #convertReportData
* @see net.sf.jasperreports.engine.JRDataSource
* @see net.sf.jasperreports.engine.data.JRBeanCollectionDataSource
* @see net.sf.jasperreports.engine.data.JRBeanArrayDataSource
*/
public void setReportDataKey(String reportDataKey) {
this.reportDataKey = reportDataKey;
}
/**
* Specify resource paths which must be loaded as instances of
* JasperReport
and passed to the JasperReports engine for
* rendering as sub-reports, under the same keys as in this mapping.
*
* @param subReports mapping between model keys and resource paths
* (Spring resource locations)
* @see #setUrl
* @see org.springframework.context.ApplicationContext#getResource
*/
public void setSubReportUrls(Properties subReports) {
this.subReportUrls = subReports;
}
/**
* Set the list of names corresponding to the model parameters that will contain
* data source objects for use in sub-reports. Spring will convert these objects
* to instances of JRDataSource
where applicable and will then
* include the resulting JRDataSource
in the parameters passed into
* the JasperReports engine.
*
The name specified in the list should correspond to an attribute in the
* model Map, and to a sub-report data source parameter in your report file.
* If you pass in JRDataSource
objects as model attributes,
* specifing this list of keys is not required.
*
If you specify a list of sub-report data keys, it is required to also
* specify a reportDataKey
for the main report, to avoid confusion
* between the data source objects for the various reports involved.
*
* @param subReportDataKeys list of names for sub-report data source objects
* @see #setReportDataKey
* @see #convertReportData
* @see net.sf.jasperreports.engine.JRDataSource
* @see net.sf.jasperreports.engine.data.JRBeanCollectionDataSource
* @see net.sf.jasperreports.engine.data.JRBeanArrayDataSource
*/
public void setSubReportDataKeys(String[] subReportDataKeys) {
this.subReportDataKeys = subReportDataKeys;
}
/**
* Specify the set of headers that are included in each of response.
*
* @param headers the headers to write to each response.
*/
public void setHeaders(Properties headers) {
this.headers = headers;
}
/**
* Set the exporter parameters that should be used when rendering a view.
*
* @param parameters Map
with the fully qualified field name of the
* JRExporterParameter
instance as key
* (e.g. "net.sf.jasperreports.engine.export.JRHtmlExporterParameter.IMAGES_URI")
* and the value you wish to assign to the parameter as value
*/
public void setExporterParameters(Map parameters) {
this.exporterParameters = new HashMap(parameters.size());
for (Iterator it = parameters.entrySet().iterator(); it.hasNext();) {
Map.Entry entry = (Map.Entry) it.next();
String fieldName = (String) entry.getKey();
JRExporterParameter parameter = convertToExporterParameter(fieldName);
this.exporterParameters.put(parameter, entry.getValue());
}
}
/**
* Return the exporter parameters configured by the user.
*
* @return a Map
containing the exporter parameters with instances
* of JRExporterParameter
as the key.
*/
protected Map getExporterParameters() {
return exporterParameters;
}
/**
* Checks to see that a valid report file URL is supplied in the
* configuration. Compiles the report file is necessary.
*/
protected void initApplicationContext() throws ApplicationContextException {
super.initApplicationContext();
Resource mainReport = getApplicationContext().getResource(getUrl());
this.report = loadReport(mainReport);
// Load sub reports if required, and check data source parameters.
if (this.subReportUrls != null) {
if (this.subReportDataKeys != null && this.subReportDataKeys.length > 0 &&
this.reportDataKey == null) {
throw new ApplicationContextException("'reportDataKey' for main report is required when specifying a value for 'subReportDataKeys'");
}
this.subReports = new HashMap(this.subReportUrls.size());
for (Enumeration urls = this.subReportUrls.propertyNames(); urls.hasMoreElements();) {
String key = (String) urls.nextElement();
String path = this.subReportUrls.getProperty(key);
Resource resource = getApplicationContext().getResource(path);
this.subReports.put(key, loadReport(resource));
}
}
if (this.headers == null) {
this.headers = new Properties();
}
if (!this.headers.containsKey(HEADER_CONTENT_DISPOSITION)) {
this.headers.setProperty(HEADER_CONTENT_DISPOSITION, CONTENT_DISPOSITION_INLINE);
}
}
/**
* Convert the given fully qualified field name to a corresponding
* JRExporterParameter instance.
*
* @param fqFieldName the fully qualified field name, consisting
* of the class name followed by a dot followed by the field name
* (e.g. "net.sf.jasperreports.engine.export.JRHtmlExporterParameter.IMAGES_URI")
* @return the corresponding JRExporterParameter instance
*/
protected JRExporterParameter convertToExporterParameter(String fqFieldName) {
int index = fqFieldName.lastIndexOf('.');
if (index == -1 || index == fqFieldName.length()) {
throw new IllegalArgumentException("Parameter name [" + fqFieldName + "] is not a valid static field. " +
"The parameter name must map to a static field such as " +
"net.sf.jasperreports.engine.export.JRHtmlExporterParameter.IMAGES_URI");
}
String className = fqFieldName.substring(0, index);
String fieldName = fqFieldName.substring(index + 1);
try {
Class cls = ClassUtils.forName(className);
Field field = cls.getField(fieldName);
if (JRExporterParameter.class.isAssignableFrom(field.getType())) {
try {
return (JRExporterParameter) field.get(null);
}
catch (IllegalAccessException ex) {
throw new IllegalArgumentException("Unable to access field [" + fieldName + "] of class [" + className + "]." +
" Check that it is static and accessible.");
}
}
else {
throw new IllegalArgumentException("Field [" + fieldName + "] on class [" + className +
"] is not assignable from JRExporterParameter - check the type of this field.");
}
}
catch (ClassNotFoundException ex) {
throw new IllegalArgumentException("Class [" + className + "] in key [" + fqFieldName + "] could not be found.");
}
catch (NoSuchFieldException ex) {
throw new IllegalArgumentException("Field [" + fieldName + "] in key [" + fqFieldName +
"] could not be found on class [" + className + "].");
}
}
/**
* Loads a JasperReport
from the specified Resource
. If
* the Resource
points to an uncompiled report design file then the
* report file is compiled dynamically and loaded into memory.
*
* @param resource the Resource
containing the report definition or design
* @return a JasperReport
instance
*/
private JasperReport loadReport(Resource resource) throws ApplicationContextException {
try {
String fileName = resource.getFilename();
if (fileName.endsWith(".jasper")) {
// load pre-compiled report
if (logger.isInfoEnabled()) {
logger.info("Loading pre-compiled Jasper Report from " + resource);
}
return (JasperReport) JRLoader.loadObject(resource.getInputStream());
}
else if (fileName.endsWith(".jrxml")) {
// compile report on-the-fly
if (logger.isInfoEnabled()) {
logger.info("Compiling Jasper Report loaded from " + resource);
}
JasperDesign design = JRXmlLoader.load(resource.getInputStream());
return getReportCompiler().compileReport(design);
}
else {
throw new IllegalArgumentException("Report URL [" + getUrl() + "] must end in either .jasper or .jrxml");
}
}
catch (IOException ex) {
throw new ApplicationContextException("Could not load JasperReports report for URL [" + getUrl() + "]", ex);
}
catch (JRException ex) {
throw new ApplicationContextException("Could not parse JasperReports report for URL [" + getUrl() + "]", ex);
}
}
/**
* Return the JasperReports compiler to use for compiling a ".jrxml"
* file into a a report class. Default is JRBshCompiler
,
* which requires BeanShell on the class path.
*
* @see net.sf.jasperreports.engine.design.JRCompiler
* @see net.sf.jasperreports.engine.design.JRBshCompiler
*/
protected JRCompiler getReportCompiler() {
return new JRBshCompiler();
}
/**
* Finds the report data to use for rendering the report and then invokes the
* renderReport
method that should be implemented by the subclass.
*
* @param model the model map, as passed in for view rendering. Must contain
* a report data value that can be converted to a JRDataSource
,
* acccording to the getReportData
method.
* @see #getReportData
*/
protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.setContentType(getContentType());
// Determine JRDataSource for main report.
JRDataSource dataSource = getReportData(model);
if (this.subReports != null) {
// Expose sub-reports as model attributes.
model.putAll(this.subReports);
// Transform any collections etc into JRDataSources for sub reports.
if (this.subReportDataKeys != null) {
for (int i = 0; i < this.subReportDataKeys.length; i++) {
String key = this.subReportDataKeys[i];
model.put(key, convertReportData(model.get(key)));
}
}
}
populateHeaders(response);
renderReport(this.report, model, dataSource, response);
}
private void populateHeaders(HttpServletResponse response) {
// Apply the headers to the response.
for (Enumeration en = this.headers.propertyNames(); en.hasMoreElements();) {
String key = (String) en.nextElement();
response.addHeader(key, this.headers.getProperty(key));
}
}
/**
* Find an instance of JRDataSource
in the given model map or create an
* appropriate JRDataSource for passed-in report data.
*
The default implementation checks for a model object under the
* specified "reportDataKey" first, then falls back to looking for a value
* of type JRDataSource
, java.util.Collection
,
* object array (in that order).
*
* @param model the model map, as passed in for view rendering
* @return the JRDataSource
* @throws IllegalArgumentException if no JRDataSource found
* @see #setReportDataKey
* @see #convertReportData
* @see #getReportDataTypes
*/
protected JRDataSource getReportData(Map model) throws IllegalArgumentException {
// Try model attribute with specified name.
if (this.reportDataKey != null) {
Object value = model.get(this.reportDataKey);
return convertReportData(value);
}
// Try to find matching attribute, of given prioritized types.
Object value = CollectionUtils.findValueOfType(model.values(), getReportDataTypes());
if (value != null) {
return convertReportData(value);
}
throw new IllegalArgumentException("No report data supplied in model " + model);
}
/**
* Convert the given report data value to a JRDataSource
.
*
The default implementation delegates to JasperReportUtils
unless
* the report data value is an instance of JRDataSourceProvider
.
* A JRDataSource
, JRDataSourceProvider
,
* java.util.Collection
or object array is detected.
* JRDataSource
s are returned as is, whilst JRDataSourceProvider
s
* are used to create an instance of JRDataSource
which is then returned.
* The latter two are converted to JRBeanCollectionDataSource
or
* JRBeanArrayDataSource
, respectively.
*
* @param value the report data value to convert
* @return the JRDataSource
* @throws IllegalArgumentException if the value could not be converted
* @see org.springframework.ui.jasperreports.JasperReportsUtils#convertReportData
* @see net.sf.jasperreports.engine.JRDataSource
* @see net.sf.jasperreports.engine.JRDataSourceProvider
* @see net.sf.jasperreports.engine.data.JRBeanCollectionDataSource
* @see net.sf.jasperreports.engine.data.JRBeanArrayDataSource
*/
protected JRDataSource convertReportData(Object value) throws IllegalArgumentException {
if (value instanceof JRDataSourceProvider) {
try {
return ((JRDataSourceProvider) value).create(report);
}
catch (JRException ex) {
throw new IllegalArgumentException("Supplied JRDataSourceProvider is invalid: " + ex);
}
}
else {
return JasperReportsUtils.convertReportData(value);
}
}
/**
* Return the value types that can be converted to a JRDataSource
,
* in prioritized order. Should only return types that the
* convertReportData
method is actually able to convert.
*
Default value types are: JRDataSource
,
* JRDataSourceProvider
java.util.Collection
* and Object
array.
*
* @return the value types in prioritized order
* @see #convertReportData
*/
protected Class[] getReportDataTypes() {
return new Class[]{JRDataSource.class, JRDataSourceProvider.class, Collection.class, Object[].class};
}
/**
* Allows sub-classes to get access to the JasperReport
instance
* loaded by Spring.
*
* @return an instance of JasperReport
.
*/
protected JasperReport getReport() {
return this.report;
}
/**
* Subclasses should implement this method to perform the actual rendering process.
*
* @param report the JasperReport
to render
* @param parameters the map containing report parameters
* @param dataSource the JRDataSource
containing the report data
* @param response the HTTP response the report should be rendered to
* @throws Exception if rendering failed
*/
protected abstract void renderReport(JasperReport report, Map parameters, JRDataSource dataSource, HttpServletResponse response)
throws Exception;
}