You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by jo...@apache.org on 2023/10/13 21:03:12 UTC
[nifi] branch main updated: NIFI-12124: This closes #7791. Added a new RenameRecordField processor. In testing, also noticed that the Descendant Wildcard operator (//*) and Descendant Field Path Operator (//name, for instance) did not properly account for array of records or map elements, so addressed those concerns.
This is an automated email from the ASF dual-hosted git repository.
joewitt 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 3ae0eedee6 NIFI-12124: This closes #7791. Added a new RenameRecordField processor. In testing, also noticed that the Descendant Wildcard operator (//*) and Descendant Field Path Operator (//name, for instance) did not properly account for array of records or map elements, so addressed those concerns.
3ae0eedee6 is described below
commit 3ae0eedee6ae67c48dd59f68752fa728e4355c41
Author: Mark Payne <ma...@hotmail.com>
AuthorDate: Mon Sep 25 13:19:30 2023 -0400
NIFI-12124: This closes #7791. Added a new RenameRecordField processor. In testing, also noticed that the Descendant Wildcard operator (//*) and Descendant Field Path Operator (//name, for instance) did not properly account for array of records or map elements, so addressed those concerns.
Signed-off-by: Joseph Witt <jo...@apache.org>
---
.github/workflows/ci-workflow.yml | 3 +
.../record/path/paths/DescendantFieldPath.java | 26 ++-
.../record/path/paths/WildcardDescendantPath.java | 26 ++-
.../org/apache/nifi/record/path/util/Filters.java | 38 +++-
.../apache/nifi/record/path/TestRecordPath.java | 76 ++++++--
.../nifi/serialization/SimpleRecordSchema.java | 70 +++++--
.../nifi/serialization/record/MapRecord.java | 28 +++
.../apache/nifi/serialization/record/Record.java | 14 +-
.../nifi/serialization/record/RecordSchema.java | 10 +
.../org/apache/nifi/accumulo/data/KeySchema.java | 14 +-
.../processors/standard/RenameRecordField.java | 209 +++++++++++++++++++++
.../services/org.apache.nifi.processor.Processor | 1 +
.../processors/standard/TestRenameRecordField.java | 182 ++++++++++++++++++
.../input/complex-person.json | 12 ++
.../TestRenameRecordField/input/simple-person.json | 5 +
.../output/testNestedPath.json | 12 ++
.../testRecursivelyReferencingAllFields.json | 12 ++
.../testRecursivelyReferencingFieldName.json | 12 ++
.../output/testReferencingFieldName.json | 5 +
.../output/testRenameArray.json | 12 ++
.../output/testRenameFieldStaticValue.json | 5 +
.../output/testRenameFieldUsingAttribute.json | 5 +
.../output/testRenameMultipleFields.json | 5 +
23 files changed, 740 insertions(+), 42 deletions(-)
diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml
index 3f9ee8a7f5..439fdff8b6 100644
--- a/.github/workflows/ci-workflow.yml
+++ b/.github/workflows/ci-workflow.yml
@@ -172,6 +172,7 @@ jobs:
if: failure()
- name: Post Disk Usage
run: df
+ if: ${{ always() }}
macos-build-jp:
timeout-minutes: 150
@@ -235,6 +236,7 @@ jobs:
if: failure()
- name: Post Disk Usage
run: df
+ if: ${{ always() }}
windows-build:
timeout-minutes: 150
@@ -300,3 +302,4 @@ jobs:
if: failure()
- name: Post Disk Usage
run: df
+ if: ${{ always() }}
diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/DescendantFieldPath.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/DescendantFieldPath.java
index 8946bc102c..60852f16ac 100644
--- a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/DescendantFieldPath.java
+++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/DescendantFieldPath.java
@@ -20,14 +20,18 @@ package org.apache.nifi.record.path.paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.stream.Stream;
-
import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.MapEntryFieldValue;
import org.apache.nifi.record.path.RecordPathEvaluationContext;
import org.apache.nifi.record.path.StandardFieldValue;
import org.apache.nifi.record.path.util.Filters;
+import org.apache.nifi.serialization.record.DataType;
import org.apache.nifi.serialization.record.Record;
import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.MapDataType;
public class DescendantFieldPath extends RecordPathSegment {
private final String descendantName;
@@ -74,6 +78,26 @@ public class DescendantFieldPath extends RecordPathSegment {
if (Filters.isRecord(childField.getDataType(), recordValue)) {
final FieldValue childFieldValue = new StandardFieldValue(recordValue, childField, fieldValue);
matchingValues.addAll(findDescendants(childFieldValue));
+ } else if (Filters.isRecordArray(childField.getDataType(), recordValue)) {
+ final Object[] arrayValues = (Object[]) recordValue;
+
+ for (final Object arrayValue : arrayValues) {
+ final FieldValue childFieldValue = new StandardFieldValue(arrayValue, childField, fieldValue);
+ matchingValues.addAll(findDescendants(childFieldValue));
+ }
+ } else if (childField.getDataType().getFieldType() == RecordFieldType.MAP) {
+ final Map<String, ?> map = (Map<String, ?>) recordValue;
+
+ final DataType valueType = ((MapDataType) childField.getDataType()).getValueType();
+
+ for (final Map.Entry<String, ?> entry : map.entrySet()) {
+ final String mapKey = entry.getKey();
+ final Object mapValue = entry.getValue();
+ final RecordField elementField = new RecordField(fieldValue.getField().getFieldName(), valueType);
+ final FieldValue mapFieldValue = new MapEntryFieldValue(mapValue, elementField, fieldValue, mapKey);
+
+ matchingValues.add(mapFieldValue);
+ }
}
}
diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/WildcardDescendantPath.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/WildcardDescendantPath.java
index 3bfc0e4ce0..0e939e716d 100644
--- a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/WildcardDescendantPath.java
+++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/paths/WildcardDescendantPath.java
@@ -20,14 +20,18 @@ package org.apache.nifi.record.path.paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.stream.Stream;
-
import org.apache.nifi.record.path.FieldValue;
+import org.apache.nifi.record.path.MapEntryFieldValue;
import org.apache.nifi.record.path.RecordPathEvaluationContext;
import org.apache.nifi.record.path.StandardFieldValue;
import org.apache.nifi.record.path.util.Filters;
+import org.apache.nifi.serialization.record.DataType;
import org.apache.nifi.serialization.record.Record;
import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.MapDataType;
public class WildcardDescendantPath extends RecordPathSegment {
@@ -66,6 +70,26 @@ public class WildcardDescendantPath extends RecordPathSegment {
if (Filters.isRecord(childField.getDataType(), value)) {
final FieldValue childFieldValue = new StandardFieldValue(value, childField, fieldValue);
matchingValues.addAll(findDescendants(childFieldValue));
+ } else if (Filters.isRecordArray(childField.getDataType(), value)) {
+ final Object[] arrayValues = (Object[]) value;
+
+ for (final Object arrayValue : arrayValues) {
+ final FieldValue childFieldValue = new StandardFieldValue(arrayValue, childField, fieldValue);
+ matchingValues.addAll(findDescendants(childFieldValue));
+ }
+ } else if (childField.getDataType().getFieldType() == RecordFieldType.MAP) {
+ final Map<String, ?> map = (Map<String, ?>) value;
+
+ final DataType valueType = ((MapDataType) childField.getDataType()).getValueType();
+
+ for (final Map.Entry<String, ?> entry : map.entrySet()) {
+ final String mapKey = entry.getKey();
+ final Object mapValue = entry.getValue();
+ final RecordField elementField = new RecordField(fieldValue.getField().getFieldName(), valueType);
+ final FieldValue mapFieldValue = new MapEntryFieldValue(mapValue, elementField, fieldValue, mapKey);
+
+ matchingValues.add(mapFieldValue);
+ }
}
}
diff --git a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/util/Filters.java b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/util/Filters.java
index 4800d59419..3b1168b62f 100644
--- a/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/util/Filters.java
+++ b/nifi-commons/nifi-record-path/src/main/java/org/apache/nifi/record/path/util/Filters.java
@@ -17,14 +17,15 @@
package org.apache.nifi.record.path.util;
+import java.lang.reflect.Array;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
-
import org.apache.nifi.record.path.FieldValue;
import org.apache.nifi.serialization.record.DataType;
import org.apache.nifi.serialization.record.Record;
import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.type.ArrayDataType;
public class Filters {
@@ -71,4 +72,39 @@ public class Filters {
return false;
}
+
+ public static boolean isRecordArray(final DataType dataType, final Object value) {
+ if (dataType.getFieldType() != RecordFieldType.ARRAY) {
+ return false;
+ }
+
+ final ArrayDataType arrayDataType = (ArrayDataType) dataType;
+ final DataType elementType = arrayDataType.getElementType();
+
+ if (elementType != null && elementType.getFieldType() == RecordFieldType.RECORD) {
+ return true;
+ }
+
+ if (value == null) {
+ return false;
+ }
+
+ if (!value.getClass().isArray()) {
+ return false;
+ }
+
+ final int length = Array.getLength(value);
+ if (length == 0) {
+ return false;
+ }
+
+ for (int i = 0; i < length; i++) {
+ final Object val = Array.get(value, i);
+ if (!(val instanceof Record)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
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 e27c61d0fc..dfa6449105 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
@@ -17,21 +17,6 @@
package org.apache.nifi.record.path;
-import org.apache.nifi.record.path.exception.RecordPathException;
-import org.apache.nifi.serialization.SimpleRecordSchema;
-import org.apache.nifi.serialization.record.DataType;
-import org.apache.nifi.serialization.record.MapRecord;
-import org.apache.nifi.serialization.record.Record;
-import org.apache.nifi.serialization.record.RecordField;
-import org.apache.nifi.serialization.record.RecordFieldType;
-import org.apache.nifi.serialization.record.RecordSchema;
-import org.apache.nifi.serialization.record.type.ArrayDataType;
-import org.apache.nifi.serialization.record.util.DataTypeUtils;
-import org.apache.nifi.uuid5.Uuid5Util;
-import org.junit.jupiter.api.AfterAll;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.StandardCharsets;
import java.sql.Date;
@@ -52,6 +37,20 @@ import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
+import org.apache.nifi.record.path.exception.RecordPathException;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.MapRecord;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.RecordSchema;
+import org.apache.nifi.serialization.record.type.ArrayDataType;
+import org.apache.nifi.serialization.record.util.DataTypeUtils;
+import org.apache.nifi.uuid5.Uuid5Util;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -1364,6 +1363,7 @@ public class TestRecordPath {
assertEquals("John Smith", results.get(0).getValue());
assertEquals("Jane Smith", results.get(1).getValue());
}
+
@Test
public void testFieldName() {
final List<RecordField> fields = new ArrayList<>();
@@ -1382,6 +1382,52 @@ public class TestRecordPath {
assertEquals(0L, RecordPath.compile("//name[not(startsWith(fieldName(.), 'n'))]").evaluate(record).getSelectedFields().count());
}
+ @Test
+ public void testRecursiveWithMap() {
+ final List<RecordField> fields = new ArrayList<>();
+ fields.add(new RecordField("map", RecordFieldType.MAP.getMapDataType(RecordFieldType.STRING.getDataType())));
+ final RecordSchema schema = new SimpleRecordSchema(fields);
+
+ final Map<String, String> mapValues = new HashMap<>();
+ mapValues.put("a", "z");
+ mapValues.put("b", "Y");
+ mapValues.put("c", "x");
+
+ final Map<String, Object> values = new HashMap<>();
+ values.put("map", mapValues);
+
+ final Record record = new MapRecord(schema, values);
+ assertEquals("Y", RecordPath.compile("//*[. = toUpperCase(.)]").evaluate(record).getSelectedFields().findFirst().get().getValue());
+ }
+
+ @Test
+ public void testRecursiveWithChoiceThatIncludesRecord() {
+ final RecordSchema personSchema = new SimpleRecordSchema(Arrays.asList(
+ new RecordField("name", RecordFieldType.STRING.getDataType()),
+ new RecordField("age", RecordFieldType.INT.getDataType())
+ ));
+
+ final DataType personDataType = RecordFieldType.RECORD.getRecordDataType(personSchema);
+ final DataType stringDataType = RecordFieldType.STRING.getDataType();
+
+ final List<RecordField> fields = new ArrayList<>();
+ fields.add(new RecordField("person", RecordFieldType.CHOICE.getChoiceDataType(stringDataType, personDataType)));
+ final RecordSchema schema = new SimpleRecordSchema(fields);
+
+ final Map<String, Object> personValueMap = new HashMap<>();
+ personValueMap.put("name", "John Doe");
+ personValueMap.put("age", 30);
+ final Record personRecord = new MapRecord(personSchema, personValueMap);
+
+ final Map<String, Object> values = new HashMap<>();
+ values.put("person", personRecord);
+
+ final Record record = new MapRecord(schema, values);
+ final List<Object> expectedValues = List.of(personRecord, "John Doe", 30);
+ assertEquals(expectedValues, RecordPath.compile("//*").evaluate(record).getSelectedFields().map(FieldValue::getValue).toList());
+ }
+
+
@Test
public void testToDateFromString() {
final List<RecordField> fields = new ArrayList<>();
diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/SimpleRecordSchema.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/SimpleRecordSchema.java
index 88f31c6cc0..d6b9479140 100644
--- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/SimpleRecordSchema.java
+++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/SimpleRecordSchema.java
@@ -17,12 +17,6 @@
package org.apache.nifi.serialization;
-import org.apache.nifi.serialization.record.DataType;
-import org.apache.nifi.serialization.record.RecordField;
-import org.apache.nifi.serialization.record.RecordFieldRemovalPath;
-import org.apache.nifi.serialization.record.RecordSchema;
-import org.apache.nifi.serialization.record.SchemaIdentifier;
-
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -31,6 +25,11 @@ import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldRemovalPath;
+import org.apache.nifi.serialization.record.RecordSchema;
+import org.apache.nifi.serialization.record.SchemaIdentifier;
public class SimpleRecordSchema implements RecordSchema {
private List<RecordField> fields = null;
@@ -188,12 +187,17 @@ public class SimpleRecordSchema implements RecordSchema {
public int hashCode() {
int computed = this.hashCode;
if (computed == 0) {
- computed = this.hashCode = 143 + 3 * fields.hashCode();
+ computed = this.hashCode = calculateHashCode();
}
return computed;
}
+ private int calculateHashCode() {
+ return 143 + 3 * fields.hashCode();
+ }
+
+
private static String createText(final List<RecordField> fields) {
final StringBuilder sb = new StringBuilder("[");
@@ -264,18 +268,15 @@ public class SimpleRecordSchema implements RecordSchema {
@Override
public void removeField(final String fieldName) {
- final List<RecordField> remainingFields = fields.stream()
- .filter(field -> !field.getFieldName().equals(fieldName)).collect(Collectors.toList());
+ final List<RecordField> remainingFields = new ArrayList<>();
+ for (final RecordField field : fields) {
+ if (!field.getFieldName().equals(fieldName)) {
+ remainingFields.add(field);
+ }
+ }
if (remainingFields.size() != fields.size()) {
- fields = null;
- setFields(remainingFields);
- text.set(createText(fields));
- textAvailable = true;
- schemaFormat = null;
- schemaIdentifier = SchemaIdentifier.EMPTY;
- hashCode = 0; // set to 0 to trigger re-calculation
- hashCode = hashCode();
+ resetFields(remainingFields);
}
}
@@ -288,6 +289,41 @@ public class SimpleRecordSchema implements RecordSchema {
}
}
+
+ @Override
+ public boolean renameField(final String currentName, final String newName) {
+ final List<RecordField> updatedFields = new ArrayList<>(fields.size());
+
+ boolean renamed = false;
+ for (final RecordField recordField : fields) {
+ if (recordField.getFieldName().equals(currentName)) {
+ final RecordField updated = new RecordField(newName, recordField.getDataType(), recordField.getDefaultValue(), recordField.getAliases(), recordField.isNullable());
+ updatedFields.add(updated);
+ renamed = true;
+ } else {
+ updatedFields.add(recordField);
+ }
+ }
+
+ if (!renamed) {
+ return false;
+ }
+
+ resetFields(updatedFields);
+ return true;
+ }
+
+ private void resetFields(final List<RecordField> updatedFields) {
+ this.fields = null;
+ setFields(updatedFields);
+ textAvailable = false;
+ text.set(null);
+ schemaFormat = null;
+ schemaIdentifier = SchemaIdentifier.EMPTY;
+ hashCode = calculateHashCode();
+ }
+
+
@Override
public boolean isRecursive() {
return getFields().stream().anyMatch(field -> field.getDataType().isRecursive(Collections.singletonList(this)));
diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/MapRecord.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/MapRecord.java
index cf48c0e06a..e26c6bce22 100644
--- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/MapRecord.java
+++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/MapRecord.java
@@ -25,6 +25,8 @@ import org.apache.nifi.serialization.record.type.MapDataType;
import org.apache.nifi.serialization.record.type.RecordDataType;
import org.apache.nifi.serialization.record.util.DataTypeUtils;
import org.apache.nifi.serialization.record.util.IllegalTypeConversionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
@@ -42,6 +44,8 @@ import java.util.Set;
import java.util.function.Supplier;
public class MapRecord implements Record {
+ private static final Logger logger = LoggerFactory.getLogger(MapRecord.class);
+
private RecordSchema schema;
private final Map<String, Object> values;
private Optional<SerializedForm> serializedForm;
@@ -406,6 +410,30 @@ public class MapRecord implements Record {
existingField.ifPresent(recordField -> values.remove(recordField.getFieldName()));
}
+ @Override
+ public boolean rename(final RecordField field, final String newName) {
+ final Optional<RecordField> resolvedField = resolveField(field);
+ if (resolvedField.isEmpty()) {
+ logger.debug("Could not rename {} to {} because the field could not be resolved to any field in the schema", field, newName);
+ return false;
+ }
+
+ // If the new name already exists in the schema, and there's already a value, do not rename.
+ if (schema.getField(newName).isPresent()) {
+ throw new IllegalArgumentException("Could not rename [" + field + "] to [" + newName + "] because a field already exists with the name [" + newName + "]");
+ }
+
+ final String currentName = resolvedField.get().getFieldName();
+ final boolean renamed = schema.renameField(currentName, newName);
+ if (!renamed) {
+ return false;
+ }
+
+ final Object currentValue = values.remove(currentName);
+ values.put(newName, currentValue);
+ return true;
+ }
+
@Override
public void regenerateSchema() {
final List<RecordField> schemaFields = new ArrayList<>(schema.getFieldCount());
diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/Record.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/Record.java
index 053417864b..8aebe8275e 100644
--- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/Record.java
+++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/Record.java
@@ -17,12 +17,11 @@
package org.apache.nifi.serialization.record;
-import org.apache.nifi.serialization.record.util.IllegalTypeConversionException;
-
import java.util.Date;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import org.apache.nifi.serialization.record.util.IllegalTypeConversionException;
public interface Record {
@@ -152,6 +151,17 @@ public interface Record {
*/
void remove(RecordField field);
+ /**
+ * Renames the given field to the new name
+ *
+ * @param field the RecordField to update
+ * @param newName the new name for the field
+ * @return <code>true</code> if the field was renamed, <code>false</code> if the field could not be found
+ * @throws IllegalArgumentException if unable to rename field due to a naming conflict, such as the new name already existing in the schema
+ */
+ boolean rename(RecordField field, String newName);
+
+
/**
* Creates a new schema for the Record based on the Record's field types.
* In case any of the Record's fields were changed, this method propagates the changes to the parent Record.
diff --git a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/RecordSchema.java b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/RecordSchema.java
index b2512afdd2..18b6b4d718 100644
--- a/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/RecordSchema.java
+++ b/nifi-commons/nifi-record/src/main/java/org/apache/nifi/serialization/record/RecordSchema.java
@@ -97,6 +97,16 @@ public interface RecordSchema {
*/
void removePath(RecordFieldRemovalPath path);
+ /**
+ * Renames the field that corresponds to the given RecordField so that its new name is equal to the given name
+ *
+ * @param currentName the current name of the field
+ * @param newName the new name for the field
+ * @return <code>true</code> if the field is renamed, <code>false</code> if the field with the given current name cannot be found
+ * @throws IllegalArgumentException if unable to rename the field due to a naming conflict
+ */
+ boolean renameField(String currentName, String newName);
+
/**
* @return true if the schema contains itself as a nested field type, false if it does not
*/
diff --git a/nifi-nar-bundles/nifi-accumulo-bundle/nifi-accumulo-processors/src/main/java/org/apache/nifi/accumulo/data/KeySchema.java b/nifi-nar-bundles/nifi-accumulo-bundle/nifi-accumulo-processors/src/main/java/org/apache/nifi/accumulo/data/KeySchema.java
index 08800f6879..adb7da8d4f 100644
--- a/nifi-nar-bundles/nifi-accumulo-bundle/nifi-accumulo-processors/src/main/java/org/apache/nifi/accumulo/data/KeySchema.java
+++ b/nifi-nar-bundles/nifi-accumulo-bundle/nifi-accumulo-processors/src/main/java/org/apache/nifi/accumulo/data/KeySchema.java
@@ -18,6 +18,10 @@
package org.apache.nifi.accumulo.data;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.nifi.serialization.record.DataType;
import org.apache.nifi.serialization.record.RecordField;
@@ -26,11 +30,6 @@ import org.apache.nifi.serialization.record.RecordFieldType;
import org.apache.nifi.serialization.record.RecordSchema;
import org.apache.nifi.serialization.record.SchemaIdentifier;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
public class KeySchema implements RecordSchema {
private static final List<RecordField> KEY_FIELDS = new ArrayList<>();
@@ -125,6 +124,11 @@ public class KeySchema implements RecordSchema {
throw new NotImplementedException("Path removal from Accumulo KeySchema is not implemented.");
}
+ @Override
+ public boolean renameField(final String currentName, final String newName) {
+ throw new NotImplementedException("Field renaming from Accumulo KeySchema is not implemented.");
+ }
+
@Override
public boolean isRecursive() {
throw new NotImplementedException("Determining if an Accumulo KeySchema is recursive is not implemented.");
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/RenameRecordField.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/RenameRecordField.java
new file mode 100644
index 0000000000..c1dd5ab339
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/RenameRecordField.java
@@ -0,0 +1,209 @@
+/*
+ * 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.processors.standard;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.nifi.annotation.behavior.DynamicProperty;
+import org.apache.nifi.annotation.behavior.InputRequirement;
+import org.apache.nifi.annotation.behavior.SideEffectFree;
+import org.apache.nifi.annotation.behavior.SupportsBatching;
+import org.apache.nifi.annotation.behavior.WritesAttribute;
+import org.apache.nifi.annotation.behavior.WritesAttributes;
+import org.apache.nifi.annotation.documentation.CapabilityDescription;
+import org.apache.nifi.annotation.documentation.SeeAlso;
+import org.apache.nifi.annotation.documentation.Tags;
+import org.apache.nifi.annotation.documentation.UseCase;
+import org.apache.nifi.annotation.lifecycle.OnScheduled;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyValue;
+import org.apache.nifi.components.ValidationContext;
+import org.apache.nifi.components.ValidationResult;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.record.path.RecordPath;
+import org.apache.nifi.record.path.RecordPathResult;
+import org.apache.nifi.record.path.util.RecordPathCache;
+import org.apache.nifi.record.path.validation.RecordPathPropertyNameValidator;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.util.DataTypeUtils;
+
+@SideEffectFree
+@SupportsBatching
+@InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
+@Tags({"update", "record", "rename", "field", "generic", "schema", "json", "csv", "avro", "log", "logs"})
+@CapabilityDescription("Renames one or more fields in each Record of a FlowFile. "
+ + "This Processor requires that at least one user-defined Property be added. The name of the Property should indicate a RecordPath that determines the field that should "
+ + "be updated. The value of the Property is the new name to assign to the Record Field that matches the RecordPath. The property value may use Expression Language to reference "
+ + "FlowFile attributes as well as the variables `field.name`, `field.value`, `field.type`, and `record.index`")
+@WritesAttributes({
+ @WritesAttribute(attribute = "record.index", description = "This attribute provides the current row index and is only available inside the literal value expression.")
+})
+@DynamicProperty(name = "A RecordPath that identifies which field(s) to update",
+ value = "The new name to assign to the Record field",
+ description = "Allows users to specify a new name for each field that matches the RecordPath.",
+ expressionLanguageScope = ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+@SeeAlso({UpdateRecord.class, RemoveRecordField.class})
+@UseCase(
+ description = "Rename a field in each Record to a specific, known name.",
+ keywords = {"rename", "field", "static", "specific", "name"},
+ configuration = """
+ Configure the 'Record Reader' according to the input format.
+ Configure the 'Record Writer' according to the desired output format.
+
+ Add a property to the Processor such that the name of the property is a RecordPath to identifies the field to rename. \
+ The value of the property is the new name of the property.
+
+ For example, to rename the `name` field to `full_name`, add a property with a name of `/name` and a value of `full_name`.
+
+ Many properties can be added following this pattern in order to rename multiple fields.
+ """
+)
+@UseCase(
+ description = "Rename a field in each Record to a name that is derived from a FlowFile attribute.",
+ keywords = {"rename", "field", "expression language", "EL", "flowfile", "attribute"},
+ configuration = """
+ Configure the 'Record Reader' according to the input format.
+ Configure the 'Record Writer' according to the desired output format.
+
+ Add a property to the Processor such that the name of the property is a RecordPath to identifies the field to rename. \
+ The value of the property is an Expression Language expression that can be used to determine the new name of the field.
+
+ For example, to rename the `addr` field to whatever value is stored in the `preferred_address_name` attribute, \
+ add a property with a name of `/name` and a value of `${preferred_address_name}`.
+
+ Many properties can be added following this pattern in order to rename multiple fields.
+ """
+)
+@UseCase(
+ description = "Rename a field in each Record to a new name that is derived from the current field name.",
+ notes = "This might be used, for example, to add a prefix or a suffix to some fields, or to transform the name of the field by making it uppercase.",
+ keywords = {"rename", "field", "expression language", "EL", "field.name"},
+ configuration = """
+ Configure the 'Record Reader' according to the input format.
+ Configure the 'Record Writer' according to the desired output format.
+
+ Add a property to the Processor such that the name of the property is a RecordPath to identifies the field to rename. \
+ The value of the property is an Expression Language expression that references the `field.name` property.
+
+ For example, to rename all fields with a prefix of `pre_`, we add a property named `/*` and a value of `pre_${field.name}`. \
+ If we would like this to happen recursively, to nested fields as well, we use a property name of `//*` with the value of `pre_${field.name}`.
+
+ To make all field names uppercase, we can add a property named `//*` with a value of `${field.name:toUpper()}`.
+
+ Many properties can be added following this pattern in order to rename multiple fields.
+ """
+)
+public class RenameRecordField extends AbstractRecordProcessor {
+
+ private static final String FIELD_NAME = "field.name";
+ private static final String FIELD_VALUE = "field.value";
+ private static final String FIELD_TYPE = "field.type";
+
+ private static final String RECORD_INDEX = "record.index";
+
+ private volatile RecordPathCache recordPathCache;
+ private volatile List<String> recordPaths;
+
+
+ @Override
+ protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) {
+ return new PropertyDescriptor.Builder()
+ .name(propertyDescriptorName)
+ .description("Specifies the new name to use for any record field that match the RecordPath: " + propertyDescriptorName)
+ .required(false)
+ .dynamic(true)
+ .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+ .addValidator(new RecordPathPropertyNameValidator())
+ .build();
+ }
+
+
+ @Override
+ protected Collection<ValidationResult> customValidate(final ValidationContext validationContext) {
+ final boolean containsDynamic = validationContext.getProperties().keySet().stream().anyMatch(PropertyDescriptor::isDynamic);
+
+ if (containsDynamic) {
+ return Collections.emptyList();
+ }
+
+ return Collections.singleton(new ValidationResult.Builder()
+ .subject("User-defined Properties")
+ .valid(false)
+ .explanation("At least one RecordPath must be specified")
+ .build());
+ }
+
+
+ @OnScheduled
+ public void createRecordPaths(final ProcessContext context) {
+ recordPathCache = new RecordPathCache(context.getProperties().size());
+
+ final List<String> recordPaths = new ArrayList<>(context.getProperties().size() - 2);
+ for (final PropertyDescriptor property : context.getProperties().keySet()) {
+ if (property.isDynamic()) {
+ recordPaths.add(property.getName());
+ }
+ }
+
+ this.recordPaths = recordPaths;
+ }
+
+
+ @Override
+ protected Record process(final Record record, final FlowFile flowFile, final ProcessContext context, final long count) {
+ for (final String propertyName : recordPaths) {
+ final RecordPath recordPath = recordPathCache.getCompiled(propertyName);
+ final RecordPathResult result = recordPath.evaluate(record);
+
+ final PropertyValue newFieldNamePropertyValue = context.getProperty(propertyName);
+
+ if (newFieldNamePropertyValue.isExpressionLanguagePresent()) {
+ final Map<String, String> fieldVariables = new HashMap<>();
+
+ result.getSelectedFields().forEach(fieldVal -> {
+ fieldVariables.clear();
+ fieldVariables.put(FIELD_NAME, fieldVal.getField().getFieldName());
+ fieldVariables.put(FIELD_VALUE, DataTypeUtils.toString(fieldVal.getValue(), (String) null));
+ fieldVariables.put(FIELD_TYPE, fieldVal.getField().getDataType().getFieldType().name());
+ fieldVariables.put(RECORD_INDEX, String.valueOf(count));
+
+ final String newFieldName = newFieldNamePropertyValue.evaluateAttributeExpressions(flowFile, fieldVariables).getValue();
+ fieldVal.getParentRecord().ifPresent(parentRecord -> {
+ parentRecord.rename(fieldVal.getField(), newFieldName);
+ });
+ });
+ } else {
+ final String newFieldName = newFieldNamePropertyValue.evaluateAttributeExpressions(flowFile).getValue();
+
+ result.getSelectedFields().forEach(fieldVal -> {
+ fieldVal.getParentRecord().ifPresent(parentRecord -> {
+ parentRecord.rename(fieldVal.getField(), newFieldName);
+ });
+ });
+ }
+ }
+
+ return record;
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index f3baf43949..e303ba8c89 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -99,6 +99,7 @@ org.apache.nifi.processors.standard.QueryDatabaseTable
org.apache.nifi.processors.standard.QueryDatabaseTableRecord
org.apache.nifi.processors.standard.QueryRecord
org.apache.nifi.processors.standard.RemoveRecordField
+org.apache.nifi.processors.standard.RenameRecordField
org.apache.nifi.processors.standard.ReplaceText
org.apache.nifi.processors.standard.ReplaceTextWithMapping
org.apache.nifi.processors.standard.RetryFlowFile
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestRenameRecordField.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestRenameRecordField.java
new file mode 100644
index 0000000000..a4866f3191
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestRenameRecordField.java
@@ -0,0 +1,182 @@
+/*
+ * 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.processors.standard;
+
+import org.apache.avro.Schema;
+import org.apache.nifi.json.JsonRecordSetWriter;
+import org.apache.nifi.json.JsonTreeReader;
+import org.apache.nifi.reporting.InitializationException;
+import org.apache.nifi.schema.access.SchemaAccessUtils;
+import org.apache.nifi.serialization.SchemaRegistryRecordSetWriter;
+import org.apache.nifi.util.MockFlowFile;
+import org.apache.nifi.util.TestRunner;
+import org.apache.nifi.util.TestRunners;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class TestRenameRecordField {
+ private final Path INPUT_FILES = Paths.get("src/test/resources/TestRenameRecordField/input");
+ private final Path OUTPUT_FILES = Paths.get("src/test/resources/TestRenameRecordField/output");
+ private TestRunner runner;
+
+
+ @BeforeEach
+ public void setup() throws InitializationException {
+ runner = TestRunners.newTestRunner(RenameRecordField.class);
+
+ final JsonRecordSetWriter jsonWriter = new JsonRecordSetWriter();
+ runner.addControllerService("writer", jsonWriter);
+ runner.setProperty(jsonWriter, SchemaAccessUtils.SCHEMA_ACCESS_STRATEGY, SchemaAccessUtils.INHERIT_RECORD_SCHEMA);
+ runner.setProperty(jsonWriter, JsonRecordSetWriter.SUPPRESS_NULLS, JsonRecordSetWriter.NEVER_SUPPRESS);
+ runner.setProperty(jsonWriter, JsonRecordSetWriter.PRETTY_PRINT_JSON, "true");
+ runner.setProperty(jsonWriter, jsonWriter.getSchemaWriteStrategyDescriptor(), SchemaRegistryRecordSetWriter.AVRO_SCHEMA_ATTRIBUTE);
+ runner.enableControllerService(jsonWriter);
+ runner.setProperty(AbstractRecordProcessor.RECORD_WRITER, "writer");
+
+ final JsonTreeReader reader = new JsonTreeReader();
+ runner.addControllerService("reader", reader);
+ runner.enableControllerService(reader);
+ runner.setProperty(AbstractRecordProcessor.RECORD_READER, "reader");
+ }
+
+ @Test
+ public void testRenameFieldStaticValue() throws IOException {
+ runner.setProperty("/application", "favoriteApplication");
+ runner.enqueue(INPUT_FILES.resolve("simple-person.json"));
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(AbstractRecordProcessor.REL_SUCCESS, 1);
+ final MockFlowFile out = runner.getFlowFilesForRelationship(AbstractRecordProcessor.REL_SUCCESS).get(0);
+ assertContentsEqual(out, OUTPUT_FILES.resolve("testRenameFieldStaticValue.json"));
+ }
+
+ @Test
+ public void testRenameFieldUsingAttribute() throws IOException {
+ runner.setProperty("/application", "${desiredKey}");
+ runner.enqueue(INPUT_FILES.resolve("simple-person.json"), Collections.singletonMap("desiredKey", "favorite"));
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(AbstractRecordProcessor.REL_SUCCESS, 1);
+ final MockFlowFile out = runner.getFlowFilesForRelationship(AbstractRecordProcessor.REL_SUCCESS).get(0);
+ assertContentsEqual(out, OUTPUT_FILES.resolve("testRenameFieldUsingAttribute.json"));
+ }
+
+ @Test
+ public void testRenameMultipleFields() throws IOException {
+ runner.setProperty("/application", "app");
+ runner.setProperty("/name", "${nameKey}");
+ runner.enqueue(INPUT_FILES.resolve("simple-person.json"), Collections.singletonMap("nameKey", "full_name"));
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(AbstractRecordProcessor.REL_SUCCESS, 1);
+ final MockFlowFile out = runner.getFlowFilesForRelationship(AbstractRecordProcessor.REL_SUCCESS).get(0);
+ assertContentsEqual(out, OUTPUT_FILES.resolve("testRenameMultipleFields.json"));
+
+ final String outputSchemaText = out.getAttribute("avro.schema");
+ final Schema outputSchema = new Schema.Parser().parse(outputSchemaText);
+ assertEquals(3, outputSchema.getFields().size());
+ assertNull(outputSchema.getField("application"));
+ assertNotNull(outputSchema.getField("app"));
+ assertNull(outputSchema.getField("name"));
+ assertNotNull(outputSchema.getField("full_name"));
+ }
+
+ @Test
+ public void testRenameArray() throws IOException {
+ runner.setProperty("/addresses", "addrs");
+ runner.enqueue(INPUT_FILES.resolve("complex-person.json"));
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(AbstractRecordProcessor.REL_SUCCESS, 1);
+ final MockFlowFile out = runner.getFlowFilesForRelationship(AbstractRecordProcessor.REL_SUCCESS).get(0);
+ assertContentsEqual(out, OUTPUT_FILES.resolve("testRenameArray.json"));
+ }
+
+
+ @Test
+ public void testNestedPath() throws IOException {
+ runner.setProperty("/addresses[*]/street", "streetAddress");
+ runner.enqueue(INPUT_FILES.resolve("complex-person.json"));
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(AbstractRecordProcessor.REL_SUCCESS, 1);
+ final MockFlowFile out = runner.getFlowFilesForRelationship(AbstractRecordProcessor.REL_SUCCESS).get(0);
+ assertContentsEqual(out, OUTPUT_FILES.resolve("testNestedPath.json"));
+ }
+
+ @Test
+ public void testNamingConflict() throws IOException {
+ runner.setProperty("/application", "name");
+ runner.enqueue(INPUT_FILES.resolve("simple-person.json"));
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(AbstractRecordProcessor.REL_FAILURE, 1);
+ final MockFlowFile out = runner.getFlowFilesForRelationship(AbstractRecordProcessor.REL_FAILURE).get(0);
+ // Output should be unchanged.
+ assertContentsEqual(out, INPUT_FILES.resolve("simple-person.json"));
+ }
+
+ @Test
+ public void testReferencingFieldName() throws IOException {
+ runner.setProperty("/*", "UPDATED_${field.name}");
+ runner.enqueue(INPUT_FILES.resolve("simple-person.json"));
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(AbstractRecordProcessor.REL_SUCCESS, 1);
+ final MockFlowFile out = runner.getFlowFilesForRelationship(AbstractRecordProcessor.REL_SUCCESS).get(0);
+ assertContentsEqual(out, OUTPUT_FILES.resolve("testReferencingFieldName.json"));
+ }
+
+ @Test
+ public void testRecursivelyReferencingAllFields() throws IOException {
+ runner.setProperty("//*", "${field.name:toUpper()}");
+ runner.enqueue(INPUT_FILES.resolve("complex-person.json"));
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(AbstractRecordProcessor.REL_SUCCESS, 1);
+ final MockFlowFile out = runner.getFlowFilesForRelationship(AbstractRecordProcessor.REL_SUCCESS).get(0);
+ assertContentsEqual(out, OUTPUT_FILES.resolve("testRecursivelyReferencingAllFields.json"));
+ }
+
+ @Test
+ public void testRecursivelyReferencingFieldName() throws IOException {
+ runner.setProperty("//name", "${field.name:toUpper()}");
+ runner.enqueue(INPUT_FILES.resolve("complex-person.json"));
+ runner.run();
+
+ runner.assertAllFlowFilesTransferred(AbstractRecordProcessor.REL_SUCCESS, 1);
+ final MockFlowFile out = runner.getFlowFilesForRelationship(AbstractRecordProcessor.REL_SUCCESS).get(0);
+ assertContentsEqual(out, OUTPUT_FILES.resolve("testRecursivelyReferencingFieldName.json"));
+ }
+
+ private void assertContentsEqual(final MockFlowFile flowFile, final Path expectedContent) throws IOException {
+ final String flowFileContent = flowFile.getContent();
+ final String fileContent = new String(Files.readAllBytes(expectedContent));
+ assertEquals(flowFileContent.replace("\r", ""), fileContent.replace("\r", ""));
+ }
+}
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/input/complex-person.json b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/input/complex-person.json
new file mode 100644
index 0000000000..5de7972913
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/input/complex-person.json
@@ -0,0 +1,12 @@
+[ {
+ "name" : "John Doe",
+ "age" : 30,
+ "application" : "Apache NiFi",
+ "addresses" : [ {
+ "name" : "Home",
+ "street" : "7777 Main Street",
+ "city" : "My City",
+ "state" : "MS",
+ "zip" : "90210"
+ } ]
+} ]
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/input/simple-person.json b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/input/simple-person.json
new file mode 100644
index 0000000000..9254388546
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/input/simple-person.json
@@ -0,0 +1,5 @@
+[ {
+ "name" : "John Doe",
+ "age" : 30,
+ "application" : "Apache NiFi"
+} ]
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testNestedPath.json b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testNestedPath.json
new file mode 100644
index 0000000000..c1ef08b79d
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testNestedPath.json
@@ -0,0 +1,12 @@
+[ {
+ "name" : "John Doe",
+ "age" : 30,
+ "application" : "Apache NiFi",
+ "addresses" : [ {
+ "name" : "Home",
+ "streetAddress" : "7777 Main Street",
+ "city" : "My City",
+ "state" : "MS",
+ "zip" : "90210"
+ } ]
+} ]
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRecursivelyReferencingAllFields.json b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRecursivelyReferencingAllFields.json
new file mode 100644
index 0000000000..72df009e4c
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRecursivelyReferencingAllFields.json
@@ -0,0 +1,12 @@
+[ {
+ "NAME" : "John Doe",
+ "AGE" : 30,
+ "APPLICATION" : "Apache NiFi",
+ "ADDRESSES" : [ {
+ "NAME" : "Home",
+ "STREET" : "7777 Main Street",
+ "CITY" : "My City",
+ "STATE" : "MS",
+ "ZIP" : "90210"
+ } ]
+} ]
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRecursivelyReferencingFieldName.json b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRecursivelyReferencingFieldName.json
new file mode 100644
index 0000000000..40f3530c61
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRecursivelyReferencingFieldName.json
@@ -0,0 +1,12 @@
+[ {
+ "NAME" : "John Doe",
+ "age" : 30,
+ "application" : "Apache NiFi",
+ "addresses" : [ {
+ "NAME" : "Home",
+ "street" : "7777 Main Street",
+ "city" : "My City",
+ "state" : "MS",
+ "zip" : "90210"
+ } ]
+} ]
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testReferencingFieldName.json b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testReferencingFieldName.json
new file mode 100644
index 0000000000..a72fc8b360
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testReferencingFieldName.json
@@ -0,0 +1,5 @@
+[ {
+ "UPDATED_name" : "John Doe",
+ "UPDATED_age" : 30,
+ "UPDATED_application" : "Apache NiFi"
+} ]
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameArray.json b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameArray.json
new file mode 100644
index 0000000000..3f11cf24d2
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameArray.json
@@ -0,0 +1,12 @@
+[ {
+ "name" : "John Doe",
+ "age" : 30,
+ "application" : "Apache NiFi",
+ "addrs" : [ {
+ "name" : "Home",
+ "street" : "7777 Main Street",
+ "city" : "My City",
+ "state" : "MS",
+ "zip" : "90210"
+ } ]
+} ]
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameFieldStaticValue.json b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameFieldStaticValue.json
new file mode 100644
index 0000000000..58aec362da
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameFieldStaticValue.json
@@ -0,0 +1,5 @@
+[ {
+ "name" : "John Doe",
+ "age" : 30,
+ "favoriteApplication" : "Apache NiFi"
+} ]
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameFieldUsingAttribute.json b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameFieldUsingAttribute.json
new file mode 100644
index 0000000000..35e9f95e2b
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameFieldUsingAttribute.json
@@ -0,0 +1,5 @@
+[ {
+ "name" : "John Doe",
+ "age" : 30,
+ "favorite" : "Apache NiFi"
+} ]
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameMultipleFields.json b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameMultipleFields.json
new file mode 100644
index 0000000000..6a1b8d5b88
--- /dev/null
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestRenameRecordField/output/testRenameMultipleFields.json
@@ -0,0 +1,5 @@
+[ {
+ "full_name" : "John Doe",
+ "age" : 30,
+ "app" : "Apache NiFi"
+} ]
\ No newline at end of file