All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.servlet.view.jasperreports.AbstractJasperReportsView Maven / Gradle / Ivy

There is a newer version: 5.3.34
Show newest version
/*
 * Copyright 2002-2006 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.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;

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.JRParameter;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.design.JRCompiler;
import net.sf.jasperreports.engine.design.JRDefaultCompiler;
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.context.support.MessageSourceResourceBundle;
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.support.RequestContextUtils;
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). * *

If no JRDataSource can be found in the model, then reports will * be filled using the configured javax.sql.DataSource if any. If neither * a JRDataSource or javax.sql.DataSource is available then * an IllegalArgumentException is raised. * *

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 * @since 1.1.3 * @see #setUrl * @see #setReportDataKey * @see #setSubReportUrls * @see #setSubReportDataKeys * @see #setHeaders * @see #setExporterParameters * @see #setJdbcDataSource */ public abstract class AbstractJasperReportsView extends AbstractUrlBasedView { /** * Constant that defines "Content-Disposition" header. */ protected static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition"; /** * The default Content-Disposition header. Used to make IE play nice. */ protected 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; /** * Stores the headers to written with each response */ private Properties headers; /** * Stores the exporter parameters passed in by the user as passed in by the user. May be keyed as * Strings with the fully qualified name of the exporter parameter field. */ private Map exporterParameters = new HashMap(); /** * Stores the converted exporter parameters - keyed by JRExporterParameter. */ private Map convertedExporterParameters; /** * Stores the DataSource, if any, used as the report data source. */ private DataSource jdbcDataSource; /** * Holds the JRCompiler implementation to use for compiling reports on-the-fly. */ private JRCompiler reportCompiler = JRDefaultCompiler.getInstance(); /** * The JasperReport that is used to render the view. */ private JasperReport report; /** * Holds mappings between sub-report keys and JasperReport objects. */ private Map subReports; /** * 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) { // NOTE: Removed conversion from here since configuration of parameters // can also happen through access to the underlying Map using // getExporterParameters(). Conversion now happens in initApplicationContext, // and subclasses use getConvertedExporterParameters() to access the converted // parameter Map - robh. this.exporterParameters = parameters; } /** * Return the exporter parameters that this view uses, if any. */ public Map getExporterParameters() { return this.exporterParameters; } /** * Allows subclasses to retrieve the converted exporter parameters. */ protected Map getConvertedExporterParameters() { return this.convertedExporterParameters; } /** * Specify the javax.sql.DataSource to use for reports with * embedded SQL statements. */ public void setJdbcDataSource(DataSource jdbcDataSource) { this.jdbcDataSource = jdbcDataSource; } /** * Return the javax.sql.DataSource that this view uses, if any. */ protected DataSource getJdbcDataSource() { return this.jdbcDataSource; } /** * Specify the JRCompiler implementation to use for compiling a ".jrxml" * report file on-the-fly into a report class. *

By default, a JRDefaultCompiler will be used, delegating to the * Eclipse JDT compiler or the Sun JDK compiler underneath. * @see net.sf.jasperreports.engine.design.JRDefaultCompiler */ public void setReportCompiler(JRCompiler reportCompiler) { this.reportCompiler = (reportCompiler != null ? reportCompiler : JRDefaultCompiler.getInstance()); } /** * Return the JRCompiler instance to use for compiling ".jrxml" report files. */ protected JRCompiler getReportCompiler() { return this.reportCompiler; } /** * Checks to see that a valid report file URL is supplied in the * configuration. Compiles the report file is necessary. *

Subclasses can add custom initialization logic by overriding * the {@link #onInit} method. * @see #onInit() */ protected final void initApplicationContext() throws ApplicationContextException { 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)); } } // Convert user-supplied exporterParameters. convertExporterParameters(); if (this.headers == null) { this.headers = new Properties(); } if (!this.headers.containsKey(HEADER_CONTENT_DISPOSITION)) { this.headers.setProperty(HEADER_CONTENT_DISPOSITION, CONTENT_DISPOSITION_INLINE); } onInit(); } /** * Subclasses can override this to add some custom initialization logic. Called * by {@link #initApplicationContext()} as soon as all standard initialization logic * has finished executing. * @see #initApplicationContext() */ protected void onInit() { } /** * Converts the exporter parameters passed in by the user which may be keyed * by Strings corresponding to the fully qualified name of the * JRExporterParameter into parameters which are keyed by * JRExporterParameter. * @see #getExporterParameter(Object) */ protected final void convertExporterParameters() { if (this.exporterParameters != null && !this.exporterParameters.isEmpty()) { this.convertedExporterParameters = new HashMap(this.exporterParameters.size()); for (Iterator it = this.exporterParameters.entrySet().iterator(); it.hasNext();) { Map.Entry entry = (Map.Entry) it.next(); JRExporterParameter exporterParameter = getExporterParameter(entry.getKey()); this.convertedExporterParameters.put(exporterParameter, convertParameterValue(exporterParameter, entry.getValue())); } } } /** * Convert the supplied parameter value into the actual type required by the * corresponding {@link JRExporterParameter}. *

