001/*
002 * Copyright 2002-2016 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.servlet.view.jasperreports;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.lang.reflect.Field;
022import java.sql.Connection;
023import java.sql.SQLException;
024import java.util.Collection;
025import java.util.Enumeration;
026import java.util.HashMap;
027import java.util.Locale;
028import java.util.Map;
029import java.util.Properties;
030import java.util.TimeZone;
031import javax.servlet.http.HttpServletRequest;
032import javax.servlet.http.HttpServletResponse;
033import javax.sql.DataSource;
034
035import net.sf.jasperreports.engine.JRDataSource;
036import net.sf.jasperreports.engine.JRDataSourceProvider;
037import net.sf.jasperreports.engine.JRException;
038import net.sf.jasperreports.engine.JRParameter;
039import net.sf.jasperreports.engine.JasperCompileManager;
040import net.sf.jasperreports.engine.JasperFillManager;
041import net.sf.jasperreports.engine.JasperPrint;
042import net.sf.jasperreports.engine.JasperReport;
043import net.sf.jasperreports.engine.design.JasperDesign;
044import net.sf.jasperreports.engine.util.JRLoader;
045import net.sf.jasperreports.engine.xml.JRXmlLoader;
046
047import org.springframework.context.ApplicationContextException;
048import org.springframework.context.support.MessageSourceResourceBundle;
049import org.springframework.core.io.Resource;
050import org.springframework.ui.jasperreports.JasperReportsUtils;
051import org.springframework.util.ClassUtils;
052import org.springframework.util.CollectionUtils;
053import org.springframework.web.servlet.support.RequestContext;
054import org.springframework.web.servlet.view.AbstractUrlBasedView;
055
056/**
057 * Base class for all JasperReports views. Applies on-the-fly compilation
058 * of report designs as required and coordinates the rendering process.
059 * The resource path of the main report needs to be specified as {@code url}.
060 *
061 * <p>This class is responsible for getting report data from the model that has
062 * been provided to the view. The default implementation checks for a model object
063 * under the specified {@code reportDataKey} first, then falls back to looking
064 * for a value of type {@code JRDataSource}, {@code java.util.Collection},
065 * object array (in that order).
066 *
067 * <p>If no {@code JRDataSource} can be found in the model, then reports will
068 * be filled using the configured {@code javax.sql.DataSource} if any. If neither
069 * a {@code JRDataSource} or {@code javax.sql.DataSource} is available then
070 * an {@code IllegalArgumentException} is raised.
071 *
072 * <p>Provides support for sub-reports through the {@code subReportUrls} and
073 * {@code subReportDataKeys} properties.
074 *
075 * <p>When using sub-reports, the master report should be configured using the
076 * {@code url} property and the sub-reports files should be configured using
077 * the {@code subReportUrls} property. Each entry in the {@code subReportUrls}
078 * Map corresponds to an individual sub-report. The key of an entry must match up
079 * to a sub-report parameter in your report file of type
080 * {@code net.sf.jasperreports.engine.JasperReport},
081 * and the value of an entry must be the URL for the sub-report file.
082 *
083 * <p>For sub-reports that require an instance of {@code JRDataSource}, that is,
084 * they don't have a hard-coded query for data retrieval, you can include the
085 * appropriate data in your model as would with the data source for the parent report.
086 * However, you must provide a List of parameter names that need to be converted to
087 * {@code JRDataSource} instances for the sub-report via the
088 * {@code subReportDataKeys} property. When using {@code JRDataSource}
089 * instances for sub-reports, you <i>must</i> specify a value for the
090 * {@code reportDataKey} property, indicating the data to use for the main report.
091 *
092 * <p>Allows for exporter parameters to be configured declatively using the
093 * {@code exporterParameters} property. This is a {@code Map} typed
094 * property where the key of an entry corresponds to the fully-qualified name
095 * of the static field for the {@code JRExporterParameter} and the value
096 * of an entry is the value you want to assign to the exporter parameter.
097 *
098 * <p>Response headers can be controlled via the {@code headers} property. Spring
099 * will attempt to set the correct value for the {@code Content-Diposition} header
100 * so that reports render correctly in Internet Explorer. However, you can override this
101 * setting through the {@code headers} property.
102 *
103 * <p><b>This class is compatible with classic JasperReports releases back until 2.x.</b>
104 * As a consequence, it keeps using the {@link net.sf.jasperreports.engine.JRExporter}
105 * API which got deprecated as of JasperReports 5.5.2 (early 2014).
106 *
107 * @author Rob Harrop
108 * @author Juergen Hoeller
109 * @since 1.1.3
110 * @see #setUrl
111 * @see #setReportDataKey
112 * @see #setSubReportUrls
113 * @see #setSubReportDataKeys
114 * @see #setHeaders
115 * @see #setExporterParameters
116 * @see #setJdbcDataSource
117 */
118@SuppressWarnings({"deprecation", "rawtypes"})
119public abstract class AbstractJasperReportsView extends AbstractUrlBasedView {
120
121        /**
122         * Constant that defines "Content-Disposition" header.
123         */
124        protected static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
125
126        /**
127         * The default Content-Disposition header. Used to make IE play nice.
128         */
129        protected static final String CONTENT_DISPOSITION_INLINE = "inline";
130
131
132        /**
133         * A String key used to lookup the {@code JRDataSource} in the model.
134         */
135        private String reportDataKey;
136
137        /**
138         * Stores the paths to any sub-report files used by this top-level report,
139         * along with the keys they are mapped to in the top-level report file.
140         */
141        private Properties subReportUrls;
142
143        /**
144         * Stores the names of any data source objects that need to be converted to
145         * {@code JRDataSource} instances and included in the report parameters
146         * to be passed on to a sub-report.
147         */
148        private String[] subReportDataKeys;
149
150        /**
151         * Stores the headers to written with each response
152         */
153        private Properties headers;
154
155        /**
156         * Stores the exporter parameters passed in by the user as passed in by the user. May be keyed as
157         * {@code String}s with the fully qualified name of the exporter parameter field.
158         */
159        private Map<?, ?> exporterParameters = new HashMap<Object, Object>();
160
161        /**
162         * Stores the converted exporter parameters - keyed by {@code JRExporterParameter}.
163         */
164        private Map<net.sf.jasperreports.engine.JRExporterParameter, Object> convertedExporterParameters;
165
166        /**
167         * Stores the {@code DataSource}, if any, used as the report data source.
168         */
169        private DataSource jdbcDataSource;
170
171        /**
172         * The {@code JasperReport} that is used to render the view.
173         */
174        private JasperReport report;
175
176        /**
177         * Holds mappings between sub-report keys and {@code JasperReport} objects.
178         */
179        private Map<String, JasperReport> subReports;
180
181
182        /**
183         * Set the name of the model attribute that represents the report data.
184         * If not specified, the model map will be searched for a matching value type.
185         * <p>A {@code JRDataSource} will be taken as-is. For other types, conversion
186         * will apply: By default, a {@code java.util.Collection} will be converted
187         * to {@code JRBeanCollectionDataSource}, and an object array to
188         * {@code JRBeanArrayDataSource}.
189         * <p><b>Note:</b> If you pass in a Collection or object array in the model map
190         * for use as plain report parameter, rather than as report data to extract fields
191         * from, you need to specify the key for the actual report data to use, to avoid
192         * mis-detection of report data by type.
193         * @see #convertReportData
194         * @see net.sf.jasperreports.engine.JRDataSource
195         * @see net.sf.jasperreports.engine.data.JRBeanCollectionDataSource
196         * @see net.sf.jasperreports.engine.data.JRBeanArrayDataSource
197         */
198        public void setReportDataKey(String reportDataKey) {
199                this.reportDataKey = reportDataKey;
200        }
201
202        /**
203         * Specify resource paths which must be loaded as instances of
204         * {@code JasperReport} and passed to the JasperReports engine for
205         * rendering as sub-reports, under the same keys as in this mapping.
206         * @param subReports mapping between model keys and resource paths
207         * (Spring resource locations)
208         * @see #setUrl
209         * @see org.springframework.context.ApplicationContext#getResource
210         */
211        public void setSubReportUrls(Properties subReports) {
212                this.subReportUrls = subReports;
213        }
214
215        /**
216         * Set the list of names corresponding to the model parameters that will contain
217         * data source objects for use in sub-reports. Spring will convert these objects
218         * to instances of {@code JRDataSource} where applicable and will then
219         * include the resulting {@code JRDataSource} in the parameters passed into
220         * the JasperReports engine.
221         * <p>The name specified in the list should correspond to an attribute in the
222         * model Map, and to a sub-report data source parameter in your report file.
223         * If you pass in {@code JRDataSource} objects as model attributes,
224         * specifying this list of keys is not required.
225         * <p>If you specify a list of sub-report data keys, it is required to also
226         * specify a {@code reportDataKey} for the main report, to avoid confusion
227         * between the data source objects for the various reports involved.
228         * @param subReportDataKeys list of names for sub-report data source objects
229         * @see #setReportDataKey
230         * @see #convertReportData
231         * @see net.sf.jasperreports.engine.JRDataSource
232         * @see net.sf.jasperreports.engine.data.JRBeanCollectionDataSource
233         * @see net.sf.jasperreports.engine.data.JRBeanArrayDataSource
234         */
235        public void setSubReportDataKeys(String... subReportDataKeys) {
236                this.subReportDataKeys = subReportDataKeys;
237        }
238
239        /**
240         * Specify the set of headers that are included in each of response.
241         * @param headers the headers to write to each response.
242         */
243        public void setHeaders(Properties headers) {
244                this.headers = headers;
245        }
246
247        /**
248         * Set the exporter parameters that should be used when rendering a view.
249         * @param parameters {@code Map} with the fully qualified field name
250         * of the {@code JRExporterParameter} instance as key
251         * (e.g. "net.sf.jasperreports.engine.export.JRHtmlExporterParameter.IMAGES_URI")
252         * and the value you wish to assign to the parameter as value
253         */
254        public void setExporterParameters(Map<?, ?> parameters) {
255                this.exporterParameters = parameters;
256        }
257
258        /**
259         * Return the exporter parameters that this view uses, if any.
260         */
261        public Map<?, ?> getExporterParameters() {
262                return this.exporterParameters;
263        }
264
265        /**
266         * Allows subclasses to populate the converted exporter parameters.
267         */
268        protected void setConvertedExporterParameters(Map<net.sf.jasperreports.engine.JRExporterParameter, Object> parameters) {
269                this.convertedExporterParameters = parameters;
270        }
271
272        /**
273         * Allows subclasses to retrieve the converted exporter parameters.
274         */
275        protected Map<net.sf.jasperreports.engine.JRExporterParameter, Object> getConvertedExporterParameters() {
276                return this.convertedExporterParameters;
277        }
278
279        /**
280         * Specify the {@code javax.sql.DataSource} to use for reports with
281         * embedded SQL statements.
282         */
283        public void setJdbcDataSource(DataSource jdbcDataSource) {
284                this.jdbcDataSource = jdbcDataSource;
285        }
286
287        /**
288         * Return the {@code javax.sql.DataSource} that this view uses, if any.
289         */
290        protected DataSource getJdbcDataSource() {
291                return this.jdbcDataSource;
292        }
293
294
295        /**
296         * JasperReports views do not strictly required a 'url' value.
297         * Alternatively, the {@link #getReport()} template method may be overridden.
298         */
299        @Override
300        protected boolean isUrlRequired() {
301                return false;
302        }
303
304        /**
305         * Checks to see that a valid report file URL is supplied in the
306         * configuration. Compiles the report file is necessary.
307         * <p>Subclasses can add custom initialization logic by overriding
308         * the {@link #onInit} method.
309         */
310        @Override
311        protected final void initApplicationContext() throws ApplicationContextException {
312                this.report = loadReport();
313
314                // Load sub reports if required, and check data source parameters.
315                if (this.subReportUrls != null) {
316                        if (this.subReportDataKeys != null && this.subReportDataKeys.length > 0 && this.reportDataKey == null) {
317                                throw new ApplicationContextException(
318                                                "'reportDataKey' for main report is required when specifying a value for 'subReportDataKeys'");
319                        }
320                        this.subReports = new HashMap<String, JasperReport>(this.subReportUrls.size());
321                        for (Enumeration<?> urls = this.subReportUrls.propertyNames(); urls.hasMoreElements();) {
322                                String key = (String) urls.nextElement();
323                                String path = this.subReportUrls.getProperty(key);
324                                Resource resource = getApplicationContext().getResource(path);
325                                this.subReports.put(key, loadReport(resource));
326                        }
327                }
328
329                // Convert user-supplied exporterParameters.
330                convertExporterParameters();
331
332                if (this.headers == null) {
333                        this.headers = new Properties();
334                }
335                if (!this.headers.containsKey(HEADER_CONTENT_DISPOSITION)) {
336                        this.headers.setProperty(HEADER_CONTENT_DISPOSITION, CONTENT_DISPOSITION_INLINE);
337                }
338
339                onInit();
340        }
341
342        /**
343         * Subclasses can override this to add some custom initialization logic. Called
344         * by {@link #initApplicationContext()} as soon as all standard initialization logic
345         * has finished executing.
346         * @see #initApplicationContext()
347         */
348        protected void onInit() {
349        }
350
351        /**
352         * Converts the exporter parameters passed in by the user which may be keyed
353         * by {@code String}s corresponding to the fully qualified name of the
354         * {@code JRExporterParameter} into parameters which are keyed by
355         * {@code JRExporterParameter}.
356         * @see #getExporterParameter(Object)
357         */
358        protected final void convertExporterParameters() {
359                if (!CollectionUtils.isEmpty(this.exporterParameters)) {
360                        this.convertedExporterParameters =
361                                        new HashMap<net.sf.jasperreports.engine.JRExporterParameter, Object>(this.exporterParameters.size());
362                        for (Map.Entry<?, ?> entry : this.exporterParameters.entrySet()) {
363                                net.sf.jasperreports.engine.JRExporterParameter exporterParameter = getExporterParameter(entry.getKey());
364                                this.convertedExporterParameters.put(
365                                                exporterParameter, convertParameterValue(exporterParameter, entry.getValue()));
366                        }
367                }
368        }
369
370        /**
371         * Convert the supplied parameter value into the actual type required by the
372         * corresponding {@code JRExporterParameter}.
373         * <p>The default implementation simply converts the String values "true" and
374         * "false" into corresponding {@code Boolean} objects, and tries to convert
375         * String values that start with a digit into {@code Integer} objects
376         * (simply keeping them as String if number conversion fails).
377         * @param parameter the parameter key
378         * @param value the parameter value
379         * @return the converted parameter value
380         */
381        protected Object convertParameterValue(net.sf.jasperreports.engine.JRExporterParameter parameter, Object value) {
382                if (value instanceof String) {
383                        String str = (String) value;
384                        if ("true".equals(str)) {
385                                return Boolean.TRUE;
386                        }
387                        else if ("false".equals(str)) {
388                                return Boolean.FALSE;
389                        }
390                        else if (str.length() > 0 && Character.isDigit(str.charAt(0))) {
391                                // Looks like a number... let's try.
392                                try {
393                                        return Integer.valueOf(str);
394                                }
395                                catch (NumberFormatException ex) {
396                                        // OK, then let's keep it as a String value.
397                                        return str;
398                                }
399                        }
400                }
401                return value;
402        }
403
404        /**
405         * Return a {@code JRExporterParameter} for the given parameter object,
406         * converting it from a String if necessary.
407         * @param parameter the parameter object, either a String or a JRExporterParameter
408         * @return a JRExporterParameter for the given parameter object
409         * @see #convertToExporterParameter(String)
410         */
411        protected net.sf.jasperreports.engine.JRExporterParameter getExporterParameter(Object parameter) {
412                if (parameter instanceof net.sf.jasperreports.engine.JRExporterParameter) {
413                        return (net.sf.jasperreports.engine.JRExporterParameter) parameter;
414                }
415                if (parameter instanceof String) {
416                        return convertToExporterParameter((String) parameter);
417                }
418                throw new IllegalArgumentException(
419                                "Parameter [" + parameter + "] is invalid type. Should be either String or JRExporterParameter.");
420        }
421
422        /**
423         * Convert the given fully qualified field name to a corresponding
424         * JRExporterParameter instance.
425         * @param fqFieldName the fully qualified field name, consisting
426         * of the class name followed by a dot followed by the field name
427         * (e.g. "net.sf.jasperreports.engine.export.JRHtmlExporterParameter.IMAGES_URI")
428         * @return the corresponding JRExporterParameter instance
429         */
430        protected net.sf.jasperreports.engine.JRExporterParameter convertToExporterParameter(String fqFieldName) {
431                int index = fqFieldName.lastIndexOf('.');
432                if (index == -1 || index == fqFieldName.length()) {
433                        throw new IllegalArgumentException(
434                                        "Parameter name [" + fqFieldName + "] is not a valid static field. " +
435                                        "The parameter name must map to a static field such as " +
436                                        "[net.sf.jasperreports.engine.export.JRHtmlExporterParameter.IMAGES_URI]");
437                }
438                String className = fqFieldName.substring(0, index);
439                String fieldName = fqFieldName.substring(index + 1);
440
441                try {
442                        Class<?> cls = ClassUtils.forName(className, getApplicationContext().getClassLoader());
443                        Field field = cls.getField(fieldName);
444
445                        if (net.sf.jasperreports.engine.JRExporterParameter.class.isAssignableFrom(field.getType())) {
446                                try {
447                                        return (net.sf.jasperreports.engine.JRExporterParameter) field.get(null);
448                                }
449                                catch (IllegalAccessException ex) {
450                                        throw new IllegalArgumentException(
451                                                        "Unable to access field [" + fieldName + "] of class [" + className + "]. " +
452                                                        "Check that it is static and accessible.");
453                                }
454                        }
455                        else {
456                                throw new IllegalArgumentException("Field [" + fieldName + "] on class [" + className +
457                                                "] is not assignable from JRExporterParameter - check the type of this field.");
458                        }
459                }
460                catch (ClassNotFoundException ex) {
461                        throw new IllegalArgumentException(
462                                        "Class [" + className + "] in key [" + fqFieldName + "] could not be found.");
463                }
464                catch (NoSuchFieldException ex) {
465                        throw new IllegalArgumentException("Field [" + fieldName + "] in key [" + fqFieldName +
466                                        "] could not be found on class [" + className + "].");
467                }
468        }
469
470        /**
471         * Load the main {@code JasperReport} from the specified {@code Resource}.
472         * If the {@code Resource} points to an uncompiled report design file then the
473         * report file is compiled dynamically and loaded into memory.
474         * @return a {@code JasperReport} instance, or {@code null} if no main
475         * report has been statically defined
476         */
477        protected JasperReport loadReport() {
478                String url = getUrl();
479                if (url == null) {
480                        return null;
481                }
482                Resource mainReport = getApplicationContext().getResource(url);
483                return loadReport(mainReport);
484        }
485
486        /**
487         * Loads a {@code JasperReport} from the specified {@code Resource}.
488         * If the {@code Resource} points to an uncompiled report design file then
489         * the report file is compiled dynamically and loaded into memory.
490         * @param resource the {@code Resource} containing the report definition or design
491         * @return a {@code JasperReport} instance
492         */
493        protected final JasperReport loadReport(Resource resource) {
494                try {
495                        String filename = resource.getFilename();
496                        if (filename != null) {
497                                if (filename.endsWith(".jasper")) {
498                                        // Load pre-compiled report.
499                                        if (logger.isInfoEnabled()) {
500                                                logger.info("Loading pre-compiled Jasper Report from " + resource);
501                                        }
502                                        InputStream is = resource.getInputStream();
503                                        try {
504                                                return (JasperReport) JRLoader.loadObject(is);
505                                        }
506                                        finally {
507                                                is.close();
508                                        }
509                                }
510                                else if (filename.endsWith(".jrxml")) {
511                                        // Compile report on-the-fly.
512                                        if (logger.isInfoEnabled()) {
513                                                logger.info("Compiling Jasper Report loaded from " + resource);
514                                        }
515                                        InputStream is = resource.getInputStream();
516                                        try {
517                                                JasperDesign design = JRXmlLoader.load(is);
518                                                return JasperCompileManager.compileReport(design);
519                                        }
520                                        finally {
521                                                is.close();
522                                        }
523                                }
524                        }
525                        throw new IllegalArgumentException(
526                                        "Report filename [" + filename + "] must end in either .jasper or .jrxml");
527                }
528                catch (IOException ex) {
529                        throw new ApplicationContextException(
530                                        "Could not load JasperReports report from " + resource, ex);
531                }
532                catch (JRException ex) {
533                        throw new ApplicationContextException(
534                                        "Could not parse JasperReports report from " + resource, ex);
535                }
536        }
537
538
539        /**
540         * Finds the report data to use for rendering the report and then invokes the
541         * {@link #renderReport} method that should be implemented by the subclass.
542         * @param model the model map, as passed in for view rendering. Must contain
543         * a report data value that can be converted to a {@code JRDataSource},
544         * according to the rules of the {@link #fillReport} method.
545         */
546        @Override
547        protected void renderMergedOutputModel(
548                        Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
549
550                if (this.subReports != null) {
551                        // Expose sub-reports as model attributes.
552                        model.putAll(this.subReports);
553
554                        // Transform any collections etc into JRDataSources for sub reports.
555                        if (this.subReportDataKeys != null) {
556                                for (String key : this.subReportDataKeys) {
557                                        model.put(key, convertReportData(model.get(key)));
558                                }
559                        }
560                }
561
562                // Expose Spring-managed Locale and MessageSource.
563                exposeLocalizationContext(model, request);
564
565                // Fill the report.
566                JasperPrint filledReport = fillReport(model);
567                postProcessReport(filledReport, model);
568
569                // Prepare response and render report.
570                populateHeaders(response);
571                renderReport(filledReport, model, response);
572        }
573
574        /**
575         * Expose current Spring-managed Locale and MessageSource to JasperReports i18n
576         * ($R expressions etc). The MessageSource should only be exposed as JasperReports
577         * resource bundle if no such bundle is defined in the report itself.
578         * <p>The default implementation exposes the Spring RequestContext Locale and a
579         * MessageSourceResourceBundle adapter for the Spring ApplicationContext,
580         * analogous to the {@code JstlUtils.exposeLocalizationContext} method.
581         * @see org.springframework.web.servlet.support.RequestContextUtils#getLocale
582         * @see org.springframework.context.support.MessageSourceResourceBundle
583         * @see #getApplicationContext()
584         * @see net.sf.jasperreports.engine.JRParameter#REPORT_LOCALE
585         * @see net.sf.jasperreports.engine.JRParameter#REPORT_RESOURCE_BUNDLE
586         * @see org.springframework.web.servlet.support.JstlUtils#exposeLocalizationContext
587         */
588        protected void exposeLocalizationContext(Map<String, Object> model, HttpServletRequest request) {
589                RequestContext rc = new RequestContext(request, getServletContext());
590                Locale locale = rc.getLocale();
591                if (!model.containsKey(JRParameter.REPORT_LOCALE)) {
592                        model.put(JRParameter.REPORT_LOCALE, locale);
593                }
594                TimeZone timeZone = rc.getTimeZone();
595                if (timeZone != null && !model.containsKey(JRParameter.REPORT_TIME_ZONE)) {
596                        model.put(JRParameter.REPORT_TIME_ZONE, timeZone);
597                }
598                JasperReport report = getReport();
599                if ((report == null || report.getResourceBundle() == null) &&
600                                !model.containsKey(JRParameter.REPORT_RESOURCE_BUNDLE)) {
601                        model.put(JRParameter.REPORT_RESOURCE_BUNDLE,
602                                        new MessageSourceResourceBundle(rc.getMessageSource(), locale));
603                }
604        }
605
606        /**
607         * Create a populated {@code JasperPrint} instance from the configured
608         * {@code JasperReport} instance.
609         * <p>By default, this method will use any {@code JRDataSource} instance
610         * (or wrappable {@code Object}) that can be located using {@link #setReportDataKey},
611         * a lookup for type {@code JRDataSource} in the model Map, or a special value
612         * retrieved via {@link #getReportData}.
613         * <p>If no {@code JRDataSource} can be found, this method will use a JDBC
614         * {@code Connection} obtained from the configured {@code javax.sql.DataSource}
615         * (or a DataSource attribute in the model). If no JDBC DataSource can be found
616         * either, the JasperReports engine will be invoked with plain model Map,
617         * assuming that the model contains parameters that identify the source
618         * for report data (e.g. Hibernate or JPA queries).
619         * @param model the model for this request
620         * @throws IllegalArgumentException if no {@code JRDataSource} can be found
621         * and no {@code javax.sql.DataSource} is supplied
622         * @throws SQLException if there is an error when populating the report using
623         * the {@code javax.sql.DataSource}
624         * @throws JRException if there is an error when populating the report using
625         * a {@code JRDataSource}
626         * @return the populated {@code JasperPrint} instance
627         * @see #getReportData
628         * @see #setJdbcDataSource
629         */
630        protected JasperPrint fillReport(Map<String, Object> model) throws Exception {
631                // Determine main report.
632                JasperReport report = getReport();
633                if (report == null) {
634                        throw new IllegalStateException("No main report defined for 'fillReport' - " +
635                                        "specify a 'url' on this view or override 'getReport()' or 'fillReport(Map)'");
636                }
637
638                JRDataSource jrDataSource = null;
639                DataSource jdbcDataSourceToUse = null;
640
641                // Try model attribute with specified name.
642                if (this.reportDataKey != null) {
643                        Object reportDataValue = model.get(this.reportDataKey);
644                        if (reportDataValue instanceof DataSource) {
645                                jdbcDataSourceToUse = (DataSource) reportDataValue;
646                        }
647                        else {
648                                jrDataSource = convertReportData(reportDataValue);
649                        }
650                }
651                else {
652                        Collection<?> values = model.values();
653                        jrDataSource = CollectionUtils.findValueOfType(values, JRDataSource.class);
654                        if (jrDataSource == null) {
655                                JRDataSourceProvider provider = CollectionUtils.findValueOfType(values, JRDataSourceProvider.class);
656                                if (provider != null) {
657                                        jrDataSource = createReport(provider);
658                                }
659                                else {
660                                        jdbcDataSourceToUse = CollectionUtils.findValueOfType(values, DataSource.class);
661                                        if (jdbcDataSourceToUse == null) {
662                                                jdbcDataSourceToUse = this.jdbcDataSource;
663                                        }
664                                }
665                        }
666                }
667
668                if (jdbcDataSourceToUse != null) {
669                        return doFillReport(report, model, jdbcDataSourceToUse);
670                }
671                else {
672                        // Determine JRDataSource for main report.
673                        if (jrDataSource == null) {
674                                jrDataSource = getReportData(model);
675                        }
676                        if (jrDataSource != null) {
677                                // Use the JasperReports JRDataSource.
678                                if (logger.isDebugEnabled()) {
679                                        logger.debug("Filling report with JRDataSource [" + jrDataSource + "]");
680                                }
681                                return JasperFillManager.fillReport(report, model, jrDataSource);
682                        }
683                        else {
684                                // Assume that the model contains parameters that identify
685                                // the source for report data (e.g. Hibernate or JPA queries).
686                                logger.debug("Filling report with plain model");
687                                return JasperFillManager.fillReport(report, model);
688                        }
689                }
690        }
691
692        /**
693         * Fill the given report using the given JDBC DataSource and model.
694         */
695        private JasperPrint doFillReport(JasperReport report, Map<String, Object> model, DataSource ds) throws Exception {
696                // Use the JDBC DataSource.
697                if (logger.isDebugEnabled()) {
698                        logger.debug("Filling report using JDBC DataSource [" + ds + "]");
699                }
700                Connection con = ds.getConnection();
701                try {
702                        return JasperFillManager.fillReport(report, model, con);
703                }
704                finally {
705                        try {
706                                con.close();
707                        }
708                        catch (Throwable ex) {
709                                logger.debug("Could not close JDBC Connection", ex);
710                        }
711                }
712        }
713
714        /**
715         * Populates the headers in the {@code HttpServletResponse} with the
716         * headers supplied by the user.
717         */
718        private void populateHeaders(HttpServletResponse response) {
719                // Apply the headers to the response.
720                for (Enumeration<?> en = this.headers.propertyNames(); en.hasMoreElements();) {
721                        String key = (String) en.nextElement();
722                        response.addHeader(key, this.headers.getProperty(key));
723                }
724        }
725
726        /**
727         * Determine the {@code JasperReport} to fill.
728         * Called by {@link #fillReport}.
729         * <p>The default implementation returns the report as statically configured
730         * through the 'url' property (and loaded by {@link #loadReport()}).
731         * Can be overridden in subclasses in order to dynamically obtain a
732         * {@code JasperReport} instance. As an alternative, consider
733         * overriding the {@link #fillReport} template method itself.
734         * @return an instance of {@code JasperReport}
735         */
736        protected JasperReport getReport() {
737                return this.report;
738        }
739
740        /**
741         * Create an appropriate {@code JRDataSource} for passed-in report data.
742         * Called by {@link #fillReport} when its own lookup steps were not successful.
743         * <p>The default implementation looks for a value of type {@code java.util.Collection}
744         * or object array (in that order). Can be overridden in subclasses.
745         * @param model the model map, as passed in for view rendering
746         * @return the {@code JRDataSource} or {@code null} if the data source is not found
747         * @see #getReportDataTypes
748         * @see #convertReportData
749         */
750        protected JRDataSource getReportData(Map<String, Object> model) {
751                // Try to find matching attribute, of given prioritized types.
752                Object value = CollectionUtils.findValueOfType(model.values(), getReportDataTypes());
753                return (value != null ? convertReportData(value) : null);
754        }
755
756        /**
757         * Convert the given report data value to a {@code JRDataSource}.
758         * <p>The default implementation delegates to {@code JasperReportUtils} unless
759         * the report data value is an instance of {@code JRDataSourceProvider}.
760         * A {@code JRDataSource}, {@code JRDataSourceProvider},
761         * {@code java.util.Collection} or object array is detected.
762         * {@code JRDataSource}s are returned as is, whilst {@code JRDataSourceProvider}s
763         * are used to create an instance of {@code JRDataSource} which is then returned.
764         * The latter two are converted to {@code JRBeanCollectionDataSource} or
765         * {@code JRBeanArrayDataSource}, respectively.
766         * @param value the report data value to convert
767         * @return the JRDataSource
768         * @throws IllegalArgumentException if the value could not be converted
769         * @see org.springframework.ui.jasperreports.JasperReportsUtils#convertReportData
770         * @see net.sf.jasperreports.engine.JRDataSource
771         * @see net.sf.jasperreports.engine.JRDataSourceProvider
772         * @see net.sf.jasperreports.engine.data.JRBeanCollectionDataSource
773         * @see net.sf.jasperreports.engine.data.JRBeanArrayDataSource
774         */
775        protected JRDataSource convertReportData(Object value) throws IllegalArgumentException {
776                if (value instanceof JRDataSourceProvider) {
777                        return createReport((JRDataSourceProvider) value);
778                }
779                else {
780                        return JasperReportsUtils.convertReportData(value);
781                }
782        }
783
784        /**
785         * Create a report using the given provider.
786         * @param provider the JRDataSourceProvider to use
787         * @return the created report
788         */
789        protected JRDataSource createReport(JRDataSourceProvider provider) {
790                try {
791                        JasperReport report = getReport();
792                        if (report == null) {
793                                throw new IllegalStateException("No main report defined for JRDataSourceProvider - " +
794                                                "specify a 'url' on this view or override 'getReport()'");
795                        }
796                        return provider.create(report);
797                }
798                catch (JRException ex) {
799                        throw new IllegalArgumentException("Supplied JRDataSourceProvider is invalid", ex);
800                }
801        }
802
803        /**
804         * Return the value types that can be converted to a {@code JRDataSource},
805         * in prioritized order. Should only return types that the
806         * {@link #convertReportData} method is actually able to convert.
807         * <p>Default value types are: {@code java.util.Collection} and {@code Object} array.
808         * @return the value types in prioritized order
809         */
810        protected Class<?>[] getReportDataTypes() {
811                return new Class<?>[] {Collection.class, Object[].class};
812        }
813
814
815        /**
816         * Template method to be overridden for custom post-processing of the
817         * populated report. Invoked after filling but before rendering.
818         * <p>The default implementation is empty.
819         * @param populatedReport the populated {@code JasperPrint}
820         * @param model the map containing report parameters
821         * @throws Exception if post-processing failed
822         */
823        protected void postProcessReport(JasperPrint populatedReport, Map<String, Object> model) throws Exception {
824        }
825
826        /**
827         * Subclasses should implement this method to perform the actual rendering process.
828         * <p>Note that the content type has not been set yet: Implementers should build
829         * a content type String and set it via {@code response.setContentType}.
830         * If necessary, this can include a charset clause for a specific encoding.
831         * The latter will only be necessary for textual output onto a Writer, and only
832         * in case of the encoding being specified in the JasperReports exporter parameters.
833         * <p><b>WARNING:</b> Implementers should not use {@code response.setCharacterEncoding}
834         * unless they are willing to depend on Servlet API 2.4 or higher. Prefer a
835         * concatenated content type String with a charset clause instead.
836         * @param populatedReport the populated {@code JasperPrint} to render
837         * @param model the map containing report parameters
838         * @param response the HTTP response the report should be rendered to
839         * @throws Exception if rendering failed
840         * @see #getContentType()
841         * @see javax.servlet.ServletResponse#setContentType
842         * @see javax.servlet.ServletResponse#setCharacterEncoding
843         */
844        protected abstract void renderReport(
845                        JasperPrint populatedReport, Map<String, Object> model, HttpServletResponse response)
846                        throws Exception;
847
848}