/*
* 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 javax.swing.table.TableModel;
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.function.Expression;
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 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 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;
}
}