Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NIFI-14041 - Improve MockPropertyValue and EL evaluation validation #9549

Merged
merged 2 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
}