org.rhq.bindings.ScriptEngineFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.rhq.bindings.ScriptEngineFactory.java

Source

/*
 * RHQ Management Platform
 * Copyright (C) 2005-2011 Red Hat, Inc.
 * 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 version 2 of the License.
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

package org.rhq.bindings;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.rhq.bindings.engine.JsEngineInitializer;
import org.rhq.bindings.engine.ScriptEngineInitializer;
import org.rhq.bindings.util.NoTopLevelIndirection;
import org.rhq.bindings.util.PackageFinder;

/**
 * This is RHQ specific imitation of ScriptEngineFactory.
 * 
 * In RHQ, we provide a standard set of bound variables in the script context
 * and also import classes from our standard packages to ease the development
 * of the scripts.
 * <p>
 * This factory is able to instantiate a script engine and initialize it consistently
 * so that all users of the script engine get the uniform environment to write the scripts
 * in.
 *
 * @author Lukas Krejci
 */
public class ScriptEngineFactory {
    private static final Log LOG = LogFactory.getLog(ScriptEngineFactory.class);

    private static final ScriptEngineInitializer[] KNOWN_ENGINES = { new JsEngineInitializer() };

    private ScriptEngineFactory() {

    }

    /**
     * Initializes the script engine for given language.
     * 
     * @param language the language of the script to instantiate
     * @param packageFinder the package finder to find the standard packages in user provided locations
     * @param bindings the initial standard bindings or null if none required
     * @return the initialized engine or null if the engine for given language isn't known.
     * 
     * @throws ScriptException on error during initialization of the script environment
     * @throws IOException if the package finder fails to find the packages
     */
    public static ScriptEngine getScriptEngine(String language, PackageFinder packageFinder,
            StandardBindings bindings) throws ScriptException, IOException {
        ScriptEngineInitializer initializer = getInitializer(language);

        if (initializer == null) {
            return null;
        }

        ScriptEngine engine = initializer.instantiate(packageFinder.findPackages("org.rhq.core.domain"));

        if (bindings != null) {
            injectStandardBindings(engine, bindings, true);
        }

        return engine;
    }

