NIFI-6782: Added repeat() String EL function

NIFI-6782: Fixed intermittent unit test failure

This closes #3825

Signed-off-by: Mike Thomsen <mthomsen@apache.org>
This commit is contained in:
Matthew Burgess 2019-10-17 15:37:14 -04:00 committed by Mike Thomsen
parent c72a5618c0
commit f1be730c94
No known key found for this signature in database
GPG Key ID: 88511C3D4CAD246F
6 changed files with 172 additions and 1 deletions

View File

@ -182,6 +182,7 @@ JOIN : 'join';
TO_LITERAL : 'literal'; TO_LITERAL : 'literal';
JSON_PATH : 'jsonPath'; JSON_PATH : 'jsonPath';
JSON_PATH_DELETE : 'jsonPathDelete'; JSON_PATH_DELETE : 'jsonPathDelete';
REPEAT : 'repeat';
// 2 arg functions // 2 arg functions
SUBSTRING : 'substring'; SUBSTRING : 'substring';

View File

@ -79,7 +79,7 @@ oneArgString : ((SUBSTRING_BEFORE | SUBSTRING_BEFORE_LAST | SUBSTRING_AFTER | SU
PREPEND | APPEND | STARTS_WITH | ENDS_WITH | CONTAINS | JOIN | JSON_PATH | JSON_PATH_DELETE | FROM_RADIX) LPAREN! anyArg RPAREN!) | PREPEND | APPEND | STARTS_WITH | ENDS_WITH | CONTAINS | JOIN | JSON_PATH | JSON_PATH_DELETE | FROM_RADIX) LPAREN! anyArg RPAREN!) |
(TO_RADIX LPAREN! anyArg (COMMA! anyArg)? RPAREN!); (TO_RADIX LPAREN! anyArg (COMMA! anyArg)? RPAREN!);
twoArgString : ((REPLACE | REPLACE_FIRST | REPLACE_ALL | IF_ELSE | JSON_PATH_SET | JSON_PATH_ADD) LPAREN! anyArg COMMA! anyArg RPAREN!) | twoArgString : ((REPLACE | REPLACE_FIRST | REPLACE_ALL | IF_ELSE | JSON_PATH_SET | JSON_PATH_ADD) LPAREN! anyArg COMMA! anyArg RPAREN!) |
((SUBSTRING | FORMAT | PAD_LEFT | PAD_RIGHT) LPAREN! anyArg (COMMA! anyArg)? RPAREN!); ((SUBSTRING | FORMAT | PAD_LEFT | PAD_RIGHT | REPEAT) LPAREN! anyArg (COMMA! anyArg)? RPAREN!);
threeArgString: ((JSON_PATH_PUT) LPAREN! anyArg COMMA! anyArg COMMA! anyArg RPAREN!); threeArgString: ((JSON_PATH_PUT) LPAREN! anyArg COMMA! anyArg COMMA! anyArg RPAREN!);
fiveArgString : GET_DELIMITED_FIELD LPAREN! anyArg (COMMA! anyArg (COMMA! anyArg (COMMA! anyArg (COMMA! anyArg)?)?)?)? RPAREN!; fiveArgString : GET_DELIMITED_FIELD LPAREN! anyArg (COMMA! anyArg (COMMA! anyArg (COMMA! anyArg (COMMA! anyArg)?)?)?)? RPAREN!;

View File

