sf.net.experimaestro.utils.JSUtils.java Source code

Java tutorial

Introduction

Here is the source code for sf.net.experimaestro.utils.JSUtils.java

Source

/*
 * This file is part of experimaestro.
 * Copyright (c) 2012 B. Piwowarski <benjamin@bpiwowar.net>
 *
 * experimaestro 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 3 of the License, or
 * (at your option) any later version.
 *
 * experimaestro 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 experimaestro.  If not, see <http://www.gnu.org/licenses/>.
 */

package sf.net.experimaestro.utils;

import com.google.common.collect.AbstractIterator;
import org.apache.commons.vfs2.FileObject;
import org.mozilla.javascript.*;
import org.mozilla.javascript.xml.XMLObject;
import org.mozilla.javascript.xmlimpl.XMLLibImpl;
import org.w3c.dom.*;
import org.w3c.dom.Node;
import sf.net.experimaestro.exceptions.XPMRuntimeException;
import sf.net.experimaestro.manager.Manager;
import sf.net.experimaestro.manager.QName;
import sf.net.experimaestro.manager.js.*;
import sf.net.experimaestro.manager.json.*;
import sf.net.experimaestro.scheduler.Resource;
import sf.net.experimaestro.utils.log.Logger;

import javax.xml.namespace.NamespaceContext;
import java.lang.reflect.Array;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.String.format;

public class JSUtils {
    final static private Logger LOGGER = Logger.getLogger();

    /**
     * Get an object from a scriptable
     *
     * @param object
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> T get(Scriptable scope, String name, NativeObject object, boolean allowNull) {
        final Object _value = object.get(name, scope);
        if (_value == UniqueTag.NOT_FOUND)
            if (allowNull)
                return null;
            else
                throw new RuntimeException(format("Could not find property '%s'", name));
        return (T) unwrap(_value);
    }

    public static <T> T get(Scriptable scope, String name, NativeObject object) {
        return get(scope, name, object, false);
    }

    /**
     * Unwrap a JavaScript object (if necessary)
     *
     * @param object
     * @return
     */
    public static Object unwrap(Object object) {
        if (object == null)
            return null;

        if (object instanceof Wrapper)
            object = ((Wrapper) object).unwrap();

        if (object == Scriptable.NOT_FOUND)
            return null;
        return object;
    }

    /**
     * Get an object from a scriptable
     *
     * @param object
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> T get(Scriptable scope, String name, NativeObject object, T defaultValue) {
        final Object _value = object.get(name, scope);
        if (_value == UniqueTag.NOT_FOUND)
            return defaultValue;
        return (T) unwrap(_value);
    }

    /**
     * Transforms a DOM node to a E4X scriptable object
     *
     * @param node
     * @param cx
     * @param scope
     * @return
     */
    public static Object domToE4X(Node node, Context cx, Scriptable scope) {
        if (node == null) {
            LOGGER.info("XML is null");
            return Context.getUndefinedValue();
        }
        if (node instanceof Document)
            node = ((Document) node).getDocumentElement();

        if (node instanceof DocumentFragment) {

            final Document document = node.getOwnerDocument();
            Element root = document.createElement("root");
            document.appendChild(root);

            Scriptable xml = cx.newObject(scope, "XML", new Node[] { root });

            final Scriptable list = (Scriptable) xml.get("*", xml);
            int count = 0;
            for (Node child : XMLUtils.children(node)) {
                list.put(count++, list, cx.newObject(scope, "XML", new Node[] { child }));
            }
            return list;
        }

        LOGGER.debug("XML is of type %s [%s]; %s", node.getClass(), XMLUtils.toStringObject(node),
                node.getUserData("org.mozilla.javascript.xmlimpl.XmlNode"));
        return cx.newObject(scope, "XML", new Node[] { node });
    }

    public static Object get(Scriptable scope, String name) {
        Scriptable start = scope;
        Object result;
        do {
            result = scope.get(name, start);
            if (result != Scriptable.NOT_FOUND)
                break;
            scope = scope.getPrototype();
        } while (scope != null);

        return result;
    }

