NIFI-10754 Added getUri NIFI Expression Language function

This closes #6689

Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
dan-s1 2022-11-18 20:19:44 +00:00 committed by exceptionfactory
parent b69721cac9
commit 3a60013876
No known key found for this signature in database
GPG Key ID: 29B6A52D2AAE8DBA
7 changed files with 191 additions and 32 deletions

View File

@ -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

View File

@ -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);

View File

@ -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<Evaluator<String>> 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());
}

View File

@ -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<Evaluator<String>> uriArgs;
public GetUriEvaluator(List<Evaluator<String>> uriArgs) {
this.uriArgs = uriArgs;
}
@Override
public QueryResult<String> evaluate(EvaluationContext evaluationContext) {
List<String> 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;
}
}

View File

@ -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

View File

@ -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<String, String> attributes, final Object expectedResult) {
verifyEquals(expression,attributes, null, ParameterLookup.EMPTY, expectedResult);
}

View File

@ -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