/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core;
import java.awt.print.PageFormat;
import java.awt.print.PrinterJob;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Date;
import javax.swing.table.DefaultTableModel;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.engine.classic.core.designtime.AttributeChange;
import org.pentaho.reporting.engine.classic.core.designtime.StyleChange;
import org.pentaho.reporting.engine.classic.core.event.ReportModelEvent;
import org.pentaho.reporting.engine.classic.core.event.ReportModelListener;
import org.pentaho.reporting.engine.classic.core.filter.types.bands.MasterReportType;
import org.pentaho.reporting.engine.classic.core.modules.parser.bundle.LegacyBundleResourceRegistry;
import org.pentaho.reporting.engine.classic.core.parameters.DefaultParameterDefinition;
import org.pentaho.reporting.engine.classic.core.parameters.ModifiableReportParameterDefinition;
import org.pentaho.reporting.engine.classic.core.parameters.ReportParameterDefinition;
import org.pentaho.reporting.engine.classic.core.style.css.ElementStyleDefinition;
import org.pentaho.reporting.engine.classic.core.util.LibLoaderResourceBundleFactory;
import org.pentaho.reporting.engine.classic.core.util.ReportParameterValues;
import org.pentaho.reporting.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.base.config.ExtendedConfiguration;
import org.pentaho.reporting.libraries.base.config.HierarchicalConfiguration;
import org.pentaho.reporting.libraries.base.config.ModifiableConfiguration;
import org.pentaho.reporting.libraries.base.util.ArgumentNullException;
import org.pentaho.reporting.libraries.docbundle.BundleUtilities;
import org.pentaho.reporting.libraries.docbundle.DocumentBundle;
import org.pentaho.reporting.libraries.docbundle.MemoryDocumentBundle;
import org.pentaho.reporting.libraries.docbundle.ODFMetaAttributeNames;
import org.pentaho.reporting.libraries.repository.ContentIOException;
import org.pentaho.reporting.libraries.resourceloader.Resource;
import org.pentaho.reporting.libraries.resourceloader.ResourceException;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
/**
* A JFreeReport instance is used as report template to define the visual layout of a report and to collect all data
* sources for the reporting. Possible data sources are the {@link javax.swing.table.TableModel}, {@link Expression}s or
* {@link ReportParameterValues}. The report is made up of 'bands', which are used repeatedly as necessary to generate
* small sections of the report.
* <p/>
* <h2>Accessing the bands and the elements:</h2>
* <p/>
* The different bands can be accessed using the main report definition (this class):
* <p/>
* <ul>
* <li>the report header and footer can be reached by using <code>getReportHeader()</code> and
* <code>getReportFooter()</code>
* <p/>
* <li>the page header and page footer can be reached by using <code>getPageHeader()</code> and
* <code>getPageFooter()</code>
* <p/>
* <li>the item band is reachable with <code>getItemBand()</code>
* <p/>
* <li>the no-data band is reachable with <code>getNoDataBand()</code>
* <p/>
* <li>the watermark band is reachable with <code>getWaterMark()</code>
* </ul>
* <p/>
* Groups can be queried using <code>getGroup(int groupLevel)</code>. The group header and footer are accessible through
* the group object, so use <code>getGroup(int groupLevel).getGroupHeader()<code> and <code>getGroup(int
* groupLevel).getGroupFooter()<code>.
* <p/>
* All report elements share the same stylesheet collection. Report elements cannot be shared between two different
* report instances. Adding a report element to one band will remove it from the other one.
* <p/>
* For dynamic computation of content you can add {@link Expression}s and {@link org.pentaho.reporting.engine.classic
* .core.function.Function}s
* to the report.
* <p/>
* Creating a new instance of JFreeReport seems to lock down the JDK on some Windows Systems, where no printer driver is
* installed. To prevent that behaviour on these systems, you can set the {@link Configuration} key
* "org.pentaho.reporting.engine.classic.core.NoPrinterAvailable" to "false" and JFreeReport will use a hardcoded
* default page format instead.
* <p/>
* A JFreeReport object always acts as Master-Report. The JFreeReport object defines the global report-configuration,
* the report's datasource (through the DataFactory property) and the ResourceBundleFactory (for localization).
*
* @author David Gilbert
* @author Thomas Morgner
*/
public class MasterReport extends AbstractReportDefinition {
/**
* Listens for changes to the DocumentBundle being used by a report and will update the ResourceManager to use that
* DocumentBundle.
*/
private static class ResourceBundleChangeHandler implements ReportModelListener {
private ResourceBundleChangeHandler() {
}
public void nodeChanged( final ReportModelEvent event ) {
if ( event.isNodeStructureChanged() ) {
return;
}
if ( event.getParameter() instanceof StyleChange ) {
return;
}
final Object element = event.getElement();
if ( element instanceof MasterReport == false ) {
return;
}
final MasterReport report = (MasterReport) element;
report.updateResourceBundleFactoryInternal();
}
}
/**
* Listens for changes to the DocumentBundle being used by a report and will update the ResourceManager to use that
* DocumentBundle.
*/
private static class DocumentBundleChangeHandler implements ReportModelListener {
private static final Log log = LogFactory.getLog( DocumentBundleChangeHandler.class );
private DocumentBundleChangeHandler() {
}
public void nodeChanged( final ReportModelEvent event ) {
if ( event.getElement() instanceof MasterReport == false ) {
return;
}
final MasterReport report = (MasterReport) event.getElement();
if ( event.getParameter() instanceof AttributeChange ) {
final AttributeChange attributeChange = (AttributeChange) event.getParameter();
// This is an attribute change event on the master report ... see if it is one we are concerned about
if ( AttributeNames.Core.NAMESPACE.equals( attributeChange.getNamespace() )
&& AttributeNames.Core.BUNDLE.equals( attributeChange.getName() ) ) {
final Object value = attributeChange.getNewValue();
if ( ( value instanceof DocumentBundle ) == false ) {
return;
}
// Insert the DocumentBundle's ResourceManager as the MasterReports resource manager
log.debug( "DocumentBundle change detected - changing the ResourceManager for the MasterReport" );
final DocumentBundle newDocumentBundle = (DocumentBundle) value;
final ResourceManager resourceManager = newDocumentBundle.getResourceManager();
report.setContentBase( newDocumentBundle.getBundleKey() );
report.setResourceManager( resourceManager );
}
} else if ( event.getParameter() instanceof ResourceManager ) {
final ResourceManager mgr = report.getResourceManager();
final ResourceBundleFactory resourceBundleFactory = report.getResourceBundleFactory();
if ( resourceBundleFactory instanceof LibLoaderResourceBundleFactory ) {
LibLoaderResourceBundleFactory ll = (LibLoaderResourceBundleFactory) resourceBundleFactory;
ll.setResourceLoader( mgr, report.getContentBase() );
}
}
}
}
/**
* Key for the 'report date' property.
*/
public static final String REPORT_DATE_PROPERTY = "report.date";
/**
* The data factory is used to query data for the reporting.
*/
private DataFactory dataFactory;
/**
* The report configuration.
*/
private HierarchicalConfiguration reportConfiguration;
/**
* The resource manager is used to load the report resources.
*/
private transient ResourceManager resourceManager;
private ReportParameterDefinition parameterDefinition;
private ReportEnvironment reportEnvironment;
private ReportParameterValues parameterValues;
/**
* The resource bundle factory is used when generating localized reports.
*/
private ResourceBundleFactory resourceBundleFactory;
/**
* The default constructor. Creates an empty but fully initialized report.
*/
public MasterReport() {
setElementType( new MasterReportType() );
setResourceBundleFactory( new LibLoaderResourceBundleFactory() );
this.reportConfiguration = new HierarchicalConfiguration( ClassicEngineBoot.getInstance().getGlobalConfig() );
this.parameterValues = new ReportParameterValues();
setPageDefinition( null );
final TableDataFactory dataFactory = new TableDataFactory();
dataFactory.addTable( "default", new DefaultTableModel() );
this.dataFactory = dataFactory;
setQuery( "default" );
// Add a listener that will handle keeping the ResourceManager in sync with changes to the Document Bundle
addReportModelListener( new DocumentBundleChangeHandler() );
this.reportEnvironment = new DefaultReportEnvironment( getConfiguration() );
this.parameterDefinition = new DefaultParameterDefinition();
final MemoryDocumentBundle documentBundle = new MemoryDocumentBundle();
documentBundle.getWriteableDocumentMetaData().setBundleType( ClassicEngineBoot.BUNDLE_TYPE );
documentBundle.getWriteableDocumentMetaData().setBundleAttribute( ODFMetaAttributeNames.Meta.NAMESPACE,
ODFMetaAttributeNames.Meta.CREATION_DATE, new Date() );
setBundle( documentBundle );
setContentBase( documentBundle.getBundleMainKey() );
addReportModelListener( new ResourceBundleChangeHandler() );
updateResourceBundleFactoryInternal();
}
public static ResourceBundleFactory computeAndInitResourceBundleFactory(
final ResourceBundleFactory resourceBundleFactory, final ReportEnvironment environment ) {
if ( resourceBundleFactory instanceof ExtendedResourceBundleFactory == false ) {
return resourceBundleFactory;
}
final ExtendedResourceBundleFactory rawResourceBundleFactory =
(ExtendedResourceBundleFactory) resourceBundleFactory;
try {
final ExtendedResourceBundleFactory extendedResourceBundleFactory =
(ExtendedResourceBundleFactory) rawResourceBundleFactory.clone();
if ( extendedResourceBundleFactory.getLocale() == null ) {
extendedResourceBundleFactory.setLocale( environment.getLocale() );
}
if ( extendedResourceBundleFactory.getTimeZone() == null ) {
extendedResourceBundleFactory.setTimeZone( environment.getTimeZone() );
}
return extendedResourceBundleFactory;
} catch ( CloneNotSupportedException e ) {
throw new IllegalStateException( "Cannot clone resource-bundle factory" );
}
}
/**
* Returns the resource bundle factory for this report definition. The {@link ResourceBundleFactory} is used in
* internationalized reports to create the resourcebundles holding the localized resources.
*
* @return the assigned resource bundle factory.
*/
public ResourceBundleFactory getResourceBundleFactory() {
return resourceBundleFactory;
}
/**
* Redefines the resource bundle factory for the report.
*
* @param resourceBundleFactory
* the new resource bundle factory, never null.
* @throws NullPointerException
* if the given ResourceBundleFactory is null.
*/
public void setResourceBundleFactory( final ResourceBundleFactory resourceBundleFactory ) {
ArgumentNullException.validate( "resourceBundleFactory", resourceBundleFactory );
this.resourceBundleFactory = resourceBundleFactory;
this.notifyNodePropertiesChanged();
}
public DocumentBundle getBundle() {
final Object o = getAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.BUNDLE );
if ( o instanceof DocumentBundle ) {
return (DocumentBundle) o;
}
return null;
}
public void setBundle( final DocumentBundle bundle ) {
setAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.BUNDLE, bundle );
}
public ReportParameterDefinition getParameterDefinition() {
return parameterDefinition;
}
public void setParameterDefinition( final ReportParameterDefinition parameterDefinition ) {
if ( parameterDefinition == null ) {
throw new NullPointerException();
}
this.parameterDefinition = parameterDefinition;
notifyNodePropertiesChanged();
}
public ModifiableReportParameterDefinition getModifiableParameterDefinition() {
if ( this.parameterDefinition instanceof ModifiableReportParameterDefinition ) {
return (ModifiableReportParameterDefinition) this.parameterDefinition;
}
return null;
}
public ReportEnvironment getReportEnvironment() {
return reportEnvironment;
}
public void setReportEnvironment( final ReportEnvironment reportEnvironment ) {
if ( reportEnvironment == null ) {
throw new NullPointerException();
}
this.reportEnvironment = reportEnvironment;
notifyNodePropertiesChanged();
}
public String getTitle() {
final DocumentBundle bundle = getBundle();
if ( bundle != null ) {
final Object o =
bundle.getMetaData().getBundleAttribute( ODFMetaAttributeNames.DublinCore.NAMESPACE,
ODFMetaAttributeNames.DublinCore.TITLE );
if ( o != null ) {
return o.toString();
}
}
return null;
}
/**
* Returns the logical page definition for this report.
*
* @return the page definition.
*/
public PageDefinition getPageDefinition() {
final PageDefinition pageDefinition =
(PageDefinition) getAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.PAGE_DEFINITION );
if ( pageDefinition == null ) {
return createDefaultPageDefinition();
}
return pageDefinition;
}
/**
* Defines the logical page definition for this report. If no format is defined the system's default page format is
* used.
* <p/>
* If there is no printer available and the JDK blocks during the printer discovery, you can set the
* {@link Configuration} key "org.pentaho.reporting.engine.classic.core.NoPrinterAvailable" to "false" and JFreeReport
* will use a hardcoded default page format instead.
*
* @param format
* the default format (<code>null</code> permitted).
*/
public void setPageDefinition( PageDefinition format ) {
if ( format == null ) {
format = createDefaultPageDefinition();
}
setAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.PAGE_DEFINITION, format );
notifyNodePropertiesChanged();
}
private PageDefinition createDefaultPageDefinition() {
final PageDefinition format;
final ExtendedConfiguration config = ClassicEngineBoot.getInstance().getExtendedConfig();
if ( config.getBoolProperty( ClassicEngineCoreModule.NO_PRINTER_AVAILABLE_KEY ) ) {
format = new SimplePageDefinition( new PageFormat() );
} else {
format = new SimplePageDefinition( PrinterJob.getPrinterJob().defaultPage() );
}
return format;
}
/**
* Returns the data factory that has been assigned to this report. The data factory will never be null.
*
* @return the data factory.
*/
public DataFactory getDataFactory() {
return dataFactory;
}
/**
* Sets the data factory for the report.
*
* @param dataFactory
* the data factory for the report, never null.
*/
public void setDataFactory( final DataFactory dataFactory ) {
if ( dataFactory == null ) {
throw new NullPointerException();
}
final DataFactory old = this.dataFactory;
this.dataFactory = dataFactory;
notifyNodeChildRemoved( old );
notifyNodeChildAdded( dataFactory );
}
/**
* Clones the report.
*
* @return the clone.
*/
public MasterReport clone() {
final MasterReport report = (MasterReport) super.clone();
report.reportConfiguration = (HierarchicalConfiguration) reportConfiguration.clone();
report.reportEnvironment = (ReportEnvironment) reportEnvironment.clone();
if ( report.reportEnvironment instanceof DefaultReportEnvironment ) {
// this is a ugly hack. Needs to be addressed in Sugar
final DefaultReportEnvironment dre = (DefaultReportEnvironment) report.reportEnvironment;
dre.update( report.reportConfiguration );
}
report.parameterDefinition = (ReportParameterDefinition) parameterDefinition.clone();
report.parameterValues = (ReportParameterValues) parameterValues.clone();
report.dataFactory = dataFactory.derive();
// Add a listener that will handle keeping the ResourceManager in sync with changes to the Document Bundle
report.addReportModelListener( new DocumentBundleChangeHandler() );
report.addReportModelListener( new ResourceBundleChangeHandler() );
return report;
}
public MasterReport derive( final boolean preserveElementInstanceIds ) {
final MasterReport report = (MasterReport) super.derive( preserveElementInstanceIds );
report.reportConfiguration = (HierarchicalConfiguration) reportConfiguration.clone();
report.reportEnvironment = (ReportEnvironment) reportEnvironment.clone();
if ( report.reportEnvironment instanceof DefaultReportEnvironment ) {
// this is a ugly hack. Needs to be addressed in Sugar
final DefaultReportEnvironment dre = (DefaultReportEnvironment) report.reportEnvironment;
dre.update( report.reportConfiguration );
}
report.parameterDefinition = (ReportParameterDefinition) parameterDefinition.clone();
report.parameterValues = (ReportParameterValues) parameterValues.clone();
report.dataFactory = dataFactory.derive();
// Add a listener that will handle keeping the ResourceManager in sync with changes to the Document Bundle
report.addReportModelListener( new DocumentBundleChangeHandler() );
report.addReportModelListener( new ResourceBundleChangeHandler() );
return report;
}
/**
* Returns the report configuration.
* <p/>
* The report configuration is automatically set up when the report is first created, and uses the global JFreeReport
* configuration as its parent.
*
* @return the report configuration.
*/
public ModifiableConfiguration getReportConfiguration() {
return reportConfiguration;
}
/**
* Returns the report's configuration.
*
* @return the configuration.
*/
public Configuration getConfiguration() {
return reportConfiguration;
}
/**
* Returns the resource manager that was responsible for loading the report. This method will return a default manager
* if the report had been constructed otherwise.
* <p/>
* The resource manager of the report should be used for all resource loading activities during the report processing.
*
* @return the resource manager, never null.
*/
public ResourceManager getResourceManager() {
if ( resourceManager == null ) {
resourceManager = new ResourceManager();
updateResourceBundleFactoryInternal();
}
return resourceManager;
}
/**
* Assigns a new resource manager or clears the current one. If no resource manager is set anymore, the next call to
* 'getResourceManager' will recreate one.
*
* @param resourceManager
* the new resource manager or null.
*/
public void setResourceManager( final ResourceManager resourceManager ) {
this.resourceManager = resourceManager;
notifyNodePropertiesChanged( resourceManager );
}
public ReportParameterValues getParameterValues() {
return parameterValues;
}
protected void updateChangedFlagInternal( final ReportElement element, final int type, final Object parameter ) {
fireModelLayoutChanged( element, type, parameter );
}
/**
* A helper method that serializes the element object.
*
* @param stream
* the stream to which the element should be serialized.
* @throws IOException
* if an IO error occured or a property was not serializable.
*/
private void writeObject( final ObjectOutputStream stream ) throws IOException {
stream.defaultWriteObject();
try {
final DocumentBundle bundle = getBundle();
stream.writeObject( bundle.getMetaData().getBundleType() );
final MemoryDocumentBundle mem = new MemoryDocumentBundle();
BundleUtilities.copyStickyInto( mem, bundle );
BundleUtilities.copyInto( mem, bundle, LegacyBundleResourceRegistry.getInstance().getRegisteredFiles(), true );
BundleUtilities.copyMetaData( mem, bundle );
mem.getWriteableDocumentMetaData().setBundleType( "application/vnd.pentaho.serialized-bundle" );
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BundleUtilities.writeAsZip( outputStream, mem );
stream.writeObject( outputStream.toByteArray() );
} catch ( ContentIOException e ) {
throw new IOException( "Unable to serialize the bundle", e );
}
}
/**
* A helper method that deserializes a object from the given stream.
*
* @param stream
* the stream from which to read the object data.
* @throws IOException
* if an IO error occured.
* @throws ClassNotFoundException
* if an referenced class cannot be found.
*/
private void readObject( final ObjectInputStream stream ) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
updateResourceBundleFactoryInternal();
reportConfiguration.reconnectConfiguration( ClassicEngineBoot.getInstance().getGlobalConfig() );
addReportModelListener( new DocumentBundleChangeHandler() );
try {
final String bundleType = (String) stream.readObject();
final byte[] bundleRawZip = (byte[]) stream.readObject();
final ResourceManager mgr = getResourceManager();
final Resource bundleResource = mgr.createDirectly( bundleRawZip, DocumentBundle.class );
final DocumentBundle bundle = (DocumentBundle) bundleResource.getResource();
final MemoryDocumentBundle mem = new MemoryDocumentBundle( getContentBase() );
BundleUtilities.copyStickyInto( mem, bundle );
BundleUtilities.copyInto( mem, bundle, LegacyBundleResourceRegistry.getInstance().getRegisteredFiles(), true );
BundleUtilities.copyMetaData( mem, bundle );
mem.getWriteableDocumentMetaData().setBundleType( bundleType );
setBundle( mem );
} catch ( ResourceException e ) {
throw new IOException( e );
}
}
private void updateResourceBundleFactoryInternal() {
if ( resourceBundleFactory instanceof ExtendedResourceBundleFactory ) {
final ExtendedResourceBundleFactory erbf = (ExtendedResourceBundleFactory) resourceBundleFactory;
erbf.setResourceLoader( getResourceManager(), getContentBase() );
}
}
public Integer getCompatibilityLevel() {
final Object definedCompatLevel =
getAttribute( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.COMAPTIBILITY_LEVEL );
if ( definedCompatLevel instanceof Integer ) {
return (Integer) definedCompatLevel;
}
return null;
}
public void setCompatibilityLevel( final Integer level ) {
setAttribute( AttributeNames.Internal.NAMESPACE, AttributeNames.Internal.COMAPTIBILITY_LEVEL, level );
}
public void updateLegacyConfiguration() {
}
public ElementStyleDefinition getStyleDefinition() {
return (ElementStyleDefinition) getAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.STYLE_SHEET );
}
public void setStyleDefinition( final ElementStyleDefinition styleDefinition ) {
setAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.STYLE_SHEET, styleDefinition );
}
public ResourceKey getStyleSheetReference() {
return (ResourceKey) getAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.STYLE_SHEET_REFERENCE );
}
public void setStyleSheetReference( final ResourceKey styleSheetReference ) {
setAttribute( AttributeNames.Core.NAMESPACE, AttributeNames.Core.STYLE_SHEET_REFERENCE, styleSheetReference );
}
public String getContentCacheKey() {
return (String) getAttribute( AttributeNames.Pentaho.NAMESPACE, AttributeNames.Pentaho.CONTENT_CACHE_KEY );
}
public void setContentCacheKey( final String contentCacheKey ) {
setAttribute( AttributeNames.Pentaho.NAMESPACE, AttributeNames.Pentaho.CONTENT_CACHE_KEY, contentCacheKey );
}
public boolean isStrictLegacyMode() {
return "true".equals( getReportConfiguration().getConfigProperty(
"org.pentaho.reporting.engine.classic.core.legacy.StrictCompatibility" ) );
}
public void setStrictLegacyMode( final boolean strict ) {
getReportConfiguration().setConfigProperty( "org.pentaho.reporting.engine.classic.core.legacy.StrictCompatibility",
String.valueOf( strict ) );
}
public ReportDefinition getMasterReport() {
return this;
}
}