/*
* JBoss, Home of Professional Open Source
* Copyright 2007, JBoss Inc., and others contributors as indicated
* by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*
* (C) 2005-2007,
*/
package org.jboss.soa.esb.actions.scripting;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import org.apache.bsf.BSFException;
import org.apache.bsf.BSFManager;
import org.apache.log4j.Logger;
import org.jboss.internal.soa.esb.util.StreamUtils;
import org.jboss.soa.esb.actions.AbstractActionPipelineProcessor;
import org.jboss.soa.esb.actions.ActionLifecycleException;
import org.jboss.soa.esb.actions.ActionProcessingException;
import org.jboss.soa.esb.helpers.ConfigTree;
import org.jboss.soa.esb.listeners.message.MessageDeliverException;
import org.jboss.soa.esb.message.Message;
import org.jboss.soa.esb.message.MessagePayloadProxy;
import org.jboss.soa.esb.util.ClassUtil;
/**
* <a href="http://jakarta.apache.org/bsf/">BSF</a> Scripting action pipeline processor.
* <p/>
* Based on {@link GroovyActionProcessor} by Gregory Pierce and Tom Fennelly.
* <p>
* <pre>
* <action name="helloWorld" class="org.jboss.soa.esb.actions.scripting.ScriptingAction">
* <property name="script" value="/scripts/helloWorld.ext"/>
* <-- The language property is not required; it is deduced via the script extension but can be overridden -->
* <property name="language" value="languageDescriptor"/>
* </action>
* </pre>
* </p>
* The {@link Message} is bound into the script with the name "message".
* The {@link ConfigTree} is bound into the script with the name "config".
* The {@link MessagePayloadProxy} is bound into the script with the name "payloadProxy".
* The {@link Logger} is bound into the script with the name "logger".
* <p/>
* The script can also be supplied to this action as the message payload, allowing you to
* dynamically supply the action script. For message based scripts to be executable,
* the "script" action property must be omitted and the "supportMessageBasedScripting" property must
* be set to "true". There are obvious security issues around executing message based scripts,
* so use this feature controlled manner.
* <p/>
* The following are the supported scripting languages, although the developer
* is responsible for including the appropriate 3rd party libraries (from bsf.jar's
* org/apache/bsf/Languages.properties):
*
* <pre>
* # List of script types and their associated scripting engines
* #
* # languageDescriptor = engineClass, ext1|ext2|... {, codebaseURL, ...}
* #
* # where exti are extensions for the language. Note that we leave
* # all the engines enabled now and allow them to fail at load time.
* # This way engines can be added by just adding to the classpath
* # without having to edit this file. Cheating, really, but it works.
* #
* javascript = org.apache.bsf.engines.javascript.JavaScriptEngine, js
* jacl = org.apache.bsf.engines.jacl.JaclEngine, jacl
* netrexx = org.apache.bsf.engines.netrexx.NetRexxEngine, nrx
* java = org.apache.bsf.engines.java.JavaEngine, java
* javaclass = org.apache.bsf.engines.javaclass.JavaClassEngine, class
* bml = org.apache.bml.ext.BMLEngine, bml
* vbscript = org.apache.bsf.engines.activescript.ActiveScriptEngine, vbs
* jscript = org.apache.bsf.engines.activescript.ActiveScriptEngine, jss
* perlscript = org.apache.bsf.engines.activescript.ActiveScriptEngine, pls
* perl = org.apache.bsf.engines.perl.PerlEngine, pl
* jpython = org.apache.bsf.engines.jpython.JPythonEngine, py
* jython = org.apache.bsf.engines.jython.JythonEngine, py
* lotusscript = org.apache.bsf.engines.lotusscript.LsEngine, lss
* xslt = org.apache.bsf.engines.xslt.XSLTEngine, xslt
* pnuts = pnuts.ext.PnutsBSFEngine, pnut
* beanbasic = org.apache.bsf.engines.beanbasic.BeanBasicEngine, bb
* beanshell = bsh.util.BeanShellBSFEngine, bsh
* ruby = org.jruby.javasupport.bsf.JRubyEngine, rb
* judoscript = com.judoscript.BSFJudoEngine, judo|jud
* </pre>
*
* @author dward at jboss.org
*/
public class ScriptingAction extends AbstractActionPipelineProcessor
{
private static Logger logger = Logger.getLogger(ScriptingAction.class);
// this is a map of extension(s) -> language
private static final Map<String,String> EXTN2LANG = new HashMap<String,String>();
static
{
InputStream is = null;
try
{
is = BSFManager.class.getClassLoader().getResourceAsStream("org/apache/bsf/Languages.properties");
is = new BufferedInputStream(is);
Properties props = new Properties();
props.load(is);
for (Enumeration<?> names = props.propertyNames(); names.hasMoreElements();)
{
String lang = (String)names.nextElement();
StringTokenizer st = new StringTokenizer(props.getProperty(lang), ",");
st.nextToken(); // throw out the engine class name
st = new StringTokenizer(st.nextToken(), "|");
while ( st.hasMoreTokens() ) // now get each extension
EXTN2LANG.put(st.nextToken().trim(), lang);
}
}
catch (IOException ioe) {}
finally
{
try { if (is != null) is.close(); } catch (Throwable t) {}
}
}
private ConfigTree config;
private MessagePayloadProxy payloadProxy;
private String script ;
private String source ;
private String language ;
public ScriptingAction(ConfigTree config)
{
this.config = config;
payloadProxy = new MessagePayloadProxy(config);
}
public void initialise() throws ActionLifecycleException
{
// attempt to get the script
String scriptPath = getAttribute("script");
final String source ;
if (scriptPath == null)
{
boolean supportMessageBasedScripting = config.getBooleanAttribute("supportMessageBasedScripting", false);
if(supportMessageBasedScripting)
{
if ( logger.isDebugEnabled() )
{
logger.debug("No script specified on action config " + config.getAttribute("name")
+ ". Expecting script to be in message.");
}
source = "Embedded script in message" ;
}
else
{
throw new ActionLifecycleException("'script' not configured on the action and message based scripting is not enabled ('supportMessageBasedScripting=false').");
}
}
else
{
InputStream scriptStream = null;
try
{
scriptStream = ClassUtil.getResourceAsStream(scriptPath, ScriptingAction.class);
if (scriptStream != null)
{
scriptStream = new BufferedInputStream(scriptStream);
script = new String( StreamUtils.readStream(scriptStream) );
}
else
{
throw new ActionLifecycleException("script '" + scriptPath + "' not found on classpath");
}
}
catch (Throwable t)
{
throw new ActionLifecycleException(t);
}
finally
{
try { if (scriptStream != null) scriptStream.close(); } catch (Throwable t) {}
}
source = scriptPath ;
}
this.source = source ;
// attempt to get the language
language = getAttribute("language");
if (language == null && script != null)
{
// the language attribute was not set but we found the script from the scriptPath,
// so deduce the extension from the scriptPath
int pos = scriptPath.lastIndexOf('.');
if (pos > -1 && pos < scriptPath.length()-1)
language = scriptPath.substring( pos+1, scriptPath.length() ).toLowerCase();
}
if (language != null)
{
if ( EXTN2LANG.containsKey(language) )
{
// either the user set the language property to the extension OR
// the language property was not set and we deduced the extension...
// either way, we now know two things:
// 1) we need to swap out the extension for the language
// 2) the language is already registered with BSF
language = EXTN2LANG.get(language);
}
else
{
// here we still need to make sure the language is registered with BSF
if ( !BSFManager.isLanguageRegistered(language) )
throw new ActionLifecycleException("language '" + language + "' not registered");
}
}
else
{
throw new ActionLifecycleException("language not specified");
}
}
public Message process(Message message) throws ActionProcessingException
{
BSFManager bsf = new BSFManager();
try
{
bsf.declareBean( "message", message, message.getClass() );
bsf.declareBean( "config", config, config.getClass() );
bsf.declareBean( "payloadProxy", payloadProxy, payloadProxy.getClass() );
bsf.declareBean( "logger", logger, logger.getClass() );
// NOTE: cannot use eval here since it does not work for all engines (jython in particular)
bsf.exec( language, source, 0, 0, getScript(message) );
}
catch (BSFException bsfe)
{
final String error = "Exception caught while processing script: '" + source + "'" ;
if (logger.isDebugEnabled())
{
logger.debug(error, bsfe) ;
}
throw new ActionProcessingException(error, bsfe);
}
finally
{
bsf.terminate();
}
return message;
}
private String getScript(Message message) throws ActionProcessingException
{
if (script != null)
return script;
else
{
Object messageScript;
try
{
messageScript = payloadProxy.getPayload(message);
}
catch (MessageDeliverException mde)
{
throw new ActionProcessingException(mde);
}
if (messageScript instanceof String)
return (String)messageScript;
else if (messageScript instanceof byte[])
return new String( (byte[])messageScript );
else
{
throw new ActionProcessingException("script not specified in message");
}
}
}
private String getAttribute(String name)
{
String value = config.getAttribute(name);
if (value != null)
{
value = value.trim();
if (value.length() == 0)
value = null;
}
return value;
}
}