blob: b762ecebc49cb16201fead3a0e3bac0468ec80a9 [file] [log] [blame]
/*
* 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();
}
}