    /**
     * This method is similar to the {@link #getScriptEngine(String, PackageFinder, StandardBindings)} method
     * but additionally applies a security wrapper on the returned script engine so that the scripts execute
     * with the provided java permissions.
     * 
     * @see #getScriptEngine(String, PackageFinder, StandardBindings)
     */
    public static ScriptEngine getSecuredScriptEngine(final String language, final PackageFinder packageFinder,
            final StandardBindings bindings, final PermissionCollection permissions)
            throws ScriptException, IOException {
        CodeSource src = new CodeSource(new URL("http://rhq-project.org/scripting"), (Certificate[]) null);
        ProtectionDomain scriptDomain = new ProtectionDomain(src, permissions);
        AccessControlContext ctx = new AccessControlContext(new ProtectionDomain[] { scriptDomain });
        try {
            return AccessController.doPrivileged(new PrivilegedExceptionAction<ScriptEngine>() {
                @Override
                public ScriptEngine run() throws Exception {
                    //This might seem a bit excessive but is necessary due to the 
                    //change in security handling in the rhino script engine
                    //that occured in Java6u27 (due to a CVE desribed here:
                    //https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2011-3544)

                    //In Java 6u26 and earlier, it was enough to wrap a script engine
                    //in the sandbox and everything would work.

                    //Java 6u27 introduced new behavior where the rhino script engine
                    //remembers the access control context with which it has been 
                    //constructed and combines that with the callers protection domain
                    //when a script is executed. Because this class has all perms and
                    //all the code in RHQ that called ScriptEngine.eval* also
                    //had all perms, the scripts would never be sandboxed even if the call
                    //was pushed through the SandboxedScriptEngine.

                    //This means that the below wrapping is necessary for the security
                    //to work in java6 pre u27 while the surrounding privileged block 
                    //is necessary for the security to be applied in java6 u27 and later.
                    return new SandboxedScriptEngine(getScriptEngine(language, packageFinder, bindings),
                            permissions);
                }
            }, ctx);
        } catch (PrivilegedActionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof IOException) {
                throw (IOException) cause;
            } else if (cause instanceof ScriptException) {
                throw (ScriptException) cause;
            } else {
                throw new ScriptException(e);
            }
        }
    }

    /**
     * Injects the values provided in the bindings into the {@link ScriptContext#ENGINE_SCOPE engine scope}
     * of the provided script engine.
     * 
     * @param engine the engine
     * @param bindings the bindings
     * @param deleteExistingBindings true if the existing bindings should be replaced by the provided ones, false
     * if the provided bindings should be added to the existing ones (possibly overwriting bindings with the same name).
     */
    public static void injectStandardBindings(ScriptEngine engine, StandardBindings bindings,
            boolean deleteExistingBindings) {
        bindings.preInject(engine);

        Bindings engineBindings = deleteExistingBindings ? engine.createBindings()
                : engine.getBindings(ScriptContext.ENGINE_SCOPE);

        for (Map.Entry<String, Object> entry : bindings.entrySet()) {
            engineBindings.put(entry.getKey(), entry.getValue());
        }

        engine.setBindings(engineBindings, ScriptContext.ENGINE_SCOPE);

        bindings.postInject(engine);
    }

    /**
     * Remove the specified bindings from the engine.
     * 
     * @param engine the engine
     * @param keySet the binding keys to be removed
     */
    public static void removeBindings(ScriptEngine engine, Set<String> keySet) {

        Bindings engineBindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);

        for (String key : keySet) {
            engineBindings.remove(key);
        }

        engine.setBindings(engineBindings, ScriptContext.ENGINE_SCOPE);
    }

    /**
     * Goes through the methods of the object found in the <code>scriptEngine</code>'s ENGINE_SCOPE
     * and for each of them generates a top-level function that is called the same name and accepts the same
     * parameters.
     * 
     * @param scriptEngine the script engine to generate the top-level functions in
     * @param bindingName the name of the object in the script engine to generate the functions from
     * 
     * @see ScriptEngineInitializer#generateIndirectionMethod(String, Method)
     * @see NoTopLevelIndirection
     */
    public static void bindIndirectionMethods(ScriptEngine scriptEngine, String bindingName) {
        Object object = scriptEngine.get(bindingName);
        if (object == null) {
            LOG.debug("The script engine doesn't contain a binding called '" + bindingName
                    + "'. No indirection functions will be generated.");
            return;
        }

        ScriptEngineInitializer initializer = getInitializer(scriptEngine.getFactory().getLanguageName());
        try {
            BeanInfo beanInfo = Introspector.getBeanInfo(object.getClass(), Object.class);
            MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();

            Map<String, Set<Method>> overloadsPerMethodName = new HashMap<String, Set<Method>>();
            for (MethodDescriptor methodDescriptor : methodDescriptors) {
                Method method = methodDescriptor.getMethod();
                if (shouldIndirect(method)) {
                    Set<Method> overloads = overloadsPerMethodName.get(method.getName());
                    if (overloads == null) {
                        overloads = new HashSet<Method>();
                        overloadsPerMethodName.put(method.getName(), overloads);
                    }
                    overloads.add(method);
                }
            }

            for (Set<Method> overloads : overloadsPerMethodName.values()) {
                Set<String> methodDefs = initializer.generateIndirectionMethods(bindingName, overloads);
                for (String methodDef : methodDefs) {
                    try {
                        scriptEngine.eval(methodDef);
                    } catch (ScriptException e) {
                        LOG.warn("Unable to define global function declared as:\n" + methodDef, e);
                    }
                }
            }
        } catch (IntrospectionException e) {
            LOG.debug("Could not inspect class " + object.getClass().getName()
                    + ". No indirection methods for variable '" + bindingName + "' will be generated.", e);
        }
    }

    public static ScriptEngineInitializer getInitializer(String language) {
        for (ScriptEngineInitializer i : KNOWN_ENGINES) {
            if (i.implementsLanguage(language)) {
                return i;
            }
        }

        return null;
    }

    private static boolean shouldIndirect(Method method) {
        return method.getAnnotation(NoTopLevelIndirection.class) == null;
    }
}