zk/src/main/java/org/zkoss/zk/scripting/bsh/BSHInterpreter.java

Summary

Maintainability
F
6 days
Test Coverage
/* BSHInterpreter.java

    Purpose:
        
    Description:
        
    History:
        Thu Jun  1 14:28:43     2006, Created by tomyeh

Copyright (C) 2006 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zk.scripting.bsh;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

import bsh.BshClassManager;
import bsh.BshMethod;
import bsh.ClassIdentifier;
import bsh.EvalError;
import bsh.NameSpace;
import bsh.Primitive;
import bsh.UtilEvalError;
import bsh.Variable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.lang.Classes;
import org.zkoss.lang.Library;
import org.zkoss.lang.reflect.Fields;
import org.zkoss.xel.Function;
import org.zkoss.zk.scripting.HierarchicalAware;
import org.zkoss.zk.scripting.SerializableAware;
import org.zkoss.zk.scripting.util.GenericInterpreter;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.IdSpace;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.ext.Scope;
import org.zkoss.zk.ui.ext.ScopeListener;
import org.zkoss.zk.ui.sys.ExecutionCtrl;

/**
 * The interpreter that uses BeanShell to interpret zscript codes.
 *
 * <p>Unlike many other implementations, it supports the hierarchical
 * scopes ({@link HierarchicalAware}).
 * That is, it uses an independent BeanShell NameSpace
 * (a.k.a. interpreter's scope) to store the variables/classes/methods
 * defined in BeanShell script for each ZK scope ({@link Scope}).
 * Since one-to-one relationship between BeanShell's scope and ZK scope,
 * the invocation of BeanShell methods can execute correctly without knowing
 * what scope it is.
 * However, if you want your codes portable across different interpreters,
 * you had better to call
 * {@link org.zkoss.zk.ui.ext.Scopes#beforeInterpret}
 * to prepare the proper scope, before calling any method defined in
 * zscript.
 *
 * <h2>How serialization work?</p>
 *
 * <p>First, all NameSpace objects have to serialize. Second,
 * the top-level namespace (GlobalNS) is wrapped with NSWrap, which
 * is not serializable. It is serialized when {@link SerializableAware#write}
 * is called (triggered by PageImpl's write).
 *
 * <p>On the other hand, all non-top-level namespaces (NS) are wrapped with
 * NSWrapSR which is serializable, so they are serialized with
 * the attributes of a ID space owner being serialized.
 *
 * @author tomyeh
 */
public class BSHInterpreter extends GenericInterpreter implements SerializableAware, HierarchicalAware {
    /*package*/ static final Logger log = LoggerFactory.getLogger(BSHInterpreter.class);

    /** A variable in {@link Scope}. The value is an instance of
     * BeanShell's NameSpace.
     */
    private static final String VAR_NSW = "z_bshnsw";
    private bsh.Interpreter _ip;
    private GlobalNS _bshns;

    /*static {
        bsh.Interpreter.LOCALSCOPING = false;
            //must be false (default); otherwise, the following fails
            //class X {
            //  String x;
            //  X(String v) {x = v;}
    }*/

    public BSHInterpreter() {
    }

    //Deriving to override//
    /** Called when the top-level BeanShell scope is created.
     * By default, it does nothing.
     *
     * <p>Note: to speed up the performance, this implementation
     * disabled {@link bsh.NameSpace#loadDefaultImports}.
     * It only imports the java.lang and java.util packages.
     * If you want the built command and import packages, you can override
     * this method. For example,
     * <pre><code>
     *protected void loadDefaultImports(NameSpace bshns) {
     *  bshns.importCommands("/bsh/commands");
     *}</code></pre>
     *
     * @since 3.0.2
     */
    protected void loadDefaultImports(NameSpace bshns) {
    }

    //GenericInterpreter//
    protected void exec(String script) {
        try {
            final Scope scope = getCurrent();
            if (scope != null)
                _ip.eval(script, prepareNS(scope));
            else
                _ip.eval(script); //unlikely (but just in case)
        } catch (EvalError ex) {
            throw UiException.Aide.wrap(ex);
        }
    }

