| /* |
| * 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.IOException; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.util.HashMap; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| import javax.script.Bindings; |
| import javax.script.Compilable; |
| import javax.script.CompiledScript; |
| import javax.script.ScriptContext; |
| import javax.script.ScriptEngine; |
| import javax.script.ScriptEngineFactory; |
| import javax.script.ScriptException; |
| |
| import org.apache.commons.io.IOUtils; |
| 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.api.CachedScript; |
| import org.apache.sling.scripting.api.ScriptCache; |
| import org.apache.sling.scripting.api.ScriptNameAware; |
| import org.apache.sling.scripting.core.ScriptNameAwareReader; |
| 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.Script; |
| 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 implements Compilable { |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger(RhinoJavaScriptEngine.class); |
| private static final String NO_SCRIPT_NAME = "NO_SCRIPT_NAME"; |
| |
| private Scriptable rootScope; |
| private ScriptCache scriptCache; |
| |
| public RhinoJavaScriptEngine(ScriptEngineFactory factory, Scriptable rootScope, ScriptCache scriptCache) { |
| super(factory); |
| this.rootScope = rootScope; |
| this.scriptCache = scriptCache; |
| } |
| |
| public CompiledScript compile(String script) throws ScriptException { |
| Reader reader = null; |
| try { |
| reader = new InputStreamReader(IOUtils.toInputStream(script)); |
| return compile(reader); |
| } finally { |
| IOUtils.closeQuietly(reader); |
| } |
| } |
| |
| public CompiledScript compile(Reader scriptReader) throws ScriptException { |
| final String scriptName = getScriptName(scriptReader); |
| CachedScript cachedScript = scriptCache.getScript(scriptName); |
| if (cachedScript != null) { |
| LOGGER.debug("Detected cached script for {}.", scriptName); |
| return cachedScript.getCompiledScript(); |
| } else { |
| scriptReader = wrapReaderIfEspScript(scriptReader, scriptName); |
| try { |
| final Context rhinoContext = Context.enter(); |
| rhinoContext.setOptimizationLevel(optimizationLevel()); |
| |
| if (!ScriptRuntime.hasTopCall(rhinoContext)) { |
| // setup the context for use |
| WrapFactory wrapFactory = ((RhinoJavaScriptEngineFactory) getFactory()).getWrapFactory(); |
| rhinoContext.setWrapFactory(wrapFactory); |
| } |
| |
| final int lineNumber = 1; |
| final Object securityDomain = null; |
| |
| final Script script = rhinoContext.compileReader(scriptReader, scriptName, lineNumber, securityDomain); |
| final SlingCompiledScript slingCompiledScript = new SlingCompiledScript(script, this); |
| cachedScript = new CachedScript() { |
| @Override |
| public String getScriptPath() { |
| return scriptName; |
| } |
| |
| @Override |
| public CompiledScript getCompiledScript() { |
| return slingCompiledScript; |
| } |
| }; |
| // SLING-4935 avoid caching scripts for which we cannot determine a name |
| if (!scriptName.equals(NO_SCRIPT_NAME)) { |
| scriptCache.putScript(cachedScript); |
| } |
| LOGGER.debug("Added {} script to Script Cache.", scriptName); |
| return slingCompiledScript; |
| } catch (IOException e) { |
| final ScriptException se = new ScriptException("Failure running script " + scriptName + ": " + e.getMessage()); |
| se.initCause(e); |
| throw se; |
| } finally { |
| Context.exit(); |
| } |
| } |
| } |
| |
| public Object eval(Reader scriptReader, ScriptContext scriptContext) throws ScriptException { |
| String scriptName = getScriptName(scriptReader); |
| Reader reader = wrapReaderIfEspScript(scriptReader, scriptName); |
| if (!(scriptReader instanceof ScriptNameAware)) { |
| if (NO_SCRIPT_NAME.equals(scriptName)) { |
| String script = (String) scriptContext.getBindings(ScriptContext.ENGINE_SCOPE).get(ScriptEngine.FILENAME); |
| if (script != null) { |
| for (String extension : getFactory().getExtensions()) { |
| if (script.endsWith(extension)) { |
| scriptName = script; |
| reader = new ScriptNameAwareReader(reader, scriptName); |
| break; |
| } |
| } |
| } |
| } |
| } |
| return compile(reader).eval(scriptContext); |
| } |
| |
| private Reader wrapReaderIfEspScript(Reader scriptReader, String scriptName) { |
| if (scriptName.endsWith(RhinoJavaScriptEngineFactory.ESP_SCRIPT_EXTENSION)) { |
| scriptReader = new EspReader(scriptReader); |
| } |
| return scriptReader; |
| } |
| |
| 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(); |
| } |
| |
| private class SlingCompiledScript extends CompiledScript { |
| |
| private final Script script; |
| private final ScriptEngine engine; |
| |
| SlingCompiledScript(Script script, ScriptEngine engine) { |
| this.script = script; |
| this.engine = engine; |
| } |
| |
| @Override |
| public Object eval(ScriptContext scriptContext) throws ScriptException { |
| Bindings bindings = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE); |
| |
| // 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); |
| |
| Object result = script.exec(rhinoContext, scope); |
| |
| 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; |
| String scriptName = getScriptName(scriptContext); |
| 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 class loader on this thread is a dynamic class loader and it's dirty we should clear Rhino's class cache |
| * to avoid class loader leaks |
| */ |
| if (scope != null) { |
| ClassCache classCache = ClassCache.get(scope); |
| classCache.clearCaches(); |
| LOGGER.info("Detected dirty class loader on thread {}. Emptying Rhino's class cache.", Thread.currentThread() |
| .getName()); |
| } |
| } |
| } |
| Context.exit(); |
| } |
| } |
| |
| @Override |
| public ScriptEngine getEngine() { |
| return engine; |
| } |
| } |
| |
| private String getScriptName(Reader scriptReader) { |
| if(scriptReader instanceof ScriptNameAware){ |
| return ((ScriptNameAware) scriptReader).getScriptName(); |
| } |
| return NO_SCRIPT_NAME; |
| } |
| |
| private String getScriptName(ScriptContext scriptContext) { |
| Bindings bindings = scriptContext.getBindings(ScriptContext.ENGINE_SCOPE); |
| String scriptName = (String) bindings.get(ScriptEngine.FILENAME); |
| if (scriptName != null && !"".equals(scriptName)) { |
| return scriptName; |
| } |
| SlingScriptHelper sling = (SlingScriptHelper) bindings.get(SlingBindings.SLING); |
| if (sling != null) { |
| return sling.getScript().getScriptResource().getPath(); |
| } |
| return NO_SCRIPT_NAME; |
| } |
| } |