    /**
     * @see org.mozilla.javascript.RhinoException#getScriptStack()
     * @param ex
     * @return A stack
     */
    static public ScriptStackElement[] getScriptStackTrace(Throwable ex) {
        List<ScriptStackElement> list = new ArrayList<>();
        ScriptStackElement[][] interpreterStack = null;
        int interpreterStackIndex = 0;
        StackTraceElement[] stack = ex.getStackTrace();
        // Pattern to recover function name from java method name -
        // see Codegen.getBodyMethodName()
        // kudos to Marc Guillemot for coming up with this
        Pattern pattern = Pattern.compile("_c_(.*)_\\d+");
        for (StackTraceElement e : stack) {
            String fileName = e.getFileName();
            if (e.getMethodName().startsWith("_c_") && e.getLineNumber() > -1 && fileName != null
                    && !fileName.endsWith(".java")) {
                String methodName = e.getMethodName();
                Matcher match = pattern.matcher(methodName);
                // the method representing the main script is always "_c_script_0" -
                // at least we hope so
                methodName = !"_c_script_0".equals(methodName) && match.find() ? match.group(1) : null;
                list.add(new ScriptStackElement(fileName, methodName, e.getLineNumber()));
            } else if ("org.mozilla.javascript.Interpreter".equals(e.getClassName())
                    && "interpretLoop".equals(e.getMethodName()) && interpreterStack != null
                    && interpreterStack.length > interpreterStackIndex) {
                for (ScriptStackElement elem : interpreterStack[interpreterStackIndex++]) {
                    list.add(elem);
                }
            }
        }
        return list.toArray(new ScriptStackElement[list.size()]);

    }

    /**
     * Transform an object to JSON
     *
     * @param scope The javascript scope for evaluating expressions
     * @param value The object to transform
     * @return
     */
    public static Json toJSON(Scriptable scope, Object value) {
        if (value instanceof Json)
            return (Json) value;

        // No unwrap for JSBaseObject
        value = value instanceof JSBaseObject ? value : unwrap(value);

        // --- Simple cases
        if (value == null)
            return JsonNull.getSingleton();

        if (value instanceof Json)
            return (Json) value;

        if (value instanceof JSJson)
            return ((JSJson) value).getJson();

        if (value instanceof String)
            return new JsonString((String) value);

        if (value instanceof Double) {
            if ((double) ((Double) value).longValue() == (double) value)
                return new JsonInteger(((Double) value).longValue());
            return new JsonReal((Double) value);
        }
        if (value instanceof Float) {
            if ((double) ((Float) value).longValue() == (float) value)
                return new JsonInteger(((Float) value).longValue());
            return new JsonReal((Float) value);
        }

        if (value instanceof Integer)
            return new JsonInteger((Integer) value);

        if (value instanceof Long)
            return new JsonInteger((Long) value);

        if (value instanceof Boolean)
            return new JsonBoolean((Boolean) value);

        // --- A JS object
        if (value instanceof NativeObject) {
            JsonObject json = new JsonObject();
            for (Map.Entry<Object, Object> entry : JSUtils.iterable(((NativeObject) value))) {
                JSNamespaceContext nsContext = new JSNamespaceContext(scope);
                QName qname = QName.parse(JSUtils.toString(entry.getKey()), nsContext);
                Object pValue = entry.getValue();

                if (qname.equals(Manager.XP_TYPE))
                    pValue = QName.parse(JSUtils.toString(pValue), nsContext).toString();

                String key = qname.toString();
                final Json key_value = toJSON(scope, pValue);
                json.put(key, key_value);
            }
            return json;
        }

        // -- An array
        if (value instanceof NativeArray) {
            NativeArray array = (NativeArray) value;
            JsonArray json = new JsonArray();
            for (int i = 0; i < array.getLength(); i++)
                json.add(toJSON(scope, array.get(i)));
            return json;
        }

        if (value.getClass().isArray()) {
            final int length = Array.getLength(value);
            JsonArray json = new JsonArray();
            for (int i = 0; i < length; i++)
                json.add(toJSON(scope, Array.get(value, i)));
            return json;
        }

        if (value instanceof FileObject)
            return new JsonFileObject((FileObject) value);

        if (value instanceof JSFileObject)
            return new JsonFileObject(((JSFileObject) value).getFile());

        if (value instanceof Resource)
            return new JsonResource((Resource) value);

        // -- Undefined
        if (value instanceof Undefined)
            return JsonNull.getSingleton();

        return new JsonString(value.toString());
    }

    private static Iterable<? extends Map.Entry<Object, Object>> iterable(final NativeObject object) {
        final Object[] ids = object.getIds();
        return new Iterable<Map.Entry<Object, Object>>() {
            @Override
            public Iterator<Map.Entry<Object, Object>> iterator() {
                return new AbstractIterator<Map.Entry<Object, Object>>() {
                    int i = 0;

                    @Override
                    protected Map.Entry<Object, Object> computeNext() {
                        if (i >= ids.length)
                            return endOfData();
                        final Object id = ids[i++];
                        String key = id.toString();
                        final Object value = object.get(id);
                        return new AbstractMap.SimpleImmutableEntry<>(key, value);
                    }
                };
            }
        };
    }

    /**
     * Returns true if the object is a well defined JavaScript/Java object
     *
     * @param object
     * @return
     */
    public static boolean isDefined(Object object) {
        return object != null && object != UniqueTag.NOT_FOUND;
    }

    /**
     * Transform objects into an XML node or a NodeLsit
     *
     * @param object
     * @return a {@linkplain Node} or a {@linkplain NodeList}
     */
    public static Object toDOM(Scriptable scope, Object object) {
        return toDOM(scope, object, new OptionalDocument());
    }

    public static Object toDOM(Scriptable scope, Object object, OptionalDocument document) {
        // Unwrap if needed (if this is not a JSBaseObject)
        if (object instanceof Wrapper && !(object instanceof JSBaseObject))
            object = ((Wrapper) object).unwrap();

        // It is already a DOM node
        if (object instanceof Node)
            return object;

        if (object instanceof XMLObject) {
            final XMLObject xmlObject = (XMLObject) object;
            String className = xmlObject.getClassName();

            if (className.equals("XMLList")) {
                LOGGER.debug("Transforming from XMLList [%s]", object);
                final Object[] ids = xmlObject.getIds();
                if (ids.length == 1)
                    return toDOM(scope, xmlObject.get((Integer) ids[0], xmlObject), document);

                Document doc = XMLUtils.newDocument();
                DocumentFragment fragment = doc.createDocumentFragment();

                for (int i = 0; i < ids.length; i++) {
                    Node node = (Node) toDOM(scope, xmlObject.get((Integer) ids[i], xmlObject), document);
                    if (node instanceof Document)
                        node = ((Document) node).getDocumentElement();
                    fragment.appendChild(doc.adoptNode(node));
                }

                return fragment;
            }

            // XML node
            if (className.equals("XML")) {
                // FIXME: this strips all whitespaces!
                Node node = XMLLibImpl.toDomNode(object);
                LOGGER.debug("Got node from JavaScript [%s / %s] from [%s]", node.getClass(),
                        XMLUtils.toStringObject(node), object.toString());

                if (node instanceof Document)
                    node = ((Document) node).getDocumentElement();

                node = document.get().adoptNode(node.cloneNode(true));
                return node;
            }

            throw new RuntimeException(format("Not implemented: convert %s to XML", className));

        }

        if (object instanceof NativeArray) {
            NativeArray array = (NativeArray) object;
            ArrayNodeList list = new ArrayNodeList();
            for (Object x : array) {
                Object o = toDOM(scope, x, document);
                if (o instanceof Node)
                    list.add(document.cloneAndAdopt((Node) o));
                else {
                    for (Node node : XMLUtils.iterable((NodeList) o)) {
                        list.add(document.cloneAndAdopt(node));
                    }
                }
            }
            return list;
        }

        if (object instanceof NativeObject) {
            // JSON case: each key of the JS object is an XML element
            NativeObject json = (NativeObject) object;
            ArrayNodeList list = new ArrayNodeList();

            for (Object _id : json.getIds()) {

                String jsQName = JSUtils.toString(_id);

                if (jsQName.length() == 0) {
                    final Object seq = toDOM(scope, json.get(jsQName, json), document);
                    for (Node node : XMLUtils.iterable(seq)) {
                        if (node instanceof Document)
                            node = ((Document) node).getDocumentElement();
                        list.add(document.cloneAndAdopt(node));
                    }
                } else if (jsQName.charAt(0) == '@') {
                    final QName qname = QName.parse(jsQName.substring(1), null, new JSNamespaceBinder(scope));
                    Attr attribute = document.get().createAttributeNS(qname.getNamespaceURI(),
                            qname.getLocalPart());
                    StringBuilder sb = new StringBuilder();
                    for (Node node : XMLUtils.iterable(toDOM(scope, json.get(jsQName, json), document))) {
                        sb.append(node.getTextContent());
                    }

                    attribute.setTextContent(sb.toString());
                    list.add(attribute);
                } else {
                    final QName qname = QName.parse(jsQName, null, new JSNamespaceBinder(scope));
                    Element element = qname.hasNamespace()
                            ? document.get().createElementNS(qname.getNamespaceURI(), qname.getLocalPart())
                            : document.get().createElement(qname.getLocalPart());

                    list.add(element);

                    final Object seq = toDOM(scope, json.get(jsQName, json), document);
                    for (Node node : XMLUtils.iterable(seq)) {
                        if (node instanceof Document)
                            node = ((Document) node).getDocumentElement();
                        node = document.get().adoptNode(node.cloneNode(true));
                        if (node.getNodeType() == Node.ATTRIBUTE_NODE)
                            element.setAttributeNodeNS((Attr) node);
                        else
                            element.appendChild(node);
                    }
                }
            }

            return list;
        }

        if (object instanceof Double) {
            // Wrap a double
            final Double x = (Double) object;
            if (x.longValue() == x.doubleValue())
                return document.get().createTextNode(Long.toString(x.longValue()));
            return document.get().createTextNode(Double.toString(x));
        }

        if (object instanceof Integer) {
            return document.get().createTextNode(Integer.toString((Integer) object));
        }

        if (object instanceof CharSequence) {
            return document.get().createTextNode(object.toString());
        }

        if (object instanceof UniqueTag)
            throw new XPMRuntimeException("Undefined cannot be converted to XML", object.getClass());

        if (object instanceof JSNode) {
            Node node = ((JSNode) object).getNode();
            if (document.has()) {
                if (node instanceof Document)
                    node = ((Document) node).getDocumentElement();
                return document.get().adoptNode(node);
            }
            return node;
        }

        if (object instanceof JSNodeList) {
            return ((JSNodeList) object).getList();
        }

        if (object instanceof Scriptable) {
            ((Scriptable) object).getDefaultValue(String.class);
        }

        // By default, convert to string
        return document.get().createTextNode(object.toString());
    }