The default implementation simply converts the String values "true" and * "false" into corresponding Boolean objects, and tries to convert * String values that start with a digit into Integer objects * (simply keeping them as String if number conversion fails). */ protected Object convertParameterValue(JRExporterParameter parameter, Object value) { if (value instanceof String) { String str = (String) value; if ("true".equals(str)) { return Boolean.TRUE; } else if ("false".equals(str)) { return Boolean.FALSE; } else if (str.length() > 0 && Character.isDigit(str.charAt(0))) { // Looks like a number... let's try. try { return new Integer(str); } catch (NumberFormatException ex) { // OK, then let's keep it as a String value. return str; } } } return value; } /** * Return a JRExporterParameter for the given parameter object, * converting it from a String if necessary. * @param parameter the parameter object, either a String or a JRExporterParameter * @return a JRExporterParameter for the given parameter object * @see #convertToExporterParameter(String) */ protected JRExporterParameter getExporterParameter(Object parameter) { if (parameter instanceof JRExporterParameter) { return (JRExporterParameter) parameter; } if (parameter instanceof String) { return convertToExporterParameter((String) parameter); } throw new IllegalArgumentException( "Parameter [" + parameter + "] is invalid type. Should be either String or JRExporterParameter."); } /** * 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); } } /** * 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 { 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))); } } } // Expose Spring-managed Locale and MessageSource. exposeLocalizationContext(model, request); // Fill the report. JasperPrint filledReport = fillReport(model); postProcessReport(filledReport, model); // Prepare response and render report. response.reset(); populateHeaders(response); renderReport(filledReport, model, response); } /** * Expose current Spring-managed Locale and MessageSource to JasperReports i18n * ($R expressions etc). The MessageSource should only be exposed as JasperReports * resource bundle if no such bundle is defined in the report itself. *

Default implementation exposes the Spring RequestContext Locale and a * MessageSourceResourceBundle adapter for the Spring ApplicationContext, * analogous to the JstlUtils.exposeLocalizationContext method. * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale * @see org.springframework.context.support.MessageSourceResourceBundle * @see #getApplicationContext() * @see net.sf.jasperreports.engine.JRParameter#REPORT_LOCALE * @see net.sf.jasperreports.engine.JRParameter#REPORT_RESOURCE_BUNDLE * @see org.springframework.web.servlet.support.JstlUtils#exposeLocalizationContext */ protected void exposeLocalizationContext(Map model, HttpServletRequest request) { Locale locale = RequestContextUtils.getLocale(request); model.put(JRParameter.REPORT_LOCALE, locale); if (this.report.getResourceBundle() == null) { ResourceBundle bundle = new MessageSourceResourceBundle(getApplicationContext(), locale); model.put(JRParameter.REPORT_RESOURCE_BUNDLE, bundle); } } /** * Creates a populated JasperPrint instance from the configured * JasperReport instance. By default, will use any JRDataSource * instance (or wrappable Object) that can be located using * getReportData(Map). If no JRDataSource can be found, will use a * Connection obtained from the configured javax.sql.DataSource. * @param model the model for this request * @throws IllegalArgumentException if no JRDataSource can be found * and no javax.sql.DataSource is supplied * @throws SQLException if there is an error when populating the report using * the javax.sql.DataSource * @throws JRException if there is an error when populating the report using * a JRDataSource * @return the populated JasperPrint instance * @see #getReportData * @see #setJdbcDataSource */ protected JasperPrint fillReport(Map model) throws IllegalArgumentException, SQLException, JRException { // Determine JRDataSource for main report. JRDataSource jrDataSource = getReportData(model); if (jrDataSource != null) { // Use the JasperReports JRDataSource. if (logger.isDebugEnabled()) { logger.debug("Filling report with JRDataSource [" + jrDataSource + "]."); } return JasperFillManager.fillReport(this.report, model, jrDataSource); } else { if (this.jdbcDataSource == null) { this.jdbcDataSource = (DataSource) CollectionUtils.findValueOfType(model.values(), DataSource.class); if (this.jdbcDataSource == null) { throw new IllegalArgumentException( "No report data source found in model, " + "and no [javax.sql.DataSource] specified in configuration or in model"); } } // Use the JDBC DataSource. if (logger.isDebugEnabled()) { logger.debug("Filling report with JDBC DataSource [" + this.jdbcDataSource + "]."); } Connection con = this.jdbcDataSource.getConnection(); try { return JasperFillManager.fillReport(this.report, model, con); } finally { try { con.close(); } catch (SQLException ex) { logger.warn("Could not close JDBC Connection", ex); } } } } /** * Populates the headers in the HttpServletResponse with the * headers supplied by the user. */ 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 or null if the data source is not found * @see #setReportDataKey * @see #convertReportData * @see #getReportDataTypes */ protected JRDataSource getReportData(Map model) { // 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); } return null; } /** * 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. * JRDataSources are returned as is, whilst JRDataSourceProviders * 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(this.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; } /** * Template method to be overridden for custom post-processing of the * populated report. Invoked after filling but before rendering. *

The default implementation is empty. * @param populatedReport the populated JasperPrint * @param model the map containing report parameters * @throws Exception if post-processing failed */ protected void postProcessReport(JasperPrint populatedReport, Map model) throws Exception { } /** * Subclasses should implement this method to perform the actual rendering process. *

Note that the content type has not been set yet: Implementors should build * a content type String and set it via response.setContentType. * If necessary, this can include a charset clause for a specific encoding. * The latter will only be necessary for textual output onto a Writer, and only * in case of the encoding being specified in the JasperReports exporter parameters. *

WARNING: Implementors should not use response.setCharacterEncoding * unless they are willing to depend on Servlet API 2.4 or higher. Prefer a * concatenated content type String with a charset clause instead. * @param populatedReport the populated JasperPrint to render * @param model the map containing report parameters * @param response the HTTP response the report should be rendered to * @throws Exception if rendering failed * @see #getContentType() * @see javax.servlet.ServletResponse#setContentType * @see javax.servlet.ServletResponse#setCharacterEncoding */ protected abstract void renderReport(JasperPrint populatedReport, Map model, HttpServletResponse response) throws Exception; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy