/*!
* 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) 2002-2013 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.extensions.datasources.scriptable;
import java.util.LinkedHashMap;
import javax.swing.table.TableModel;
import org.apache.bsf.BSFException;
import org.apache.bsf.BSFManager;
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.DataFactory;
import org.pentaho.reporting.engine.classic.core.DataRow;
import org.pentaho.reporting.engine.classic.core.ReportDataFactoryException;
import org.pentaho.reporting.engine.classic.core.ResourceBundleFactory;
import org.pentaho.reporting.engine.classic.core.states.LegacyDataRowWrapper;
import org.pentaho.reporting.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
/**
* A datafactory that uses a bean-scripting framework script to produce a tablemodel.
*
* @author Thomas Morgner
*/
public class ScriptableDataFactory extends AbstractDataFactory {
private static final Log logger = LogFactory.getLog( ScriptableDataFactory.class );
private LinkedHashMap<String, String> queries;
private String language;
private transient BSFManager interpreter;
private transient LegacyDataRowWrapper dataRowWrapper;
private String script;
private String shutdownScript;
public ScriptableDataFactory() {
queries = new LinkedHashMap<String, String>();
}
public String getLanguage() {
return language;
}
public void setLanguage( final String language ) {
this.language = language;
}
public void setQuery( final String name, final String value ) {
if ( value == null ) {
queries.remove( name );
} else {
queries.put( name, value );
}
}
public String getScript() {
return script;
}
public void setScript( final String script ) {
this.script = script;
}
public String getShutdownScript() {
return shutdownScript;
}
public void setShutdownScript( final String shutdownScript ) {
this.shutdownScript = shutdownScript;
}
public String getQuery( final String name ) {
return queries.get( name );
}
public String[] getQueryNames() {
return queries.keySet().toArray( new String[queries.size()] );
}
/**
* Creates a new interpreter instance.
*
* @return the interpreter or null, if there was an error.
*/
protected BSFManager createInterpreter() throws BSFException {
final BSFManager interpreter = new BSFManager();
initializeInterpreter( interpreter );
return interpreter;
}
/**
* Initializes the Bean-Scripting Framework manager.
*
* @param interpreter
* the BSF-Manager that should be initialized.
* @throws BSFException
* if an error occurred.
*/
protected void initializeInterpreter( final BSFManager interpreter ) throws BSFException {
dataRowWrapper = new LegacyDataRowWrapper();
interpreter.declareBean( "dataRow", dataRowWrapper, DataRow.class ); //$NON-NLS-1$
interpreter.declareBean( "configuration", getConfiguration(), Configuration.class ); //$NON-NLS-1$
interpreter.declareBean( "contextKey", getContextKey(), ResourceKey.class ); //$NON-NLS-1$
interpreter.declareBean( "resourceManager", getResourceManager(), ResourceManager.class ); //$NON-NLS-1$
interpreter.declareBean( "resourceBundleFactory", getResourceBundleFactory(), ResourceBundleFactory.class ); //$NON-NLS-1$
interpreter.declareBean( "dataFactoryContext", getDataFactoryContext(), ResourceBundleFactory.class ); //$NON-NLS-1$
if ( script != null ) {
interpreter.exec( getLanguage(), "startup-script", 1, 1, getScript() ); //$NON-NLS-1$
}
}
/**
* Queries a datasource. The string 'query' defines the name of the query. The Parameterset given here may contain
* more data than actually needed for the query.
* <p/>
* The parameter-dataset may change between two calls, do not assume anything, and do not hold references to the
* parameter-dataset or the position of the columns in the dataset.
*
* @param query
* the query string
* @param parameters
* the parameters for the query
* @return the result of the query as table model.
* @throws ReportDataFactoryException
* if an error occurred while performing the query.
*/
public TableModel queryData( final String query, final DataRow parameters ) throws ReportDataFactoryException {
final String queryScript = queries.get( query );
if ( queryScript == null ) {
throw new ReportDataFactoryException( "No such query" );
}
if ( interpreter == null ) {
try {
this.interpreter = createInterpreter();
} catch ( BSFException e ) {
throw new ReportDataFactoryException( "Failed to initialize the BSF-Framework", e );
}
}
try {
dataRowWrapper.setParent( parameters );
final Object o = interpreter.eval( getLanguage(), "expression", 1, 1, queryScript );
if ( o instanceof TableModel == false ) {
throw new ReportDataFactoryException( "Resulting value is not a tablemodel" );
}
return (TableModel) o; //$NON-NLS-1$
} catch ( ReportDataFactoryException rde ) {
throw rde;
} catch ( Exception e ) {
throw new ReportDataFactoryException( "Evaluation error", e );
}
}
public ScriptableDataFactory clone() {
final ScriptableDataFactory dataFactory = (ScriptableDataFactory) super.clone();
dataFactory.queries = (LinkedHashMap<String, String>) queries.clone();
dataFactory.interpreter = null;
dataFactory.dataRowWrapper = null;
return dataFactory;
}
/**
* Returns a copy of the data factory that is not affected by its ancestor and holds no connection to the ancestor
* anymore. A data-factory will be derived at the beginning of the report processing.
*
* @return a copy of the data factory.
*/
public DataFactory derive() {
return clone();
}
/**
* Closes the data factory and frees all resources held by this instance.
*/
public void close() {
if ( this.interpreter != null && this.shutdownScript != null ) {
try {
this.interpreter.eval( getLanguage(), "shutdown-script", 1, 1, getShutdownScript() );
} catch ( BSFException e ) {
logger.warn( "Failed to evaluate shutdown-script", e );
}
}
this.dataRowWrapper = null;
this.interpreter = null;
}
/**
* Checks whether the query would be executable by this datafactory. This performs a rough check, not a full query.
*
* @param query
* @param parameters
* @return
*/
public boolean isQueryExecutable( final String query, final DataRow parameters ) {
return queries.containsKey( query );
}
public void cancelRunningQuery() {
// not all scripting engines actually support that.
if ( interpreter != null ) {
interpreter.terminate();
}
}
}