    /**
     * Returns true if the object is XML
     *
     * @param input
     * @return
     */
    public static boolean isXML(Object input) {
        return input instanceof XMLObject;
    }

    /**
     * Converts a JavaScript object into an XML document
     *
     * @param srcObject The javascript object to convert
     * @param wrapName  If the object is not already a document and has more than one
     *                  element child (or zero), use this to wrap the elements
     * @return
     */
    public static Document toDocument(Scriptable scope, Object srcObject, QName wrapName) {
        final Object object = toDOM(scope, srcObject);

        if (object instanceof Document)
            return (Document) object;

        Document document = XMLUtils.newDocument();

        // Add a new root element if needed
        NodeList childNodes;

        if (!(object instanceof Node)) {
            childNodes = (NodeList) object;
        } else {
            final Node dom = (Node) object;
            if (dom.getNodeType() == Node.ELEMENT_NODE) {
                childNodes = new NodeList() {
                    @Override
                    public Node item(int index) {
                        if (index == 0)
                            return dom;
                        throw new IndexOutOfBoundsException(Integer.toString(index) + " out of bounds");
                    }

                    @Override
                    public int getLength() {
                        return 1;
                    }
                };
            } else
                childNodes = dom.getChildNodes();
        }

        int elementCount = 0;
        for (int i = 0; i < childNodes.getLength(); i++)
            if (childNodes.item(i).getNodeType() == Node.ELEMENT_NODE)
                elementCount++;

        Node root = document;
        if (elementCount != 1) {
            root = document.createElementNS(wrapName.getNamespaceURI(), wrapName.getLocalPart());
            document.appendChild(root);
        }

        // Copy back in the DOM
        for (int i = 0; i < childNodes.getLength(); i++) {
            Node node = childNodes.item(i);
            node = node.cloneNode(true);
            document.adoptNode(node);
            root.appendChild(node);
        }

        return document;
    }

