Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
package org.flowable.common.engine.impl.scripting;

import java.io.Writer;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -23,6 +24,7 @@
import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;

import org.apache.commons.lang3.StringUtils;
import org.flowable.common.engine.api.FlowableException;
Expand All @@ -47,6 +49,12 @@ public class JSR223FlowableScriptEngine implements FlowableScriptEngine {
protected boolean cacheScriptingEngines = true;
protected Map<String, ScriptEngine> cachedEngines;

/**
* The writer used by the scripts for their output (e.g. {@code println} in Groovy, {@code out:print} in JUEL).
* When not set, the writer of the script engine is used, which is usually {@code System.out}.
*/
protected Writer scriptOutputWriter;

public JSR223FlowableScriptEngine() {
this(new ScriptEngineManager());
}
Expand Down Expand Up @@ -132,6 +140,18 @@ public ScriptEngineManager getScriptEngineManager() {
return scriptEngineManager;
}

public Writer getScriptOutputWriter() {
return scriptOutputWriter;
}

/**
* Set the writer used by the scripts for their output.
* Use {@link Writer#nullWriter()} to discard the output, or e.g. a {@link java.io.StringWriter} to keep it in memory.
*/
public void setScriptOutputWriter(Writer scriptOutputWriter) {
this.scriptOutputWriter = scriptOutputWriter;
}


protected class JavaScriptingFlowableScriptEvaluationRequest implements FlowableScriptEvaluationRequest {

Expand Down Expand Up @@ -197,7 +217,12 @@ public ScriptEvaluation evaluate() throws FlowableScriptException {
ScriptEngine scriptEngine = getEngineByName(language);
Bindings bindings = createBindings();
try {
Object result = scriptEngine.eval(script, bindings);
Object result;
if (scriptOutputWriter != null) {
result = scriptEngine.eval(script, createScriptContext(scriptEngine, bindings));
} else {
result = scriptEngine.eval(script, bindings);
}
return new ScriptEvaluationImpl(resolver, result);
} catch (ScriptException e) {
throw new FlowableScriptException(e.getMessage(), e);
Expand All @@ -207,5 +232,19 @@ public ScriptEvaluation evaluate() throws FlowableScriptException {
protected Bindings createBindings() {
return new ScriptBindings(Collections.singletonList(resolver), scopeContainer, inputVariableContainer, storeScriptVariables);
}

/**
* Creates the same script context as {@link javax.script.AbstractScriptEngine#eval(String, Bindings)} would, but with the configured script output writer.
*/
protected ScriptContext createScriptContext(ScriptEngine scriptEngine, Bindings bindings) {
ScriptContext engineContext = scriptEngine.getContext();
ScriptContext scriptContext = new SimpleScriptContext();
scriptContext.setBindings(bindings, ScriptContext.ENGINE_SCOPE);
scriptContext.setBindings(engineContext.getBindings(ScriptContext.GLOBAL_SCOPE), ScriptContext.GLOBAL_SCOPE);
scriptContext.setReader(engineContext.getReader());
scriptContext.setWriter(scriptOutputWriter);
scriptContext.setErrorWriter(engineContext.getErrorWriter());
return scriptContext;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

Expand Down Expand Up @@ -59,11 +60,28 @@
*/
public class JuelScriptEngine extends AbstractScriptEngine implements Compilable {

/**
* The script context of the expression that is currently being evaluated on this thread.
* EL functions are static methods, so this is how {@link #print(Object)} gets access to the writer of the script context.
*/
protected static final ThreadLocal<ScriptContext> CURRENT_SCRIPT_CONTEXT = new ThreadLocal<>();

private ScriptEngineFactory scriptEngineFactory;
private ExpressionFactory expressionFactory;

/**
* Whether the {@code lang:import} function is available in scripts.
* It allows loading any class and calling any of its public static methods, so it is disabled by default.
*/
protected boolean importFunctionEnabled;

public JuelScriptEngine(ScriptEngineFactory scriptEngineFactory) {
this(scriptEngineFactory, false);
}

public JuelScriptEngine(ScriptEngineFactory scriptEngineFactory, boolean importFunctionEnabled) {
this.scriptEngineFactory = scriptEngineFactory;
this.importFunctionEnabled = importFunctionEnabled;
// Resolve the ExpressionFactory
expressionFactory = ExpressionFactoryResolver.resolveExpressionFactory();
}
Expand Down Expand Up @@ -111,10 +129,18 @@ public Bindings createBindings() {
}

private Object evaluateExpression(ValueExpression expr, ScriptContext ctx) throws ScriptException {
ScriptContext previousScriptContext = CURRENT_SCRIPT_CONTEXT.get();
CURRENT_SCRIPT_CONTEXT.set(ctx);
try {
return expr.getValue(createElContext(ctx));
} catch (ELException elexp) {
throw new ScriptException(elexp);
} finally {
if (previousScriptContext != null) {
CURRENT_SCRIPT_CONTEXT.set(previousScriptContext);
} else {
CURRENT_SCRIPT_CONTEXT.remove();
}
}
}

Expand Down Expand Up @@ -165,8 +191,7 @@ private ELContext createElContext(final ScriptContext scriptCtx) {
// Built-in function are added to ScriptCtx
scriptCtx.setAttribute("out:print", getPrintMethod(), ScriptContext.ENGINE_SCOPE);

SecurityManager securityManager = System.getSecurityManager();
if (securityManager == null) {
if (importFunctionEnabled) {
scriptCtx.setAttribute("lang:import", getImportMethod(), ScriptContext.ENGINE_SCOPE);
}

Expand Down Expand Up @@ -206,7 +231,19 @@ private static Method getPrintMethod() {
}

public static void print(Object object) {
System.out.print(object);
ScriptContext scriptContext = CURRENT_SCRIPT_CONTEXT.get();
Writer writer = scriptContext != null ? scriptContext.getWriter() : null;
if (writer == null) {
System.out.print(object);
return;
}

try {
writer.write(String.valueOf(object));
writer.flush();
} catch (IOException e) {
throw new ELException(e);
}
}

private static Method getImportMethod() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public class JuelScriptEngineFactory implements ScriptEngineFactory {
private static List<String> extensions;
private static List<String> mimeTypes;

/**
* Whether the {@code lang:import} function is available in the created engines.
* It allows loading any class and calling any of its public static methods, so it is disabled by default.
*/
protected boolean importFunctionEnabled;

static {
names = Collections.unmodifiableList(Collections.singletonList("juel"));
extensions = names;
Expand Down Expand Up @@ -137,7 +143,15 @@ public String getProgram(String... statements) {

@Override
public ScriptEngine getScriptEngine() {
return new JuelScriptEngine(this);
return new JuelScriptEngine(this, importFunctionEnabled);
}

public boolean isImportFunctionEnabled() {
return importFunctionEnabled;
}

public void setImportFunctionEnabled(boolean importFunctionEnabled) {
this.importFunctionEnabled = importFunctionEnabled;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* Licensed 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.flowable.engine.impl.scripting;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.io.StringWriter;

import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;

import org.junit.jupiter.api.Test;

/**
* @author Filip Hrisafov
*/
class JuelScriptEngineTest {

@Test
void importFunctionIsDisabledByDefault() {
ScriptEngine scriptEngine = new JuelScriptEngineFactory().getScriptEngine();
ScriptContext scriptContext = new SimpleScriptContext();

assertThatThrownBy(() -> scriptEngine.eval("${lang:import(context, 'math', 'java.lang.Math')}", scriptContext))
.isInstanceOf(ScriptException.class)
.hasMessageContaining("lang:import");
assertThat(scriptContext.getAttribute("lang:import")).isNull();
}

@Test
void importFunctionCanBeEnabled() throws ScriptException {
JuelScriptEngineFactory scriptEngineFactory = new JuelScriptEngineFactory();
scriptEngineFactory.setImportFunctionEnabled(true);
ScriptEngine scriptEngine = scriptEngineFactory.getScriptEngine();
ScriptContext scriptContext = new SimpleScriptContext();

scriptEngine.eval("${lang:import(context, 'integer', 'java.lang.Integer')}", scriptContext);

assertThat(scriptEngine.eval("${integer:toHexString(255)}", scriptContext)).isEqualTo("ff");
}

@Test
void printWritesToScriptContextWriter() throws ScriptException {
ScriptEngine scriptEngine = new JuelScriptEngineFactory().getScriptEngine();
ScriptContext scriptContext = new SimpleScriptContext();
StringWriter writer = new StringWriter();
scriptContext.setWriter(writer);
scriptContext.setAttribute("name", "Kermit", ScriptContext.ENGINE_SCOPE);

scriptEngine.eval("${out:print('Hello ')}${out:print(name)}", scriptContext);

assertThat(writer).hasToString("Hello Kermit");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.Assertions.entry;

import java.io.StringWriter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.flowable.common.engine.api.FlowableException;
import org.flowable.common.engine.api.FlowableIllegalArgumentException;
import org.flowable.common.engine.impl.scripting.FlowableScriptEvaluationException;
import org.flowable.common.engine.impl.scripting.JSR223FlowableScriptEngine;
import org.flowable.common.engine.impl.util.CollectionUtil;
import org.flowable.engine.impl.test.PluggableFlowableTestCase;
import org.flowable.engine.repository.ProcessDefinition;
Expand Down Expand Up @@ -246,4 +249,19 @@ public void testSkipExpressionDisabled() {
.containsOnly(entry("persistentResult", "success"));
}

@Test
@Deployment
public void testScriptOutputWriter() {
JSR223FlowableScriptEngine scriptEngine = (JSR223FlowableScriptEngine) processEngineConfiguration.getScriptEngine();
StringWriter writer = new StringWriter();
scriptEngine.setScriptOutputWriter(writer);
try {
runtimeService.startProcessInstanceByKey("scriptOutputWriter", Collections.singletonMap("name", "Kermit"));

assertThat(writer.toString()).isEqualToNormalizingNewlines("Hello Kermit from Groovy\n");
} finally {
scriptEngine.setScriptOutputWriter(null);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<definitions id="definitions"
xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:flowable="http://flowable.org/bpmn"
targetNamespace="Examples">

<process id="scriptOutputWriter">

<startEvent id="theStart" />

<scriptTask id="juelScriptTask" scriptFormat="juel">
<script>${out:print('Hello ')}${out:print(name)}</script>
</scriptTask>

<scriptTask id="groovyScriptTask" scriptFormat="groovy">
<script>println " from Groovy"</script>
</scriptTask>

<endEvent id="theEnd" />

<sequenceFlow id="flow1" sourceRef="theStart" targetRef="juelScriptTask" />
<sequenceFlow id="flow2" sourceRef="juelScriptTask" targetRef="groovyScriptTask" />
<sequenceFlow id="flow3" sourceRef="groovyScriptTask" targetRef="theEnd" />

</process>

</definitions>
Loading
Loading