From f1be730c9470e56e6042be8418cb03a70845eb69 Mon Sep 17 00:00:00 2001 From: Matthew Burgess Date: Thu, 17 Oct 2019 15:37:14 -0400 Subject: [PATCH] NIFI-6782: Added repeat() String EL function NIFI-6782: Fixed intermittent unit test failure This closes #3825 Signed-off-by: Mike Thomsen --- .../language/antlr/AttributeExpressionLexer.g | 1 + .../antlr/AttributeExpressionParser.g | 2 +- .../language/compile/ExpressionCompiler.java | 15 ++++ .../evaluation/functions/RepeatEvaluator.java | 74 +++++++++++++++++++ .../expression/language/TestQuery.java | 39 ++++++++++ .../asciidoc/expression-language-guide.adoc | 42 +++++++++++ 6 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/RepeatEvaluator.java diff --git a/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g b/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g index 23e59fe15a..16844128bc 100644 --- a/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g +++ b/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionLexer.g @@ -182,6 +182,7 @@ JOIN : 'join'; TO_LITERAL : 'literal'; JSON_PATH : 'jsonPath'; JSON_PATH_DELETE : 'jsonPathDelete'; +REPEAT : 'repeat'; // 2 arg functions SUBSTRING : 'substring'; diff --git a/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionParser.g b/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionParser.g index 71e7ce939d..b2e9039fc0 100644 --- a/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionParser.g +++ b/nifi-commons/nifi-expression-language/src/main/antlr3/org/apache/nifi/attribute/expression/language/antlr/AttributeExpressionParser.g @@ -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!) | (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!) | - ((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!); fiveArgString : GET_DELIMITED_FIELD LPAREN! anyArg (COMMA! anyArg (COMMA! anyArg (COMMA! anyArg (COMMA! anyArg)?)?)?)? RPAREN!; diff --git a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java index 3f438fb097..46240589df 100644 --- a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java +++ b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/compile/ExpressionCompiler.java @@ -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.PrependEvaluator; 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.ReplaceEmptyEvaluator; 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.PREPEND; 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_ALL; 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"); } } + 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: { verifyArgCount(argEvaluators, 1, "join"); return addToken(new JoinEvaluator(toStringEvaluator(subjectEvaluator), toStringEvaluator(argEvaluators.get(0))), "join"); diff --git a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/RepeatEvaluator.java b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/RepeatEvaluator.java new file mode 100644 index 0000000000..d605f0da46 --- /dev/null +++ b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/RepeatEvaluator.java @@ -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 subject; + private final Evaluator minRepeats; + private final Evaluator maxRepeats; + + public RepeatEvaluator(final Evaluator subject, final Evaluator minRepeats, final Evaluator maxRepeats) { + this.subject = subject; + this.minRepeats = minRepeats; + this.maxRepeats = maxRepeats; + } + + public RepeatEvaluator(final Evaluator subject, final Evaluator minRepeats) { + this.subject = subject; + this.minRepeats = minRepeats; + this.maxRepeats = null; + } + + @Override + public QueryResult 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; + } +} diff --git a/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java b/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java index 3753ee80f3..d48129dfd4 100644 --- a/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java +++ b/nifi-commons/nifi-expression-language/src/test/java/org/apache/nifi/attribute/expression/language/TestQuery.java @@ -2098,6 +2098,45 @@ public class TestQuery { verifyEmpty("${nullString:padRight(10, \"@\")}", attributes); } + @Test + public void testRepeat() { + final Map 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 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 attributes, final Object expectedResult) { verifyEquals(expression,attributes, null, ParameterLookup.EMPTY, expectedResult); } diff --git a/nifi-docs/src/main/asciidoc/expression-language-guide.adoc b/nifi-docs/src/main/asciidoc/expression-language-guide.adoc index 6445aa3b16..70fb9d9408 100644 --- a/nifi-docs/src/main/asciidoc/expression-language-guide.adoc +++ b/nifi-docs/src/main/asciidoc/expression-language-guide.adoc @@ -1123,6 +1123,48 @@ Expressions will provide the following results: 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/Decode Functions