/*
* This file is part of the WfMOpen project.
* Copyright (C) 2001-2003 Danet GmbH (www.danet.de), GS-AN.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* $Id: JSExecutor.java 2284 2007-02-27 14:34:23Z drmlipp $
*
* $Log$
* Revision 1.6 2007/01/22 16:23:14 drmlipp
* Fixed conversion of JS XML result with namespaces.
*
* Revision 1.5 2006/11/19 21:53:47 mlipp
* Finished support for native Java types.
*
* Revision 1.4 2006/11/17 12:19:25 drmlipp
* Added access to activity's unique key from JS.
*
* Revision 1.3 2006/09/29 12:32:13 drmlipp
* Consistently using WfMOpen as projct name now.
*
* Revision 1.2 2006/03/08 14:46:44 drmlipp
* Synchronized with 1.3.3p5.
*
* Revision 1.1.1.4.6.5 2006/03/07 13:51:31 drmlipp
* Finished transition to E4X usage for actual parameter evaluation.
*
* Revision 1.1.1.4.6.4 2006/02/08 19:29:46 drmlipp
* Added E4X support.
*
* Revision 1.1.1.4.6.3 2005/12/20 22:05:31 drmlipp
* Removed deprecated exceptions and completed argument convertion.
*
* Revision 1.1.1.4.6.2 2005/12/15 22:55:54 drmlipp
* Continued XML support for JS tool.
*
* Revision 1.1.1.4.6.1 2005/12/11 20:31:34 drmlipp
* Evaluating JavaScript XML result.
*
* Revision 1.1.1.4 2004/08/18 15:17:39 drmlipp
* Update to 1.2
*
* Revision 1.46 2004/04/12 19:33:52 lipp
* Clarified application invocation interface.
*
* Revision 1.45 2004/04/01 13:28:58 lipp
* Improved naming once more.
*
* Revision 1.44 2004/04/01 09:42:32 lipp
* Adapted context name.
*
* Revision 1.43 2004/04/01 09:32:07 lipp
* Improved tool agent context implementtaion.
*
* Revision 1.42 2004/03/31 19:36:20 lipp
* Completed implementation of Activity.abandon(String).
*
* Revision 1.41 2004/03/29 11:46:17 lipp
* Preliminary version of "abandoned" for JSExceutor.
*
* Revision 1.40 2004/03/28 20:41:10 lipp
* Started implementing call to abandon from JSExecutor.
*
* Revision 1.39 2004/02/20 09:53:01 lipp
* Fixed exception handling.
*
* Revision 1.38 2004/02/13 10:01:34 lipp
* Changed result type for result provider to Map which is more
* appropriate.
*
* Revision 1.37 2004/01/26 15:11:19 montag
* Tool JSExecutor now returns SchemaType objects.
*
* Revision 1.36 2003/11/26 17:00:37 lipp
* Using new ResultProvider.
*
* Revision 1.35 2003/11/26 15:21:18 lipp
* Proper handling of (top-level) RemoteException.
*
* Revision 1.34 2003/09/24 13:45:52 lipp
* Added support for Date type in process relevant data.
*
* Revision 1.33 2003/06/27 08:51:44 lipp
* Fixed copyright/license information.
*
* Revision 1.32 2003/06/17 14:19:07 lipp
* Improved comment.
*
* Revision 1.31 2003/05/15 07:46:42 lipp
* Proper handling of JavaScript default double result.
*
* Revision 1.30 2003/05/06 13:21:30 lipp
* Resolved cyclic dependency.
*
* Revision 1.29 2003/05/02 14:55:58 lipp
* Resolved some more package dependencies.
*
* Revision 1.28 2003/04/26 16:11:14 lipp
* Moved some classes to reduce package dependencies.
*
* Revision 1.27 2003/04/25 14:50:58 lipp
* Fixed javadoc errors and warnings.
*
* Revision 1.26 2003/04/02 11:20:06 lipp
* Moved type adaption to framework.
*
* Revision 1.25 2003/04/02 09:30:05 lipp
* Supporting more data types.
*
* Revision 1.24 2003/03/31 16:50:28 huaiyang
* Logging using common-logging.
*
* Revision 1.23 2003/03/28 15:41:09 lipp
* Removed no longer needed running trace.
*
* Revision 1.22 2003/02/12 11:57:31 lipp
* Improved deadlock (RemoteException) handling for tools. Imroved debug
* information.
*
* Revision 1.21 2003/02/11 14:50:48 lipp
* Better exception/logger messages.
*
* Revision 1.20 2002/12/19 21:37:42 lipp
* Reorganized interfaces.
*
* Revision 1.19 2002/11/22 13:16:44 lipp
* Made ResourceNotAvailableException a RemoteException.
*
* Revision 1.18 2002/11/20 09:26:11 lipp
* New method doFinish for better transaction handling in tools.
*
* Revision 1.17 2002/11/11 09:52:54 lipp
* Added retries for result actions.
*
* Revision 1.16 2002/10/06 20:14:38 lipp
* Updated argument handling.
*
* Revision 1.15 2002/10/02 12:15:49 lipp
* Fixed logging bug.
*
* Revision 1.14 2002/10/02 10:58:13 lipp
* Modifications for tool invocation.
*
* Revision 1.13 2002/09/30 13:06:03 lipp
* Removed access to activity.
*
* Revision 1.12 2002/09/25 15:15:47 lipp
* Towards full functionallity...
*
* Revision 1.11 2002/09/24 15:53:52 lipp
* Now really invoking...
*
* Revision 1.10 2002/09/24 12:25:19 lipp
* setResult implemented.
*
* Revision 1.9 2002/09/23 20:31:28 lipp
* Implemented async/sync invocation.
*
* Revision 1.8 2002/09/23 15:12:39 lipp
* Extended tool implementation definition and usage.
*
* Revision 1.7 2002/09/23 12:00:20 huaiyang
* Remove the mothods of set/getEmailAddress.
*
* Revision 1.6 2002/09/22 19:57:09 lipp
* Fixed doc error, prepared execution loop.
*
* Revision 1.5 2002/09/17 13:54:01 huaiyang
* Add method of setEmailaddress.
*
* Revision 1.4 2002/09/17 09:20:12 lipp
* Added ApplicationNotStoppedException.
*
* Revision 1.3 2002/09/16 15:30:41 huaiyang
* Identify it as seriazable so that can be called remotely.
*
* Revision 1.2 2002/09/11 14:16:54 lipp
* Extended attributes.
*
* Revision 1.1 2002/09/02 11:03:37 lipp
* Started javascript tool.
*
*/
package de.danet.an.workflow.tools.rhino;
import java.io.IOException;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.rmi.RemoteException;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.apache.xmlbeans.XmlSaxHandler;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.EvaluatorException;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.NotAFunctionException;
import org.mozilla.javascript.PropertyException;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.Wrapper;
import org.mozilla.javascript.xml.XMLObject;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import de.danet.an.util.sax.BodyFilter;
import de.danet.an.util.sax.NamespaceAttributesFilter;
import de.danet.an.workflow.api.Activity;
import de.danet.an.workflow.api.ActivityUniqueKey;
import de.danet.an.workflow.api.ExternalReference;
import de.danet.an.workflow.api.FormalParameter;
import de.danet.an.workflow.api.SAXEventBuffer;
import de.danet.an.workflow.spis.aii.ApplicationNotStoppedException;
import de.danet.an.workflow.spis.aii.CannotExecuteException;
import de.danet.an.workflow.spis.aii.ContextRequester;
import de.danet.an.workflow.spis.aii.ResultProvider;
import de.danet.an.workflow.spis.aii.ResultProvider.ExceptionResult;
import de.danet.an.workflow.spis.aii.ToolAgent;
import de.danet.an.workflow.spis.aii.ToolAgentContext;
import de.danet.an.workflow.util.SAXEventBufferImpl;
import de.danet.an.workflow.util.XPDLUtil;
/**
* This class provides a tool that executes JavaScript.
*
* <P>Enabling log level debug for this class logs invokation, the
* script to be executed, completion and termination.
*
* @author <a href="mailto:lipp@danet.de"></a>
* @version $Revision: 2284 $
*/
public class JSExecutor implements ToolAgent, ResultProvider, ContextRequester,
Serializable {
private static final org.apache.commons.logging.Log logger
= org.apache.commons.logging.LogFactory.getLog(JSExecutor.class);
/** The script to be executed on invocation. */
private String script = null;
/** The tool agent context. */
private ThreadLocal ctx = new ThreadLocal ();
/** The result container. */
private ThreadLocal result = new ThreadLocal ();
/**
* Creates an instance of <code>JSExecutor</code>
* with all attributes initialized to default values.
*/
public JSExecutor () {
}
/**
* Get the value of script.
* @return value of script.
* @see #setScript
*/
public String getScript() {
return script;
}
/**
* Set the value of script.
* @param newScript value to assign to script.
* @see #getScript
*/
public void setScript(String newScript) {
this.script = newScript;
}
/**
* Makes a context available to the tool agent.
* @param context the tool agent context
*/
public void setToolAgentContext (ToolAgentContext context) {
ctx.set(context);
}
/**
* Describe <code>invoke</code> method here.
*
* @param activity a <code>WfActivity</code> value
* @param formPars the formal parameters.
* @param map a <code>Map</code> value
* @throws CannotExecuteException if an error occurs
* @throws RemoteException if a system level error occurs
*/
public void invoke(Activity activity, FormalParameter[] formPars, Map map)
throws CannotExecuteException, RemoteException {
ActivityUniqueKey auk = null;
try {
// Do not attempt to perform any operation on the activity
// except key() and uniqueKey() before executing the
// script. Else, the activity becomes part of the EJB
// transaction and is locked, i.e. all accesses (even
// display in the management client) are deferred until
// the javascript has completed. The time this takes is
// not controllable.
auk = activity.uniqueKey();
if (logger.isDebugEnabled()) {
logger.debug ("Invoked for " + auk + ", script:");
logger.debug (script);
}
if (script == null) {
throw new CannotExecuteException ("No script.");
}
Context cx = Context.enter();
Scriptable scope = prepareScope (cx, activity, formPars, map);
Reader sr = new StringReader (script);
cx.evaluateReader (scope, sr, "<script>", 1, null);
result.set(convertResult(cx, scope, formPars));
} catch (AbandonedException e) {
// thrown when the activity was deliberatly abandoned.
result.set (new ExceptionResult (e.getMessage ()));
} catch (RemoteException e) {
throw e;
} catch (IOException e) {
logger.error (e.getMessage(), e);
throw new CannotExecuteException (e.getMessage());
} catch (SAXException e) {
logger.error (e.getMessage(), e);
throw new CannotExecuteException (e.getMessage());
} catch (JavaScriptException e) {
logger.error (e.getMessage(), e);
throw new CannotExecuteException (e.getMessage());
} finally {
Context.exit();
if (logger.isDebugEnabled()) {
logger.debug ("Finished invocation of " + auk);
}
}
}
/**
* A base class for creating context methods.
*/
public abstract class FunctionBase
extends ScriptableObject implements Function {
/**
* The class name.
* @return the class name
*/
public String getClassName() {
return "FunctionBase";
}
/**
* Instances cannot be called as constructors.
* @param cx the context
* @param scope the scope
* @param args the arguments
* @return never returns a result
* @throws JavaScriptException if an error occurs
*/
public Scriptable construct (Context cx, Scriptable scope,
Object[] args)
throws JavaScriptException {
throw new EvaluatorException
("Function cannot be used as constructor");
}
};
/**
* Prepare an environment for evaluating the script.
* @param cx the context.
* @param activity the activity.
* @param formPars the formal parameter definitons.
* @param map the actual parameter values.
* @return
* @throws JavaScriptException
*/
private Scriptable prepareScope
(Context cx, Activity activity, FormalParameter[] formPars, Map map)
throws RemoteException {
Scriptable scope = cx.initStandardObjects(null);
ScriptableObject wfe = new ScriptableObject () {
public String getClassName() {
return "ToolAgentContext";
}
};
Function fo = new FunctionBase () {
public Object call (Context cx, Scriptable scope,
Scriptable thisObj, Object[] args)
throws JavaScriptException {
if (logger.isDebugEnabled ()) {
logger.debug ("abandon called with: " + args[0]);
}
throw new AbandonedException ((String)args[0]);
}
};
wfe.defineProperty ("abandon", fo, ScriptableObject.PERMANENT);
wfe.defineProperty
("activityUniqueKey", Context.javaToJS(activity.uniqueKey(), scope),
ScriptableObject.PERMANENT | ScriptableObject.READONLY);
scope.put("scriptingContext", scope, wfe);
prepareArguments(cx, scope, formPars, map);
return scope;
}
/**
* @param cx the context.
* @param scope the scope.
* @param formPars the formal parameter definitons.
* @param map the actual parameter values.
*/
protected void prepareArguments
(Context cx, Scriptable scope, FormalParameter[] formPars, Map map) {
ScriptableObject args = new ScriptableObject () {
public String getClassName() {
return "Arguments";
}
};
for (int i = 0; i < formPars.length; i++) {
String fp = formPars[i].id();
args.defineProperty
(fp,
convertArgument(cx, scope, formPars[i].type(), map.get(fp)),
formPars[i].mode() == FormalParameter.Mode.IN
? ScriptableObject.PERMANENT | ScriptableObject.READONLY
: ScriptableObject.PERMANENT);
}
scope.put("args", scope, args);
}
/**
* Additionally convert SchemaType arguments
* @see JSExecutor#convertArgument
*/
protected Object convertArgument
(Context cx, Scriptable scope, Object argType, Object argument) {
if (argType instanceof ExternalReference) {
if (XPDLUtil.isJavaType((ExternalReference)argType)) {
return Context.javaToJS(argument, scope);
}
}
return argument;
}
/**
* Convert the result.
* @param cx the context.
* @param scope the scope.
* @param formPars the formal parameter definitons.
* @return
* @throws JavaScriptException
* @throws SAXException
*/
protected Map convertResult
(Context cx, Scriptable scope, FormalParameter[] fps)
throws JavaScriptException, SAXException {
Scriptable sres = (Scriptable)scope.get ("args", scope);
Map resData = new HashMap ();
for (int i = 0; i < fps.length; i++) {
if (fps[i].mode() == FormalParameter.Mode.IN) {
continue;
}
String fpn = fps[i].id();
Object v = sres.get(fpn, sres);
v = convertResultValue(cx, scope, fps[i], v);
resData.put (fpn, v);
}
return resData;
}
/**
* @param cx the context.
* @param scope the scope.
* @param formPars the formal parameter definitons.
* @param value
* @return
* @throws JavaScriptException
* @throws SAXException
*/
protected Object convertResultValue
(Context cx, Scriptable scope, FormalParameter fp, Object value)
throws JavaScriptException, SAXException {
if (fp.type() instanceof ExternalReference) {
if (XPDLUtil.isJavaType((ExternalReference)fp.type())) {
try {
return Context.jsToJava
(value,
XPDLUtil.getJavaType((ExternalReference)fp.type()));
} catch (ClassNotFoundException e) {
(new IllegalArgumentException(e.getMessage())).initCause(e);
}
}
}
if (fp.type() == Long.class && (value instanceof Double)) {
return new Long (((Double)value).longValue());
}
if (value instanceof Wrapper) {
return ((Wrapper)value).unwrap ();
}
if (value instanceof XMLObject) {
SAXEventBufferImpl seb = new SAXEventBufferImpl();
if (((XMLObject)value).getClassName().equals("XMLList")) {
seb.startDocument();
for (int i = 0; true; i++) {
Object item = ((XMLObject)value).get(i, (XMLObject)value);
if (item.equals(Scriptable.NOT_FOUND)) {
break;
}
xmlObjectToSax(seb, (XMLObject)item, true);
}
seb.endDocument();
} else {
xmlObjectToSax(seb, (XMLObject)value, false);
}
seb.pack();
return seb;
}
if ((value instanceof Scriptable)
&& ((Scriptable)value).getClassName().equals ("Date")) {
Scriptable s = (Scriptable)value;
Object gt = Scriptable.NOT_FOUND;
for (Scriptable c = s;
c != null && gt.equals(Scriptable.NOT_FOUND);
c = c.getPrototype()) {
gt = c.get("getTime", s);
}
Number millis = (Number)((Function)gt)
.call(cx, scope, s, new Object[] {});
return new Date (millis.longValue());
}
return value;
}
/**
* Serialize a JavaScript XML object into the SAX event buffer.
*
* @param seb the SAX event buffer
* @param xmlObj the XML object
* @param fragment if <code>startDocument</code> and
* <code>endDocument</code> events are to be suppressed
* @throws SAXException
*/
private void xmlObjectToSax
(SAXEventBufferImpl seb, XMLObject xmlObj, boolean fragment)
throws SAXException {
Wrapper wrap = (Wrapper) ScriptableObject.callMethod
((XMLObject)xmlObj, "getXmlObject", new Object[0]);
XmlObject result = (XmlObject) wrap.unwrap();
XmlCursor cursor = result.newCursor();
result = cursor.getObject();
ContentHandler ch = new NamespaceAttributesFilter(seb);
if (fragment) {
ch = new BodyFilter (ch);
}
result.save(ch, seb, (new XmlOptions()).setSaveOuter());
}
/* Comment copied from interface. */
public Object result () {
Object res = result.get();
result.set (null);
return res;
}
/**
* Describe <code>terminate</code> method here.
*
* @param activity a <code>WfActivity</code> value
* @throws ApplicationNotStoppedException if the application could
* not be terminated.
*/
public void terminate(Activity activity)
throws ApplicationNotStoppedException {
throw new ApplicationNotStoppedException
("Terminate not implemented for JSExecutor.");
}
}