This is an automated email from the ASF dual-hosted git repository. chriss pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push: new 2ead9e5494 NIFI-10970: Added a count RecordPath function 2ead9e5494 is described below commit 2ead9e54947a56f59faead0e8a5f324056d47171 Author: Mark Payne <marka...@hotmail.com> AuthorDate: Mon Dec 12 14:43:41 2022 -0500 NIFI-10970: Added a count RecordPath function Signed-off-by: Chris Sampson <chris.sampso...@gmail.com> This closes #6778 --- .../apache/nifi/record/path/functions/Count.java | 45 +++++++++++ .../nifi/record/path/paths/RecordPathCompiler.java | 5 ++ .../apache/nifi/record/path/TestRecordPath.java | 91 +++++++++++++++------- nifi-docs/src/main/asciidoc/record-path-guide.adoc | 31 ++++++++ 4 files changed, 142 insertions(+), 30 deletions(-) diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Count.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Count.java new file mode 100644 index 0000000000..2ed6825345 --- /dev/null +++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/functions/Count.java @@ -0,0 +1,45 @@ +/* + * 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.record.path.functions; + +import org.apache.nifi.record.path.FieldValue; +import org.apache.nifi.record.path.RecordPathEvaluationContext; +import org.apache.nifi.record.path.StandardFieldValue; +import org.apache.nifi.record.path.paths.RecordPathSegment; +import org.apache.nifi.serialization.record.RecordField; +import org.apache.nifi.serialization.record.RecordFieldType; + +import java.util.stream.Stream; + +public class Count extends RecordPathSegment { + private final RecordPathSegment recordPath; + + public Count(final RecordPathSegment recordPath, final boolean absolute) { + super("count", null, absolute); + this.recordPath = recordPath; + } + + @Override + public Stream<FieldValue> evaluate(final RecordPathEvaluationContext context) { + final Stream<FieldValue> stream = recordPath.evaluate(context); + final long count = stream.count(); + final RecordField recordField = new RecordField("count", RecordFieldType.LONG.getDataType()); + final FieldValue fieldValue = new StandardFieldValue(count, recordField, null); + return Stream.of(fieldValue); + } +} diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java index d966382569..7ae5b45dbc 100644 --- a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java +++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/RecordPathCompiler.java @@ -39,6 +39,7 @@ import org.apache.nifi.record.path.functions.Base64Decode; import org.apache.nifi.record.path.functions.Base64Encode; import org.apache.nifi.record.path.functions.Coalesce; import org.apache.nifi.record.path.functions.Concat; +import org.apache.nifi.record.path.functions.Count; import org.apache.nifi.record.path.functions.EscapeJson; import org.apache.nifi.record.path.functions.FieldName; import org.apache.nifi.record.path.functions.FilterFunction; @@ -377,6 +378,10 @@ public class RecordPathCompiler { return new Coalesce(argPaths, absolute); } + case "count": { + final RecordPathSegment[] args = getArgPaths(argumentListTree, 1, functionName, absolute); + return new Count(args[0], absolute); + } case "not": case "contains": case "containsRegex": diff --git a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java index db3b13b786..41596e443d 100644 --- a/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java +++ b/nifi-commons/nifi-record-path/src/test/java/org/apache/nifi/record/path/TestRecordPath.java @@ -53,12 +53,12 @@ import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; public class TestRecordPath { @@ -1094,23 +1094,20 @@ public class TestRecordPath { // Special character cases values.put("name", "John Doe"); - assertEquals("Replacing whitespace to new line", + assertEquals( "John\nDoe", RecordPath.compile("replaceRegex(/name, '[\\s]', '\\n')") .evaluate(record).getSelectedFields().findFirst().get().getValue()); values.put("name", "John\nDoe"); - assertEquals("Replacing new line to whitespace", - "John Doe", RecordPath.compile("replaceRegex(/name, '\\n', ' ')") + assertEquals("John Doe", RecordPath.compile("replaceRegex(/name, '\\n', ' ')") .evaluate(record).getSelectedFields().findFirst().get().getValue()); values.put("name", "John Doe"); - assertEquals("Replacing whitespace to tab", - "John\tDoe", RecordPath.compile("replaceRegex(/name, '[\\s]', '\\t')") + assertEquals("John\tDoe", RecordPath.compile("replaceRegex(/name, '[\\s]', '\\t')") .evaluate(record).getSelectedFields().findFirst().get().getValue()); values.put("name", "John\tDoe"); - assertEquals("Replacing tab to whitespace", - "John Doe", RecordPath.compile("replaceRegex(/name, '\\t', ' ')") + assertEquals("John Doe", RecordPath.compile("replaceRegex(/name, '\\t', ' ')") .evaluate(record).getSelectedFields().findFirst().get().getValue()); } @@ -1132,23 +1129,19 @@ public class TestRecordPath { // NOTE: At Java code, a single back-slash needs to be escaped with another-back slash, but needn't to do so at NiFi UI. // The test record path is equivalent to replaceRegex(/name, '\'', '"') values.put("name", "'John' 'Doe'"); - assertEquals("Replacing quote to double-quote", - "\"John\" \"Doe\"", RecordPath.compile("replaceRegex(/name, '\\'', '\"')") + assertEquals("\"John\" \"Doe\"", RecordPath.compile("replaceRegex(/name, '\\'', '\"')") .evaluate(record).getSelectedFields().findFirst().get().getValue()); values.put("name", "\"John\" \"Doe\""); - assertEquals("Replacing double-quote to single-quote", - "'John' 'Doe'", RecordPath.compile("replaceRegex(/name, '\"', '\\'')") + assertEquals("'John' 'Doe'", RecordPath.compile("replaceRegex(/name, '\"', '\\'')") .evaluate(record).getSelectedFields().findFirst().get().getValue()); values.put("name", "'John' 'Doe'"); - assertEquals("Replacing quote to double-quote, the function arguments are wrapped by double-quote", - "\"John\" \"Doe\"", RecordPath.compile("replaceRegex(/name, \"'\", \"\\\"\")") + assertEquals("\"John\" \"Doe\"", RecordPath.compile("replaceRegex(/name, \"'\", \"\\\"\")") .evaluate(record).getSelectedFields().findFirst().get().getValue()); values.put("name", "\"John\" \"Doe\""); - assertEquals("Replacing double-quote to single-quote, the function arguments are wrapped by double-quote", - "'John' 'Doe'", RecordPath.compile("replaceRegex(/name, \"\\\"\", \"'\")") + assertEquals("'John' 'Doe'", RecordPath.compile("replaceRegex(/name, \"\\\"\", \"'\")") .evaluate(record).getSelectedFields().findFirst().get().getValue()); } @@ -1170,13 +1163,11 @@ public class TestRecordPath { // NOTE: At Java code, a single back-slash needs to be escaped with another-back slash, but needn't to do so at NiFi UI. // The test record path is equivalent to replaceRegex(/name, '\\', '/') values.put("name", "John\\Doe"); - assertEquals("Replacing a back-slash to forward-slash", - "John/Doe", RecordPath.compile("replaceRegex(/name, '\\\\', '/')") + assertEquals("John/Doe", RecordPath.compile("replaceRegex(/name, '\\\\', '/')") .evaluate(record).getSelectedFields().findFirst().get().getValue()); values.put("name", "John/Doe"); - assertEquals("Replacing a forward-slash to back-slash", - "John\\Doe", RecordPath.compile("replaceRegex(/name, '/', '\\\\')") + assertEquals("John\\Doe", RecordPath.compile("replaceRegex(/name, '/', '\\\\')") .evaluate(record).getSelectedFields().findFirst().get().getValue()); } @@ -1196,13 +1187,11 @@ public class TestRecordPath { // Brackets values.put("name", "J[o]hn Do[e]"); - assertEquals("Square brackets can be escaped with back-slash", - "J(o)hn Do(e)", RecordPath.compile("replaceRegex(replaceRegex(/name, '\\[', '('), '\\]', ')')") + assertEquals("J(o)hn Do(e)", RecordPath.compile("replaceRegex(replaceRegex(/name, '\\[', '('), '\\]', ')')") .evaluate(record).getSelectedFields().findFirst().get().getValue()); values.put("name", "J(o)hn Do(e)"); - assertEquals("Brackets can be escaped with back-slash", - "J[o]hn Do[e]", RecordPath.compile("replaceRegex(replaceRegex(/name, '\\(', '['), '\\)', ']')") + assertEquals("J[o]hn Do[e]", RecordPath.compile("replaceRegex(replaceRegex(/name, '\\(', '['), '\\)', ']')") .evaluate(record).getSelectedFields().findFirst().get().getValue()); } @@ -1870,7 +1859,7 @@ public class TestRecordPath { assertEquals("MyString", RecordPath.compile("padRight(/someString, 3)").evaluate(record).getSelectedFields().findFirst().get().getValue()); assertEquals("MyString", RecordPath.compile("padRight(/someString, -10)").evaluate(record).getSelectedFields().findFirst().get().getValue()); assertEquals("@@@@@@@@@@", RecordPath.compile("padRight(/emptyString, 10, '@')").evaluate(record).getSelectedFields().findFirst().get().getValue()); - assertNull(null, RecordPath.compile("padRight(/nullString, 10, '@')").evaluate(record).getSelectedFields().findFirst().get().getValue()); + assertNull(RecordPath.compile("padRight(/nullString, 10, '@')").evaluate(record).getSelectedFields().findFirst().get().getValue()); assertEquals("MyStringxy", RecordPath.compile("padRight(/someString, 10, \"xy\")").evaluate(record).getSelectedFields().findFirst().get().getValue()); assertEquals("MyStringaV", RecordPath.compile("padRight(/someString, 10, \"aVeryLongPadding\")").evaluate(record).getSelectedFields().findFirst().get().getValue()); assertEquals("MyStringfewfewfewfew", RecordPath.compile("padRight(/someString, 20, \"few\")").evaluate(record).getSelectedFields().findFirst().get().getValue()); @@ -1940,6 +1929,48 @@ public class TestRecordPath { assertEquals(Boolean.FALSE, RecordPath.compile("not(/id = 48)").evaluate(record).getSelectedFields().findFirst().get().getValue()); } + @Test + public void testCountArrayElements() { + final RecordSchema schema = new SimpleRecordSchema(getDefaultFields()); + + final Map<String, Object> values = new HashMap<>(); + values.put("id", 48); + values.put("numbers", new Object[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); + final Record record = new MapRecord(schema, values); + + final List<FieldValue> fieldValues = RecordPath.compile("count(/numbers[*])").evaluate(record).getSelectedFields().collect(Collectors.toList()); + assertEquals(1, fieldValues.size()); + assertEquals(10L, fieldValues.get(0).getValue()); + } + + @Test + public void testCountComparison() { + final RecordSchema schema = new SimpleRecordSchema(getDefaultFields()); + + final Map<String, Object> values = new HashMap<>(); + values.put("id", 48); + values.put("numbers", new Object[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); + final Record record = new MapRecord(schema, values); + + final List<FieldValue> fieldValues = RecordPath.compile("count(/numbers[*]) > 9").evaluate(record).getSelectedFields().collect(Collectors.toList()); + assertEquals(1, fieldValues.size()); + assertEquals(true, fieldValues.get(0).getValue()); + } + + @Test + public void testCountAsFilter() { + final RecordSchema schema = new SimpleRecordSchema(getDefaultFields()); + + final Map<String, Object> values = new HashMap<>(); + values.put("id", 48); + values.put("numbers", new Object[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); + final Record record = new MapRecord(schema, values); + + final List<FieldValue> fieldValues = RecordPath.compile("/id[count(/numbers[*]) > 2]").evaluate(record).getSelectedFields().collect(Collectors.toList()); + assertEquals(1, fieldValues.size()); + assertEquals(48, fieldValues.get(0).getValue()); + } + private List<RecordField> getDefaultFields() { final List<RecordField> fields = new ArrayList<>(); fields.add(new RecordField("id", RecordFieldType.INT.getDataType())); diff --git a/nifi-docs/src/main/asciidoc/record-path-guide.adoc b/nifi-docs/src/main/asciidoc/record-path-guide.adoc index b59f78ade6..17fd585fba 100644 --- a/nifi-docs/src/main/asciidoc/record-path-guide.adoc +++ b/nifi-docs/src/main/asciidoc/record-path-guide.adoc @@ -1071,6 +1071,37 @@ take the value of the supplied record path and use it as the namespace. Please note that the namespace must always be a valid UUID string. An empty string, another data type, etc. will result in an error. This is by design because the most common use case for UUID v5 is to uniquely identify records across data sets. + +=== count + +Returns the count of the number of elements. This is commonly used in conjunction with arrays. For example, if we have the following record: + +---- +{ + "id": "1234", + "elements": [{ + "name": "book", + "color": "red" + }, { + "name": "computer", + "color": "black" + }] +} +---- + +We could determine the number of `elements` by using the `count` function. Using: + +---- +count(/elements[*]) +---- + +Would yield a value of `2`. We could also use this as a filter, such as: +---- +/id[ count(/elements[*]) = 2 ] +---- +Which will return the `id` element with a value of `1234`. + + [[filter_functions]] == Filter Functions