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}