    protected boolean contains(String name) {
        try {
            return _ip.getNameSpace().getVariable(name) != Primitive.VOID;
            //Primitive.VOID means not defined
        } catch (UtilEvalError ex) {
            throw UiException.Aide.wrap(ex);
        }
    }

    protected boolean contains(Scope scope, String name) {
        if (scope != null) {
            final NameSpace bshns = prepareNS(scope);
            //note: we have to create NameSpace (with prepareNS)
            //to have the correct chain
            if (bshns != _bshns) {
                try {
                    return bshns.getVariable(name) != Primitive.VOID;
                } catch (UtilEvalError ex) {
                    throw UiException.Aide.wrap(ex);
                }
            }
        }
        return contains(name);
    }

    protected Object get(String name) {
        try {
            Object result = Primitive.unwrap(_ip.get(name));
            if (result instanceof ClassIdentifier) {
                return null; // Class is not a variable
            }
            return result;
        } catch (EvalError ex) {
            throw UiException.Aide.wrap(ex);
        }
    }

    protected Object get(Scope scope, String name) {
        if (scope != null) {
            final NameSpace bshns = prepareNS(scope);
            //note: we have to create NameSpace (with prepareNS)
            //to have the correct chain
            if (bshns != _bshns) {
                try {
                    Object result = Primitive.unwrap(bshns.getVariable(name));
                    if (result instanceof ClassIdentifier) {
                        return null; // Class is not a variable
                    }
                    return result;
                } catch (UtilEvalError ex) {
                    throw UiException.Aide.wrap(ex);
                }
            }
        }
        return get(name);
    }
    
    protected void set(String name, Object val) {
        try {
            _ip.set(name, val);
            //unlike NameSpace.setVariable, _ip.set() handles null
        } catch (EvalError ex) {
            throw UiException.Aide.wrap(ex);
        }
    }

    protected void set(Scope scope, String name, Object val) {
        if (scope != null) {
            final NameSpace bshns = prepareNS(scope);
            //note: we have to create NameSpace (with prepareNS)
            //to have the correct chain
            if (bshns != _bshns) {
                try {
                    bshns.setVariable(name, val != null ? val : Primitive.NULL, false);
                    return;
                } catch (UtilEvalError ex) {
                    throw UiException.Aide.wrap(ex);
                }
            }
        }
        set(name, val);
    }

    protected void unset(String name) {
        try {
            _ip.unset(name);
        } catch (EvalError ex) {
            throw UiException.Aide.wrap(ex);
        }
    }

    protected void unset(Scope scope, String name) {
        if (scope != null) {
            final NameSpace bshns = prepareNS(scope);
            //note: we have to create NameSpace (with prepareNS)
            //to have the correct chain
            if (bshns != _bshns) {
                bshns.unsetVariable(name);
                return;
            }
        }
        unset(name);
    }

    //-- Interpreter --//
    public void init(Page owner, String zslang) {
        super.init(owner, zslang);

        _ip = new bsh.Interpreter();
        _ip.setClassLoader(Classes.getContextClassLoader(BSHInterpreter.class));

        _bshns = new GlobalNS(_ip.getClassManager(), "global");
        _ip.setNameSpace(_bshns);
    }

    public void destroy() {
        getOwner().removeAttribute(VAR_NSW);

        //bug 1814819 ,clear variable, dennis
        try {
            _bshns.clear();
            _ip.setNameSpace(null);
        } catch (Throwable t) { //silently ignore (in case of upgrading to new bsh)
        }

        _ip = null;
        _bshns = null;
        super.destroy();
    }

    /** Returns the native interpreter, or null if it is not initialized
     * or destroyed.
     * From application's standpoint, it never returns null, and the returned
     * object must be an instance of {@link bsh.Interpreter}
     * @since 3.0.2
     */
    public Object getNativeInterpreter() {
        return _ip;
    }

    public Class<?> getClass(String clsnm) {
        try {
            return _bshns.getClass(clsnm);
        } catch (UtilEvalError ex) {
            throw new UiException("Failed to load class " + clsnm, ex);
        }
    }

    public Function getFunction(String name, Class[] argTypes) {
        return getFunction0(_bshns, name, argTypes);
    }

