Skip to content

Commit 0ba09fa

Browse files
feat(engine): script preprocessing feature addition to fluxnova engine
Signed-off-by: Palanisamy, Prakash <prakash.palanisamy@fmr.com>
1 parent 98fb847 commit 0ba09fa

19 files changed

Lines changed: 1928 additions & 20 deletions

engine/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@
116116
<artifactId>gson</artifactId>
117117
</dependency>
118118

119+
<dependency>
120+
<groupId>commons-codec</groupId>
121+
<artifactId>commons-codec</artifactId>
122+
<version>1.15</version>
123+
</dependency>
124+
119125
<!-- provided dependencies -->
120126

121127
<dependency>

engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/cfg/ProcessEngineConfigurationImpl.java

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@
347347
import org.finos.fluxnova.bpm.engine.impl.scripting.engine.VariableScopeResolverFactory;
348348
import org.finos.fluxnova.bpm.engine.impl.scripting.env.ScriptEnvResolver;
349349
import org.finos.fluxnova.bpm.engine.impl.scripting.env.ScriptingEnvironment;
350+
import org.finos.fluxnova.bpm.engine.impl.scripting.preprocessor.CompositeScriptPreprocessor;
351+
import org.finos.fluxnova.bpm.engine.impl.scripting.preprocessor.ScriptPreprocessor;
350352
import org.finos.fluxnova.bpm.engine.impl.telemetry.dto.DatabaseImpl;
351353
import org.finos.fluxnova.bpm.engine.impl.telemetry.dto.InternalsImpl;
352354
import org.finos.fluxnova.bpm.engine.impl.telemetry.dto.JdkImpl;
@@ -608,6 +610,10 @@ public abstract class ProcessEngineConfigurationImpl extends ProcessEngineConfig
608610
protected boolean enableScriptEngineNashornCompatibility = false;
609611
protected boolean configureScriptEngineHostAccess = true;
610612
protected boolean skipIsolationLevelCheck = false;
613+
private volatile boolean enableScriptPreprocessing = false;
614+
private volatile List<ScriptPreprocessor> scriptPreprocessors;
615+
private volatile ScriptPreprocessor effectiveScriptPreprocessor;
616+
private final Object scriptPreprocessorLock = new Object();
611617

612618
/**
613619
* When set to false, the following behavior changes:
@@ -4266,6 +4272,50 @@ public void setScriptFactory(ScriptFactory scriptFactory) {
42664272
this.scriptFactory = scriptFactory;
42674273
}
42684274

4275+
/**
4276+
* @return whether script preprocessing is enabled for script execution.
4277+
*/
4278+
public boolean isEnableScriptPreprocessing() {
4279+
return enableScriptPreprocessing;
4280+
}
4281+
4282+
/**
4283+
* Enables/disables script preprocessing and resets the cached effective preprocessor.
4284+
*/
4285+
public void setEnableScriptPreprocessing(boolean enableScriptPreprocessing) {
4286+
synchronized (scriptPreprocessorLock) {
4287+
this.enableScriptPreprocessing = enableScriptPreprocessing;
4288+
this.effectiveScriptPreprocessor = null;
4289+
}
4290+
}
4291+
4292+
/**
4293+
* @return a copy of configured preprocessors, or {@code null} when none are configured.
4294+
*/
4295+
public List<ScriptPreprocessor> getScriptPreprocessors() {
4296+
synchronized (scriptPreprocessorLock) {
4297+
if (scriptPreprocessors == null) {
4298+
return null;
4299+
}
4300+
return new ArrayList<>(scriptPreprocessors);
4301+
}
4302+
}
4303+
4304+
/**
4305+
* Replaces configured preprocessors and resets the cached effective preprocessor.
4306+
*/
4307+
public void setScriptPreprocessors(List<ScriptPreprocessor> scriptPreprocessors) {
4308+
synchronized (scriptPreprocessorLock) {
4309+
if (scriptPreprocessors == null) {
4310+
this.scriptPreprocessors = null;
4311+
} else {
4312+
this.scriptPreprocessors = new ArrayList<>(scriptPreprocessors);
4313+
}
4314+
this.effectiveScriptPreprocessor = null;
4315+
}
4316+
}
4317+
4318+
42694319
public ScriptEngineResolver getScriptEngineResolver() {
42704320
return scriptEngineResolver;
42714321
}
@@ -5330,4 +5380,56 @@ public ProcessEngineConfiguration setLegacyJobRetryBehaviorEnabled(boolean legac
53305380
this.legacyJobRetryBehaviorEnabled = legacyJobRetryBehaviorEnabled;
53315381
return this;
53325382
}
5383+
5384+
/**
5385+
* Adds a preprocessor to the configured chain and invalidates the cached effective preprocessor.
5386+
*/
5387+
public void addScriptPreprocessor(ScriptPreprocessor scriptPreprocessor) {
5388+
if (scriptPreprocessor == null) {
5389+
return;
5390+
}
5391+
synchronized (scriptPreprocessorLock) {
5392+
List<ScriptPreprocessor> updatedScriptPreprocessors = this.scriptPreprocessors == null
5393+
? new ArrayList<>()
5394+
: new ArrayList<>(this.scriptPreprocessors);
5395+
updatedScriptPreprocessors.add(scriptPreprocessor);
5396+
this.scriptPreprocessors = updatedScriptPreprocessors;
5397+
this.effectiveScriptPreprocessor = null;
5398+
}
5399+
}
5400+
5401+
/**
5402+
* Returns the effective preprocessor for runtime use.
5403+
*
5404+
* @return {@code null} when preprocessing is disabled or no preprocessors are configured.
5405+
*/
5406+
public ScriptPreprocessor getEffectiveScriptPreprocessor() {
5407+
if (!enableScriptPreprocessing) {
5408+
return null;
5409+
}
5410+
ScriptPreprocessor cached = effectiveScriptPreprocessor;
5411+
if (cached != null) {
5412+
return cached;
5413+
}
5414+
5415+
synchronized (scriptPreprocessorLock) {
5416+
if (!enableScriptPreprocessing) {
5417+
return null;
5418+
}
5419+
5420+
if (effectiveScriptPreprocessor == null) {
5421+
List<ScriptPreprocessor> configuredScriptPreprocessors = scriptPreprocessors;
5422+
if (configuredScriptPreprocessors == null || configuredScriptPreprocessors.isEmpty()) {
5423+
return null;
5424+
}
5425+
5426+
if (configuredScriptPreprocessors.size() == 1) {
5427+
effectiveScriptPreprocessor = configuredScriptPreprocessors.get(0);
5428+
} else {
5429+
effectiveScriptPreprocessor = new CompositeScriptPreprocessor(new ArrayList<>(configuredScriptPreprocessors));
5430+
}
5431+
}
5432+
return effectiveScriptPreprocessor;
5433+
}
5434+
}
53335435
}

engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/scripting/CompiledExecutableScript.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class CompiledExecutableScript extends ExecutableScript {
3030

3131
private final static ScriptLogger LOG = ProcessEngineLogger.SCRIPT_LOGGER;
3232

33-
protected CompiledScript compiledScript;
33+
protected volatile CompiledScript compiledScript;
3434

3535
protected CompiledExecutableScript(String language) {
3636
this(language, null);
@@ -50,9 +50,13 @@ public void setCompiledScript(CompiledScript compiledScript) {
5050
}
5151

5252
public Object evaluate(ScriptEngine scriptEngine, VariableScope variableScope, Bindings bindings) {
53+
return evaluateCompiledScript(getCompiledScript(), variableScope, bindings);
54+
}
55+
56+
protected Object evaluateCompiledScript(CompiledScript compiledScript, VariableScope variableScope, Bindings bindings) {
5357
try {
5458
LOG.debugEvaluatingCompiledScript(language);
55-
return getCompiledScript().eval(bindings);
59+
return compiledScript.eval(bindings);
5660
} catch (ScriptException e) {
5761
if (e.getCause() instanceof BpmnError) {
5862
throw (BpmnError) e.getCause();

engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/scripting/DynamicExecutableScript.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ protected DynamicExecutableScript(Expression scriptExpression, String language)
4141

4242
public Object evaluate(ScriptEngine scriptEngine, VariableScope variableScope, Bindings bindings) {
4343
String source = getScriptSource(variableScope);
44+
source = preprocessScript(source, variableScope);
4445
try {
4546
return scriptEngine.eval(source, bindings);
4647
}

engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/scripting/ExecutableScript.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323
import org.finos.fluxnova.bpm.engine.delegate.DelegateCaseExecution;
2424
import org.finos.fluxnova.bpm.engine.delegate.DelegateExecution;
2525
import org.finos.fluxnova.bpm.engine.delegate.VariableScope;
26+
import org.finos.fluxnova.bpm.engine.impl.ProcessEngineLogger;
27+
import org.finos.fluxnova.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl;
28+
import org.finos.fluxnova.bpm.engine.impl.context.Context;
2629
import org.finos.fluxnova.bpm.engine.impl.persistence.entity.TaskEntity;
30+
import org.finos.fluxnova.bpm.engine.impl.scripting.preprocessor.ScriptPreprocessor;
31+
import org.finos.fluxnova.bpm.engine.impl.scripting.preprocessor.ScriptPreprocessorRequest;
2732

2833
/**
2934
* <p>Represents an executable script.</p>
@@ -34,6 +39,8 @@
3439
*/
3540
public abstract class ExecutableScript {
3641

42+
private static final ScriptLogger LOG = ProcessEngineLogger.SCRIPT_LOGGER;
43+
3744
/** The language of the script. Used to resolve the
3845
* {@link ScriptEngine}. */
3946
protected final String language;
@@ -94,4 +101,60 @@ protected String getActivityIdExceptionMessage(VariableScope variableScope) {
94101

95102
protected abstract Object evaluate(ScriptEngine scriptEngine, VariableScope variableScope, Bindings bindings);
96103

104+
/**
105+
* Preprocesses a script before execution.
106+
*
107+
* <p>If script preprocessing is disabled, no preprocessor is configured, or the preprocessor
108+
* returns {@code null}, the original script is returned unchanged.</p>
109+
*
110+
* <p>If preprocessing fails with an exception, a warning is logged and the original script is
111+
* returned as a safe fallback to ensure scripts continue to execute.</p>
112+
*
113+
* @param script the script text to preprocess; may be {@code null}
114+
* @param variableScope the current execution context; may be {@code null}
115+
* @return the preprocessed script, or the original script if preprocessing is disabled,
116+
* not configured, returns {@code null}, or fails with an exception
117+
*/
118+
protected String preprocessScript(String script, VariableScope variableScope) {
119+
if (script == null) {
120+
return script;
121+
}
122+
123+
ProcessEngineConfigurationImpl processEngineConfiguration = Context.getProcessEngineConfiguration();
124+
if (processEngineConfiguration == null
125+
|| !processEngineConfiguration.isEnableScriptPreprocessing()) {
126+
return script;
127+
}
128+
129+
ScriptPreprocessor scriptPreprocessor = processEngineConfiguration.getEffectiveScriptPreprocessor();
130+
if (scriptPreprocessor == null) {
131+
return script;
132+
}
133+
134+
String preprocessorName = getPreprocessorName(scriptPreprocessor);
135+
136+
ScriptPreprocessorRequest request = new ScriptPreprocessorRequest(script, language, variableScope);
137+
String processedScript;
138+
try {
139+
processedScript = scriptPreprocessor.process(request);
140+
} catch (Throwable e) {
141+
LOG.warnScriptPreprocessingFailed(language, preprocessorName, e);
142+
return script;
143+
}
144+
145+
if (processedScript == null) {
146+
return script;
147+
}
148+
149+
return processedScript;
150+
}
151+
152+
private String getPreprocessorName(ScriptPreprocessor scriptPreprocessor) {
153+
try {
154+
String name = scriptPreprocessor.getName();
155+
return name != null ? name : "<unknown>";
156+
} catch (Exception e) {
157+
return "<unknown>";
158+
}
159+
}
97160
}

engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/scripting/ScriptFactory.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@
1919
import org.finos.fluxnova.bpm.engine.delegate.Expression;
2020

2121
/**
22-
* <p>A script factory is responsible for creating a {@link ExecutableScript}
23-
* instance. Users may customize (subclass) this class in order to customize script
24-
* creation. For instance, some users may choose to pre-process scripts before
25-
* they are created.</p>
22+
* <p>A script factory is responsible for creating {@link ExecutableScript}
23+
* instances. Users may customize (subclass) this class in order to customize script
24+
* creation.</p>
25+
*
26+
* <p>The default executable script implementations created by this factory preserve
27+
* configured script preprocessing behavior automatically. Custom subclasses that
28+
* return their own {@link ExecutableScript} implementations should ensure that
29+
* configured script preprocessors are still applied before evaluation if they
30+
* want those custom implementations to participate in the script preprocessing
31+
* feature.</p>
2632
*
2733
* @author Daniel Meyer
2834
*

engine/src/main/java/org/finos/fluxnova/bpm/engine/impl/scripting/ScriptLogger.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,24 @@ public void debugEvaluatingNonCompiledScript(String scriptSource) {
3939
"001", "Evaluating non-compiled script {}", scriptSource);
4040
}
4141

42+
public void warnScriptPreprocessorRequestNull(String preprocessorName) {
43+
logWarn(
44+
"004",
45+
"ScriptPreprocessorRequest is null in preprocessor {}; returning null (no preprocessing will be done).",
46+
preprocessorName);
47+
}
48+
49+
public void warnScriptPreprocessingFailed(
50+
String language,
51+
String preprocessorName,
52+
Throwable throwable) {
53+
logWarn(
54+
"005",
55+
"Script preprocessing failed for language {} using preprocessor {}. Error: {}. Falling back to the original script.",
56+
language,
57+
preprocessorName,
58+
throwable.getMessage(),
59+
throwable);
60+
}
61+
4262
}

0 commit comments

Comments
 (0)