blob: 67908d8f9f45f5a634612484e61f9e27e693a79a [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.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.apache.sling.scripting.api.AbstractScriptEngineFactory;
import org.apache.sling.scripting.api.ScriptCache;
import org.apache.sling.scripting.javascript.RhinoHostObjectProvider;
import org.apache.sling.scripting.javascript.SlingWrapper;
import org.apache.sling.scripting.javascript.helper.SlingContextFactory;
import org.apache.sling.scripting.javascript.helper.SlingWrapFactory;
import org.apache.sling.scripting.javascript.wrapper.ScriptableCalendar;
import org.apache.sling.scripting.javascript.wrapper.ScriptableItemMap;
import org.apache.sling.scripting.javascript.wrapper.ScriptableMap;
import org.apache.sling.scripting.javascript.wrapper.ScriptableNode;
import org.apache.sling.scripting.javascript.wrapper.ScriptablePrintWriter;
import org.apache.sling.scripting.javascript.wrapper.ScriptableProperty;
import org.apache.sling.scripting.javascript.wrapper.ScriptableResource;
import org.apache.sling.scripting.javascript.wrapper.ScriptableVersion;
import org.apache.sling.scripting.javascript.wrapper.ScriptableVersionHistory;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ImporterTopLevel;
import org.mozilla.javascript.NativeJavaClass;
import org.mozilla.javascript.NativeJavaPackage;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.tools.debugger.ScopeProvider;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component(
service = ScriptEngineFactory.class,
property = {
Constants.SERVICE_DESCRIPTION + "=Apache Sling Rhino Javascript Engine Factory",
Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
"extensions=" + RhinoJavaScriptEngineFactory.ECMA_SCRIPT_EXTENSION,
"extensions=" + RhinoJavaScriptEngineFactory.ESP_SCRIPT_EXTENSION,
"mimeTypes=text/ecmascript",
"mimeTypes=text/javascript",
"mimeTypes=application/ecmascript",
"mimeTypes=application/javascript",
"names=rhino",
"names=Rhino",
"names=javascript",
"names=JavaScript",
"names=ecmascript",
"names=ECMAScript"
},
reference = @Reference(
name = "HostObjectProvider",
service = RhinoHostObjectProvider.class,
cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC,
bind = "addHostObjectProvider",
unbind = "removeHostObjectProvider"
)
)
@Designate(
ocd = RhinoJavaScriptEngineFactoryConfiguration.class
)
public class RhinoJavaScriptEngineFactory extends AbstractScriptEngineFactory implements ScopeProvider {
public final static int DEFAULT_OPTIMIZATION_LEVEL = 9;
public final static String ECMA_SCRIPT_EXTENSION = "ecma";
public final static String ESP_SCRIPT_EXTENSION = "esp";
private static final Class<?>[] HOSTOBJECT_CLASSES = {
ScriptableResource.class, ScriptableNode.class,
ScriptableProperty.class, ScriptableItemMap.class,
ScriptablePrintWriter.class, ScriptableVersionHistory.class,
ScriptableVersion.class, ScriptableCalendar.class, ScriptableMap.class
};
/**
* default log
*/
private final Logger log = LoggerFactory.getLogger(getClass());
private int optimizationLevel;
private static final int RHINO_LANGUAGE_VERSION = Context.VERSION_ES6;
private static final String LANGUAGE_VERSION = "partial ECMAScript 2015 support";
private static final String LANGUAGE_NAME = "ECMAScript";
private SlingWrapFactory wrapFactory;
private Scriptable rootScope;
private final Set<RhinoHostObjectProvider> hostObjectProvider = new HashSet<RhinoHostObjectProvider>();
@Reference
private DynamicClassLoaderManager dynamicClassLoaderManager = null;
@Reference
private ScriptCache scriptCache = null;
public ScriptEngine getScriptEngine() {
return new RhinoJavaScriptEngine(this, getRootScope(), scriptCache);
}
public String getLanguageName() {
return LANGUAGE_NAME;
}
public String getLanguageVersion() {
return LANGUAGE_VERSION;
}
/**
* Get the optimization level that should be used when running JS scripts
* with Rhino
*
* @return an integer from 0-9 with 9 being the most aggressive optimization, or
* -1 if interpreted mode is to be used
*/
int getOptimizationLevel() {
return optimizationLevel;
}
int rhinoLanguageVersion() {
return Context.VERSION_ES6;
}
public Object getParameter(String name) {
if ("THREADING".equals(name)) {
return "MULTITHREADED";
}
return super.getParameter(name);
}
public Scriptable getScope() {
return getRootScope();
}
SlingWrapFactory getWrapFactory() {
return wrapFactory;
}
@SuppressWarnings("unchecked")
private Scriptable getRootScope() {
if (rootScope == null) {
final Context rhinoContext = Context.enter();
try {
rhinoContext.setOptimizationLevel(optimizationLevel);
rhinoContext.setLanguageVersion(RHINO_LANGUAGE_VERSION);
Scriptable tmpScope = rhinoContext.initStandardObjects(new ImporterTopLevel(rhinoContext), false);
// default classes
addHostObjects(tmpScope, (Class<? extends ScriptableObject>[]) HOSTOBJECT_CLASSES);
// provided classes
for (RhinoHostObjectProvider provider : hostObjectProvider) {
addHostObjects(tmpScope, provider.getHostObjectClasses());
addImportedClasses(rhinoContext, tmpScope, provider.getImportedClasses());
addImportedPackages(rhinoContext, tmpScope, provider.getImportedPackages());
}
// only assign the root scope when complete set up
rootScope = tmpScope;
} finally {
// ensure the context is exited after setting up the
// the new root scope
Context.exit();
}
}
return rootScope;
}
private void dropRootScope() {
// ensure the debugger is closed if the root scope will
// be replaced to ensure no references to the old scope
// and context remain
ContextFactory contextFactory = ContextFactory.getGlobal();
if (contextFactory instanceof SlingContextFactory) {
((SlingContextFactory) contextFactory).exitDebugger();
}
// drop the scope
rootScope = null;
}
// ---------- SCR integration
@Activate
protected void activate(final ComponentContext context, final RhinoJavaScriptEngineFactoryConfiguration configuration) {
Dictionary<?, ?> props = context.getProperties();
boolean debugging = getProperty("org.apache.sling.scripting.javascript.debug", props, context.getBundleContext(), false);
// try to get the manifest
String rhinoVersion = null;
InputStream ins = null;
try {
Enumeration<URL> resources = RhinoJavaScriptEngineFactory.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
try {
URL url = resources.nextElement();
ins = url.openStream();
if (ins != null) {
Manifest manifest = new Manifest(ins);
Attributes attrs = manifest.getMainAttributes();
rhinoVersion = attrs.getValue("Rhino-Version");
if (rhinoVersion != null) {
break;
}
}
} finally {
if (ins != null) {
try {
ins.close();
} catch (IOException ignore) {
}
}
}
}
} catch (IOException ioe) {
log.warn("Unable to read Rhino version.", ioe);
}
optimizationLevel = readOptimizationLevel(configuration);
// setup the wrap factory
wrapFactory = new SlingWrapFactory();
// initialize the Rhino Context Factory
SlingContextFactory.setup(this, RHINO_LANGUAGE_VERSION);
setEngineName(getEngineName() + " (Rhino " + (rhinoVersion != null ? rhinoVersion : "unknown") + ")");
setExtensions(PropertiesUtil.toStringArray(props.get("extensions")));
setMimeTypes(PropertiesUtil.toStringArray(props.get("mimeTypes")));
setNames(PropertiesUtil.toStringArray(props.get("names")));
final ContextFactory contextFactory = ContextFactory.getGlobal();
if (contextFactory instanceof SlingContextFactory) {
((SlingContextFactory) contextFactory).setDebugging(debugging);
}
// set the dynamic class loader as the application class loader
final DynamicClassLoaderManager dclm = this.dynamicClassLoaderManager;
if (dclm != null) {
contextFactory.initApplicationClassLoader(dynamicClassLoaderManager.getDynamicClassLoader());
}
log.info("Activated with optimization level {}", optimizationLevel);
}
@Deactivate
@SuppressWarnings("unused")
protected void deactivate(ComponentContext context) {
// remove the root scope
dropRootScope();
// remove our context factory
SlingContextFactory.teardown();
// remove references
wrapFactory = null;
hostObjectProvider.clear();
}
@SuppressWarnings("unused")
protected void addHostObjectProvider(RhinoHostObjectProvider provider) {
hostObjectProvider.add(provider);
if (rootScope != null) {
addHostObjects(rootScope, provider.getHostObjectClasses());
}
}
@SuppressWarnings("unused")
protected void removeHostObjectProvider(RhinoHostObjectProvider provider) {
// remove the current root scope and have it recreated using the
// new host object classes
if (hostObjectProvider.remove(provider)) {
dropRootScope();
}
}
// ---------- internal
private void addHostObjects(Scriptable scope, Class<? extends Scriptable>[] classes) {
if (classes != null) {
for (Class<? extends Scriptable> clazz : classes) {
try {
// register the host object
ScriptableObject.defineClass(scope, clazz);
if (SlingWrapper.class.isAssignableFrom(clazz)) {
// SlingWrappers can map to several classes if needed
final SlingWrapper hostWrapper = (SlingWrapper) clazz.newInstance();
for (Class<?> c : hostWrapper.getWrappedClasses()) {
getWrapFactory().registerWrapper(c, hostWrapper.getClassName());
}
} else {
// but other Scriptable host objects need to be
// registered as well
final Scriptable host = clazz.newInstance();
getWrapFactory().registerWrapper(host.getClass(), host.getClassName());
}
} catch (Throwable t) {
log.warn("addHostObjects: Cannot prepare host object " + clazz, t);
}
}
}
}
private void addImportedClasses(Context cx, Scriptable scope,
Class<?>[] classes) {
if (classes != null && classes.length > 0) {
NativeJavaClass[] np = new NativeJavaClass[classes.length];
for (int i = 0; i < classes.length; i++) {
np[i] = new NativeJavaClass(scope, classes[i]);
}
ScriptableObject.callMethod(cx, scope, "importClass", np);
}
}
private void addImportedPackages(Context cx, Scriptable scope,
String[] packages) {
if (packages != null && packages.length > 0) {
NativeJavaPackage[] np = new NativeJavaPackage[packages.length];
for (int i = 0; i < packages.length; i++) {
np[i] = new NativeJavaPackage(packages[i]);
}
ScriptableObject.callMethod(cx, scope, "importPackage", np);
}
}
private boolean getProperty(String name, Dictionary<?, ?> props,
BundleContext bundleContext, boolean defaultValue) {
Object value = props.get(name);
if (value == null) {
value = bundleContext.getProperty(name);
}
return (value != null)
? Boolean.parseBoolean(String.valueOf(value))
: defaultValue;
}
private int readOptimizationLevel(final RhinoJavaScriptEngineFactoryConfiguration configuration) {
int optLevel = configuration.org_apache_sling_scripting_javascript_rhino_optLevel();
if (!Context.isValidOptimizationLevel(optLevel)) {
log.warn("Invalid optimization level {}, using default value", optLevel);
optLevel = DEFAULT_OPTIMIZATION_LEVEL;
}
return optLevel;
}
}