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

MikeThomsen 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 2d34148940e NIFI-16001 Fix property value truncation around an EL 
attribute reference in Python processors (#11735)
2d34148940e is described below

commit 2d34148940e5b2007b0c40fc9efc71faf383fce8
Author: Nanjesh Ramesh <[email protected]>
AuthorDate: Mon Oct 12 10:24:43 2026 -0700

    NIFI-16001 Fix property value truncation around an EL attribute reference 
in Python processors (#11735)
    
    PropertyContext.create_python_property_value detects a property value that 
is
    entirely a single attribute reference, such as "${attr}", so
    PythonPropertyValue.evaluateAttributeExpressions can take a fast path and 
look
    the attribute up directly instead of calling back into the Java expression
    language engine. That check used re.match, which only anchors at the start 
of
    the string, so a value like "${attr}_suffix" matched on the "${attr}" prefix
    and took the fast path anyway, silently dropping the "_suffix" literal text.
    Any WritePropertyToFlowFile-style property whose value referenced an 
attribute
    with any literal text before or after it was truncated down to just the
    attribute's value.
    
    Switch both the trivial and the escaped/quoted reference checks to
    re.fullmatch, which requires the whole string to match. A bare "${attr}" or
    "${'attr'}" still takes the fast path exactly as before; anything with
    surrounding text now falls through to full expression evaluation, which
    already handles this correctly.
    
    Added a unit test module for this class under
    nifi-python-extension-api/src/test/python, wired into the existing
    python-unit-tests Maven profile, and a system test case in PythonProcessorIT
    that exercises the fix through an actual Python processor.
---
 .../nifi-python-extension-api/pom.xml              | 41 ++++++++++++
 .../src/main/python/src/nifiapi/properties.py      |  4 +-
 .../src/test/python/nifiapi/TestPropertyContext.py | 73 ++++++++++++++++++++++
 .../src/test/python/nifiapi/testutils.py           | 50 +++++++++++++++
 .../tests/system/python/PythonProcessorIT.java     | 42 +++++++++++++
 5 files changed, 208 insertions(+), 2 deletions(-)

diff --git 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/pom.xml
 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/pom.xml
index 452e8ced773..3739ee41eb8 100644
--- 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/pom.xml
+++ 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/pom.xml
@@ -42,4 +42,45 @@
         </plugins>
     </build>
 
+    <profiles>
+        <profile>
+            <id>python-unit-tests</id>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.codehaus.mojo</groupId>
+                        <artifactId>exec-maven-plugin</artifactId>
+                        <executions>
+                            <execution>
+                                <id>python-test</id>
+                                <phase>test</phase>
+                                <goals>
+                                    <goal>exec</goal>
+                                </goals>
+                                <configuration>
+                                    <executable>python3</executable>
+                                    <environmentVariables>
+                                        
<PYTHONPATH>src/main/python/src</PYTHONPATH>
+                                    </environmentVariables>
+                                    <arguments>
+                                        <argument>-m</argument>
+                                        <argument>unittest</argument>
+                                        <argument>discover</argument>
+                                        <argument>-s</argument>
+                                        
<argument>src/test/python/nifiapi</argument>
+                                        <argument>-p</argument>
+                                        <argument>Test*.py</argument>
+                                    </arguments>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
 </project>
\ No newline at end of file
diff --git 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/properties.py
 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/properties.py
index 2b392d24c97..3b6a4629efe 100644
--- 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/properties.py
+++ 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/src/main/python/src/nifiapi/properties.py
@@ -406,11 +406,11 @@ class PropertyContext:
         el_present = java_property_value.isExpressionLanguagePresent()
         referenced_attribute = None
         if el_present:
-            trivial_match = 
self.__trivial_attribute_reference__.match(string_value)
+            trivial_match = 
self.__trivial_attribute_reference__.fullmatch(string_value)
             if trivial_match is not None:
                 referenced_attribute = trivial_match.group(1)
             else:
-                escaped_match = 
self.__escaped_attribute_reference__.match(string_value)
+                escaped_match = 
self.__escaped_attribute_reference__.fullmatch(string_value)
                 if escaped_match is not None:
                     referenced_attribute = escaped_match.group(1)
 
diff --git 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/src/test/python/nifiapi/TestPropertyContext.py
 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/src/test/python/nifiapi/TestPropertyContext.py
new file mode 100644
index 00000000000..421c152570c
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/src/test/python/nifiapi/TestPropertyContext.py
@@ -0,0 +1,73 @@
+# 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.
+
+import unittest
+
+from testutils import set_up_env
+
+set_up_env()
+
+from nifiapi.properties import PropertyContext
+
+
+class FakeJavaPropertyValue:
+    def __init__(self, expression_language_present):
+        self.expression_language_present = expression_language_present
+
+    def isExpressionLanguagePresent(self):
+        return self.expression_language_present
+
+
+class RecognizeTrivialAttributeReferenceUseCase(unittest.TestCase):
+    """PropertyContext.create_python_property_value looks for a property value 
that is
+    *entirely* a single attribute reference, such as "${attr}" or "${'attr 
with spaces'}", so
+    that PythonPropertyValue.evaluateAttributeExpressions can take a fast path 
and look the
+    attribute up directly instead of invoking the full expression language 
engine.
+
+    A value that merely *contains* such a reference, with literal text before 
or after it,
+    must not take that fast path: the literal text has to survive evaluation 
too."""
+
+    def setUp(self):
+        self.context = PropertyContext()
+
+    def matched_attribute(self, string_value):
+        property_value = self.context.create_python_property_value(True, 
FakeJavaPropertyValue(True), string_value)
+        return property_value.referenced_attribute
+
+    def test_bare_reference_is_recognized(self):
+        self.assertEqual('my_attribute', 
self.matched_attribute('${my_attribute}'))
+
+    def test_bare_escaped_reference_is_recognized(self):
+        self.assertEqual('my attribute', self.matched_attribute("${'my 
attribute'}"))
+
+    def test_reference_with_trailing_text_is_not_trivial(self):
+        self.assertIsNone(self.matched_attribute('${my_attribute}_foo_bar'))
+
+    def test_reference_with_leading_text_is_not_trivial(self):
+        self.assertIsNone(self.matched_attribute('foo_bar_${my_attribute}'))
+
+    def test_escaped_reference_with_trailing_text_is_not_trivial(self):
+        self.assertIsNone(self.matched_attribute("${'my attribute'}_foo_bar"))
+
+    def test_two_references_are_not_trivial(self):
+        self.assertIsNone(self.matched_attribute('${first}${second}'))
+
+    def test_no_expression_language_present_is_not_trivial(self):
+        property_value = self.context.create_python_property_value(True, 
FakeJavaPropertyValue(False), '${my_attribute}')
+        self.assertIsNone(property_value.referenced_attribute)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git 
a/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/src/test/python/nifiapi/testutils.py
 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/src/test/python/nifiapi/testutils.py
new file mode 100644
index 00000000000..b459575b916
--- /dev/null
+++ 
b/nifi-framework-bundle/nifi-framework-extensions/nifi-py4j-framework-bundle/nifi-python-extension-api/src/test/python/nifiapi/testutils.py
@@ -0,0 +1,50 @@
+# 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.
+
+import sys
+import types
+
+
+class AutoAttr:
+    """Stands in for a py4j JVM/gateway view. Any attribute access or call 
returns another
+    AutoAttr, so module-level lookups like JvmHolder.jvm.org.apache.nifi... 
succeed without
+    needing to model the full Java package tree."""
+
+    def __getattr__(self, name):
+        return AutoAttr()
+
+    def __call__(self, *args, **kwargs):
+        return AutoAttr()
+
+
+def _stub_py4j_if_missing():
+    # py4j is only bundled with the framework at runtime, so it is not on the 
PYTHONPATH here.
+    try:
+        import py4j.protocol  # noqa: F401
+    except ImportError:
+        py4j = types.ModuleType("py4j")
+        protocol = types.ModuleType("py4j.protocol")
+        protocol.Py4JError = type("Py4JError", (Exception,), {})
+        protocol.Py4JJavaError = type("Py4JJavaError", (protocol.Py4JError,), 
{})
+        py4j.protocol = protocol
+        sys.modules["py4j"] = py4j
+        sys.modules["py4j.protocol"] = protocol
+
+
+def set_up_env():
+    _stub_py4j_if_missing()
+    from nifiapi.__jvm__ import JvmHolder
+    JvmHolder.jvm = AutoAttr()
+    JvmHolder.gateway = AutoAttr()
diff --git 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/python/PythonProcessorIT.java
 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/python/PythonProcessorIT.java
index 520b1bc7a0c..d1abf3da750 100644
--- 
a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/python/PythonProcessorIT.java
+++ 
b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/python/PythonProcessorIT.java
@@ -84,6 +84,48 @@ public class PythonProcessorIT extends NiFiSystemIT {
         assertEquals(messageContents, contents);
     }
 
+    @Test
+    public void testFlowFileTransformWithELAndSurroundingLiteralText() throws 
NiFiClientException, IOException, InterruptedException {
+        final String attributeValue = "Hello World";
+        final String expectedContents = "Hello World, welcome!";
+
+        final ProcessorEntity generate = 
getClientUtil().createProcessor("GenerateFlowFile");
+        final ProcessorEntity writeProperty = 
getClientUtil().createPythonProcessor("WritePropertyToFlowFile");
+        final ProcessorEntity terminate = 
getClientUtil().createProcessor("TerminateFlowFile");
+
+        // Config GenerateFlowFile to add a "greeting" attribute with a value 
of "Hello World"
+        final ProcessorConfigDTO generateConfig = 
generate.getComponent().getConfig();
+        generateConfig.setProperties(Collections.singletonMap("greeting", 
attributeValue));
+        getClientUtil().updateProcessorConfig(generate, generateConfig);
+
+        // Configure the WritePropertyToFlowFile processor with a property 
value that references an
+        // attribute but also has literal text around it. Only the referenced 
attribute's value should be
+        // substituted; the surrounding literal text must be preserved.
+        final ProcessorConfigDTO writePropertyConfig = 
writeProperty.getComponent().getConfig();
+        writePropertyConfig.setProperties(Collections.singletonMap("Message", 
"${greeting}, welcome!"));
+        getClientUtil().updateProcessorConfig(writeProperty, 
writePropertyConfig);
+
+        // Connect flow
+        getClientUtil().createConnection(generate, writeProperty, "success");
+        getClientUtil().setAutoTerminatedRelationships(writeProperty, 
"failure");
+        final ConnectionEntity outputConnection = 
getClientUtil().createConnection(writeProperty, terminate, "success");
+
+        // Wait for processor validation to complete
+        getClientUtil().waitForValidProcessor(generate.getId());
+        getClientUtil().waitForValidProcessor(writeProperty.getId());
+
+        // Run the flow
+        getClientUtil().startProcessor(generate);
+        getClientUtil().startProcessor(writeProperty);
+
+        // Wait for output to be queued up
+        waitForQueueCount(outputConnection.getId(), 1);
+
+        // Validate the output
+        final String contents = 
getClientUtil().getFlowFileContentAsUtf8(outputConnection.getId(), 0);
+        assertEquals(expectedContents, contents);
+    }
+
     @Test
     public void testRecordTransform() throws NiFiClientException, IOException, 
InterruptedException {
         final ProcessorEntity generate = 
getClientUtil().createProcessor("GenerateFlowFile");

Reply via email to