You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by ex...@apache.org on 2026/04/01 16:47:25 UTC

(nifi) branch main updated: NIFI-15739 Added Expression Language validation to ExecuteGroovyScript dynamic properties (#11059)

This is an automated email from the ASF dual-hosted git repository.

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 1ef94fc2413 NIFI-15739 Added Expression Language validation to ExecuteGroovyScript dynamic properties (#11059)
1ef94fc2413 is described below

commit 1ef94fc2413bdd74549779cbf121e47a8554ff25
Author: dan-s1 <ds...@gmail.com>
AuthorDate: Wed Apr 1 12:47:18 2026 -0400

    NIFI-15739 Added Expression Language validation to ExecuteGroovyScript dynamic properties (#11059)
    
    - Added session.get() in the onTrigger method to ensure there is a FlowFile to send to the failure relationship
    
    Signed-off-by: David Handermann <ex...@apache.org>
---
 .../processors/groovyx/ExecuteGroovyScript.java    | 21 +++++++++----
 .../groovyx/flow/ProcessSessionWrap.java           |  2 +-
 .../groovyx/ExecuteGroovyScriptTest.java           | 35 +++++++++++++++++-----
 3 files changed, 45 insertions(+), 13 deletions(-)

diff --git a/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/main/java/org/apache/nifi/processors/groovyx/ExecuteGroovyScript.java b/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/main/java/org/apache/nifi/processors/groovyx/ExecuteGroovyScript.java
index d3686e62d27..489f4d62e37 100644
--- a/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/main/java/org/apache/nifi/processors/groovyx/ExecuteGroovyScript.java
+++ b/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/main/java/org/apache/nifi/processors/groovyx/ExecuteGroovyScript.java
@@ -40,6 +40,7 @@ import org.apache.nifi.components.state.Scope;
 import org.apache.nifi.controller.ControllerService;
 import org.apache.nifi.dbcp.DBCPService;
 import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
 import org.apache.nifi.migration.PropertyConfiguration;
 import org.apache.nifi.processor.AbstractProcessor;
 import org.apache.nifi.processor.ProcessContext;
@@ -114,7 +115,8 @@ public class ExecuteGroovyScript extends AbstractProcessor {
             .expressionLanguageSupported(ExpressionLanguageScope.NONE)
             .build();
 
-    public static final String[] VALID_FAIL_STRATEGY = {"rollback", "transfer to failure"};
+    static final String ROLLBACK = "rollback";
+    static final String TRANSFER_TO_FAILURE = "transfer to failure";
     public static final PropertyDescriptor FAIL_STRATEGY = new PropertyDescriptor.Builder()
             .name("Failure Strategy")
             .description("What to do with unhandled exceptions. If you want to manage exception by code then keep the default value `rollback`."
@@ -124,8 +126,8 @@ public class ExecuteGroovyScript extends AbstractProcessor {
                     + " If the processor has no incoming connections then this parameter has no effect."
                 )
             .required(true).expressionLanguageSupported(ExpressionLanguageScope.NONE)
-            .allowableValues(VALID_FAIL_STRATEGY)
-            .defaultValue(VALID_FAIL_STRATEGY[0])
+            .allowableValues(ROLLBACK, TRANSFER_TO_FAILURE)
+            .defaultValue(ROLLBACK)
             .build();
 
     public static final PropertyDescriptor ADD_CLASSPATH = new PropertyDescriptor.Builder()
@@ -444,10 +446,18 @@ public class ExecuteGroovyScript extends AbstractProcessor {
 
     @Override
     public void onTrigger(final ProcessContext context, final ProcessSession processSession) throws ProcessException {
-        boolean toFailureOnError = VALID_FAIL_STRATEGY[1].equals(context.getProperty(FAIL_STRATEGY).getValue());
+        boolean toFailureOnError = TRANSFER_TO_FAILURE.equals(context.getProperty(FAIL_STRATEGY).getValue());
         //create wrapped session to control list of newly created and files got from this session.
         //so transfer original input to failure will be possible
         GroovyProcessSessionWrap session = new GroovyProcessSessionWrap(processSession, toFailureOnError);
+        if (toFailureOnError) {
+            // FlowFile must be read otherwise if there is a failure before the script is executed, a
+            // never ending loop occurs since the GroovyProcessSessionWrap has nothing to send to the failure relationship.
+            FlowFile flowFile = session.get();
+            if (flowFile == null) {
+                return;
+            }
+        }
 
         Map<String, Object> ctl = new AccessMap("CTL");
         Map<String, Object> sql = new AccessMap("SQL");
@@ -571,7 +581,8 @@ public class ExecuteGroovyScript extends AbstractProcessor {
         return new PropertyDescriptor.Builder()
                 .name(propertyDescriptorName)
                 .required(false)
-                .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+                .addValidator(StandardValidators.NON_EMPTY_EL_VALIDATOR)
+                .addValidator(StandardValidators.ATTRIBUTE_EXPRESSION_LANGUAGE_VALIDATOR)
                 .expressionLanguageSupported(ExpressionLanguageScope.ENVIRONMENT)
                 .dynamic(true)
                 .build();
diff --git a/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/main/java/org/apache/nifi/processors/groovyx/flow/ProcessSessionWrap.java b/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/main/java/org/apache/nifi/processors/groovyx/flow/ProcessSessionWrap.java
index 9f291f6b261..2cde23f1590 100644
--- a/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/main/java/org/apache/nifi/processors/groovyx/flow/ProcessSessionWrap.java
+++ b/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/main/java/org/apache/nifi/processors/groovyx/flow/ProcessSessionWrap.java
@@ -224,7 +224,7 @@ public abstract class ProcessSessionWrap implements ProcessSession {
                 session.transfer(f);
             }
         }
-        session.commit();
+        session.commitAsync();
         onClear();
     }
     /*============================================= NATIVE METHODS ================================================*/
diff --git a/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/test/java/org/apache/nifi/processors/groovyx/ExecuteGroovyScriptTest.java b/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/test/java/org/apache/nifi/processors/groovyx/ExecuteGroovyScriptTest.java
index 897f286588a..b7eee39ea80 100644
--- a/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/test/java/org/apache/nifi/processors/groovyx/ExecuteGroovyScriptTest.java
+++ b/nifi-extension-bundles/nifi-groovyx-bundle/nifi-groovyx-processors/src/test/java/org/apache/nifi/processors/groovyx/ExecuteGroovyScriptTest.java
@@ -23,6 +23,7 @@ import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.embedded.database.EmbeddedDatabaseConnectionService;
 import org.apache.nifi.expression.ExpressionLanguageScope;
 import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.processors.groovyx.flow.ProcessSessionWrap;
 import org.apache.nifi.serialization.RecordSetWriterFactory;
 import org.apache.nifi.serialization.record.MockRecordParser;
 import org.apache.nifi.serialization.record.MockRecordWriter;
@@ -142,7 +143,6 @@ public class ExecuteGroovyScriptTest {
     @Test
     public void testReadFlowFileContentAndStoreInFlowFileAttribute() {
         runner.setProperty(ExecuteGroovyScript.SCRIPT_BODY, "def flowFile = session.get(); if(!flowFile)return; flowFile.testAttr = flowFile.read().getText('UTF-8'); REL_SUCCESS << flowFile;");
-        //runner.setProperty(proc.FAIL_STRATEGY, "rollback");
 
         runner.assertValid();
         runner.enqueue("test content".getBytes(StandardCharsets.UTF_8));
@@ -187,7 +187,6 @@ public class ExecuteGroovyScriptTest {
     @Test
     public void test_onTrigger_groovy() {
         runner.setProperty(ExecuteGroovyScript.SCRIPT_FILE, TEST_RESOURCE_LOCATION + "test_onTrigger.groovy");
-        //runner.setProperty(proc.FAIL_STRATEGY, "rollback");
         runner.assertValid();
 
         runner.enqueue("test content".getBytes(StandardCharsets.UTF_8));
@@ -200,7 +199,6 @@ public class ExecuteGroovyScriptTest {
     @Test
     public void test_onTriggerX_groovy() {
         runner.setProperty(ExecuteGroovyScript.SCRIPT_FILE, TEST_RESOURCE_LOCATION + "test_onTriggerX.groovy");
-        //runner.setProperty(proc.FAIL_STRATEGY, "rollback");
         runner.assertValid();
 
         runner.enqueue("test content".getBytes(StandardCharsets.UTF_8));
@@ -213,7 +211,6 @@ public class ExecuteGroovyScriptTest {
     @Test
     public void test_onTrigger_changeContent_groovy() {
         runner.setProperty(ExecuteGroovyScript.SCRIPT_FILE, TEST_RESOURCE_LOCATION + "test_onTrigger_changeContent.groovy");
-        //runner.setProperty(proc.FAIL_STRATEGY, "rollback");
         runner.assertValid();
 
         runner.enqueue(TEST_CSV_DATA.getBytes(StandardCharsets.UTF_8));
@@ -229,7 +226,6 @@ public class ExecuteGroovyScriptTest {
     @Test
     public void test_onTrigger_changeContentX_groovy() {
         runner.setProperty(ExecuteGroovyScript.SCRIPT_FILE, TEST_RESOURCE_LOCATION + "test_onTrigger_changeContentX.groovy");
-        //runner.setProperty(proc.FAIL_STRATEGY, "rollback");
         runner.assertValid();
 
         runner.enqueue(TEST_CSV_DATA.getBytes(StandardCharsets.UTF_8));
@@ -245,7 +241,6 @@ public class ExecuteGroovyScriptTest {
     @Test
     public void test_no_input_groovy() {
         runner.setProperty(ExecuteGroovyScript.SCRIPT_FILE, TEST_RESOURCE_LOCATION + "test_no_input.groovy");
-        //runner.setProperty(proc.FAIL_STRATEGY, "rollback");
         runner.assertValid();
         runner.run();
         runner.assertAllFlowFilesTransferred(ExecuteGroovyScript.REL_SUCCESS.getName(), 1);
@@ -369,7 +364,6 @@ public class ExecuteGroovyScriptTest {
     @Test
     public void test_filter_01() {
         runner.setProperty(ExecuteGroovyScript.SCRIPT_BODY, "def ff = session.get{it.FILTER=='3'}; if(!ff)return; REL_SUCCESS << ff;");
-        //runner.setProperty(proc.FAIL_STRATEGY, "rollback");
 
         runner.assertValid();
 
@@ -566,6 +560,33 @@ public class ExecuteGroovyScriptTest {
         runner.run();
     }
 
+    @Test
+    void invalidExpressionLanguageInDynamicProperty() {
+        runner.setProperty("myProperty", "${myparam:isempty()}");
+        runner.setProperty(ExecuteGroovyScript.SCRIPT_BODY, "assert true == true");
+        runner.setProperty(ExecuteGroovyScript.FAIL_STRATEGY, ExecuteGroovyScript.TRANSFER_TO_FAILURE);
+        runner.assertNotValid();
+    }
+
+    @Test
+    void validExpressionLanguageInDynamicPropertyWithVariableValueWhichCannotBeUsedWithELExpression() {
+        runner.setProperty("myProperty", "${test:toDate('yyyy-MM-dd')}");
+        runner.setProperty(ExecuteGroovyScript.SCRIPT_BODY, "assert true == true");
+        runner.setProperty(ExecuteGroovyScript.FAIL_STRATEGY, ExecuteGroovyScript.TRANSFER_TO_FAILURE);
+        runner.assertValid();
+
+        runner.setEnvironmentVariableValue("test", "cannot be converted to a date");
+        runner.enqueue("test content".getBytes(StandardCharsets.UTF_8));
+
+        runner.run();
+
+        runner.assertAllFlowFilesTransferred(ExecuteGroovyScript.REL_FAILURE.getName(), 1);
+        final MockFlowFile flowFile = runner.getFlowFilesForRelationship(ExecuteGroovyScript.REL_FAILURE).getFirst();
+        flowFile.assertAttributeExists(ProcessSessionWrap.ERROR_MESSAGE);
+        flowFile.assertAttributeExists(ProcessSessionWrap.ERROR_STACKTRACE);
+        assertTrue(flowFile.getAttribute(ProcessSessionWrap.ERROR_MESSAGE).contains("IllegalAttributeException"));
+    }
+
     @Test
     void testMigrateProperties() {
         final Map<String, String> expectedRenamed = Map.ofEntries(