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