diff --git a/nifi-mock/src/main/java/org/apache/nifi/util/MockPropertyValue.java b/nifi-mock/src/main/java/org/apache/nifi/util/MockPropertyValue.java index 45dbfde38b..cde9529297 100644 --- a/nifi-mock/src/main/java/org/apache/nifi/util/MockPropertyValue.java +++ b/nifi-mock/src/main/java/org/apache/nifi/util/MockPropertyValue.java @@ -93,14 +93,14 @@ public class MockPropertyValue implements PropertyValue { } } - private void validateExpressionScope(boolean attributesAvailable) { + private void validateExpressionScope(boolean flowFileProvided, boolean additionalAttributesAvailable) { if (expressionLanguageScope == null) { return; } // language scope is not null, we have attributes available but scope is not equal to FF attributes // it means that we're not evaluating against flow file attributes even though attributes are available - if (attributesAvailable && !ExpressionLanguageScope.FLOWFILE_ATTRIBUTES.equals(expressionLanguageScope)) { + if (flowFileProvided && !ExpressionLanguageScope.FLOWFILE_ATTRIBUTES.equals(expressionLanguageScope)) { throw new IllegalStateException("Attempting to evaluate expression language for " + propertyDescriptor.getName() + " using flow file attributes but the scope evaluation is set to " + expressionLanguageScope + ". The" + " proper scope should be set in the property descriptor using" @@ -124,8 +124,8 @@ public class MockPropertyValue implements PropertyValue { return; } - // we're trying to evaluate against flow files attributes but we don't have any attributes available. - if (!attributesAvailable && ExpressionLanguageScope.FLOWFILE_ATTRIBUTES.equals(expressionLanguageScope)) { + // we're trying to evaluate against flow files attributes but we don't have a FlowFile available. + if (!flowFileProvided && !additionalAttributesAvailable && ExpressionLanguageScope.FLOWFILE_ATTRIBUTES.equals(expressionLanguageScope)) { throw new IllegalStateException("Attempting to evaluate expression language for " + propertyDescriptor.getName() + " without using flow file attributes but the scope evaluation is set to " + expressionLanguageScope + ". The" + " proper scope should be set in the property descriptor using" @@ -263,7 +263,7 @@ public class MockPropertyValue implements PropertyValue { } if (!alreadyValidated) { - validateExpressionScope(flowFile != null || additionalAttributes != null); + validateExpressionScope(flowFile != null, additionalAttributes != null); } if (additionalAttributes == null ) { diff --git a/nifi-mock/src/test/java/org/apache/nifi/util/TestMockPropertyValue.java b/nifi-mock/src/test/java/org/apache/nifi/util/TestMockPropertyValue.java new file mode 100644 index 0000000000..a9253567fe --- /dev/null +++ b/nifi-mock/src/test/java/org/apache/nifi/util/TestMockPropertyValue.java @@ -0,0 +1,267 @@ +/* + * 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. + */ +package org.apache.nifi.util; + +import org.apache.nifi.annotation.behavior.InputRequirement; +import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.expression.ExpressionLanguageScope; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.util.StandardValidators; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TestMockPropertyValue { + + private final static String FF_SCOPE_WITH_FF = "Test FlowFile scope and providing a flow file"; + private final static String ENV_SCOPE_WITH_FF = "Test Env scope and providing a flow file"; + private final static String NONE_SCOPE_WITH_FF = "Test None scope and providing a flow file"; + private final static String FF_SCOPE_NO_FF = "Test FlowFile scope and no flow file"; + private final static String ENV_SCOPE_NO_FF = "Test Env scope and no flow file"; + private final static String NONE_SCOPE_NO_FF = "Test None scope and no flow file"; + private final static String FF_SCOPE_WITH_MAP = "Test FlowFile scope and providing a Map"; + private final static String ENV_SCOPE_WITH_MAP = "Test Env scope and providing a Map"; + private final static String NONE_SCOPE_WITH_MAP = "Test None scope and providing a Map"; + + @Test + public void testELScopeValidationProcessorWithInput() { + final DummyProcessorWithInput processor = new DummyProcessorWithInput(); + final TestRunner runner = TestRunners.newTestRunner(processor); + + runner.setProperty(DummyProcessorWithInput.PROP_FF_SCOPE, "${test}"); + runner.setProperty(DummyProcessorWithInput.PROP_ENV_SCOPE, "${test}"); + runner.setProperty(DummyProcessorWithInput.PROP_NONE_SCOPE, "${test}"); + + // This case is expected to work: we evaluate against a FlowFile and the + // property has the expected FlowFile scope + processor.setTestCase(FF_SCOPE_WITH_FF); + runner.enqueue(""); + runner.run(); + + // This case is not expected to work: we evaluate against a FlowFile but the + // property is not supposed to support evaluation against a FlowFile + processor.setTestCase(ENV_SCOPE_WITH_FF); + runner.enqueue(""); + assertThrows(AssertionError.class, runner::run); + + // This case is not expected to work: no EL support on the property + processor.setTestCase(NONE_SCOPE_WITH_FF); + runner.enqueue(""); + assertThrows(AssertionError.class, runner::run); + + // This case is supposed to fail: there is an incoming connection, there is a + // FlowFile available, we have EL scope but we don't evaluate against the FF + processor.setTestCase(FF_SCOPE_NO_FF); + runner.enqueue(""); + assertThrows(AssertionError.class, runner::run); + + processor.setTestCase(ENV_SCOPE_NO_FF); + runner.enqueue(""); + runner.run(); + + // This case is not expected to work: no EL support on the property + processor.setTestCase(NONE_SCOPE_NO_FF); + runner.enqueue(""); + assertThrows(AssertionError.class, runner::run); + + // This case is accepted as we have an incoming connection, we may be evaluating + // against a map made of the flow file attributes + additional key/value pairs. + processor.setTestCase(FF_SCOPE_WITH_MAP); + runner.enqueue(""); + runner.run(); + + processor.setTestCase(ENV_SCOPE_WITH_MAP); + runner.enqueue(""); + runner.run(); + + // This case is not expected to work: no EL support on the property + processor.setTestCase(NONE_SCOPE_WITH_MAP); + runner.enqueue(""); + assertThrows(AssertionError.class, runner::run); + } + + @Test + public void testELScopeValidationProcessorNoInput() { + final DummyProcessorNoInput processor = new DummyProcessorNoInput(); + final TestRunner runner = TestRunners.newTestRunner(processor); + + runner.setProperty(DummyProcessorNoInput.PROP_FF_SCOPE, "${test}"); + runner.setProperty(DummyProcessorNoInput.PROP_ENV_SCOPE, "${test}"); + runner.setProperty(DummyProcessorNoInput.PROP_NONE_SCOPE, "${test}"); + + // This case is supposed to be OK: in that case, we don't care if attributes are + // not available even though scope is FLOWFILE_ATTRIBUTES it likely means that + // the property has been defined in a common/abstract class used by multiple + // processors with different input requirements. + processor.setTestCase(FF_SCOPE_NO_FF); + runner.run(); + + processor.setTestCase(ENV_SCOPE_NO_FF); + runner.run(); + + // This case is not expected to work: no EL support on the property + processor.setTestCase(NONE_SCOPE_NO_FF); + assertThrows(AssertionError.class, runner::run); + + // This case is supposed to be OK: in that case, we don't care if attributes are + // not available even though scope is FLOWFILE_ATTRIBUTES it likely means that + // the property has been defined in a common/abstract class used by multiple + // processors with different input requirements. + processor.setTestCase(FF_SCOPE_WITH_MAP); + runner.run(); + + processor.setTestCase(ENV_SCOPE_WITH_MAP); + runner.run(); + + // This case is not expected to work: no EL support on the property + processor.setTestCase(NONE_SCOPE_WITH_MAP); + assertThrows(AssertionError.class, runner::run); + } + + @InputRequirement(Requirement.INPUT_ALLOWED) + private static class DummyProcessorWithInput extends AbstractProcessor { + static final PropertyDescriptor PROP_FF_SCOPE = new PropertyDescriptor.Builder() + .name("Property with FF scope") + .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + static final PropertyDescriptor PROP_ENV_SCOPE = new PropertyDescriptor.Builder() + .name("Property with Env scope") + .expressionLanguageSupported(ExpressionLanguageScope.ENVIRONMENT) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + static final PropertyDescriptor PROP_NONE_SCOPE = new PropertyDescriptor.Builder() + .name("Property with None scope") + .expressionLanguageSupported(ExpressionLanguageScope.NONE) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + static final Relationship SUCCESS = new Relationship.Builder() + .name("success") + .build(); + + private String testCase; + + @Override + protected List getSupportedPropertyDescriptors() { + return List.of(PROP_ENV_SCOPE, PROP_FF_SCOPE, PROP_NONE_SCOPE); + } + + @Override + public Set getRelationships() { + return Set.of(SUCCESS); + } + + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { + FlowFile ff = session.get(); + + final Map map = Map.of("test", "test"); + + switch (getTestCase()) { + case FF_SCOPE_WITH_FF -> context.getProperty(PROP_FF_SCOPE).evaluateAttributeExpressions(ff); + case ENV_SCOPE_WITH_FF -> context.getProperty(PROP_ENV_SCOPE).evaluateAttributeExpressions(ff); + case NONE_SCOPE_WITH_FF -> context.getProperty(PROP_NONE_SCOPE).evaluateAttributeExpressions(ff); + case FF_SCOPE_NO_FF -> context.getProperty(PROP_FF_SCOPE).evaluateAttributeExpressions(); + case ENV_SCOPE_NO_FF -> context.getProperty(PROP_ENV_SCOPE).evaluateAttributeExpressions(); + case NONE_SCOPE_NO_FF -> context.getProperty(PROP_NONE_SCOPE).evaluateAttributeExpressions(); + case FF_SCOPE_WITH_MAP -> context.getProperty(PROP_FF_SCOPE).evaluateAttributeExpressions(map); + case ENV_SCOPE_WITH_MAP -> context.getProperty(PROP_ENV_SCOPE).evaluateAttributeExpressions(map); + case NONE_SCOPE_WITH_MAP -> context.getProperty(PROP_NONE_SCOPE).evaluateAttributeExpressions(map); + } + + if (ff != null) { + session.transfer(ff, SUCCESS); + } + } + + public String getTestCase() { + return testCase; + } + + public void setTestCase(String testCase) { + this.testCase = testCase; + } + } + + @InputRequirement(Requirement.INPUT_FORBIDDEN) + private static class DummyProcessorNoInput extends AbstractProcessor { + static final PropertyDescriptor PROP_FF_SCOPE = new PropertyDescriptor.Builder() + .name("Property with FF scope") + .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + static final PropertyDescriptor PROP_ENV_SCOPE = new PropertyDescriptor.Builder() + .name("Property with Env scope") + .expressionLanguageSupported(ExpressionLanguageScope.ENVIRONMENT) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + static final PropertyDescriptor PROP_NONE_SCOPE = new PropertyDescriptor.Builder() + .name("Property with None scope") + .expressionLanguageSupported(ExpressionLanguageScope.NONE) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .build(); + + static final Relationship SUCCESS = new Relationship.Builder() + .name("success") + .build(); + + private String testCase; + + @Override + protected List getSupportedPropertyDescriptors() { + return List.of(PROP_ENV_SCOPE, PROP_FF_SCOPE, PROP_NONE_SCOPE); + } + + @Override + public Set getRelationships() { + return Set.of(SUCCESS); + } + + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { + final Map map = Map.of("test", "test"); + + switch (getTestCase()) { + case FF_SCOPE_NO_FF -> context.getProperty(PROP_FF_SCOPE).evaluateAttributeExpressions(); + case ENV_SCOPE_NO_FF -> context.getProperty(PROP_ENV_SCOPE).evaluateAttributeExpressions(); + case NONE_SCOPE_NO_FF -> context.getProperty(PROP_NONE_SCOPE).evaluateAttributeExpressions(); + case FF_SCOPE_WITH_MAP -> context.getProperty(PROP_FF_SCOPE).evaluateAttributeExpressions(map); + case ENV_SCOPE_WITH_MAP -> context.getProperty(PROP_ENV_SCOPE).evaluateAttributeExpressions(map); + case NONE_SCOPE_WITH_MAP -> context.getProperty(PROP_NONE_SCOPE).evaluateAttributeExpressions(map); + } + } + + public String getTestCase() { + return testCase; + } + + public void setTestCase(String testCase) { + this.testCase = testCase; + } + } +} \ No newline at end of file