    /**
     * Add a new javascript function to the scope
     *
     * @param aClass    The class where the function should be searched
     * @param scope     The scope where the function should be defined
     * @param fname     The function name
     * @param prototype The prototype or null. If null, uses the standard Context, Scriptablem Object[], Function prototype
     *                  used by Rhino JS
     * @throws NoSuchMethodException If
     */
    public static void addFunction(Class<?> aClass, Scriptable scope, final String fname, Class<?>[] prototype)
            throws NoSuchMethodException {
        final FunctionObject f = new FunctionObject(fname, aClass.getMethod("js_" + fname, prototype), scope);
        ScriptableObject.putProperty(scope, fname, f);
    }

    public static String toString(Object object) {
        return Context.toString(unwrap(object));
    }

    public static int getInteger(Object object) {
        return (Integer) unwrap(object);
    }

    public static void addFunction(Scriptable scope, FunctionDefinition definition) throws NoSuchMethodException {
        addFunction(definition.clazz, scope, definition.name, definition.arguments);
    }

    /**
     * Convert a property into a boolean
     *
     * @param scope  The JS scope
     * @param object The JS object
     * @param name   The name of the property
     * @return <tt>false</tt> if the property does not exist.
     */
    public static boolean toBoolean(Scriptable scope, Scriptable object, String name) {
        if (!object.has(name, scope))
            return false;
        Object value = object.get(name, scope);
        if (value instanceof Boolean)
            return (Boolean) value;
        return Boolean.parseBoolean(JSUtils.toString(value));
    }

    public static NamespaceContext getNamespaceContext(final Scriptable scope) {
        return new JSNamespaceContext(scope);
    }

    static public class OptionalDocument {
        Document document;

        Document get() {
            if (document == null)
                document = XMLUtils.newDocument();
            return document;
        }

        public boolean has() {
            return document != null;
        }

        /**
         * Clone and adopt node if not already owned
         *
         * @param node
         * @return
         */
        public Node cloneAndAdopt(Node node) {
            if (node.getOwnerDocument() != get())
                return get().adoptNode(node.cloneNode(true));
            return node;
        }
    }

    /**
     * Defines a JavaScript function by refering a class, a name and its parameters
     */
    static public class FunctionDefinition {
        Class<?> clazz;
        String name;
        Class<?>[] arguments;

        public FunctionDefinition(Class<?> clazz, String name, Class<?>... arguments) {
            this.clazz = clazz;
            this.name = name;
            if (arguments == null || arguments.length == 0)
                this.arguments = new Class[] { Context.class, Scriptable.class, Object[].class, Function.class };
            else
                this.arguments = arguments;
        }

        public Class<?> getClazz() {
            return clazz;
        }

        public String getName() {
            return name;
        }

        public Class<?>[] getArguments() {
            return arguments;
        }
    }

}