/* * Copyright 2006 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: Redistributions of source code * must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of * conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. Neither the name of the Sun Microsystems nor the names of * is contributors may be used to endorse or promote products derived from this software * without specific prior written permission. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * XPathScriptEngine.java * @author A. Sundararajan */ package org.jbpm.pvm.internal.script; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.script.AbstractScriptEngine; import javax.script.Bindings; import javax.script.Compilable; import javax.script.CompiledScript; import javax.script.Invocable; import javax.script.ScriptContext; import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import javax.script.ScriptException; import javax.script.SimpleBindings; import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; import javax.xml.namespace.QName; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathFactory; import javax.xml.xpath.XPathFunction; import javax.xml.xpath.XPathFunctionResolver; import javax.xml.xpath.XPathVariableResolver; import org.w3c.dom.Document; import org.xml.sax.InputSource; public class XPathScriptEngine extends AbstractScriptEngine implements Compilable { // my factory, may be null private ScriptEngineFactory factory; private XPathFactory xpathFactory; // special context variables for XPath result type and input public static final String XPATH_RESULT_TYPE = "com.sun.script.xpath.resultType"; public static final String XPATH_INPUT_SRC = "com.sun.script.xpath.inputSource"; // XML namespace prefixes and URIs. public static final String XMLNS_COLON = "xmlns:"; public static final String XPATH_CONTEXT_PREFIX = "context"; public static final String XPATH_CONTEXT_URI = "http://www.sun.com/java/jsr223/xpath/context"; private Document objectData; public XPathScriptEngine() { xpathFactory = XPathFactory.newInstance(); } // my implementation for CompiledScript private class XPathCompiledScript extends CompiledScript { private XPathExpression expr; XPathCompiledScript(XPathExpression expr) { this.expr = expr; } public ScriptEngine getEngine() { return XPathScriptEngine.this; } public Object eval(ScriptContext ctx) throws ScriptException { return evalXPath(expr, ctx); } } public CompiledScript compile(String script) throws ScriptException { XPathExpression expr = compileXPath(script, context); return new XPathCompiledScript(expr); } public CompiledScript compile(Reader reader) throws ScriptException { return compile(readFully(reader)); } public Object eval(String str, ScriptContext ctx) throws ScriptException { XPathExpression expr = compileXPath(str, ctx); return evalXPath(expr, ctx); } public Object eval(Reader reader, ScriptContext ctx) throws ScriptException { return eval(readFully(reader), ctx); } public ScriptEngineFactory getFactory() { synchronized (this) { if (factory == null) { factory = new XPathScriptEngineFactory(); } } return factory; } public Bindings createBindings() { return new SimpleBindings(); } void setFactory(ScriptEngineFactory factory) { this.factory = factory; } // Internals only below this point // find a variable of given qname in given context private static Object findVariable(QName qname, ScriptContext ctx) { String name; int scope; if (XPATH_CONTEXT_URI.equals(qname.getNamespaceURI())) { name = qname.getLocalPart(); synchronized (ctx) { scope = ctx.getAttributesScope(name); if (scope != -1) { return ctx.getAttribute(name, scope); } // else fallthru } } if (qname.getPrefix() == null || "".equals(qname.getPrefix())) { name = qname.getLocalPart(); } else { name = qname.getPrefix() + ":" + qname.getLocalPart(); } synchronized (ctx) { scope = ctx.getAttributesScope(name); if (scope != -1) { return ctx.getAttribute(name, scope); } // else fallthru } return null; } private static void collectNamespaces(Map map, Bindings scope) { for (String key : scope.keySet()) { if (key.startsWith(XMLNS_COLON)) { Object uri = scope.get(key); // collect all variables starting with "xmlns:" and // collect the prefix to URI mappings. String prefix = key.substring(XMLNS_COLON.length()); if (uri instanceof String) { String tmp = (String) uri; if (tmp.length() != 0) { map.put(prefix, tmp); } } } } } private static NamespaceContext makeNamespaceContext(ScriptContext ctx) { // namespace prefix-to-URI mappings final Map namespaces = new HashMap(); for (int scope : ctx.getScopes()) { Bindings bind = ctx.getBindings(scope); if (bind != null) { // TODO: See what needs to be done.... // collectNamespaces(namespaces, bind); } } // look for mapping for default XML namespace Object def = ctx.getAttribute(XMLConstants.XMLNS_ATTRIBUTE); if (def instanceof String) { namespaces.put(XMLConstants.DEFAULT_NS_PREFIX, (String) def); } // standard required mappings by XPath standard namespaces.put(XMLConstants.XML_NS_PREFIX, XMLConstants.XML_NS_URI); namespaces.put(XMLConstants.XMLNS_ATTRIBUTE, XMLConstants.XMLNS_ATTRIBUTE_NS_URI); // add prefix mapping for XPATH_CONTEXT_PREFIX namespaces.put(XPATH_CONTEXT_PREFIX, XPATH_CONTEXT_URI); return new NamespaceContext() { public String getNamespaceURI(String prefix) { if (prefix == null) { throw new IllegalArgumentException(); } String uri = namespaces.get(prefix); if (uri == null) { return XMLConstants.NULL_NS_URI; } else { return uri; } } public String getPrefix(String namespaceURI) { if (namespaceURI == null) { throw new IllegalArgumentException(); } for (String prefix : namespaces.keySet()) { String uri = namespaces.get(prefix); if (namespaceURI.equals(uri)) { return prefix; } } return null; } public Iterator getPrefixes(String namespaceURI) { if (namespaceURI == null) { throw new IllegalArgumentException(); } List list = new ArrayList(); for (String prefix : namespaces.keySet()) { String uri = namespaces.get(prefix); if (namespaceURI.equals(uri)) { list.add(prefix); } } return Collections.unmodifiableList(list).iterator(); } }; } private static XPathFunction makeXPathFunction(final Constructor ctr, int arity) { if (ctr.getParameterTypes().length != arity) { return null; } return new XPathFunction() { public Object evaluate(List args) { try { return ctr.newInstance(args.toArray()); } catch (Exception exp) { throw new RuntimeException(exp); } } }; } private static XPathFunction makeXPathFunction(final Method method, int arity) { int modifiers = method.getModifiers(); int numArgs = method.getParameterTypes().length; if (Modifier.isStatic(modifiers) && numArgs == arity) { // static method. expose "as is". return new XPathFunction() { public Object evaluate(List args) { try { return method.invoke(null, args.toArray()); } catch (Exception exp) { throw new RuntimeException(exp); } } }; } else if ((numArgs + 1) == arity) { // instance method. treat the first arg as 'this' return new XPathFunction() { public Object evaluate(List args) { List tmp = args.subList(1, args.size()); try { return method.invoke(args.get(0), tmp.toArray()); } catch (Exception exp) { throw new RuntimeException(exp); } } }; } else { return null; } } private static XPathFunction makeXPathFunction(final String funcName, final Invocable invocable) { return new XPathFunction() { public Object evaluate(List args) { try { return invocable.invokeFunction(funcName, args.toArray()); } catch (Exception exp) { throw new RuntimeException(exp); } } }; } // make a XPathFunction from given object private static XPathFunction makeXPathFunction(QName qname, Object obj, int arity) { if (obj == null) { return null; } else if (obj instanceof XPathFunction) { // already XPathFunction - just return return (XPathFunction) obj; } else if (obj instanceof Method) { // a Method object. wrap as XPathFunction return makeXPathFunction((Method) obj, arity); } else if (obj instanceof Constructor) { // a Constructor object. wrap as XPathFunction return makeXPathFunction((Constructor) obj, arity); } else if (obj instanceof Invocable) { // wrap a script function as XPathFunction. Using this, // scripts from other languages (for eg. JavaScript) can use // this engine and pass script functions as XPathFunction extensions return makeXPathFunction(qname.getLocalPart(), (Invocable) obj); } else { // can't map the object as XPathFunction. return null; } } // Internals only below this point private XPathExpression compileXPath(String str, final ScriptContext ctx) throws ScriptException { // JSR-223 requirement ctx.setAttribute("context", ctx, ScriptContext.ENGINE_SCOPE); try { XPath xpath = xpathFactory.newXPath(); xpath.setXPathVariableResolver(new XPathVariableResolver() { public Object resolveVariable(QName qname) { return findVariable(qname, ctx); } }); xpath.setXPathFunctionResolver(new XPathFunctionResolver() { public XPathFunction resolveFunction(QName qname, int arity) { Object obj = findVariable(qname, ctx); return makeXPathFunction(qname, obj, arity); } }); xpath.setNamespaceContext(makeNamespaceContext(ctx)); // xpath.setNamespaceContext(new BpmnFunctionResolver()); // xpath.setXPathFunctionResolver(new BpmnFunctionResolver()); int begin = str.indexOf("getObjectData") > -1 ? 14 : 0; if (begin > 0) { String objectDataRef = str.substring(begin + 1, str.indexOf(")") - 1); objectData = (Document) ctx.getAttribute(objectDataRef); // ctx.setAttribute(XPATH_INPUT_SRC, objectData , 100); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Source xmlSource = new DOMSource(objectData); Result outputTarget = new StreamResult(outputStream); TransformerFactory.newInstance().newTransformer().transform(xmlSource, outputTarget); InputStream is = new ByteArrayInputStream(outputStream.toByteArray()); ctx.setReader(new InputStreamReader(is)); str = str.substring(str.indexOf(")") + 1); } XPathExpression xpe = xpath.compile(str); return xpe; } catch (Exception exp) { throw new ScriptException(exp); } } private Object getVariable(ScriptContext ctx, String name) { synchronized (ctx) { int scope = ctx.getAttributesScope(name); if (scope != -1) { return ctx.getAttribute(name, scope); } } return null; } private Object evalXPath(XPathExpression expr, final ScriptContext ctx) throws ScriptException { try { Object resultType = getVariable(ctx, XPATH_RESULT_TYPE); Object input = getVariable(ctx, XPATH_INPUT_SRC); InputSource src; if (input == null) { // no input specified, use context reader src = new InputSource(ctx.getReader()); } else { // explicit InputSource specified if (input instanceof InputSource) { src = (InputSource) input; } else if (input instanceof String) { src = new InputSource((String) input); } else if (input instanceof Reader) { src = new InputSource((Reader) input); } else if (input instanceof InputStream) { src = new InputSource((InputStream) input); } else { // some other object input type specified (Node/NodeList) src = null; } } resultType = XPathConstants.BOOLEAN; if (resultType instanceof QName) { return (src != null) ? expr.evaluate(src, (QName) resultType) : expr.evaluate(input, (QName) resultType); } else { return (src != null) ? expr.evaluate(src) : expr.evaluate(input); } } catch (Exception exp) { throw new ScriptException(exp); } } private String readFully(Reader reader) throws ScriptException { char[] arr = new char[8 * 1024]; // 8K at a time StringBuilder buf = new StringBuilder(); int numChars; try { while ((numChars = reader.read(arr, 0, arr.length)) > 0) { buf.append(arr, 0, numChars); } } catch (IOException exp) { throw new ScriptException(exp); } return buf.toString(); } }