@ -89,6 +89,7 @@ import org.apache.nifi.attribute.expression.language.evaluation.functions.PadRig
import org.apache.nifi.attribute.expression.language.evaluation.functions.PlusEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.PlusEvaluator;
import org.apache.nifi.attribute.expression.language.evaluation.functions.PrependEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.PrependEvaluator;
import org.apache.nifi.attribute.expression.language.evaluation.functions.RandomNumberGeneratorEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.RandomNumberGeneratorEvaluator;
import org.apache.nifi.attribute.expression.language.evaluation.functions.RepeatEvaluator;
import org.apache.nifi.attribute.expression.language.evaluation.functions.ReplaceAllEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.ReplaceAllEvaluator;
import org.apache.nifi.attribute.expression.language.evaluation.functions.ReplaceEmptyEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.ReplaceEmptyEvaluator;
import org.apache.nifi.attribute.expression.language.evaluation.functions.ReplaceEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.ReplaceEvaluator;
@ -208,6 +209,7 @@ import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpre
import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.PLUS; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.PLUS;
import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.PREPEND; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.PREPEND;
import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.RANDOM; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.RANDOM;
import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.REPEAT;
import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.REPLACE; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.REPLACE;
import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.REPLACE_ALL; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.REPLACE_ALL;
import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.REPLACE_EMPTY; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.REPLACE_EMPTY;
@ -721,6 +723,19 @@ public class ExpressionCompiler {
throw new AttributeExpressionLanguageParsingException("substring() function can take either 1 or 2 arguments but cannot take " + numArgs + " arguments"); throw new AttributeExpressionLanguageParsingException("substring() function can take either 1 or 2 arguments but cannot take " + numArgs + " arguments");
} }
} }
case REPEAT: {
final int numArgs = argEvaluators.size();
if (numArgs == 1) {
return addToken(new RepeatEvaluator(toStringEvaluator(subjectEvaluator),
toWholeNumberEvaluator(argEvaluators.get(0), "first argument to repeat")), "repeat");
} else if (numArgs == 2) {
return addToken(new RepeatEvaluator(toStringEvaluator(subjectEvaluator),
toWholeNumberEvaluator(argEvaluators.get(0), "first argument to repeat"),
toWholeNumberEvaluator(argEvaluators.get(1), "second argument to repeat")), "repeat");
} else {
throw new AttributeExpressionLanguageParsingException("repeat() function can take either 1 or 2 arguments but cannot take " + numArgs + " arguments");
}
}
case JOIN: { case JOIN: {
verifyArgCount(argEvaluators, 1, "join"); verifyArgCount(argEvaluators, 1, "join");
return addToken(new JoinEvaluator(toStringEvaluator(subjectEvaluator), toStringEvaluator(argEvaluators.get(0))), "join"); return addToken(new JoinEvaluator(toStringEvaluator(subjectEvaluator), toStringEvaluator(argEvaluators.get(0))), "join");

View File

@ -0,0 +1,74 @@
/*
* 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.attribute.expression.language.evaluation.functions;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.attribute.expression.language.EvaluationContext;
import org.apache.nifi.attribute.expression.language.evaluation.Evaluator;
import org.apache.nifi.attribute.expression.language.evaluation.QueryResult;
import org.apache.nifi.attribute.expression.language.evaluation.StringEvaluator;
import org.apache.nifi.attribute.expression.language.evaluation.StringQueryResult;
import org.apache.nifi.attribute.expression.language.exception.AttributeExpressionLanguageException;
public class RepeatEvaluator extends StringEvaluator {
private final Evaluator<String> subject;
private final Evaluator<Long> minRepeats;
private final Evaluator<Long> maxRepeats;
public RepeatEvaluator(final Evaluator<String> subject, final Evaluator<Long> minRepeats, final Evaluator<Long> maxRepeats) {
this.subject = subject;
this.minRepeats = minRepeats;
this.maxRepeats = maxRepeats;
}
public RepeatEvaluator(final Evaluator<String> subject, final Evaluator<Long> minRepeats) {
this.subject = subject;
this.minRepeats = minRepeats;
this.maxRepeats = null;
}
@Override
public QueryResult<String> evaluate(final EvaluationContext evaluationContext) {
final String subjectValue = subject.evaluate(evaluationContext).getValue();
if (subjectValue == null) {
return new StringQueryResult("");
}
final int firstRepeatValue = minRepeats.evaluate(evaluationContext).getValue().intValue();
if (maxRepeats == null) {
if (firstRepeatValue <= 0) {
throw new AttributeExpressionLanguageException("Number of repeats must be > 0");
}
return new StringQueryResult(StringUtils.repeat(subjectValue, firstRepeatValue));
} else {
if (firstRepeatValue <= 0) {
throw new AttributeExpressionLanguageException("Minimum number of repeats must be > 0");
}
final int maxRepeatCount = maxRepeats.evaluate(evaluationContext).getValue().intValue();
if (firstRepeatValue > maxRepeatCount) {
throw new AttributeExpressionLanguageException("Min repeats must not be greater than max repeats");
}
final int randomRepeatCount = ((int) (Math.random() * (maxRepeatCount - firstRepeatValue + 1))) + firstRepeatValue;
return new StringQueryResult(StringUtils.repeat(subjectValue, randomRepeatCount));
}
}
@Override
public Evaluator<?> getSubjectEvaluator() {
return subject;
}
}

View File

@ -2098,6 +2098,45 @@ public class TestQuery {
verifyEmpty("${nullString:padRight(10, \"@\")}", attributes); verifyEmpty("${nullString:padRight(10, \"@\")}", attributes);
} }
@Test
public void testRepeat() {
final Map<String, String> attributes = new HashMap<>();
attributes.put("str", "abc");
verifyEquals("${not_exist:repeat(1, 2)}", attributes, "");
verifyEquals("${str:repeat(1, 1)}", attributes, "abc");
// Custom verify because the result could be one of multiple options
String multipleResultExpression = "${str:repeat(1, 3)}";
String multipleResultExpectedResult1 = "abc";
String multipleResultExpectedResult2 = "abcabc";
String multipleResultExpectedResult3 = "abcabcabc";
List<String> multipleResultExpectedResults = Arrays.asList(multipleResultExpectedResult1, multipleResultExpectedResult2, multipleResultExpectedResult3);
Query.validateExpression(multipleResultExpression, false);
final String actualResult = Query.evaluateExpressions(multipleResultExpression, attributes, null, null, ParameterLookup.EMPTY);
assertTrue(multipleResultExpectedResults.contains(actualResult));
verifyEquals("${str:repeat(4)}", attributes, "abcabcabcabc");
try {
verifyEquals("${str:repeat(-1)}", attributes, "");
fail("Should have failed on numRepeats < 0");
} catch(AttributeExpressionLanguageException aele) {
// Do nothing, it is expected
}
try {
verifyEquals("${str:repeat(0)}", attributes, "");
fail("Should have failed on numRepeats = 0");
} catch(AttributeExpressionLanguageException aele) {
// Do nothing, it is expected
}
try {
verifyEquals("${str:repeat(2,1)}", attributes, "");
fail("Should have failed on minRepeats > maxRepeats");
} catch(AttributeExpressionLanguageException aele) {
// Do nothing, it is expected
}
}
private void verifyEquals(final String expression, final Map<String, String> attributes, final Object expectedResult) { private void verifyEquals(final String expression, final Map<String, String> attributes, final Object expectedResult) {
verifyEquals(expression,attributes, null, ParameterLookup.EMPTY, expectedResult); verifyEquals(expression,attributes, null, ParameterLookup.EMPTY, expectedResult);
} }

View File

@ -1123,6 +1123,48 @@ Expressions will provide the following results:
then the Expression `${query:evaluateELString()}` will return SELECT * FROM TABLE WHERE ID = 20 then the Expression `${query:evaluateELString()}` will return SELECT * FROM TABLE WHERE ID = 20
[.function]
=== repeat
*Description*:
[.description]#Returns a string that is the Subject repeated a random number of times between _min repeats_ and
_max repeats_. If _max repeats_ is not supplied, it will return the Subject repeated exactly _min repeats_ times.#
[.description]#The _min repeats_ and _max repeats_ must be positive numbers, with _max repeats_ greater than or equal
to _min repeats_#
[.description]#If either _min repeats_ or _max repeats_ is not a number, this function call will result
in an error.#
*Subject Type*: [.subject]#String#
*Arguments*:
- [.argName]#_min repeats_# : [.argDesc]#The minimum number (inclusive) of times to repeat the subject, or the exact number
of times to repeat the subject if _max repeats_ is not supplied.#
- [.argName]#_max repeats_# : [.argDesc]#The maximum number (inclusive) of times to repeat the subject.#
*Return Type*: [.returnType]#String#
*Examples*:
If we have an attribute named "str" with the value "abc",
then the following Expressions will result in the following values:
.Repeat Examples
|================================================================
| Expression | Value
| `${str:repeat(1)}` | `abc`
| `${str:repeat(2)}` | `abcabc`
| `${str:repeat(1,2)}` | `abc` or `abcabc` (at random)
| `${str:repeat( ${str:length()} )}` | `abc` or `abcabc` or `abcabcabc` (at random)
|================================================================
[[encode]] [[encode]]
== Encode/Decode Functions == Encode/Decode Functions