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");