    public Function getFunction(Scope scope, String name, Class[] argTypes) {
        return getFunction0(prepareNS(scope), name, argTypes);
    }

    private Function getFunction0(NameSpace bshns, String name, Class[] argTypes) {
        try {
            final BshMethod m = bshns.getMethod(name, argTypes != null ? argTypes : new Class[0], false);
            return m != null ? new BSHFunction(m) : null;
        } catch (UtilEvalError ex) {
            throw UiException.Aide.wrap(ex);
        }
    }

    /** Prepares the namespace for non-top-level scope.
     */
    private NameSpace prepareNS(Scope scope) {
        scope = getIdSpace(scope);
        if (scope == null || scope == getOwner())
            return _bshns;

        NSWrap nsw = (NSWrap) scope.getAttribute(VAR_NSW);
        if (nsw != null)
            return nsw.unwrap(scope);

        //bind bshns and scope
        final NS bshns = newNS(scope);
        scope.setAttribute(VAR_NSW, NSWrap.getInstance(bshns));
        return bshns;
    }

    /*package*/ NS newNS(Scope scope) {
        scope = getIdSpace(scope);
        Scope p = getParentIdSpace(scope);
        return new NS(p != null ? prepareNS(p) : _bshns, _ip.getClassManager(), scope);
        //Bug 1831534: we have to pass class manager
        //Bug 1899353: we have to use _bshns instead of null (Reason: unknown)
    }

    /** Prepares the namespace for detached components. */
    private static NameSpace prepareDetachedNS(Scope scope) {
        scope = getIdSpace(scope);
        if (scope == null)
            return null;

        NSWrap nsw = (NSWrap) scope.getAttribute(VAR_NSW);
        if (nsw != null)
            return nsw.unwrap(scope);

        //bind bshns and scope
        Scope p = getParentIdSpace(scope);
        NameSpace bshns = new NS(p != null ? prepareDetachedNS(p) : null, null, scope);
        scope.setAttribute(VAR_NSW, NSWrap.getInstance(bshns));
        return bshns;
    }

    /*package*/ static BSHInterpreter getInterpreter(Scope scope) {
        Page owner = getPage(scope);
        if (owner != null) {
            for (Iterator it = owner.getLoadedInterpreters().iterator(); it.hasNext();) {
                final Object ip = it.next();
                if (ip instanceof BSHInterpreter)
                    return (BSHInterpreter) ip;
            }
        }
        return null;
    }

    /** Returns the nearest IdSpace (scope), never null. */
    private static Scope getIdSpace(Scope scope) {
        if (scope instanceof IdSpace)
            return scope;
        if (scope instanceof Component) {
            scope = ((Component) scope).getSpaceOwner();
            if (scope != null)
                return scope;
        }
        return null;
    }

    /** Returns the parent IdSpace (scope), or null if no parent. */
    private static Scope getParentIdSpace(Scope scope) {
        if (scope == null || !(scope instanceof Component))
            return null;
        final Component p = ((Component) scope).getParent();
        return p != null ? p.getSpaceOwner() : null;
    }

    private static Page getPage(Scope scope) {
        return scope instanceof Component ? ((Component) scope).getPage()
                : scope instanceof Page ? ((Page) scope) : null;
    }

    //supporting classes//
    /** The global namespace. */
    private abstract static class AbstractNS extends NameSpace {
        private boolean _inGet;
        protected boolean _firstGet;

        protected AbstractNS(NameSpace parent, BshClassManager classManager, String name) {
            super(parent, classManager, name);
        }

        /** Deriver has to override this method. */
        protected abstract Object getFromScope(String name);

