/* * 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.cache; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.pentaho.reporting.engine.classic.core.AbstractDataFactory; import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot; import org.pentaho.reporting.engine.classic.core.CompoundDataFactory; import org.pentaho.reporting.engine.classic.core.CompoundDataFactorySupport; import org.pentaho.reporting.engine.classic.core.DataFactory; import org.pentaho.reporting.engine.classic.core.DataFactoryContext; import org.pentaho.reporting.engine.classic.core.DataFactoryDesignTimeSupport; import org.pentaho.reporting.engine.classic.core.DataRow; import org.pentaho.reporting.engine.classic.core.MetaTableModel; import org.pentaho.reporting.engine.classic.core.ReportDataFactoryException; import org.pentaho.reporting.engine.classic.core.StaticDataRow; import org.pentaho.reporting.engine.classic.core.metadata.DataFactoryMetaData; import org.pentaho.reporting.engine.classic.core.metadata.MetaDataLookupException; import org.pentaho.reporting.engine.classic.core.util.CloseableTableModel; import org.pentaho.reporting.libraries.base.config.Configuration; import org.pentaho.reporting.libraries.base.util.ArgumentNullException; import javax.swing.table.TableModel; import java.util.HashMap; public class CachingDataFactory extends AbstractDataFactory implements CompoundDataFactorySupport { private enum QueryStyle { General, Static, FreeForm } private static final Log logger = LogFactory.getLog( CachingDataFactory.class ); private DataCache dataCache; private HashMap<DataCacheKey, TableModel> sessionCache; private CompoundDataFactory backend; private boolean closed; private boolean debugDataSources; private boolean profileDataSources; private boolean noClose; private static final String[] EMPTY_NAMES = new String[0]; public CachingDataFactory( final DataFactory backend, final boolean dataCacheEnabled ) { this( backend, false, dataCacheEnabled ); } public CachingDataFactory( final DataFactory backend, final boolean noClose, final boolean dataCacheEnabled ) { this( backend, noClose, produceDefault( dataCacheEnabled ) ); } private static DataCache produceDefault( final boolean dataCacheEnabled ) { if ( dataCacheEnabled ) { return DataCacheFactory.getCache(); } else { return null; } } public CachingDataFactory( final DataFactory backend, final boolean noClose, final DataCache dataCache ) { if ( backend == null ) { throw new NullPointerException(); } this.noClose = noClose; if ( noClose ) { this.backend = CompoundDataFactory.normalize( backend, false ); } else { this.backend = CompoundDataFactory.normalize( backend, true ); } final Configuration configuration = ClassicEngineBoot.getInstance().getGlobalConfig(); this.sessionCache = new HashMap<DataCacheKey, TableModel>(); this.dataCache = dataCache; this.debugDataSources = "true".equals( configuration.getConfigProperty( "org.pentaho.reporting.engine.classic.core.DebugDataSources" ) ); this.profileDataSources = "true" .equals( configuration.getConfigProperty( "org.pentaho.reporting.engine.classic.core.ProfileDataSources" ) ); } public void initialize( final DataFactoryContext dataFactoryContext ) throws ReportDataFactoryException { super.initialize( dataFactoryContext ); backend.initialize( dataFactoryContext ); } public boolean isQueryExecutable( final String query, final DataRow parameters ) { if ( query == null ) { throw new NullPointerException(); } if ( parameters == null ) { throw new NullPointerException(); } if ( backend.isQueryExecutable( query, parameters ) ) { return true; } return false; } public boolean isFreeFormQueryExecutable( final String query, final DataRow parameter ) { if ( query == null ) { throw new NullPointerException(); } if ( parameter == null ) { throw new NullPointerException(); } return backend.isFreeFormQueryExecutable( query, parameter ); } public TableModel queryStatic( final String query, final DataRow parameters ) throws ReportDataFactoryException { if ( query == null ) { throw new NullPointerException(); } if ( parameters == null ) { throw new NullPointerException(); } final DataCacheKey key = createCacheKey( query, parameters, false ); if ( key != null ) { TableModel model = sessionCache.get( key ); if ( model == null ) { model = dataCache.get( key ); } if ( model != null ) { logger.debug( "Returning cached data for static query '" + query + "'." ); return wrapAsIndexed( model ); } } if ( !backend.isStaticQueryExecutable( query, parameters ) ) { throw new ReportDataFactoryException( "The specified query '" + query + "' is not executable here." ); } TableModel data = queryInternal( query, parameters, QueryStyle.Static ); if ( data == null ) { return null; } data = putInCache( key, data ); return wrapAsIndexed( data ); } public TableModel queryDesignTimeStructureStatic( final String query, final DataRow parameters ) throws ReportDataFactoryException { if ( query == null ) { throw new NullPointerException(); } if ( parameters == null ) { throw new NullPointerException(); } final DataCacheKey key = createCacheKey( query, parameters, false ); if ( key != null ) { TableModel model = sessionCache.get( key ); if ( model == null ) { model = dataCache.get( key ); } if ( model != null ) { logger.debug( "Returning cached data for design-time query '" + query + "'." ); return wrapAsIndexed( model ); } } if ( !backend.isStaticQueryExecutable( query, parameters ) ) { throw new ReportDataFactoryException( "The specified query '" + query + "' is not executable here." ); } TableModel data = backend.queryDesignTimeStructureStatic( query, parameters ); if ( data == null ) { return null; } data = putInCache( key, data ); return wrapAsIndexed( data ); } public TableModel queryFreeForm( final String query, final DataRow parameters ) throws ReportDataFactoryException { if ( query == null ) { throw new NullPointerException(); } if ( parameters == null ) { throw new NullPointerException(); } final DataCacheKey key = createCacheKey( query, parameters, false ); if ( key != null ) { TableModel model = sessionCache.get( key ); if ( model == null ) { model = dataCache.get( key ); } if ( model != null ) { logger.debug( "Returning cached data for freeform query '" + query + "'." ); return wrapAsIndexed( model ); } } if ( !backend.isFreeFormQueryExecutable( query, parameters ) ) { throw new ReportDataFactoryException( "The specified query '" + query + "' is not executable here." ); } TableModel data = queryInternal( query, parameters, QueryStyle.FreeForm ); if ( data == null ) { return null; } data = putInCache( key, data ); return wrapAsIndexed( data ); } public TableModel queryDesignTimeStructureFreeForm( final String query, final DataRow parameters ) throws ReportDataFactoryException { if ( query == null ) { throw new NullPointerException(); } if ( parameters == null ) { throw new NullPointerException(); } final DataCacheKey key = createCacheKey( query, parameters, false ); if ( key != null ) { TableModel model = sessionCache.get( key ); if ( model == null ) { model = dataCache.get( key ); } if ( model != null ) { logger.debug( "Returning cached data for free-form design time query '" + query + "'." ); return wrapAsIndexed( model ); } } if ( !backend.isFreeFormQueryExecutable( query, parameters ) ) { throw new ReportDataFactoryException( "The specified query '" + query + "' is not executable here." ); } TableModel data = backend.queryDesignTimeStructureFreeForm( query, parameters ); if ( data == null ) { return null; } data = putInCache( key, data ); return wrapAsIndexed( data ); } public boolean isStaticQueryExecutable( final String query, final DataRow parameters ) { if ( query == null ) { throw new NullPointerException(); } if ( parameters == null ) { throw new NullPointerException(); } return backend.isStaticQueryExecutable( query, parameters ); } /** * Queries a datasource. The string 'query' defines the name of the query. The Parameterset given here may contain * more data than actually needed. * <p/> * The dataset may change between two calls, do not assume anything! * * @param query * @param parameters * @return */ public TableModel queryData( final String query, final DataRow parameters ) throws ReportDataFactoryException { ArgumentNullException.validate( "query", query ); ArgumentNullException.validate( "parameters", parameters ); final DataCacheKey key = createCacheKey( query, parameters, false ); if ( key != null ) { TableModel model = sessionCache.get( key ); if ( model == null ) { model = dataCache.get( key ); } if ( model != null ) { logger.debug( "Returning cached data for query '" + query + "'." ); return wrapAsIndexed( model ); } } if ( backend.isQueryExecutable( query, parameters ) ) { TableModel data = queryInternal( query, parameters, QueryStyle.General ); if ( data != null ) { data = putInCache( key, data ); return wrapAsIndexed( data ); } } throw new ReportDataFactoryException( "The specified query '" + query + "' is not executable here." ); } public TableModel queryDesignTimeStructure( final String query, final DataRow parameters ) throws ReportDataFactoryException { ArgumentNullException.validate( "query", query ); ArgumentNullException.validate( "parameters", parameters ); final DataCacheKey key = createCacheKey( query, parameters, true ); if ( key != null ) { final TableModel model = dataCache.get( key ); if ( model != null ) { logger.debug( "Returning cached data for design-time query '" + query + "'." ); return wrapAsIndexed( model ); } } if ( backend.isQueryExecutable( query, parameters ) ) { TableModel data = queryDesignTimeStructureInternal( query, parameters ); if ( data != null ) { data = putInCache( key, data ); return wrapAsIndexed( data ); } } throw new ReportDataFactoryException( "The specified query '" + query + "' is not executable here." ); } private TableModel putInCache( final DataCacheKey key, TableModel data ) { if ( key != null ) { final TableModel newData = dataCache.put( key, data ); if ( newData != data && data instanceof CloseableTableModel ) { final CloseableTableModel closeableTableModel = (CloseableTableModel) data; closeableTableModel.close(); } sessionCache.put( key, newData ); data = newData; } return data; } private TableModel wrapAsIndexed( final TableModel data ) { if ( data instanceof MetaTableModel ) { return new IndexedMetaTableModel( (MetaTableModel) data ); } else { return new IndexedTableModel( data ); } } private DataCacheKey createCacheKey( final String query, final DataRow parameters, final boolean designTime ) { try { if ( dataCache == null ) { return null; } final DataCacheKey key; DataFactoryMetaData metaData = backend.getMetaData(); final String[] referencedFields = metaData.getReferencedFields( backend, query, parameters ); if ( referencedFields != null ) { final Object queryHash = metaData.getQueryHash( backend, query, parameters ); if ( queryHash == null ) { logger.debug( "Query hash is null, caching is disabled for query '" + query + "'." ); key = null; } else { key = new DataCacheKey(); for ( int i = 0; i < referencedFields.length; i++ ) { final String field = referencedFields[i]; key.addParameter( field, parameters.get( field ) ); } key.addAttribute( DataCacheKey.QUERY_CACHE, queryHash ); key.addAttribute( DataFactoryDesignTimeSupport.DESIGN_TIME, designTime ); // The data cache maps are immutable - make sure of it. key.makeReadOnly(); } } else { logger.debug( "No Referenced fields, caching is disabled for query '" + query + "'." ); key = null; } return key; } catch ( final MetaDataLookupException mle ) { logger.error( String.format( "Data-source used for query '%s' does not provide metadata. Caching will be disabled.", query ), mle ); return null; } } private TableModel queryInternal( final String query, final DataRow parameters, final QueryStyle queryStyle ) throws ReportDataFactoryException { if ( profileDataSources && CachingDataFactory.logger.isDebugEnabled() ) { CachingDataFactory.logger.debug( System.identityHashCode( Thread.currentThread() ) + ": Query processing time: Starting" ); } final long startTime = System.currentTimeMillis(); try { final StaticDataRow params = new StaticDataRow( parameters ); final TableModel dataFromQuery; switch ( queryStyle ) { case FreeForm: dataFromQuery = backend.queryFreeForm( query, params ); break; case Static: dataFromQuery = backend.queryStatic( query, params ); break; case General: dataFromQuery = backend.queryData( query, params ); break; default: throw new IllegalStateException(); } if ( dataFromQuery == null ) { // final DefaultTableModel value = new DefaultTableModel(); if ( debugDataSources && CachingDataFactory.logger.isDebugEnabled() ) { CachingDataFactory.logger.debug( "Query failed for query '" + query + '\'' ); } return null; } else { if ( debugDataSources && CachingDataFactory.logger.isDebugEnabled() ) { CachingDataFactory.printTableModelContents( dataFromQuery ); } // totally new query here. CachingDataFactory.logger.debug( "Query returned a data-set for query '" + query + '\'' ); return dataFromQuery; } } finally { final long queryTime = System.currentTimeMillis(); if ( profileDataSources && CachingDataFactory.logger.isDebugEnabled() ) { CachingDataFactory.logger.debug( System.identityHashCode( Thread.currentThread() ) + ": Query processing time: " + ( ( queryTime - startTime ) / 1000.0 ) ); } } } private TableModel queryDesignTimeStructureInternal( final String query, final DataRow parameters ) throws ReportDataFactoryException { if ( profileDataSources && CachingDataFactory.logger.isDebugEnabled() ) { CachingDataFactory.logger.debug( System.identityHashCode( Thread.currentThread() ) + ": Query processing time: Starting" ); } final long startTime = System.currentTimeMillis(); try { return backend.queryDesignTimeStructure( query, parameters ); } finally { final long queryTime = System.currentTimeMillis(); if ( profileDataSources && CachingDataFactory.logger.isDebugEnabled() ) { CachingDataFactory.logger.debug( System.identityHashCode( Thread.currentThread() ) + ": Query processing time: " + ( ( queryTime - startTime ) / 1000.0 ) ); } } } /** * Closes the report data factory and all report data instances that have been returned by this instance. */ public void close() { if ( closed == false ) { for ( final TableModel map : sessionCache.values() ) { if ( map instanceof CloseableTableModel == false ) { continue; } final CloseableTableModel ct = (CloseableTableModel) map; ct.close(); } sessionCache.clear(); if ( noClose == false ) { backend.close(); } closed = true; } } /** * Derives a freshly initialized report data factory, which is independend of the original data factory. Opening or * Closing one data factory must not affect the other factories. * * @return nothing, the method dies instead. * @throws UnsupportedOperationException * as this class is not derivable. */ public DataFactory derive() { // If you see that exception, then you've probably tried to use that // datafactory from outside of the report processing. You deserve the // exception in that case .. throw new UnsupportedOperationException( "The CachingReportDataFactory cannot be derived." ); } /** * Prints a table model to standard output. * * @param mod * the model. */ public static void printTableModelContents( final TableModel mod ) { if ( mod == null ) { throw new NullPointerException(); } logger.debug( "Tablemodel contains " + mod.getRowCount() + " rows." ); //$NON-NLS-1$ //$NON-NLS-2$ for ( int i = 0; i < mod.getColumnCount(); i++ ) { logger.debug( "Column: " + i + " Name = " + mod.getColumnName( i ) + "; DataType = " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + mod.getColumnClass( i ) ); } logger.debug( "Checking the data inside" ); //$NON-NLS-1$ for ( int rows = 0; rows < mod.getRowCount(); rows++ ) { for ( int i = 0; i < mod.getColumnCount(); i++ ) { final Object value = mod.getValueAt( rows, i ); logger.debug( "ValueAt (" + rows + ", " + i + ") is " + value ); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } } } public String[] getQueryNames() { return EMPTY_NAMES; } public void cancelRunningQuery() { } public CachingDataFactory clone() { final CachingDataFactory cdf = (CachingDataFactory) super.clone(); cdf.backend = (CompoundDataFactory) backend.clone(); cdf.sessionCache = (HashMap<DataCacheKey, TableModel>) sessionCache.clone(); return cdf; } public DataFactory getDataFactoryForQuery( final String queryName, final boolean freeform ) { return backend.getDataFactoryForQuery( queryName, freeform ); } }