| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sling.scripting.javascript.internal; |
| |
| import java.io.Reader; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| import javax.script.Bindings; |
| import javax.script.ScriptContext; |
| import javax.script.ScriptEngineFactory; |
| import javax.script.ScriptException; |
| |
| import org.apache.sling.api.scripting.SlingBindings; |
| import org.apache.sling.api.scripting.SlingScriptHelper; |
| import org.apache.sling.commons.classloader.DynamicClassLoader; |
| import org.apache.sling.scripting.api.AbstractSlingScriptEngine; |
| import org.apache.sling.scripting.javascript.io.EspReader; |
| import org.mozilla.javascript.ClassCache; |
| import org.mozilla.javascript.Context; |
| import org.mozilla.javascript.ImporterTopLevel; |
| import org.mozilla.javascript.JavaScriptException; |
| import org.mozilla.javascript.ScriptRuntime; |
| import org.mozilla.javascript.Scriptable; |
| import org.mozilla.javascript.ScriptableObject; |
| import org.mozilla.javascript.Undefined; |
| import org.mozilla.javascript.WrapFactory; |
| import org.mozilla.javascript.Wrapper; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * A ScriptEngine that uses the Rhino interpreter to process Sling requests with |
| * server-side javascript. |
| */ |
| public class RhinoJavaScriptEngine extends AbstractSlingScriptEngine { |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger(RhinoJavaScriptEngine.class); |
| |
| private Scriptable rootScope; |
| |
| public RhinoJavaScriptEngine(ScriptEngineFactory factory, |
| Scriptable rootScope) { |
| super(factory); |
| this.rootScope = rootScope; |
| } |
| |
| public Object eval(Reader scriptReader, ScriptContext scriptContext) |
| throws ScriptException { |
| Bindings bindings = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE); |
| String scriptName = "NO_SCRIPT_NAME"; |
| { |
| SlingScriptHelper helper = (SlingScriptHelper) bindings.get(SlingBindings.SLING); |
| if (helper != null) { |
| scriptName = helper.getScript().getScriptResource().getPath(); |
| } |
| } |
| |
| // wrap the reader in an EspReader for ESP scripts |
| if (scriptName.endsWith(RhinoJavaScriptEngineFactory.ESP_SCRIPT_EXTENSION)) { |
| scriptReader = new EspReader(scriptReader); |
| } |
| |
| // container for replaced properties |
| Map<String, Object> replacedProperties = null; |
| Scriptable scope = null; |
| boolean isTopLevelCall = false; |
| |
| // create a rhino Context and execute the script |
| try { |
| |
| final Context rhinoContext = Context.enter(); |
| rhinoContext.setOptimizationLevel(optimizationLevel()); |
| |
| if (ScriptRuntime.hasTopCall(rhinoContext)) { |
| // reuse the top scope if we are included |
| scope = ScriptRuntime.getTopCallScope(rhinoContext); |
| |
| } else { |
| // create the request top scope, use the ImporterToplevel here |
| // to support the importPackage and importClasses functions |
| scope = new ImporterTopLevel(); |
| |
| // Set the global scope to be our prototype |
| scope.setPrototype(rootScope); |
| |
| // We want "scope" to be a new top-level scope, so set its |
| // parent scope to null. This means that any variables created |
| // by assignments will be properties of "scope". |
| scope.setParentScope(null); |
| |
| // setup the context for use |
| WrapFactory wrapFactory = ((RhinoJavaScriptEngineFactory) getFactory()).getWrapFactory(); |
| rhinoContext.setWrapFactory(wrapFactory); |
| |
| // this is the top level call |
| isTopLevelCall = true; |
| } |
| |
| // add initial properties to the scope |
| replacedProperties = setBoundProperties(scope, bindings); |
| |
| final int lineNumber = 1; |
| final Object securityDomain = null; |
| |
| Object result = rhinoContext.evaluateReader(scope, scriptReader, scriptName, |
| lineNumber, securityDomain); |
| |
| if (result instanceof Wrapper) { |
| result = ((Wrapper) result).unwrap(); |
| } |
| |
| return (result instanceof Undefined) ? null : result; |
| |
| } catch (JavaScriptException t) { |
| |
| // prevent variables to be pushed back in case of errors |
| isTopLevelCall = false; |
| |
| final ScriptException se = new ScriptException(t.details(), |
| t.sourceName(), t.lineNumber()); |
| |
| // log the script stack trace |
| ((Logger) bindings.get(SlingBindings.LOG)).error(t.getScriptStackTrace()); |
| |
| // set the exception cause |
| Object value = t.getValue(); |
| if (value != null) { |
| if (value instanceof Wrapper) { |
| value = ((Wrapper) value).unwrap(); |
| } |
| if (value instanceof Throwable) { |
| se.initCause((Throwable) value); |
| } |
| } |
| |
| // if the cause could not be set, overwrite the stack trace |
| if (se.getCause() == null) { |
| se.setStackTrace(t.getStackTrace()); |
| } |
| |
| throw se; |
| |
| } catch (Throwable t) { |
| |
| // prevent variables to be pushed back in case of errors |
| isTopLevelCall = false; |
| |
| final ScriptException se = new ScriptException( |
| "Failure running script " + scriptName + ": " + t.getMessage()); |
| se.initCause(t); |
| throw se; |
| |
| } finally { |
| |
| // if we are the top call (the Context is now null) we have to |
| // play back any properties from the scope back to the bindings |
| if (isTopLevelCall) { |
| getBoundProperties(scope, bindings); |
| } |
| |
| // if properties have been replaced, reset them |
| resetBoundProperties(scope, replacedProperties); |
| ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); |
| if (classLoader instanceof DynamicClassLoader) { |
| DynamicClassLoader dynamicClassLoader = (DynamicClassLoader) classLoader; |
| if (!dynamicClassLoader.isLive()) { |
| /** |
| * if the classloader on this thread is a dynamic class loader and it's dirty we should clear Rhino's class cache |
| * to avoid classloader leaks |
| */ |
| if (scope != null) { |
| ClassCache classCache = ClassCache.get(scope); |
| classCache.clearCaches(); |
| LOGGER.info("Detected dirty classloader on thread {}. Emptying Rhino's class cache.", Thread.currentThread() |
| .getName()); |
| } |
| } |
| } |
| Context.exit(); |
| } |
| } |
| |
| private Map<String, Object> setBoundProperties(Scriptable scope, |
| Bindings bindings) { |
| Map<String, Object> replacedProperties = new HashMap<String, Object>(); |
| |
| for (Object entryObject : bindings.entrySet()) { |
| Entry<?, ?> entry = (Entry<?, ?>) entryObject; |
| String name = (String) entry.getKey(); |
| Object value = entry.getValue(); |
| |
| if (value != null) { |
| // get the current property value, if set |
| if (ScriptableObject.hasProperty(scope, name)) { |
| replacedProperties.put(name, ScriptableObject.getProperty( |
| scope, name)); |
| } |
| |
| // wrap the new value and set it |
| Object wrapped = ScriptRuntime.toObject(scope, value); |
| ScriptableObject.putProperty(scope, name, wrapped); |
| } |
| } |
| |
| return replacedProperties; |
| } |
| |
| private void getBoundProperties(Scriptable scope, Bindings bindings) { |
| Object[] ids = scope.getIds(); |
| for (Object id : ids) { |
| if (id instanceof String) { |
| String key = (String) id; |
| Object value = scope.get(key, scope); |
| if (value != Scriptable.NOT_FOUND) { |
| if (value instanceof Wrapper) { |
| bindings.put(key, ((Wrapper) value).unwrap()); |
| } else { |
| bindings.put(key, value); |
| } |
| } |
| } |
| } |
| } |
| |
| private void resetBoundProperties(Scriptable scope, |
| Map<String, Object> properties) { |
| if (scope != null && properties != null && properties.size() > 0) { |
| for (Entry<String, Object> entry : properties.entrySet()) { |
| ScriptableObject.putProperty(scope, entry.getKey(), |
| entry.getValue()); |
| } |
| } |
| } |
| |
| private int optimizationLevel() { |
| return ((RhinoJavaScriptEngineFactory)getFactory()).getOptimizationLevel(); |
| } |
| } |