mirror of https://github.com/apache/nifi.git
NIFI-7197 - In-place replacement in LookupRecord processor
This closes #4088 Signed-off-by: Mark Payne <markap14@hotmail.com>
This commit is contained in:
parent
f4b65afb64
commit
578430c9d9
|
@ -583,6 +583,8 @@
|
||||||
<exclude>src/test/resources/TestValidateRecord/nested-map-schema.avsc</exclude>
|
<exclude>src/test/resources/TestValidateRecord/nested-map-schema.avsc</exclude>
|
||||||
<exclude>src/test/resources/TestValidateRecord/timestamp.avsc</exclude>
|
<exclude>src/test/resources/TestValidateRecord/timestamp.avsc</exclude>
|
||||||
<exclude>src/test/resources/TestValidateRecord/timestamp.json</exclude>
|
<exclude>src/test/resources/TestValidateRecord/timestamp.json</exclude>
|
||||||
|
<exclude>src/test/resources/TestLookupRecord/lookup-array-input.json</exclude>
|
||||||
|
<exclude>src/test/resources/TestLookupRecord/lookup-array-output.json</exclude>
|
||||||
</excludes>
|
</excludes>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
|
@ -105,6 +105,14 @@ public class LookupRecord extends AbstractRouteRecord<Tuple<Map<String, RecordPa
|
||||||
static final AllowableValue RESULT_RECORD_FIELDS = new AllowableValue("record-fields", "Insert Record Fields",
|
static final AllowableValue RESULT_RECORD_FIELDS = new AllowableValue("record-fields", "Insert Record Fields",
|
||||||
"All of the fields in the Record that is retrieved from the Lookup Service will be inserted into the destination path.");
|
"All of the fields in the Record that is retrieved from the Lookup Service will be inserted into the destination path.");
|
||||||
|
|
||||||
|
static final AllowableValue USE_PROPERTY = new AllowableValue("use-property", "Use Property",
|
||||||
|
"The \"Result RecordPath\" property will be used to determine which part of the record should be updated with the value returned by the Lookup Service");
|
||||||
|
static final AllowableValue REPLACE_EXISTING_VALUES = new AllowableValue("replace-existing-values", "Replace Existing Values",
|
||||||
|
"The \"Result RecordPath\" property will be ignored and the lookup service must be a single simple key lookup service. Every dynamic property value should "
|
||||||
|
+ "be a record path. For each dynamic property, the value contained in the field corresponding to the record path will be used as the key in the Lookup "
|
||||||
|
+ "Service and the value returned by the Lookup Service will be used to replace the existing value. It is possible to configure multiple dynamic properties "
|
||||||
|
+ "to replace multiple values in one execution. This strategy only supports simple types replacements (strings, integers, etc).");
|
||||||
|
|
||||||
static final PropertyDescriptor LOOKUP_SERVICE = new PropertyDescriptor.Builder()
|
static final PropertyDescriptor LOOKUP_SERVICE = new PropertyDescriptor.Builder()
|
||||||
.name("lookup-service")
|
.name("lookup-service")
|
||||||
.displayName("Lookup Service")
|
.displayName("Lookup Service")
|
||||||
|
@ -144,6 +152,16 @@ public class LookupRecord extends AbstractRouteRecord<Tuple<Map<String, RecordPa
|
||||||
.required(true)
|
.required(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
static final PropertyDescriptor REPLACEMENT_STRATEGY = new PropertyDescriptor.Builder()
|
||||||
|
.name("record-update-strategy")
|
||||||
|
.displayName("Record Update Strategy")
|
||||||
|
.description("This property defines the strategy to use when updating the record with the value returned by the Lookup Service.")
|
||||||
|
.expressionLanguageSupported(ExpressionLanguageScope.NONE)
|
||||||
|
.allowableValues(REPLACE_EXISTING_VALUES, USE_PROPERTY)
|
||||||
|
.defaultValue(USE_PROPERTY.getValue())
|
||||||
|
.required(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
static final Relationship REL_MATCHED = new Relationship.Builder()
|
static final Relationship REL_MATCHED = new Relationship.Builder()
|
||||||
.name("matched")
|
.name("matched")
|
||||||
.description("All records for which the lookup returns a value will be routed to this relationship")
|
.description("All records for which the lookup returns a value will be routed to this relationship")
|
||||||
|
@ -182,6 +200,7 @@ public class LookupRecord extends AbstractRouteRecord<Tuple<Map<String, RecordPa
|
||||||
properties.add(RESULT_RECORD_PATH);
|
properties.add(RESULT_RECORD_PATH);
|
||||||
properties.add(ROUTING_STRATEGY);
|
properties.add(ROUTING_STRATEGY);
|
||||||
properties.add(RESULT_CONTENTS);
|
properties.add(RESULT_CONTENTS);
|
||||||
|
properties.add(REPLACEMENT_STRATEGY);
|
||||||
return properties;
|
return properties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,24 +233,37 @@ public class LookupRecord extends AbstractRouteRecord<Tuple<Map<String, RecordPa
|
||||||
}
|
}
|
||||||
|
|
||||||
final Set<String> requiredKeys = validationContext.getProperty(LOOKUP_SERVICE).asControllerService(LookupService.class).getRequiredKeys();
|
final Set<String> requiredKeys = validationContext.getProperty(LOOKUP_SERVICE).asControllerService(LookupService.class).getRequiredKeys();
|
||||||
final Set<String> missingKeys = requiredKeys.stream()
|
|
||||||
.filter(key -> !dynamicPropNames.contains(key))
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
|
|
||||||
if (!missingKeys.isEmpty()) {
|
if(validationContext.getProperty(REPLACEMENT_STRATEGY).getValue().equals(REPLACE_EXISTING_VALUES.getValue())) {
|
||||||
final List<ValidationResult> validationResults = new ArrayList<>();
|
// it must be a single key lookup service
|
||||||
for (final String missingKey : missingKeys) {
|
if(requiredKeys.size() != 1) {
|
||||||
final ValidationResult result = new ValidationResult.Builder()
|
return Collections.singleton(new ValidationResult.Builder()
|
||||||
.subject(missingKey)
|
.subject(LOOKUP_SERVICE.getDisplayName())
|
||||||
.valid(false)
|
.valid(false)
|
||||||
.explanation("The configured Lookup Services requires that a key be provided with the name '" + missingKey
|
.explanation("When using \"" + REPLACE_EXISTING_VALUES.getDisplayName() + "\" as Record Update Strategy, "
|
||||||
+ "'. Please add a new property to this Processor with a name '" + missingKey
|
+ "only a Lookup Service requiring a single key can be used.")
|
||||||
+ "' and provide a RecordPath that can be used to retrieve the appropriate value.")
|
.build());
|
||||||
.build();
|
|
||||||
validationResults.add(result);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
final Set<String> missingKeys = requiredKeys.stream()
|
||||||
|
.filter(key -> !dynamicPropNames.contains(key))
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
return validationResults;
|
if (!missingKeys.isEmpty()) {
|
||||||
|
final List<ValidationResult> validationResults = new ArrayList<>();
|
||||||
|
for (final String missingKey : missingKeys) {
|
||||||
|
final ValidationResult result = new ValidationResult.Builder()
|
||||||
|
.subject(missingKey)
|
||||||
|
.valid(false)
|
||||||
|
.explanation("The configured Lookup Services requires that a key be provided with the name '" + missingKey
|
||||||
|
+ "'. Please add a new property to this Processor with a name '" + missingKey
|
||||||
|
+ "' and provide a RecordPath that can be used to retrieve the appropriate value.")
|
||||||
|
.build();
|
||||||
|
validationResults.add(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationResults;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
@ -263,6 +295,68 @@ public class LookupRecord extends AbstractRouteRecord<Tuple<Map<String, RecordPa
|
||||||
protected Set<Relationship> route(final Record record, final RecordSchema writeSchema, final FlowFile flowFile, final ProcessContext context,
|
protected Set<Relationship> route(final Record record, final RecordSchema writeSchema, final FlowFile flowFile, final ProcessContext context,
|
||||||
final Tuple<Map<String, RecordPath>, RecordPath> flowFileContext) {
|
final Tuple<Map<String, RecordPath>, RecordPath> flowFileContext) {
|
||||||
|
|
||||||
|
final boolean isInPlaceReplacement = context.getProperty(REPLACEMENT_STRATEGY).getValue().equals(REPLACE_EXISTING_VALUES.getValue());
|
||||||
|
|
||||||
|
if(isInPlaceReplacement) {
|
||||||
|
return doInPlaceReplacement(record, flowFile, context, flowFileContext);
|
||||||
|
} else {
|
||||||
|
return doResultPathReplacement(record, flowFile, context, flowFileContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Relationship> doInPlaceReplacement(Record record, FlowFile flowFile, ProcessContext context, Tuple<Map<String, RecordPath>, RecordPath> flowFileContext) {
|
||||||
|
|
||||||
|
final String lookupKey = (String) context.getProperty(LOOKUP_SERVICE).asControllerService(LookupService.class).getRequiredKeys().iterator().next();
|
||||||
|
|
||||||
|
final Map<String, RecordPath> recordPaths = flowFileContext.getKey();
|
||||||
|
final Map<String, Object> lookupCoordinates = new HashMap<>(recordPaths.size());
|
||||||
|
|
||||||
|
for (final Map.Entry<String, RecordPath> entry : recordPaths.entrySet()) {
|
||||||
|
final String coordinateKey = entry.getKey();
|
||||||
|
final RecordPath recordPath = entry.getValue();
|
||||||
|
|
||||||
|
final RecordPathResult pathResult = recordPath.evaluate(record);
|
||||||
|
final List<FieldValue> lookupFieldValues = pathResult.getSelectedFields()
|
||||||
|
.filter(fieldVal -> fieldVal.getValue() != null)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (lookupFieldValues.isEmpty()) {
|
||||||
|
final Set<Relationship> rels = routeToMatchedUnmatched ? UNMATCHED_COLLECTION : SUCCESS_COLLECTION;
|
||||||
|
getLogger().debug("RecordPath for property '{}' did not match any fields in a record for {}; routing record to {}", new Object[] {coordinateKey, flowFile, rels});
|
||||||
|
return rels;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (FieldValue fieldValue : lookupFieldValues) {
|
||||||
|
final Object coordinateValue = (fieldValue.getValue() instanceof Number || fieldValue.getValue() instanceof Boolean)
|
||||||
|
? fieldValue.getValue() : DataTypeUtils.toString(fieldValue.getValue(), (String) null);
|
||||||
|
lookupCoordinates.put(lookupKey, coordinateValue);
|
||||||
|
|
||||||
|
final Optional<?> lookupValueOption;
|
||||||
|
try {
|
||||||
|
lookupValueOption = lookupService.lookup(lookupCoordinates, flowFile.getAttributes());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
throw new ProcessException("Failed to lookup coordinates " + lookupCoordinates + " in Lookup Service", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lookupValueOption.isPresent()) {
|
||||||
|
final Set<Relationship> rels = routeToMatchedUnmatched ? UNMATCHED_COLLECTION : SUCCESS_COLLECTION;
|
||||||
|
return rels;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object lookupValue = lookupValueOption.get();
|
||||||
|
|
||||||
|
final DataType inferredDataType = DataTypeUtils.inferDataType(lookupValue, RecordFieldType.STRING.getDataType());
|
||||||
|
fieldValue.updateValue(lookupValue, inferredDataType);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Set<Relationship> rels = routeToMatchedUnmatched ? MATCHED_COLLECTION : SUCCESS_COLLECTION;
|
||||||
|
return rels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Relationship> doResultPathReplacement(Record record, FlowFile flowFile, ProcessContext context, Tuple<Map<String, RecordPath>, RecordPath> flowFileContext) {
|
||||||
final Map<String, RecordPath> recordPaths = flowFileContext.getKey();
|
final Map<String, RecordPath> recordPaths = flowFileContext.getKey();
|
||||||
final Map<String, Object> lookupCoordinates = new HashMap<>(recordPaths.size());
|
final Map<String, Object> lookupCoordinates = new HashMap<>(recordPaths.size());
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>LookupRecord</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../../../../../css/component-usage.css" type="text/css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<p>
|
||||||
|
LookupRecord makes use of the NiFi <a href="../../../../../html/record-path-guide.html">
|
||||||
|
RecordPath Domain-Specific Language (DSL)</a> to allow the user to indicate which field(s),
|
||||||
|
depending on the Record Update Strategy, in the Record should be updated. The Record will
|
||||||
|
be updated using the value returned by the provided Lookup Service.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>Record Update Strategy - Use Property</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
In this case, the user should add, to the Processor's configuration, as much User-defined
|
||||||
|
Properties as required by the Lookup Service to form the lookup coordinates. The name of
|
||||||
|
the properties should match the names expected by the Lookup Service.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The field evaluated using the path configured in the "Result RecordPath" property will be
|
||||||
|
the field updated with the value returned by the Lookup Service.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Let's assume a Simple Key Value Lookup Service containing the following key/value pairs:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<code>
|
||||||
|
<pre>
|
||||||
|
FR => France
|
||||||
|
CA => Canada
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Let's assume the following JSON with three records as input:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<code>
|
||||||
|
<pre>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"country": null,
|
||||||
|
"code": "FR"
|
||||||
|
}, {
|
||||||
|
"country": null,
|
||||||
|
"code": "CA"
|
||||||
|
}, {
|
||||||
|
"country": null,
|
||||||
|
"code": "JP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The processor is configured with "Use Property" as "Record Update Strategy", the "Result
|
||||||
|
RecordPath" is configured with "/country" and a user-defined property is added with the
|
||||||
|
name "key" (as required by this Lookup Service) and the value "/code".
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
When triggered, the processor will look for the value associated to the "/code" path and
|
||||||
|
will use the value as the "key" of the Lookup Service. The value returned by the Lookup
|
||||||
|
Service will be used to update the value corresponding to "/country". With the above
|
||||||
|
examples, it will produce:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<code>
|
||||||
|
<pre>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"country": "France",
|
||||||
|
"code": "FR"
|
||||||
|
}, {
|
||||||
|
"country": "Canada",
|
||||||
|
"code": "CA"
|
||||||
|
}, {
|
||||||
|
"country": null,
|
||||||
|
"code": "JP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
|
||||||
|
<h3>Record Update Strategy - Replace Existing Values</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
With this strategy, the "Result RecordPath" property will be ignored and the configured Lookup
|
||||||
|
Service must be a single single key lookup service. For each user-defined property, the value
|
||||||
|
contained in the field corresponding to the record path will be used as the key in the Lookup
|
||||||
|
Service and will be replaced by the value returned by the Lookup Service. It is possible to
|
||||||
|
configure multiple dynamic properties to update multiple fields in one execution. This strategy
|
||||||
|
only supports simple types replacements (strings, integers, etc).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Since this strategy allows in-place replacement, it is possible to use Record Paths for fields
|
||||||
|
contained in arrays.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Let's assume a Simple Key Value Lookup Service containing the following key/value pairs:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<code>
|
||||||
|
<pre>
|
||||||
|
FR => France
|
||||||
|
CA => Canada
|
||||||
|
fr => French
|
||||||
|
en => English
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Let's assume the following JSON with two records as input:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<code>
|
||||||
|
<pre>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"locales": [
|
||||||
|
{
|
||||||
|
"region": "FR",
|
||||||
|
"language": "fr"
|
||||||
|
}, {
|
||||||
|
"region": "US",
|
||||||
|
"language": "en"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
"locales": [
|
||||||
|
{
|
||||||
|
"region": "CA",
|
||||||
|
"language": "fr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"region": "JP",
|
||||||
|
"language": "ja"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The processor is configured with "Replace Existing Values" as "Record Update Strategy",
|
||||||
|
two user-defined properties are added: "region" => "/locales[*]/region" and "language
|
||||||
|
=> "/locales[*]/language"..
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
When triggered, the processor will loop over the user-defined properties. First, it'll
|
||||||
|
search for the fields corresponding to "/locales[*]/region", for each value from the
|
||||||
|
record, the value will be used as the key with the Lookup Service and the value will
|
||||||
|
be replaced by the result returned by the Lookup Service. Example: the first region is
|
||||||
|
"FR" and this key is associated to the value "France" in the Lookup Service, so the
|
||||||
|
value "FR" is replaced by "France" in the record. With the above examples, it will
|
||||||
|
produce:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<code>
|
||||||
|
<pre>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"locales": [
|
||||||
|
{
|
||||||
|
"region": "France",
|
||||||
|
"language": "French"
|
||||||
|
}, {
|
||||||
|
"region": "US",
|
||||||
|
"language": "English"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
"locales": [
|
||||||
|
{
|
||||||
|
"region": "Canada",
|
||||||
|
"language": "French"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"region": "JP",
|
||||||
|
"language": "ja"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -19,9 +19,13 @@ package org.apache.nifi.processors.standard;
|
||||||
|
|
||||||
import org.apache.nifi.components.PropertyDescriptor;
|
import org.apache.nifi.components.PropertyDescriptor;
|
||||||
import org.apache.nifi.controller.AbstractControllerService;
|
import org.apache.nifi.controller.AbstractControllerService;
|
||||||
|
import org.apache.nifi.json.JsonRecordSetWriter;
|
||||||
|
import org.apache.nifi.json.JsonTreeReader;
|
||||||
import org.apache.nifi.lookup.RecordLookupService;
|
import org.apache.nifi.lookup.RecordLookupService;
|
||||||
import org.apache.nifi.lookup.StringLookupService;
|
import org.apache.nifi.lookup.StringLookupService;
|
||||||
import org.apache.nifi.reporting.InitializationException;
|
import org.apache.nifi.reporting.InitializationException;
|
||||||
|
import org.apache.nifi.schema.access.SchemaAccessUtils;
|
||||||
|
import org.apache.nifi.schema.inference.SchemaInferenceUtil;
|
||||||
import org.apache.nifi.serialization.SimpleRecordSchema;
|
import org.apache.nifi.serialization.SimpleRecordSchema;
|
||||||
import org.apache.nifi.serialization.record.MapRecord;
|
import org.apache.nifi.serialization.record.MapRecord;
|
||||||
import org.apache.nifi.serialization.record.MockRecordParser;
|
import org.apache.nifi.serialization.record.MockRecordParser;
|
||||||
|
@ -37,6 +41,8 @@ import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -436,6 +442,88 @@ public class TestLookupRecord {
|
||||||
out.assertContentEquals("John Doe,48,soccer,basketball\nJane Doe,47\n");
|
out.assertContentEquals("John Doe,48,soccer,basketball\nJane Doe,47\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLookupArray() throws InitializationException, IOException {
|
||||||
|
TestRunner runner = TestRunners.newTestRunner(LookupRecord.class);
|
||||||
|
final MapLookup lookupService = new MapLookup();
|
||||||
|
|
||||||
|
final JsonTreeReader jsonReader = new JsonTreeReader();
|
||||||
|
runner.addControllerService("reader", jsonReader);
|
||||||
|
runner.setProperty(jsonReader, SchemaAccessUtils.SCHEMA_ACCESS_STRATEGY, SchemaInferenceUtil.INFER_SCHEMA);
|
||||||
|
|
||||||
|
final JsonRecordSetWriter jsonWriter = new JsonRecordSetWriter();
|
||||||
|
runner.addControllerService("writer", jsonWriter);
|
||||||
|
runner.setProperty(jsonWriter, SchemaAccessUtils.SCHEMA_ACCESS_STRATEGY, SchemaAccessUtils.INHERIT_RECORD_SCHEMA);
|
||||||
|
|
||||||
|
runner.addControllerService("reader", jsonReader);
|
||||||
|
runner.enableControllerService(jsonReader);
|
||||||
|
runner.addControllerService("writer", jsonWriter);
|
||||||
|
runner.enableControllerService(jsonWriter);
|
||||||
|
runner.addControllerService("lookup", lookupService);
|
||||||
|
runner.enableControllerService(lookupService);
|
||||||
|
|
||||||
|
runner.setProperty(LookupRecord.ROUTING_STRATEGY, LookupRecord.ROUTE_TO_SUCCESS);
|
||||||
|
runner.setProperty(LookupRecord.REPLACEMENT_STRATEGY, LookupRecord.REPLACE_EXISTING_VALUES);
|
||||||
|
runner.setProperty(LookupRecord.RECORD_READER, "reader");
|
||||||
|
runner.setProperty(LookupRecord.RECORD_WRITER, "writer");
|
||||||
|
runner.setProperty(LookupRecord.LOOKUP_SERVICE, "lookup");
|
||||||
|
runner.setProperty("lookupLanguage", "/locales[*]/language");
|
||||||
|
runner.setProperty("lookupRegion", "/locales[*]/region");
|
||||||
|
runner.setProperty("lookupFoo", "/foo/foo");
|
||||||
|
|
||||||
|
lookupService.addValue("FR", "France");
|
||||||
|
lookupService.addValue("CA", "Canada");
|
||||||
|
lookupService.addValue("fr", "French");
|
||||||
|
lookupService.addValue("key", "value");
|
||||||
|
|
||||||
|
runner.enqueue(new File("src/test/resources/TestLookupRecord/lookup-array-input.json").toPath());
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertAllFlowFilesTransferred(LookupRecord.REL_SUCCESS);
|
||||||
|
final MockFlowFile out = runner.getFlowFilesForRelationship(LookupRecord.REL_SUCCESS).get(0);
|
||||||
|
out.assertContentEquals(new File("src/test/resources/TestLookupRecord/lookup-array-output.json").toPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLookupArrayKeyNotInLRS() throws InitializationException, IOException {
|
||||||
|
TestRunner runner = TestRunners.newTestRunner(LookupRecord.class);
|
||||||
|
final MapLookup lookupService = new MapLookup();
|
||||||
|
|
||||||
|
final JsonTreeReader jsonReader = new JsonTreeReader();
|
||||||
|
runner.addControllerService("reader", jsonReader);
|
||||||
|
runner.setProperty(jsonReader, SchemaAccessUtils.SCHEMA_ACCESS_STRATEGY, SchemaInferenceUtil.INFER_SCHEMA);
|
||||||
|
|
||||||
|
final JsonRecordSetWriter jsonWriter = new JsonRecordSetWriter();
|
||||||
|
runner.addControllerService("writer", jsonWriter);
|
||||||
|
runner.setProperty(jsonWriter, SchemaAccessUtils.SCHEMA_ACCESS_STRATEGY, SchemaAccessUtils.INHERIT_RECORD_SCHEMA);
|
||||||
|
|
||||||
|
runner.addControllerService("reader", jsonReader);
|
||||||
|
runner.enableControllerService(jsonReader);
|
||||||
|
runner.addControllerService("writer", jsonWriter);
|
||||||
|
runner.enableControllerService(jsonWriter);
|
||||||
|
runner.addControllerService("lookup", lookupService);
|
||||||
|
runner.enableControllerService(lookupService);
|
||||||
|
|
||||||
|
runner.setProperty(LookupRecord.ROUTING_STRATEGY, LookupRecord.ROUTE_TO_MATCHED_UNMATCHED);
|
||||||
|
runner.setProperty(LookupRecord.REPLACEMENT_STRATEGY, LookupRecord.REPLACE_EXISTING_VALUES);
|
||||||
|
runner.setProperty(LookupRecord.RECORD_READER, "reader");
|
||||||
|
runner.setProperty(LookupRecord.RECORD_WRITER, "writer");
|
||||||
|
runner.setProperty(LookupRecord.LOOKUP_SERVICE, "lookup");
|
||||||
|
runner.setProperty("lookupLanguage", "/locales[*]/language");
|
||||||
|
runner.setProperty("lookupRegion", "/locales[*]/region");
|
||||||
|
runner.setProperty("lookupFoo", "/foo/foo");
|
||||||
|
|
||||||
|
lookupService.addValue("FR", "France");
|
||||||
|
lookupService.addValue("CA", "Canada");
|
||||||
|
lookupService.addValue("fr", "French");
|
||||||
|
lookupService.addValue("badkey", "value");
|
||||||
|
|
||||||
|
runner.enqueue(new File("src/test/resources/TestLookupRecord/lookup-array-input.json").toPath());
|
||||||
|
runner.run();
|
||||||
|
|
||||||
|
runner.assertAllFlowFilesTransferred(LookupRecord.REL_UNMATCHED);
|
||||||
|
}
|
||||||
|
|
||||||
private static class MapLookup extends AbstractControllerService implements StringLookupService {
|
private static class MapLookup extends AbstractControllerService implements StringLookupService {
|
||||||
private final Map<String, String> values = new HashMap<>();
|
private final Map<String, String> values = new HashMap<>();
|
||||||
private Map<String, Object> expectedContext;
|
private Map<String, Object> expectedContext;
|
||||||
|
@ -449,6 +537,7 @@ public class TestLookupRecord {
|
||||||
return String.class;
|
return String.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Optional<String> lookup(final Map<String, Object> coordinates, Map<String, String> context) {
|
public Optional<String> lookup(final Map<String, Object> coordinates, Map<String, String> context) {
|
||||||
validateContext(context);
|
validateContext(context);
|
||||||
return lookup(coordinates);
|
return lookup(coordinates);
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"foo" : {
|
||||||
|
"foo" : "key"
|
||||||
|
},
|
||||||
|
"locales": [
|
||||||
|
{
|
||||||
|
"language" : "fr",
|
||||||
|
"region" : "CA"
|
||||||
|
}, {
|
||||||
|
"language" : "fr",
|
||||||
|
"region" : "FR"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
"foo" : {
|
||||||
|
"foo" : "key"
|
||||||
|
},
|
||||||
|
"locales": [
|
||||||
|
{
|
||||||
|
"language" : "fr",
|
||||||
|
"region" : "CA"
|
||||||
|
}, {
|
||||||
|
"language" : "fr",
|
||||||
|
"region" : "FR"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1 @@
|
||||||
|
[{"foo":{"foo":"value"},"locales":[{"language":"French","region":"Canada"},{"language":"French","region":"France"}]},{"foo":{"foo":"value"},"locales":[{"language":"French","region":"Canada"},{"language":"French","region":"France"}]}]
|
Loading…
Reference in New Issue