/*!
* 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-2016 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.modules.misc.datafactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot;
import org.pentaho.reporting.engine.classic.core.DataFactory;
import org.pentaho.reporting.engine.classic.core.DataFactoryContext;
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.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.pentaho.reporting.libraries.base.util.StringUtils;
import org.pentaho.reporting.libraries.fonts.encoding.EncodingRegistry;
import org.pentaho.reporting.libraries.resourceloader.ResourceData;
import org.pentaho.reporting.libraries.resourceloader.ResourceException;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceLoadingException;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import javax.swing.table.TableModel;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public final class DataFactoryScriptingSupport implements Cloneable, Serializable {
private static class QueryCarrier implements Serializable {
private String query;
private String scriptingLanguage;
private String script;
private QueryCarrier( final String query, final String scriptingLanguage, final String script ) {
this.query = query;
this.scriptingLanguage = scriptingLanguage;
this.script = script;
}
public String getQuery() {
return query;
}
public String getScriptingLanguage() {
return scriptingLanguage;
}
public String getScript() {
return script;
}
}
public static class ScriptHelper {
private ScriptContext context;
private String defaultScriptLanguage;
private ResourceManager resourceManager;
private ResourceKey contextKey;
public ScriptHelper( final ScriptContext context, final String defaultScriptLanguage,
final ResourceManager resourceManager, final ResourceKey contextKey ) {
this.context = context;
this.defaultScriptLanguage = defaultScriptLanguage;
this.resourceManager = resourceManager;
this.contextKey = contextKey;
}
public Object eval( final String script ) throws ScriptException {
return eval( script, defaultScriptLanguage );
}
public Object eval( final String script, final String scriptLanguage ) throws ScriptException {
final ScriptEngine scriptEngine = new ScriptEngineManager().getEngineByName( scriptLanguage );
if ( scriptEngine == null ) {
throw new ScriptException( String.format(
"DataFactoryScriptingSupport: Failed to locate scripting engine for language '%s'.", scriptLanguage ) );
}
scriptEngine.setContext( context );
return scriptEngine.eval( script );
}
public Object evalFile( final String file ) throws ScriptException {
return evalFile( file, defaultScriptLanguage );
}
public Object evalFile( final String file, final String language ) throws ScriptException {
return evalFile( file, language, EncodingRegistry.getPlatformDefaultEncoding() );
}
public Object evalFile( final String file, final String defaultScriptLanguage, final String encoding )
throws ScriptException {
final ResourceKey resourceKey = createKeyFromString( resourceManager, contextKey, file );
if ( resourceKey == null ) {
throw new ScriptException( "Unable to load script" );
}
try {
final ResourceData loadedResource = resourceManager.load( resourceKey );
final byte[] resource = loadedResource.getResource( resourceManager );
return eval( new String( resource, encoding ), defaultScriptLanguage );
} catch ( ResourceLoadingException e ) {
throw new ScriptException( e );
} catch ( UnsupportedEncodingException e ) {
throw new ScriptException( e );
}
}
private ResourceKey createKeyFromString( final ResourceManager resourceManager, final ResourceKey contextKey,
final String file ) {
try {
if ( contextKey != null ) {
return resourceManager.deriveKey( contextKey, file );
}
} catch ( ResourceException re ) {
// failed to load from context
logger.debug( "Failed to load datasource as derived path: ", re );
}
try {
return resourceManager.createKey( new URL( file ) );
} catch ( ResourceException re ) {
logger.debug( "Failed to load datasource as URL: ", re );
} catch ( MalformedURLException e ) {
//
}
try {
return resourceManager.createKey( new File( file ) );
} catch ( ResourceException re ) {
// failed to load from context
logger.debug( "Failed to load datasource as file: ", re );
}
return null;
}
}
private static class QueryScriptContext {
private static final String NASHORN_GLOBAL = "nashorn.global";
private Invocable invocableEngine;
private ScriptEngine scriptEngine;
private ScriptContext context;
private QueryScriptContext() {
}
public void init( final String queryName, final String scriptLanguage, final String script,
final ScriptContext globalContext, final ResourceManager resourceManager, final ResourceKey contextKey,
final DataFactory dataFactory, final Configuration configuration,
final ResourceBundleFactory resourceBundleFactory ) throws ReportDataFactoryException {
this.context = new SimpleScriptContext();
if ( globalContext != null ) {
final Bindings bindings = globalContext.getBindings( ScriptContext.ENGINE_SCOPE );
if ( bindings.containsKey( NASHORN_GLOBAL ) ) {
Bindings nashornGlobal = (Bindings) bindings.get( NASHORN_GLOBAL );
this.context.getBindings( ScriptContext.ENGINE_SCOPE ).putAll( nashornGlobal );
}
this.context.getBindings( ScriptContext.ENGINE_SCOPE ).putAll( bindings );
} else {
context.setAttribute( "dataFactory", dataFactory, ScriptContext.ENGINE_SCOPE );
context.setAttribute( "configuration", configuration, ScriptContext.ENGINE_SCOPE );
context.setAttribute( "resourceManager", resourceManager, ScriptContext.ENGINE_SCOPE );
context.setAttribute( "contextKey", contextKey, ScriptContext.ENGINE_SCOPE );
context.setAttribute( "resourceBundleFactory", resourceBundleFactory, ScriptContext.ENGINE_SCOPE );
}
this.context.setAttribute( "scriptHelper", new ScriptHelper( this.context, scriptLanguage, resourceManager,
contextKey ), ScriptContext.ENGINE_SCOPE );
this.scriptEngine = new ScriptEngineManager().getEngineByName( scriptLanguage );
if ( scriptEngine instanceof Invocable == false ) {
throw new ReportDataFactoryException( String.format( "Query script language '%s' is not usable.",
scriptLanguage ) );
}
this.invocableEngine = (Invocable) scriptEngine;
this.scriptEngine.setContext( this.context );
try {
this.scriptEngine.eval( script );
} catch ( ScriptException e ) {
throw new ReportDataFactoryException( "DataFactoryScriptingSupport: Failed to initialize local query script: "
+ queryName, e );
}
try {
this.invocableEngine.invokeFunction( "initQuery" );
} catch ( ScriptException e ) {
throw new ReportDataFactoryException( "DataFactoryScriptingSupport: Failed to invoke local init method: "
+ queryName, e );
} catch ( NoSuchMethodException e ) {
// ignored ..
logger.debug( "Global script does not contain an 'init' function" );
}
}
public String computeQuery( final String query, final String queryName, final DataRow parameter )
throws ReportDataFactoryException {
if ( invocableEngine == null ) {
return query;
}
try {
final Object computeQuery = this.invocableEngine.invokeFunction( "computeQuery", query, queryName, parameter );
final Object translated = convert( computeQuery );
if ( translated == null ) {
throw new ReportDataFactoryException(
"DataFactoryScriptingSupport: computeQuery method did not return a valid query." );
}
return String.valueOf( translated );
} catch ( ScriptException e ) {
throw new ReportDataFactoryException( "DataFactoryScriptingSupport: Failed to invoke computeQuery method.", e );
} catch ( NoSuchMethodException e ) {
// ignored ..
logger.debug( "Query script does not contain an 'computeQuery' function" );
return query;
}
}
public TableModel postProcessResult( final String query, final String queryName, final DataRow parameter,
final TableModel dataSet ) throws ReportDataFactoryException {
if ( invocableEngine == null ) {
return dataSet;
}
try {
final Object computeQuery =
this.invocableEngine.invokeFunction( "postProcessResult", query, queryName, parameter, dataSet );
final Object translated = convert( computeQuery );
if ( translated instanceof TableModel == false ) {
throw new ReportDataFactoryException(
"DataFactoryScriptingSupport: postProcessResult method did not return a valid query." );
}
return (TableModel) translated;
} catch ( ScriptException e ) {
throw new ReportDataFactoryException(
"DataFactoryScriptingSupport: Failed to invoke postProcessResult method.", e );
} catch ( NoSuchMethodException e ) {
// ignored ..
logger.debug( "Query script does not contain an 'postProcessResult' function" );
return dataSet;
}
}
public void shutdown() throws ReportDataFactoryException {
if ( invocableEngine == null ) {
return;
}
try {
invocableEngine.invokeFunction( "shutdownQuery" );
} catch ( ScriptException e ) {
throw new ReportDataFactoryException( "DataFactoryScriptingSupport: Failed to invoke query shutdown method.", e );
} catch ( NoSuchMethodException e ) {
// ignored ..
logger.debug( "Global script does not contain an 'shutdownQuery' function" );
}
}
public String[] computeAdditionalQueryFields( final String query, final String queryName )
throws ReportDataFactoryException {
if ( invocableEngine == null ) {
return new String[0];
}
try {
final Object computeQuery = this.invocableEngine.invokeFunction( "computeQueryFields", query, queryName );
final Object translated = convert( computeQuery );
if ( translated == null ) {
return null;
}
final Object[] rawArray;
if ( translated instanceof Object[] ) {
rawArray = (Object[]) translated;
} else if ( translated instanceof Collection ) {
final Collection c = (Collection) translated;
rawArray = c.toArray();
} else {
rawArray = new Object[] { translated };
}
final ArrayList<String> retval = new ArrayList<String>();
for ( int i = 0; i < rawArray.length; i++ ) {
final Object o = rawArray[i];
if ( o != null ) {
retval.add( String.valueOf( o ) );
}
}
return retval.toArray( new String[retval.size()] );
} catch ( ScriptException e ) {
throw new ReportDataFactoryException(
"DataFactoryScriptingSupport: Failed to invoke computeQueryFields method.", e );
} catch ( NoSuchMethodException e ) {
// ignored ..
logger.debug( "Query script does not contain an 'computeQueryFields' function" );
return null;
}
}
}
private static ArrayList<ScriptValueConverter> converters;
private static final Log logger = LogFactory.getLog( DataFactoryScriptingSupport.class );
private String globalScriptLanguage;
private String globalScript;
private HashMap<String, QueryCarrier> queryMappings;
private transient HashMap<String, QueryScriptContext> contextsByQuery;
private transient ScriptContext globalScriptContext;
private transient Invocable globalScriptEngine;
private transient ResourceManager resourceManager;
private transient ResourceKey contextKey;
private transient DataFactory dataFactory;
private transient Configuration configuration;
private transient ResourceBundleFactory resourceBundleFactory;
private transient boolean initialized;
private transient DataFactoryContext dataFactoryContext;
public DataFactoryScriptingSupport() {
queryMappings = new HashMap<String, QueryCarrier>();
contextsByQuery = new HashMap<String, QueryScriptContext>();
}
public Object clone() {
try {
final DataFactoryScriptingSupport clone = (DataFactoryScriptingSupport) super.clone();
clone.queryMappings = (HashMap<String, QueryCarrier>) queryMappings.clone();
clone.globalScriptContext = null;
clone.contextsByQuery = (HashMap<String, QueryScriptContext>) contextsByQuery.clone();
clone.contextsByQuery.clear();
clone.dataFactory = null;
clone.resourceBundleFactory = null;
clone.resourceManager = null;
clone.configuration = null;
clone.contextKey = null;
clone.initialized = false;
return clone;
} catch ( CloneNotSupportedException e ) {
throw new IllegalStateException();
}
}
public void setQuery( final String name, final String query, final String scriptLanguage, final String script ) {
this.queryMappings.put( name, new QueryCarrier( query, scriptLanguage, script ) );
}
public String getScriptingLanguage( final String name ) {
final QueryCarrier queryCarrier = queryMappings.get( name );
if ( queryCarrier == null ) {
return null;
}
return queryCarrier.getScriptingLanguage();
}
protected String computeScriptingLanguage( final String name ) {
final QueryCarrier queryCarrier = queryMappings.get( name );
if ( queryCarrier == null ) {
return null;
}
final String scriptingLanguage = queryCarrier.getScriptingLanguage();
if ( scriptingLanguage == null ) {
return this.globalScriptLanguage;
}
return scriptingLanguage;
}
public String getScript( final String name ) {
final QueryCarrier queryCarrier = queryMappings.get( name );
if ( queryCarrier == null ) {
return null;
}
return queryCarrier.getScript();
}
public String getQuery( final String name ) {
final QueryCarrier queryCarrier = queryMappings.get( name );
if ( queryCarrier == null ) {
return null;
}
return queryCarrier.getQuery();
}
public String[] getQueryNames() {
return queryMappings.keySet().toArray( new String[queryMappings.size()] );
}
public String getGlobalScript() {
return globalScript;
}
public void setGlobalScript( final String globalScript ) {
this.globalScript = globalScript;
}
public String getGlobalScriptLanguage() {
return globalScriptLanguage;
}
public void setGlobalScriptLanguage( final String globalScriptLanguage ) {
this.globalScriptLanguage = globalScriptLanguage;
}
public void initialize( final DataFactory dataFactory, final DataFactoryContext dataFactoryContext )
throws ReportDataFactoryException {
if ( globalScriptContext != null ) {
return;
}
this.dataFactory = dataFactory;
this.resourceManager = dataFactoryContext.getResourceManager();
this.contextKey = dataFactoryContext.getContextKey();
this.configuration = dataFactoryContext.getConfiguration();
this.resourceBundleFactory = dataFactoryContext.getResourceBundleFactory();
this.dataFactoryContext = dataFactoryContext;
globalScriptContext = new SimpleScriptContext();
globalScriptContext.setAttribute( "dataFactory", dataFactory, ScriptContext.ENGINE_SCOPE );
globalScriptContext.setAttribute( "configuration", configuration, ScriptContext.ENGINE_SCOPE );
globalScriptContext.setAttribute( "resourceManager", resourceManager, ScriptContext.ENGINE_SCOPE );
globalScriptContext.setAttribute( "contextKey", contextKey, ScriptContext.ENGINE_SCOPE );
globalScriptContext.setAttribute( "resourceBundleFactory", resourceBundleFactory, ScriptContext.ENGINE_SCOPE );
if ( StringUtils.isEmpty( globalScriptLanguage ) ) {
return;
}
globalScriptContext.setAttribute( "scriptHelper", new ScriptHelper( globalScriptContext, globalScriptLanguage,
resourceManager, contextKey ), ScriptContext.ENGINE_SCOPE );
final ScriptEngine maybeInvocableEngine = new ScriptEngineManager().getEngineByName( globalScriptLanguage );
if ( maybeInvocableEngine == null ) {
throw new ReportDataFactoryException( String.format(
"DataFactoryScriptingSupport: Failed to locate scripting engine for language '%s'.", globalScriptLanguage ) );
}
if ( maybeInvocableEngine instanceof Invocable == false ) {
return;
}
this.globalScriptEngine = (Invocable) maybeInvocableEngine;
maybeInvocableEngine.setContext( globalScriptContext );
try {
maybeInvocableEngine.eval( globalScript );
} catch ( ScriptException e ) {
throw new ReportDataFactoryException( "DataFactoryScriptingSupport: Failed to execute datafactory init script.",
e );
}
}
protected void callGlobalInitialize( final DataRow parameter ) throws ReportDataFactoryException {
if ( initialized ) {
return;
}
try {
initialized = true;
if ( globalScriptEngine != null ) {
this.globalScriptEngine.invokeFunction( "init", parameter );
}
} catch ( ScriptException e ) {
throw new ReportDataFactoryException( "DataFactoryScriptingSupport: Failed to invoke global init method.", e );
} catch ( NoSuchMethodException e ) {
// ignored ..
logger.debug( "Global script does not contain an 'init' function" );
}
}
public String computeQuery( final String queryName, final DataRow parameter ) throws ReportDataFactoryException {
callGlobalInitialize( parameter );
final String queryScriptLanguage = computeScriptingLanguage( queryName );
final String queryScript = getScript( queryName );
if ( StringUtils.isEmpty( queryScriptLanguage ) || StringUtils.isEmpty( queryScript ) ) {
return getQuery( queryName );
}
QueryScriptContext queryScriptContext = contextsByQuery.get( queryName );
if ( queryScriptContext == null ) {
queryScriptContext = new QueryScriptContext();
queryScriptContext.init( queryName, queryScriptLanguage, queryScript, globalScriptContext, resourceManager,
contextKey, dataFactory, configuration, resourceBundleFactory );
contextsByQuery.put( queryName, queryScriptContext );
}
return queryScriptContext.computeQuery( getQuery( queryName ), queryName, parameter );
}
public TableModel postProcessResult( final String queryName, final DataRow parameter, final TableModel result )
throws ReportDataFactoryException {
callGlobalInitialize( parameter );
final String queryScriptLanguage = computeScriptingLanguage( queryName );
final String queryScript = getScript( queryName );
if ( StringUtils.isEmpty( queryScriptLanguage ) || StringUtils.isEmpty( queryScript ) ) {
return result;
}
QueryScriptContext queryScriptContext = contextsByQuery.get( queryName );
if ( queryScriptContext == null ) {
queryScriptContext = new QueryScriptContext();
queryScriptContext.init( queryName, queryScriptLanguage, queryScript, globalScriptContext, resourceManager,
contextKey, dataFactory, configuration, resourceBundleFactory );
contextsByQuery.put( queryName, queryScriptContext );
}
return queryScriptContext.postProcessResult( getQuery( queryName ), queryName, parameter, result );
}
public String[] computeAdditionalQueryFields( final String queryName, final DataRow parameter )
throws ReportDataFactoryException {
callGlobalInitialize( parameter );
final String queryScriptLanguage = computeScriptingLanguage( queryName );
final String queryScript = getScript( queryName );
if ( StringUtils.isEmpty( queryScriptLanguage ) || StringUtils.isEmpty( queryScript ) ) {
return new String[0];
}
QueryScriptContext queryScriptContext = contextsByQuery.get( queryName );
if ( queryScriptContext == null ) {
queryScriptContext = new QueryScriptContext();
queryScriptContext.init( queryName, queryScriptLanguage, queryScript, globalScriptContext, resourceManager,
contextKey, dataFactory, configuration, resourceBundleFactory );
contextsByQuery.put( queryName, queryScriptContext );
}
return queryScriptContext.computeAdditionalQueryFields( getQuery( queryName ), queryName );
}
public void shutdown() {
for ( final Map.Entry<String, QueryScriptContext> entry : contextsByQuery.entrySet() ) {
try {
final QueryScriptContext context = entry.getValue();
context.shutdown();
} catch ( ReportDataFactoryException se ) {
logger.warn( "Failed to shut down query script context: " + entry.getKey() );
}
}
if ( globalScriptEngine != null ) {
try {
globalScriptEngine.invokeFunction( "shutdown" );
} catch ( ScriptException e ) {
logger.warn( "DataFactoryScriptingSupport: Failed to invoke global shutdown method.", e );
} catch ( NoSuchMethodException e ) {
// ignored ..
logger.debug( "Global script does not contain an 'shutdown' function" );
}
}
globalScriptContext = null;
resourceManager = null;
contextKey = null;
dataFactory = null;
resourceBundleFactory = null;
configuration = null;
initialized = false;
}
public static Object convert( final Object object ) {
if ( object == null ) {
return null;
}
synchronized ( DataFactoryScriptingSupport.class ) {
if ( converters == null ) {
converters = new ArrayList<ScriptValueConverter>();
final Configuration globalConfig = ClassicEngineBoot.getInstance().getGlobalConfig();
final Iterator propertyKeys =
globalConfig
.findPropertyKeys( "org.pentaho.reporting.engine.classic.core.modules.misc.datafactory.script-value-converters." );
while ( propertyKeys.hasNext() ) {
final String key = (String) propertyKeys.next();
final String impl = globalConfig.getConfigProperty( key );
final ScriptValueConverter converter =
(ScriptValueConverter) ObjectUtilities.loadAndInstantiate( impl, ScriptValueConverter.class,
ScriptValueConverter.class );
if ( converter != null ) {
converters.add( converter );
}
}
}
}
if ( converters.isEmpty() ) {
return object;
}
for ( final ScriptValueConverter converter : converters ) {
final Object convert = converter.convert( object );
if ( convert != null ) {
return convert;
}
}
return object;
}
public boolean containsQuery( final String query ) {
return queryMappings.containsKey( query );
}
public void remove( final String name ) {
queryMappings.remove( name );
}
private void readObject( final ObjectInputStream stream ) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
contextsByQuery = new HashMap<String, QueryScriptContext>();
}
}