        //super//
        protected Variable getVariableImpl(String name, boolean recurse) throws UtilEvalError {
            //Note: getVariableImpl returns null if not defined,
            //while getVariable return Primitive.VOID if not defined

            //Tom M Yeh: 20060606:
            //We cannot override getVariable because BeanShell use
            //getVariableImpl to resolve a variable recursively
            //
            //setVariable will callback this method,
            //so use _inGet to prevent dead loop
            Variable var = super.getVariableImpl(name, false);
            if (!_inGet && var == null) {
                _firstGet = true;
                Object v = getFromScope(name);
                if (v != UNDEFINED) {
                    //Variable has no public/protected constructor, so we have to
                    //store the value back (with setVariable) and retrieve again
                    _inGet = true;
                    try {
                        this.setVariable(name, v != null ? v : Primitive.NULL, false);
                        var = super.getVariableImpl(name, false);
                        this.unsetVariable(name); //restore
                    } finally {
                        _inGet = false;
                    }
                }

                if (var == null && recurse) {
                    NameSpace parent = getParent();
                    if (parent instanceof AbstractNS) {
                        var = ((AbstractNS) parent).getVariableImpl(name, true);
                    } else if (parent != null) { //show not reach here; just in case
                        try {
                            java.lang.reflect.Method m = NameSpace.class.getDeclaredMethod("getVariableImpl",
                                    new Class[] { String.class, Boolean.TYPE });
                            m.setAccessible(true);
                            var = (Variable) m.invoke(parent, name, Boolean.TRUE);
                        } catch (Exception ex) {
                            throw UiException.Aide.wrap(ex);
                        }
                    }
                }
            }

            return var;
        }

        public void loadDefaultImports() {
            //to speed up the performance
        }
    }

    /** The global NameSpace. */
    private class GlobalNS extends AbstractNS {
        private GlobalNS(BshClassManager classManager, String name) {
            super(null, classManager, name);
        }

        protected Object getFromScope(String name) {
            final Scope curr = getCurrent();
            if (curr == null) //no scope allowed
                return getImplicit(name);

            if (_firstGet) {
                _firstGet = false;
                final Execution exec = Executions.getCurrent();
                if (exec != null) {
                    Object val = exec instanceof ExecutionCtrl ? ((ExecutionCtrl) exec).getExtraXelVariable(name)
                            : null;
                    if (val != null)
                        return val;
                    val = exec.getAttribute(name);
                    if (val != null /*||exec.hasAttribute(name)*/) //exec not support hasAttribute
                        return val;
                }

                //page is the IdSpace, so it might not be curr
                if (curr instanceof Component) {
                    // Bug ZK-3046, use getShadowVariable instead.
                    Object o = ((Component) curr).getShadowVariable(name, true);
                    if (o != null)
                        return o;
                }
            }

            final Page page = getOwner();
            Object val = page.getAttributeOrFellow(name, true); //page/desktop/session
            if (val != null || page.hasAttributeOrFellow(name, true))
                return val;
            val = page.getXelVariable(null, null, name, true);
            return val != null ? val : getImplicit(name);
        }

        public void loadDefaultImports() {
            BSHInterpreter.this.loadDefaultImports(this);
        }
    }

    /** The per-IdSpace NameSpace. */
    /*package*/ static class NS extends AbstractNS {
        private Scope _scope;

        private NS(NameSpace parent, BshClassManager classManager, Scope scope) {
            super(parent, classManager, "scope" + System.identityHashCode(scope));
            _scope = scope;
            _scope.addScopeListener(new NSCListener(this));
        }

        //super//
        /** Search _scope instead. */
        protected Object getFromScope(String name) {
            final BSHInterpreter ip = getInterpreter(_scope);
            final Scope curr = ip != null ? ip.getCurrent() : null;
            if (curr == null)
                return getImplicit(name); //ignore scope

            if (_firstGet) {
                _firstGet = false;
                final Execution exec = Executions.getCurrent();
                if (exec != null && exec != curr) {
                    Object val = exec instanceof ExecutionCtrl ? ((ExecutionCtrl) exec).getExtraXelVariable(name)
                            : null;
                    if (val != null)
                        return val;
                    val = exec.getAttribute(name);
                    if (val != null /*||exec.hasAttribute(name)*/) //exec not support hasAttribute
                        return val;
                }

                //_scope is the nearest IdSpace so it might not be curr
                if (curr != _scope && curr instanceof Component) {
                    for (Component c = (Component) curr; c != null && c != _scope; c = c.getParent()) {
                        // Bug ZK-3046, use getShadowVariable instead.
                        Object o = c.getShadowVariable((Component) curr, name, false);
                        if (o != null)
                            return o;
                    }
                }
            }

            Component comp = (Component) _scope;
            //local-only since getVariableImpl will look up its parent
            Object val = comp.getAttributeOrFellow(name, false);
            return val != null || comp.hasAttributeOrFellow(name, false) ? val : getImplicit(name);
            //No need to invoke getXelVariable since it is not 'recurse'
        }
    }

