/*
* 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 - 2009 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.IOException;
import java.io.ObjectInputStream;
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.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.parameters.DefaultParameterDefinition;
import org.pentaho.reporting.engine.classic.core.parameters.ReportParameterDefinition;
import org.pentaho.reporting.engine.classic.core.util.ReportParameterValues;
import org.pentaho.reporting.engine.classic.core.util.LibLoaderResourceBundleFactory;
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.docbundle.DocumentBundle;
import org.pentaho.reporting.libraries.docbundle.MemoryDocumentBundle;
import org.pentaho.reporting.libraries.docbundle.ODFMetaAttributeNames;
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
{
private static final Log logger = LogFactory.getLog(MasterReport.class);
/**
* Key for the 'report name' property.
*/
public static final String NAME_PROPERTY = "report.name";
/**
* 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 ModifiableConfiguration 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 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());
}
/**
* 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)
{
if (resourceBundleFactory == null)
{
throw new NullPointerException("ResourceBundleFactory must not be null");
}
super.setResourceBundleFactory(resourceBundleFactory);
}
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 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 name of the report.
* <p/>
* You can reference the report name in your XML report template file using the key '<code>report.name</code>'.
*
* @return the name.
*/
public String getName()
{
final Object nameFromProperties = getProperty(MasterReport.NAME_PROPERTY);
if (nameFromProperties == null)
{
return super.getName();
}
return String.valueOf(nameFromProperties);
}
/**
* Sets the name of the report.
* <p/>
* The report name is stored as a property (key {@link MasterReport#NAME_PROPERTY}) inside the report properties. If
* you supply <code>null</code> as the name, the property is removed.
*
* @param name the name of the report.
*/
public void setName(final String name)
{
super.setName(name);
super.setProperty(MasterReport.NAME_PROPERTY, name);
}
/**
* 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.
* @throws CloneNotSupportedException this should never happen.
*/
public Object clone()
throws CloneNotSupportedException
{
final MasterReport report = (MasterReport) super.clone();
report.reportConfiguration = (ModifiableConfiguration) 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());
return report;
}
public Element derive()
throws CloneNotSupportedException
{
final MasterReport report = (MasterReport) super.derive();
report.reportConfiguration = (ModifiableConfiguration) 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());
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();
resourceManager.registerDefaults();
updateResourceBundleFactory();
}
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();
}
public ReportParameterValues getParameterValues()
{
return parameterValues;
}
/**
* Adds a property to the report.
* <p/>
* If a property with the given name already exists, the property will be updated with the new value. If the supplied
* value is <code>null</code>, the property will be removed.
* <p/>
* Developers are free to add any properties they want to a report, and then display those properties in the report.
* For example, you might add a 'user.name' property, so that you can display the username in the header of a report.
*
* @param key the key.
* @param value the value.
* @deprecated Properties should not be used. Use the master-report's parameters instead.
*/
public void setProperty(final String key, final Object value)
{
super.setProperty(key, value);
logger.warn("Use of deprecated features: Do not use Report-Properties, use the parameter-values directly.");
parameterValues.put(key, value);
}
protected void updateChangedFlagInternal(final ReportElement element, final int type, final Object parameter)
{
fireModelLayoutChanged(element, type, parameter);
}
/**
* 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();
if (getBundle() == null)
{
final MemoryDocumentBundle documentBundle = new MemoryDocumentBundle(getContentBase());
documentBundle.getWriteableDocumentMetaData().setBundleType(ClassicEngineBoot.BUNDLE_TYPE);
documentBundle.getWriteableDocumentMetaData().setBundleAttribute
(ODFMetaAttributeNames.Meta.NAMESPACE, ODFMetaAttributeNames.Meta.CREATION_DATE, new Date());
setBundle(documentBundle);
setContentBase(documentBundle.getBundleMainKey());
setResourceManager(documentBundle.getResourceManager());
}
addReportModelListener(new DocumentBundleChangeHandler());
}
/**
* 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.getParameter() instanceof AttributeChange == false || event.getElement() instanceof MasterReport == false)
{
return;
}
// This is an attribute change event on the master report ... see if it is one we are concerned about
final AttributeChange attributeChange = (AttributeChange) event.getParameter();
if (AttributeNames.Core.NAMESPACE.equals(attributeChange.getNamespace()) &&
AttributeNames.Core.BUNDLE.equals(attributeChange.getName()))
{
final Object value = attributeChange.getNewValue();
if ((value instanceof DocumentBundle) == false)
{
return;
}
if (event.getElement() instanceof MasterReport)
{
// Insert the DocumentBundle's ResourceManager as the MasterReports resource manager
log.debug("DocumentBundle change detected - changing the ResourceManager for the MasterReport");
final MasterReport report = (MasterReport) event.getElement();
final DocumentBundle newDocumentBundle = (DocumentBundle) value;
final ResourceManager resourceManager = newDocumentBundle.getResourceManager();
report.setResourceManager(resourceManager);
}
else
{
log.warn("Could not replace the ResourceKey on a DocumentBundle change - the element is not a MasterReport");
}
}
}
}
public static ResourceBundleFactory computeAndInitResourceBundleFactory
(final ResourceBundleFactory resourceBundleFactory,
final ReportEnvironment environment) throws ReportProcessingException
{
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 ReportProcessingException("Cannot clone resource-bundle factory");
}
}
}