Skip to content

Commit

Permalink
NIFI-14041 Improved MockPropertyValue and EL evaluation validation (#…
Browse files Browse the repository at this point in the history
…9549)

Signed-off-by: David Handermann <[email protected]>
  • Loading branch information
pvillard31 authored Nov 25, 2024
1 parent 134240c commit cfa495b
Show file tree
Hide file tree
Showing 2 changed files with 272 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,14 @@ private void ensureExpressionsEvaluated() {
}
}

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"
Expand All @@ -124,8 +124,8 @@ private void validateExpressionScope(boolean attributesAvailable) {
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"
Expand Down Expand Up @@ -263,7 +263,7 @@ public PropertyValue evaluateAttributeExpressions(FlowFile flowFile, Map<String,
}

if (!alreadyValidated) {
validateExpressionScope(flowFile != null || additionalAttributes != null);
validateExpressionScope(flowFile != null, additionalAttributes != null);
}

if (additionalAttributes == null ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PropertyDescriptor> getSupportedPropertyDescriptors() {
return List.of(PROP_ENV_SCOPE, PROP_FF_SCOPE, PROP_NONE_SCOPE);
}

@Override
public Set<Relationship> getRelationships() {
return Set.of(SUCCESS);
}

@Override
public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
FlowFile ff = session.get();

final Map<String, String> 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<PropertyDescriptor> getSupportedPropertyDescriptors() {
return List.of(PROP_ENV_SCOPE, PROP_FF_SCOPE, PROP_NONE_SCOPE);
}

@Override
public Set<Relationship> getRelationships() {
return Set.of(SUCCESS);
}

@Override
public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
final Map<String, String> 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;
}
}
}

0 comments on commit cfa495b

Please sign in to comment.