    private static class NSCListener implements ScopeListener {
        private final NS _bshns;

        private NSCListener(NS bshns) {
            _bshns = bshns;
        }

        public void attributeAdded(Scope scope, String name, Object value) {
        }

        public void attributeReplaced(Scope scope, String name, Object value) {
        }

        public void attributeRemoved(Scope scope, String name) {
        }

        public void parentChanged(Scope scope, Scope newparent) {
        }

        public void idSpaceChanged(Scope scope, IdSpace newIdSpace) {
            if (newIdSpace instanceof Scope) { //i.e., != null (but safer)
                final BSHInterpreter ip = getInterpreter(_bshns._scope);
                _bshns.setParent(ip != null ? ip.prepareNS(newIdSpace) : prepareDetachedNS(newIdSpace));
                return;
            }

            _bshns.setParent(null);
        }
    }

    //SerializableAware//
    public void write(ObjectOutputStream s, Filter filter) throws IOException {
        write(_bshns, s, filter);
    }

    /*package*/ static void write(NameSpace ns, ObjectOutputStream s, Filter filter) throws IOException {
        //1. variables
        final String[] vars = ns.getVariableNames();
        for (int j = vars != null ? vars.length : 0; --j >= 0;) {
            final String nm = vars[j];
            if (nm != null && !"bsh".equals(nm) && isVariableSerializable(nm)) {
                try {
                    final Object val = ns.getVariable(nm, false);
                    if ((val == null || (val instanceof Serializable) || (val instanceof Externalizable))
                            && !(val instanceof Component) && (filter == null || filter.accept(nm, val))) {
                        s.writeObject(nm);
                        s.writeObject(val);
                    }
                } catch (IOException ex) {
                    throw ex;
                } catch (Throwable ex) {
                    log.warn("Ignored failure to write " + nm, ex);
                }
            }
        }
        s.writeObject(null); //denote end-of-vars

        //2. methods
        if (shallSerializeMethod()) {
            final BshMethod[] mtds = ns.getMethods();
            for (int j = mtds != null ? mtds.length : 0; --j >= 0;) {
                final String nm = mtds[j].getName();
                if (isMethodSerializable(nm) && (filter == null || filter.accept(nm, mtds[j]))) {
                    //hack BeanShell 2.0b4 which cannot be serialized correctly
                    Field f = null;
                    boolean acs = false;
                    try {
                        f = Classes.getAnyField(BshMethod.class, "declaringNameSpace");
                        acs = f.isAccessible();
                        Fields.setAccessible(f, true);
                        final Object old = f.get(mtds[j]);
                        try {
                            f.set(mtds[j], null);
                            s.writeObject(mtds[j]);
                        } finally {
                            f.set(mtds[j], old);
                        }
                    } catch (IOException ex) {
                        throw ex;
                    } catch (Throwable ex) {
                        log.warn("Ignored failure to write " + nm, ex);
                    } finally {
                        if (f != null)
                            Fields.setAccessible(f, acs);
                    }
                }
            }
        }
        s.writeObject(null); //denote end-of-mtds

        //3. imported class
        Field f = null;
        boolean acs = false;
        try {
            f = Classes.getAnyField(NameSpace.class, "importedClasses");
            acs = f.isAccessible();
            Fields.setAccessible(f, true);
            final Map clses = (Map) f.get(ns);
            if (clses != null)
                for (Iterator it = clses.values().iterator(); it.hasNext();) {
                    final String clsnm = (String) it.next();
                    if (!clsnm.startsWith("bsh."))
                        s.writeObject(clsnm);
                }
        } catch (IOException ex) {
            throw ex;
        } catch (Throwable ex) {
            log.warn("Ignored failure to write imported classes", ex);
        } finally {
            if (f != null)
                Fields.setAccessible(f, acs);
        }
        s.writeObject(null); //denote end-of-cls

        //4. imported package
        f = null;
        acs = false;
        try {
            f = Classes.getAnyField(NameSpace.class, "importedPackages");
            acs = f.isAccessible();
            Fields.setAccessible(f, true);
            final Collection pkgs = (Collection) f.get(ns);
            if (pkgs != null)
                for (Iterator it = pkgs.iterator(); it.hasNext();) {
                    final String pkgnm = (String) it.next();
                    if (!pkgnm.startsWith("java.awt") && !pkgnm.startsWith("javax.swing"))
                        s.writeObject(pkgnm);
                }
        } catch (IOException ex) {
            throw ex;
        } catch (Throwable ex) {
            log.warn("Ignored failure to write imported packages", ex);
        } finally {
            if (f != null)
                Fields.setAccessible(f, acs);
        }
        s.writeObject(null); //denote end-of-cls
    }

