blob: 3d95960fe5e0fb9de9868156480ab3eb632f1b71 [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.sightly.js.impl;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import javax.script.SimpleScriptContext;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.scripting.SlingBindings;
import org.apache.sling.api.scripting.SlingScriptHelper;
import org.apache.sling.scripting.core.ScriptNameAwareReader;
import org.apache.sling.scripting.sightly.ResourceResolution;
import org.apache.sling.scripting.sightly.SightlyException;
import org.apache.sling.scripting.sightly.js.impl.async.AsyncContainer;
import org.apache.sling.scripting.sightly.js.impl.async.TimingBindingsValuesProvider;
import org.apache.sling.scripting.sightly.js.impl.async.UnaryCallback;
import org.apache.sling.scripting.sightly.js.impl.cjs.CommonJsModule;
import org.apache.sling.scripting.sightly.js.impl.loop.EventLoop;
import org.apache.sling.scripting.sightly.js.impl.loop.EventLoopInterop;
import org.apache.sling.scripting.sightly.js.impl.loop.Task;
import org.apache.sling.scripting.sightly.js.impl.use.DependencyResolver;
import org.apache.sling.scripting.sightly.js.impl.use.UseFunction;
import org.mozilla.javascript.Context;
import org.slf4j.LoggerFactory;
/**
* Environment for running JS scripts
*/
public class JsEnvironment {
private final ScriptEngine jsEngine;
private final Bindings engineBindings;
private EventLoop eventLoop;
public JsEnvironment(ScriptEngine jsEngine) {
this.jsEngine = jsEngine;
engineBindings = new SimpleBindings();
TimingBindingsValuesProvider.INSTANCE.addBindings(engineBindings);
}
public void initialize() {
Context context = Context.enter();
eventLoop = EventLoopInterop.obtainEventLoop(context);
}
public void cleanup() {
Context context = Context.getCurrentContext();
if (context == null) {
throw new IllegalStateException("No current context");
}
EventLoopInterop.cleanupEventLoop(context);
Context.exit();
}
/**
* Run a Js script at a given path
* @param caller the resource of the script that invokes the Js code
* @param path the path to the JS script
* @param globalBindings the global bindings for the script
* @param arguments the arguments from the use-plugin
* @param callback callback that will receive the result of the script
*/
public void run(Resource caller, String path, Bindings globalBindings, Bindings arguments, UnaryCallback callback) {
Resource scriptResource = caller.getChild(path);
SlingScriptHelper scriptHelper = (SlingScriptHelper) globalBindings.get(SlingBindings.SLING);
Resource componentCaller = ResourceResolution.getResourceForRequest(caller.getResourceResolver(), scriptHelper.getRequest());
if (scriptResource == null) {
if (isResourceOverlay(caller, componentCaller)) {
scriptResource = ResourceResolution.getResourceFromSearchPath(componentCaller, path);
} else {
scriptResource = ResourceResolution.getResourceFromSearchPath(caller, path);
}
}
if (scriptResource == null) {
throw new SightlyException("Required script resource could not be located: " + path);
}
runResource(scriptResource, globalBindings, arguments, callback);
}
public void runResource(Resource scriptResource, Bindings globalBindings, Bindings arguments, UnaryCallback callback) {
ScriptContext scriptContext = new SimpleScriptContext();
CommonJsModule module = new CommonJsModule();
Bindings scriptBindings = buildBindings(scriptResource, globalBindings, arguments, module);
scriptContext.setBindings(scriptBindings, ScriptContext.ENGINE_SCOPE);
scriptContext.setAttribute(ScriptEngine.FILENAME, scriptResource.getPath(), ScriptContext.ENGINE_SCOPE);
runScript(scriptResource, scriptContext, callback, module);
}
public AsyncContainer runResource(Resource scriptResource, Bindings globalBindings, Bindings arguments) {
AsyncContainer asyncContainer = new AsyncContainer();
runResource(scriptResource, globalBindings, arguments, asyncContainer.createCompletionCallback());
return asyncContainer;
}
/**
* Run a script at a given path
*
* @param caller the resource of the script that invokes the Js code
* @param path the path to the JS script
* @param globalBindings bindings for the JS script
* @param arguments the arguments for the JS script
* @return an asynchronous container for the result
* @throws UnsupportedOperationException if this method is run when the event loop is not empty
*/
public AsyncContainer run(Resource caller, String path, Bindings globalBindings, Bindings arguments) {
AsyncContainer asyncContainer = new AsyncContainer();
run(caller, path, globalBindings, arguments, asyncContainer.createCompletionCallback());
return asyncContainer;
}
private Bindings buildBindings(Resource scriptResource, Bindings local, Bindings arguments, CommonJsModule commonJsModule) {
Bindings bindings = new SimpleBindings();
bindings.putAll(engineBindings);
DependencyResolver dependencyResolver = new DependencyResolver(scriptResource, this, local);
UseFunction useFunction = new UseFunction(dependencyResolver, arguments);
bindings.put(Variables.JS_USE, useFunction);
bindings.put(Variables.MODULE, commonJsModule);
bindings.put(Variables.EXPORTS, commonJsModule.getExports());
bindings.put(Variables.CONSOLE, new Console(LoggerFactory.getLogger(scriptResource.getName())));
bindings.putAll(local);
return bindings;
}
private void runScript(Resource scriptResource, ScriptContext scriptContext, UnaryCallback callback, CommonJsModule commonJsModule) {
eventLoop.schedule(scriptTask(scriptResource, scriptContext, callback, commonJsModule));
}
private Task scriptTask(final Resource scriptResource, final ScriptContext scriptContext,
final UnaryCallback callback, final CommonJsModule commonJsModule) {
return new Task(new Runnable() {
@Override
public void run() {
Reader reader = null;
try {
Object result;
if (jsEngine instanceof Compilable) {
reader = new ScriptNameAwareReader(new InputStreamReader(scriptResource.adaptTo(InputStream.class)),
scriptResource.getPath());
result = ((Compilable) jsEngine).compile(reader).eval(scriptContext);
} else {
reader = new InputStreamReader(scriptResource.adaptTo(InputStream.class));
result = jsEngine.eval(reader, scriptContext);
}
if (commonJsModule.isModified()) {
result = commonJsModule.getExports();
}
if (result instanceof AsyncContainer) {
((AsyncContainer) result).addListener(callback);
} else {
callback.invoke(result);
}
} catch (ScriptException e) {
throw new SightlyException(e);
} finally {
IOUtils.closeQuietly(reader);
}
}
});
}
/**
* Using the inheritance chain created with the help of {@code sling:resourceSuperType} this method checks if {@code resourceB}
* inherits from {@code resourceA}. In case {@code resourceA} is a {@code nt:file}, its parent will be used for the inheritance check.
*
* @param resourceA the base resource
* @param resourceB the potentially overlaid resource
* @return {@code true} if {@code resourceB} overlays {@code resourceB}, {@code false} otherwise
*/
private boolean isResourceOverlay(Resource resourceA, Resource resourceB) {
String resourceBSuperType = resourceB.getResourceSuperType();
if (StringUtils.isNotEmpty(resourceBSuperType)) {
ResourceResolver resolver = resourceA.getResourceResolver();
String parentResourceType = resourceA.getResourceType();
if ("nt:file".equals(parentResourceType)) {
parentResourceType = ResourceUtil.getParent(resourceA.getPath());
}
Resource parentB = resolver.getResource(resourceBSuperType);
while (parentB != null && !"/".equals(parentB.getPath()) && StringUtils.isNotEmpty(resourceBSuperType)) {
if (parentB.getPath().equals(parentResourceType)) {
return true;
}
resourceBSuperType = parentB.getResourceSuperType();
parentB = resolver.getResource(resourceBSuperType);
}
}
return false;
}
}