From 3a6001387681f79615ac5ce776e74548dd825005 Mon Sep 17 00:00:00 2001 From: dan-s1 Date: Fri, 18 Nov 2022 20:19:44 +0000 Subject: [PATCH] NIFI-10754 Added getUri NIFI Expression Language function This closes #6689 Signed-off-by: David Handermann --- .../language/antlr/AttributeExpressionLexer.g | 2 + .../antlr/AttributeExpressionParser.g | 5 +- .../language/compile/ExpressionCompiler.java | 14 ++++ .../evaluation/functions/GetUriEvaluator.java | 75 +++++++++++++++++++ .../literals/StringLiteralEvaluator.java | 64 ++++++++-------- .../expression/language/TestQuery.java | 22 ++++++ .../asciidoc/expression-language-guide.adoc | 41 ++++++++++ 7 files changed, 191 insertions(+), 32 deletions(-) create mode 100644 nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/GetUriEvaluator.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 734794f3be..02901c4b4e 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 @@ -91,6 +91,7 @@ fragment EXP : ('e'|'E') ('+'|'-')? ('0'..'9')+ ; TRUE : 'true'; FALSE : 'false'; +NULL : 'null'; // // FUNCTION NAMES @@ -111,6 +112,7 @@ UUID : 'UUID'; HOSTNAME : 'hostname'; // requires boolean arg: prefer FQDN NOW : 'now'; THREAD : 'thread'; +GET_URI : 'getUri'; // 0 arg functions 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 c71719b92c..56d900a69f 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 @@ -106,7 +106,7 @@ booleanFunctionRef : zeroArgBool | oneArgBool | multiArgBool; numberFunctionRef : zeroArgNum | oneArgNum | zeroOrTwoArgNum | oneOrTwoArgNum | zeroOrOneOrTwoArgNum; anyArg : WHOLE_NUMBER | DECIMAL | numberFunctionRef | STRING_LITERAL | zeroArgString | oneArgString | twoArgString | fiveArgString | booleanLiteral | zeroArgBool | oneArgBool | multiArgBool - | expression | parameterReference; + | expression | parameterReference | NULL; stringArg : STRING_LITERAL | zeroArgString | oneArgString | twoArgString | expression; functionRef : stringFunctionRef | booleanFunctionRef | numberFunctionRef; @@ -137,7 +137,8 @@ booleanLiteral : TRUE | FALSE; zeroArgStandaloneFunction : (IP | UUID | NOW | NEXT_INT | HOSTNAME | THREAD | RANDOM) LPAREN! RPAREN!; oneArgStandaloneFunction : ((TO_LITERAL | MATH | GET_STATE_VALUE)^ LPAREN! anyArg RPAREN!) | (HOSTNAME^ LPAREN! booleanLiteral RPAREN!); -standaloneFunction : zeroArgStandaloneFunction | oneArgStandaloneFunction; +sevenArgStandaloneFunction : GET_URI^ LPAREN! anyArg COMMA! anyArg COMMA! anyArg COMMA! anyArg COMMA! anyArg COMMA! anyArg COMMA! anyArg RPAREN!; +standaloneFunction : zeroArgStandaloneFunction | oneArgStandaloneFunction | sevenArgStandaloneFunction; attributeRefOrFunctionCall : (attributeRef | standaloneFunction | parameterReference); 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 05bc23a2ac..f5e1e74195 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 @@ -117,6 +117,7 @@ import org.apache.nifi.attribute.expression.language.evaluation.functions.ToStri import org.apache.nifi.attribute.expression.language.evaluation.functions.ToUpperEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.EvaluateELStringEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.TrimEvaluator; +import org.apache.nifi.attribute.expression.language.evaluation.functions.GetUriEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.UrlDecodeEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.UrlEncodeEvaluator; import org.apache.nifi.attribute.expression.language.evaluation.functions.UuidEvaluator; @@ -152,6 +153,8 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionLexer.TO_MICROS; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionLexer.TO_NANOS; @@ -187,6 +190,7 @@ import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpre import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.FROM_RADIX; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.GET_DELIMITED_FIELD; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.GET_STATE_VALUE; +import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.GET_URI; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.GREATER_THAN; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.GREATER_THAN_OR_EQUAL; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.HASH; @@ -217,6 +221,7 @@ import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpre import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.NOT; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.NOT_NULL; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.NOW; +import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.NULL; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.OR; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.PAD_LEFT; import static org.apache.nifi.attribute.expression.language.antlr.AttributeExpressionParser.PAD_RIGHT; @@ -1202,6 +1207,8 @@ public class ExpressionCompiler { case TRUE: case FALSE: return buildBooleanEvaluator(tree); + case NULL: + return newStringLiteralEvaluator(null); case UUID: { return addToken(new UuidEvaluator(), "uuid"); } @@ -1267,6 +1274,13 @@ public class ExpressionCompiler { evaluators.add(eval); return eval; } + case GET_URI: { + List> uriArgs = Stream.iterate(0, i -> i + 1) + .limit(tree.getChildCount()) + .map(num -> toStringEvaluator(buildEvaluator(tree.getChild(num)))) + .collect(Collectors.toList()); + return addToken(new GetUriEvaluator(uriArgs), "getUri"); + } default: throw new AttributeExpressionLanguageParsingException("Unexpected token: " + tree.toString()); } diff --git a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/GetUriEvaluator.java b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/GetUriEvaluator.java new file mode 100644 index 0000000000..011b498910 --- /dev/null +++ b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/functions/GetUriEvaluator.java @@ -0,0 +1,75 @@ +/* + * 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.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; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.stream.Collectors; + +public class GetUriEvaluator extends StringEvaluator { + + private final List> uriArgs; + + public GetUriEvaluator(List> uriArgs) { + this.uriArgs = uriArgs; + } + + @Override + public QueryResult evaluate(EvaluationContext evaluationContext) { + List args = uriArgs.stream() + .map(uriArg -> uriArg.evaluate(evaluationContext).getValue()) + .collect(Collectors.toList()); + + try { + if (args.size() == 7) { + final String scheme = args.get(0); + final String userInfo = args.get(1); + final String host = args.get(2); + final int port = getPort(args.get(3)); + final String path = args.get(4); + final String query = args.get(5); + final String fragment = args.get(6); + final URI uri = new URI(scheme, userInfo, host, port, path, query, fragment); + return new StringQueryResult(uri.toString()); + } + throw new AttributeExpressionLanguageException("Could not evaluate 'getUri' function with " + args.size() + " argument(s)"); + } catch (URISyntaxException use) { + throw new AttributeExpressionLanguageException("Could not evaluate 'getUri' function with argument(s) " + args, use); + } + } + + private int getPort(String portArg) { + try { + return Integer.parseInt(portArg); + } catch (NumberFormatException nfe) { + throw new AttributeExpressionLanguageException("Could not evaluate 'getUri' function with argument '" + + portArg + "' which is not a number", nfe); + } + } + @Override + public Evaluator getSubjectEvaluator() { + return null; + } +} diff --git a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/StringLiteralEvaluator.java b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/StringLiteralEvaluator.java index 410449b59a..c3fffde0fb 100644 --- a/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/StringLiteralEvaluator.java +++ b/nifi-commons/nifi-expression-language/src/main/java/org/apache/nifi/attribute/expression/language/evaluation/literals/StringLiteralEvaluator.java @@ -27,40 +27,44 @@ public class StringLiteralEvaluator extends StringEvaluator { private final String value; public StringLiteralEvaluator(final String value) { - // need to escape characters after backslashes - final StringBuilder sb = new StringBuilder(); - boolean lastCharIsBackslash = false; - for (int i = 0; i < value.length(); i++) { - final char c = value.charAt(i); + if(value == null) { + this.value = null; + } else { + // need to escape characters after backslashes + final StringBuilder sb = new StringBuilder(); + boolean lastCharIsBackslash = false; + for (int i = 0; i < value.length(); i++) { + final char c = value.charAt(i); - if (lastCharIsBackslash) { - switch (c) { - case 'n': - sb.append("\n"); - break; - case 'r': - sb.append("\r"); - break; - case '\\': - sb.append("\\"); - break; - case 't': - sb.append("\\t"); - break; - default: - sb.append("\\").append(c); - break; + if (lastCharIsBackslash) { + switch (c) { + case 'n': + sb.append("\n"); + break; + case 'r': + sb.append("\r"); + break; + case '\\': + sb.append("\\"); + break; + case 't': + sb.append("\\t"); + break; + default: + sb.append("\\").append(c); + break; + } + + lastCharIsBackslash = false; + } else if (c == '\\') { + lastCharIsBackslash = true; + } else { + sb.append(c); } - - lastCharIsBackslash = false; - } else if (c == '\\') { - lastCharIsBackslash = true; - } else { - sb.append(c); } - } - this.value = sb.toString(); + this.value = sb.toString(); + } } @Override 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 4db4f0a943..55d252f7a8 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 @@ -28,6 +28,7 @@ import org.apache.nifi.parameter.Parameter; import org.apache.nifi.parameter.ParameterDescriptor; import org.apache.nifi.parameter.ParameterLookup; import org.apache.nifi.registry.VariableRegistry; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -2398,6 +2399,27 @@ public class TestQuery { verifyEquals("${nullAttr:isJson()}", attributes, false); } + @Test + void testGetUri() { + verifyEquals("${getUri('https', 'admin:admin', 'nifi.apache.org', '1234', '/path/data ', 'key=value &key2=value2', 'frag1')}", + null, "https://admin:admin@nifi.apache.org:1234/path/data%20?key=value%20&key2=value2#frag1"); + verifyEquals("${getUri('https', null, 'nifi.apache.org', 8443, '/docs.html', null, null)}", + null, "https://nifi.apache.org:8443/docs.html"); + verifyEquals("${getUri('http', null, 'nifi.apache.org', -1, '/docs.html', null, null)}", + null, "http://nifi.apache.org/docs.html"); + + assertInvalid("${getUri()}"); + assertInvalid("${getUri('http://nifi.apache.org:1234/path/data?key=value&key2=value2#frag1')}"); + assertInvalid("${getUri('https', 'admin:admin')}"); + assertInvalid("${getUri('mailto', 'dev@nifi.apache.org', '')}"); + assertInvalid("${getUri('http', 'nifi.apache.org', '/path/data', 'frag1')}"); + assertInvalid("${getUri('https', 'admin:admin@nifi.apache.org:1234', '/path/data ', 'key=value&key2=value2', 'frag1')}"); + + AttributeExpressionLanguageException thrown = assertThrows(AttributeExpressionLanguageException.class, + () -> verifyEquals("${getUri('https', 'admin:admin', 'nifi.apache.org', 'notANumber', '/path/data ', 'key=value&key2=value2', 'frag1')}", null, "")); + Assertions.assertEquals("Could not evaluate 'getUri' function with argument 'notANumber' which is not a number", thrown.getMessage()); + } + 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 8fab2761b1..fd3dd94ef5 100644 --- a/nifi-docs/src/main/asciidoc/expression-language-guide.adoc +++ b/nifi-docs/src/main/asciidoc/expression-language-guide.adoc @@ -2616,6 +2616,47 @@ names begin with the letter `a`. + +[.function] +=== getUri + +*Description*: [.description]#Returns a URI compliant with RFC 2396. This includes encoding non-US-ASCII characters and +quoting illegal characters with octets. This expression utilizes the +link:https://docs.oracle.com/javase/8/docs/api/java/net/URI.html[java.net.URI^] class to build a URI. +As described in the API, a hierarchical URI consists of seven components represented with the following types:# +|============================================================================ +| Component | Type +| `scheme` | `String` +| `user-info` | `String` +| `host` | `String` +| `port` | `int` +| `path` | `String` +| `query` | `String` +| `fragment` | `String` +|============================================================================ + +See the API for more details on character categories, encoding and quoting. + +*Subject Type*: [.subjectless]#No Subject# + +*Arguments*: This expression takes seven arguments. + +- [.argName]#_scheme_#, [.argName]#_userinfo_#, [.argName]#_host_#, [.argName]#_port_#, [.argName]#_path_#, [.argName]#_query_#, [.argName]#_fragment_# + +*NOTE:* Any component of the new URI may be left undefined by passing [.argName]#_null_# for the corresponding parameter or, in the case of the port parameter, by passing [.argName]#_-1_#. + +*Return Type*: [.returnType]#String# + +*Examples* +The following examples detail how this expression can be invoked: + +|============================================================================ +| Expression | Value +|`${getUri('https', 'admin:admin', 'nifi.apache.org', '1234', '/path/data ', 'key=value &key2=value2', 'frag1')}` | `https://admin:admin@nifi.apache.org:1234/path/data%20?key=value%20&key2=value2#frag1` +|`${getUri('https', null, 'nifi.apache.org', 8443, '/docs.html', null, null)}` | `https://nifi.apache.org:8443/docs.html` +|`${getUri('http', null, 'nifi.apache.org', -1, '/docs.html', null, null)}` | `http://nifi.apache.org/docs.html` +|============================================================================ + [[multi]] == Evaluating Multiple Attributes