    public void read(ObjectInputStream s) throws IOException, ClassNotFoundException {
        read(_bshns, s);
    }

    /*package*/ static void read(NameSpace ns, ObjectInputStream s) throws IOException {
        for (;;) {
            try {
                final String nm = (String) s.readObject();
                if (nm == null)
                    break; //no more

                ns.setVariable(nm, s.readObject(), false);
            } catch (IOException ex) {
                throw ex;
            } catch (Throwable ex) {
                log.warn("Ignored failure to read", ex);
            }
        }

        for (;;) {
            try {
                final BshMethod mtd = (BshMethod) s.readObject();
                if (mtd == null)
                    break; //no more

                //fix declaringNameSpace
                Field f = null;
                boolean acs = false;
                try {
                    f = Classes.getAnyField(BshMethod.class, "declaringNameSpace");
                    acs = f.isAccessible();
                    Fields.setAccessible(f, true);
                    f.set(mtd, ns);
                } finally {
                    if (f != null)
                        Fields.setAccessible(f, acs);
                }
                ns.setMethod(mtd.getName(), mtd);
            } catch (IOException ex) {
                throw ex;
            } catch (Throwable ex) {
                log.warn("Ignored failure to read", ex);
            }
        }

        for (;;) {
            try {
                final String nm = (String) s.readObject();
                if (nm == null)
                    break; //no more

                ns.importClass(nm);
            } catch (IOException ex) {
                throw ex;
            } catch (Throwable ex) {
                log.warn("Ignored failure to read", ex);
            }
        }

        for (;;) {
            try {
                final String nm = (String) s.readObject();
                if (nm == null)
                    break; //no more

                ns.importPackage(nm);
            } catch (IOException ex) {
                throw ex;
            } catch (Throwable ex) {
                log.warn("Ignored failure to read", ex);
            }
        }
    }

    private static boolean isVariableSerializable(String name) {
        //we have to filter out them since BeanShell will store variables
        //that was accessed (by getFromScope)
        for (int j = _nonSerNames.length; --j >= 0;)
            if (_nonSerNames[j].equals(name))
                return false;
        return true;
    }

    private static final String[] _nonSerNames = { "log", "page", "desktop", "pageScope", "desktopScope",
            "applicationScope", "requestScope", "spaceOwner", "session", "sessionScope", "execution" };

    private static boolean isMethodSerializable(String name) {
        return !"alert".equals(name);
    }

    private static boolean shallSerializeMethod() {
        final String s = Library.getProperty("org.zkoss.zk.scripting.bsh.method.serializable");
        return s == null || !"false".equals(s);
    }

    private class BSHFunction implements Function {
        private final bsh.BshMethod _method;

        private BSHFunction(bsh.BshMethod method) {
            if (method == null)
                throw new IllegalArgumentException("null");
            _method = method;
        }

        //-- Function --//
        public Class[] getParameterTypes() {
            return _method.getParameterTypes();
        }

        public Class getReturnType() {
            return _method.getReturnType();
        }

        public Object invoke(Object obj, Object... args) throws Exception {
            return _method.invoke(args != null ? args : new Object[0], _ip);
        }

        public java.lang.reflect.Method toMethod() {
            return null;
        }
    }
}