blob: 34bfccd1818a94f3b466090265c4be36be29c418 [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.pipes;
import org.apache.commons.io.IOUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.script.Bindings;
import javax.script.Invocable;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Execution bindings of a pipe, and all expression related
*/
public class PipeBindings {
/**
* interface mapping a javascript date
*/
public interface JsDate {
long getTime();
int getTimezoneOffset();
}
private static final Logger log = LoggerFactory.getLogger(PipeBindings.class);
public static final String NASHORNSCRIPTENGINE = "nashorn";
public static final String NN_ADDITIONALBINDINGS = "additionalBindings";
public static final String PN_ADDITIONALSCRIPTS = "additionalScripts";
/**
* add ${path.pipeName} binding allowing to retrieve pipeName's current resource path
*/
public static final String PATH_BINDING = "path";
/**
* add ${name.pipeName} binding allowing to retrieve pipeName's current resource name
*/
public static final String NAME_BINDING = "name";
private static final Pattern INJECTED_SCRIPT = Pattern.compile("\\$\\{(([^\\{^\\}]*(\\{[0-9,]+\\})?)*)\\}");
ScriptEngine engine;
ScriptContext scriptContext = new SimpleScriptContext();
Map<String, String> pathBindings = new HashMap<>();
Map<String, String> nameBindings = new HashMap<>();
Map<String, Resource> outputResources = new HashMap<>();
String currentError;
/**
* public constructor, built from pipe's resource
* @param resource pipe's configuration resource
* @throws ScriptException in case scripts associated with the bindings are not assessable
*/
public PipeBindings(Resource resource) throws ScriptException {
//Setup script engines
initializeScriptEngine();
//add path bindings where path.MyPipe will give MyPipe current resource path
getBindings().put(PATH_BINDING, pathBindings);
//add name bindings where name.MyPipe will give MyPipe current resource name
getBindings().put(NAME_BINDING, nameBindings);
//additional bindings (global variables to use in child pipes expressions)
Resource additionalBindings = resource.getChild(NN_ADDITIONALBINDINGS);
if (additionalBindings != null) {
ValueMap bindings = additionalBindings.adaptTo(ValueMap.class);
addBindings(bindings);
for (String ignoredProperty : BasePipe.IGNORED_PROPERTIES){
getBindings().remove(ignoredProperty);
}
}
Resource scriptsResource = resource.getChild(PN_ADDITIONALSCRIPTS);
if (scriptsResource != null) {
String[] scripts = scriptsResource.adaptTo(String[].class);
if (scripts != null) {
for (String script : scripts){
addScript(resource.getResourceResolver(), script);
}
}
}
}
/**
* add a binding
* @param name binding's name
* @param value binding's value
*/
public void addBinding(String name, Object value){
log.debug("Adding binding {}={}", name, value);
getBindings().put(name, value);
}
/**
* adds additional bindings (global variables to use in child pipes expressions)
* @param bindings key/values bindings to add to the existing bindings
*/
public void addBindings(Map bindings) {
log.info("Adding bindings {}", bindings);
getBindings().putAll(bindings);
}
/**
* add a script file to the engine
* @param resolver resolver with which the file should be read
* @param path path of the script file
*/
public void addScript(ResourceResolver resolver, String path) {
InputStream is = null;
try {
if (path.startsWith("http")) {
try {
URL remoteScript = new URL(path);
is = remoteScript.openStream();
} catch (Exception e) {
log.error("unable to retrieve remote script", e);
}
} else if (path.startsWith("/")) {
Resource scriptResource = resolver.getResource(path);
if (scriptResource != null) {
is = scriptResource.adaptTo(InputStream.class);
}
}
if (is != null) {
try {
engine.eval(new InputStreamReader(is), scriptContext);
} catch (Exception e) {
log.error("Add script: unable to evaluate script {}", path, e);
}
}
} finally {
IOUtils.closeQuietly(is);
}
}
/**
* @param expr expression with or without ${} use
* @return true if the expression is 'just' a plain string
*/
public boolean isPlainString(String expr){
return computeECMA5Expression(expr) == null;
}
/**
* Doesn't look like nashorn likes template strings :-(
* @param expr ECMA like expression <code>blah${'some' + 'ecma' + 'expression'}</code>
* @return computed expression, null if the expression is a plain string
*/
protected String computeECMA5Expression(String expr){
Matcher matcher = INJECTED_SCRIPT.matcher(expr);
if (INJECTED_SCRIPT.matcher(expr).find()) {
StringBuilder expression = new StringBuilder();
int start = 0;
while (matcher.find()) {
if (matcher.start() > start) {
if (expression.length() == 0) {
expression.append("'");
}
expression.append(expr.substring(start, matcher.start()));
}
if (expression.length() > 0) {
expression.append("' + ");
}
expression.append(matcher.group(1));
start = matcher.end();
if (start < expr.length()) {
expression.append(" + '");
}
}
if (start < expr.length()) {
expression.append(expr.substring(start) + "'");
}
return expression.toString();
}
return null;
}
/**
* copy bindings
* @param original original bindings to copy
*/
public void copyBindings(PipeBindings original){
getBindings().putAll(original.getBindings());
}
/**
* evaluate a given expression
* @param expr ecma like expression
* @return object that is the result of the expression
* @throws ScriptException in case the script fails, an exception is thrown (to let call code the opportunity to stop the execution)
*/
protected Object evaluate(String expr) throws ScriptException {
String computed = computeECMA5Expression(expr);
if (computed != null){
//computed is null in case expr is a simple string
return engine.eval(computed, scriptContext);
}
return expr;
}
/**
* return registered bindings
* @return bindings
*/
public Bindings getBindings() {
return scriptContext.getBindings(ScriptContext.ENGINE_SCOPE);
}
/**
* return Pipe <code>name</code>'s output binding
* @param name name of the pipe
* @return resource corresponding to that pipe output
*/
public Resource getExecutedResource(String name) {
return outputResources.get(name);
}
/**
* Initialize the ScriptEngine.
* In some contexts the nashorn engine cannot be obtained from thread's class loader. Do fallback to system classloader.
* @throws ScriptException
*/
private void initializeScriptEngine() throws ScriptException{
engine = new ScriptEngineManager().getEngineByName(PipeBindings.NASHORNSCRIPTENGINE);
if(engine == null){
//Fallback to system classloader
engine = new ScriptEngineManager(null).getEngineByName(PipeBindings.NASHORNSCRIPTENGINE);
//Check if nashorn can still not be instantiated
if(engine == null){
throw new ScriptException("Can not instantiate nashorn scriptengine. Check JVM version & capabilities.");
}
}
engine.setContext(scriptContext);
}
/**
* Expression is a function of variables from execution context, that
* we implement here as a String
* @param expr ecma like expression
* @return String that is the result of the expression
* @throws ScriptException in case expression computing went wrong
*/
public String instantiateExpression(String expr) throws ScriptException {
return (String)evaluate(expr);
}
/**
* Instantiate object from expression
* @param expr ecma expression
* @return instantiated object
* @throws ScriptException in case object computing went wrong
*/
public Object instantiateObject(String expr) throws ScriptException {
Object result = evaluate(expr);
if (result != null && ! result.getClass().getName().startsWith("java.lang.")) {
//special case of the date in which case jdk.nashorn.api.scripting.ScriptObjectMirror will
//be returned
JsDate jsDate = ((Invocable) engine).getInterface(result, JsDate.class);
if (jsDate != null ) {
Date date = new Date(jsDate.getTime() + jsDate.getTimezoneOffset() * 60 * 1000);
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return cal;
}
}
return result;
}
/**
* check if a given bindings is defined or not
* @param name name of the binding
* @return true if <code>name</code> is registered
*/
public boolean isBindingDefined(String name){
return getBindings().containsKey(name);
}
/**
* Update current resource of a given pipe, and appropriate binding
* @param pipe pipe we'll extract the output binding from
* @param resource current resource in the pipe execution
*/
public void updateBindings(Pipe pipe, Resource resource) {
outputResources.put(pipe.getName(), resource);
updateStaticBindings(pipe.getName(), resource);
addBinding(pipe.getName(), pipe.getOutputBinding());
}
/**
* Update all the static bindings related to a given resource
* @param name name under which static bindings should be recorded
* @param resource resource from which static bindings will be built
*/
public void updateStaticBindings(String name, Resource resource){
if (resource != null) {
pathBindings.put(name, resource.getPath());
nameBindings.put(name, resource.getName());
}
}
/**
* @return current error if any, and reset it
*/
public String popCurrentError() {
String returnValue = currentError;
currentError = null;
return returnValue;
}
/**
* @param currentError error path to set
*/
public void setCurrentError(String currentError) {
this.currentError = currentError;
}
}