blob: 889e289665ebcfd034bb980a3f925cbf4bd7de80 [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.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.pipes.internal.JxltEngine;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.script.Bindings;
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.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 {
private static final Logger log = LoggerFactory.getLogger(PipeBindings.class);
public static final String NN_ADDITIONALBINDINGS = "additionalBindings";
public static final String PN_ADDITIONALSCRIPTS = "additionalScripts";
public static final String NN_PROVIDERS = "providers";
public static final String PN_ENGINE = "engine";
public static final String FALSE_BINDING = "${false}";
/**
* 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";
public static final String INJECTED_SCRIPT_REGEXP = "\\$\\{(([^\\{^\\}]*(\\{[0-9,]+\\})?)*)\\}";
private static final Pattern INJECTED_SCRIPT = Pattern.compile(INJECTED_SCRIPT_REGEXP);
protected static final String IF_PREFIX = "$if";
protected static final Pattern CONDITIONAL_STRING = Pattern.compile("^\\" + IF_PREFIX + INJECTED_SCRIPT_REGEXP);
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
*/
public PipeBindings(@NotNull Resource resource) {
//Setup script engines
String engineName = resource.getValueMap().get(PN_ENGINE, String.class);
if (StringUtils.isNotBlank(engineName)) {
initializeScriptEngine(engineName);
}
//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);
}
/**
* 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<String, Object> 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);
}
}
private int processMatcher(int start, String expr, StringBuilder expression, Matcher matcher) {
if (matcher.start() > start) {
if (expression.length() == 0) {
expression.append("'");
}
expression.append(expr, start, matcher.start());
}
if (expression.length() > 0) {
expression.append("' + ");
}
expression.append(matcher.group(1));
start = matcher.end();
if (start < expr.length()) {
expression.append(" + '");
}
return start;
}
/**
* 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
*/
String computeTemplateExpression(String expr) {
Matcher matcher = INJECTED_SCRIPT.matcher(expr);
if (INJECTED_SCRIPT.matcher(expr).find()) {
StringBuilder expression = new StringBuilder();
int start = 0;
while (matcher.find()) {
start = processMatcher(start, expr, expression, matcher);
}
if (start < expr.length()) {
expression.append(expr.substring(start) + "'");
}
return expression.toString();
}
return null;
}
private ScriptEngine getEngine() {
if (engine == null && getBindings().containsKey(PN_ENGINE)){
initializeScriptEngine((String) getBindings().get(PN_ENGINE));
}
return engine;
}
/**
* evaluate a given expression
* @param expr ecma like expression
* @return object that is the result of the expression
*/
protected Object evaluate(String expr) {
try {
String computed = computeTemplateExpression(expr);
if (computed != null) {
return getEngine() != null ? engine.eval(computed, scriptContext) : internalEvaluate(computed);
}
} catch (ScriptException e) {
throw new IllegalArgumentException(e);
}
return expr;
}
/**
* Instantiate object from expression
* @param expr ecma expression
* @return instantiated object
*/
public Object instantiateObject(String expr) {
return evaluate(expr);
}
private Object internalEvaluate(String expr) {
JxltEngine internalEngine = new JxltEngine(getBindings());
return internalEngine.parse(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.
* @param engineName name of the engine as registered in the JVM
*/
public void initializeScriptEngine(String engineName) {
engine = new ScriptEngineManager().getEngineByName(engineName);
if(engine == null){
//Fallback to system classloader
engine = new ScriptEngineManager(null).getEngineByName(engineName);
//Check if engine can still not be instantiated
if(engine == null){
throw new IllegalArgumentException("Can not instantiate " + engineName + " scriptengine. Check JVM version & capabilities.");
}
}
engine.setContext(scriptContext);
}
/**
* Return expression, instantiated expression or null if the expression is conditional and evaluation is falsy
* @param conditionalExpression can be static, or dynamic, can be conditional in which case it must be of following
* format <code>$if${condition}someString</code>. someString will be returned if condition is true, otherwise null
* @return instantiated expression or null if expression is conditional (see above) and condition is falsy
*/
public String conditionalString(String conditionalExpression) {
Matcher matcher = CONDITIONAL_STRING.matcher(conditionalExpression);
if (matcher.find()){
Object output = evaluate(StringUtils.substringAfter(matcher.group(0), IF_PREFIX));
if (output != null){
String s = output.toString().toLowerCase().trim();
if(StringUtils.isNotEmpty(s) && !"false".equals(s) && !"undefined".equals(s)){
return instantiateExpression(conditionalExpression.substring(matcher.group(0).length()));
}
}
} else {
return instantiateExpression(conditionalExpression);
}
return null;
}
/**
* 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
*/
public String instantiateExpression(String expr) {
Object obj = evaluate(expr);
return obj != null ? obj.toString() : null;
}
/**
* 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;
}
}