Repository: nifi Updated Branches: refs/heads/master 456c9c8fc -> 6b34d3bea
NIFI-4743 - Added configurable null suppression to PutElasticsearchHttpRecord Signed-off-by: Pierre Villard <[email protected]> This closes #2501. Project: http://git-wip-us.apache.org/repos/asf/nifi/repo Commit: http://git-wip-us.apache.org/repos/asf/nifi/commit/6b34d3be Tree: http://git-wip-us.apache.org/repos/asf/nifi/tree/6b34d3be Diff: http://git-wip-us.apache.org/repos/asf/nifi/diff/6b34d3be Branch: refs/heads/master Commit: 6b34d3bea9a1fd0923024018d73c3c0b0a807a67 Parents: 456c9c8 Author: Mike Thomsen <[email protected]> Authored: Thu Mar 1 08:00:03 2018 -0500 Committer: Pierre Villard <[email protected]> Committed: Mon Mar 26 20:41:21 2018 +0200 ---------------------------------------------------------------------- .../AbstractElasticsearchHttpProcessor.java | 4 +- .../PutElasticsearchHttpRecord.java | 34 ++- .../PutElasticsearchHttpRecordIT.java | 211 +++++++++++++++++++ 3 files changed, 245 insertions(+), 4 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/nifi/blob/6b34d3be/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/AbstractElasticsearchHttpProcessor.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/AbstractElasticsearchHttpProcessor.java b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/AbstractElasticsearchHttpProcessor.java index 49ab682..faaf523 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/AbstractElasticsearchHttpProcessor.java +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/AbstractElasticsearchHttpProcessor.java @@ -121,8 +121,8 @@ public abstract class AbstractElasticsearchHttpProcessor extends AbstractElastic OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder(); // Add a proxy if set - final String proxyHost = context.getProperty(PROXY_HOST).getValue(); - final Integer proxyPort = context.getProperty(PROXY_PORT).asInteger(); + final String proxyHost = context.getProperty(PROXY_HOST).evaluateAttributeExpressions().getValue(); + final Integer proxyPort = context.getProperty(PROXY_PORT).evaluateAttributeExpressions().asInteger(); if (proxyHost != null && proxyPort != null) { final Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); okHttpClient.proxy(proxy); http://git-wip-us.apache.org/repos/asf/nifi/blob/6b34d3be/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java index f65abb0..ae98d52 100644 --- a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/main/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecord.java @@ -32,6 +32,7 @@ import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.annotation.lifecycle.OnScheduled; +import org.apache.nifi.components.AllowableValue; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.components.ValidationContext; import org.apache.nifi.components.ValidationResult; @@ -153,6 +154,24 @@ public class PutElasticsearchHttpRecord extends AbstractElasticsearchHttpProcess .defaultValue("index") .build(); + static final AllowableValue ALWAYS_SUPPRESS = new AllowableValue("always-suppress", "Always Suppress", + "Fields that are missing (present in the schema but not in the record), or that have a value of null, will not be written out"); + + static final AllowableValue NEVER_SUPPRESS = new AllowableValue("never-suppress", "Never Suppress", + "Fields that are missing (present in the schema but not in the record), or that have a value of null, will be written out as a null value"); + + static final AllowableValue SUPPRESS_MISSING = new AllowableValue("suppress-missing", "Suppress Missing Values", + "When a field has a value of null, it will be written out. However, if a field is defined in the schema and not present in the record, the field will not be written out."); + + static final PropertyDescriptor SUPPRESS_NULLS = new PropertyDescriptor.Builder() + .name("suppress-nulls") + .displayName("Suppress Null Values") + .description("Specifies how the writer should handle a null field") + .allowableValues(NEVER_SUPPRESS, ALWAYS_SUPPRESS, SUPPRESS_MISSING) + .defaultValue(NEVER_SUPPRESS.getValue()) + .required(true) + .build(); + private static final Set<Relationship> relationships; private static final List<PropertyDescriptor> propertyDescriptors; @@ -160,6 +179,8 @@ public class PutElasticsearchHttpRecord extends AbstractElasticsearchHttpProcess private final JsonFactory factory = new JsonFactory(); + private volatile String nullSuppression; + static { final Set<Relationship> _rels = new HashSet<>(); _rels.add(REL_SUCCESS); @@ -179,6 +200,7 @@ public class PutElasticsearchHttpRecord extends AbstractElasticsearchHttpProcess descriptors.add(INDEX); descriptors.add(TYPE); descriptors.add(INDEX_OP); + descriptors.add(SUPPRESS_NULLS); propertyDescriptors = Collections.unmodifiableList(descriptors); } @@ -284,6 +306,8 @@ public class PutElasticsearchHttpRecord extends AbstractElasticsearchHttpProcess return; } + this.nullSuppression = context.getProperty(SUPPRESS_NULLS).getValue(); + final String id_path = context.getProperty(ID_RECORD_PATH).evaluateAttributeExpressions(flowFile).getValue(); final RecordPath recordPath = StringUtils.isEmpty(id_path) ? null : recordPathCache.getCompiled(id_path); final StringBuilder sb = new StringBuilder(); @@ -450,7 +474,10 @@ public class PutElasticsearchHttpRecord extends AbstractElasticsearchHttpProcess final String fieldName = field.getFieldName(); final Object value = record.getValue(field); if (value == null) { - generator.writeNullField(fieldName); + if (nullSuppression.equals(NEVER_SUPPRESS.getValue()) || (nullSuppression.equals(SUPPRESS_MISSING.getValue())) && record.getRawFieldNames().contains(fieldName)) { + generator.writeNullField(fieldName); + } + continue; } @@ -465,7 +492,10 @@ public class PutElasticsearchHttpRecord extends AbstractElasticsearchHttpProcess @SuppressWarnings("unchecked") private void writeValue(final JsonGenerator generator, final Object value, final String fieldName, final DataType dataType) throws IOException { if (value == null) { - generator.writeNull(); + if (nullSuppression.equals(NEVER_SUPPRESS.getValue()) || ((nullSuppression.equals(SUPPRESS_MISSING.getValue())) && fieldName != null && !fieldName.equals(""))) { + generator.writeNullField(fieldName); + } + return; } http://git-wip-us.apache.org/repos/asf/nifi/blob/6b34d3be/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/test/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecordIT.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/test/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecordIT.java b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/test/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecordIT.java new file mode 100644 index 0000000..0ff0f8e --- /dev/null +++ b/nifi-nar-bundles/nifi-elasticsearch-bundle/nifi-elasticsearch-processors/src/test/java/org/apache/nifi/processors/elasticsearch/PutElasticsearchHttpRecordIT.java @@ -0,0 +1,211 @@ +/* + * 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.elasticsearch; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.nifi.serialization.SimpleRecordSchema; +import org.apache.nifi.serialization.record.MapRecord; +import org.apache.nifi.serialization.record.MockRecordParser; +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.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PutElasticsearchHttpRecordIT { + protected TestRunner runner; + private MockRecordParser recordReader; + static RecordSchema personSchema; + static TestRunner FETCH_RUNNER; + ObjectMapper mapper = new ObjectMapper(); + + @BeforeClass + public static void setupTests() throws Exception { + final List<RecordField> personFields = new ArrayList<>(); + final RecordField nameField = new RecordField("name", RecordFieldType.STRING.getDataType()); + final RecordField ageField = new RecordField("age", RecordFieldType.INT.getDataType()); + final RecordField sportField = new RecordField("sport", RecordFieldType.STRING.getDataType()); + personFields.add(nameField); + personFields.add(ageField); + personFields.add(sportField); + personSchema = new SimpleRecordSchema(personFields); + + FETCH_RUNNER = TestRunners.newTestRunner(FetchElasticsearchHttp.class); + FETCH_RUNNER.setProperty(FetchElasticsearchHttp.ES_URL, "http://localhost:9200"); + FETCH_RUNNER.setProperty(FetchElasticsearchHttp.INDEX, "people_test"); + FETCH_RUNNER.setProperty(FetchElasticsearchHttp.TYPE, "person"); + FETCH_RUNNER.setProperty(FetchElasticsearchHttp.DOC_ID, "${doc_id}"); + FETCH_RUNNER.assertValid(); + } + + @Before + public void setup() throws Exception { + recordReader = new MockRecordParser(); + recordReader.addSchemaField("id", RecordFieldType.INT); + + recordReader.addSchemaField("person", RecordFieldType.RECORD); + + runner = TestRunners.newTestRunner(PutElasticsearchHttpRecord.class); + runner.addControllerService("reader", recordReader); + runner.enableControllerService(recordReader); + runner.setProperty(PutElasticsearchHttpRecord.RECORD_READER, "reader"); + runner.setProperty(PutElasticsearchHttpRecord.ES_URL, "http://localhost:9200"); + runner.setProperty(PutElasticsearchHttpRecord.INDEX, "people_test"); + runner.setProperty(PutElasticsearchHttpRecord.TYPE, "person"); + runner.setProperty(PutElasticsearchHttpRecord.ID_RECORD_PATH, "/id"); + runner.assertValid(); + } + + @After + public void tearDown() { + FETCH_RUNNER.clearTransferState(); + } + + private void setupPut() { + runner.enqueue(""); + runner.setValidateExpressionUsage(false); + runner.run(1, true, true); + runner.assertTransferCount(PutElasticsearchHttpRecord.REL_FAILURE, 0); + runner.assertTransferCount(PutElasticsearchHttpRecord.REL_RETRY, 0); + runner.assertTransferCount(PutElasticsearchHttpRecord.REL_SUCCESS, 1); + } + + private void testFetch(List<Map<String, String>> attrs) { + for (Map<String, String> attr : attrs) { + FETCH_RUNNER.enqueue("", attr); + } + + FETCH_RUNNER.run(attrs.size(), true, true); + FETCH_RUNNER.assertTransferCount(FetchElasticsearchHttp.REL_FAILURE, 0); + FETCH_RUNNER.assertTransferCount(FetchElasticsearchHttp.REL_RETRY, 0); + FETCH_RUNNER.assertTransferCount(FetchElasticsearchHttp.REL_NOT_FOUND, 0); + FETCH_RUNNER.assertTransferCount(FetchElasticsearchHttp.REL_SUCCESS, attrs.size()); + } + + @Test + public void testNoNullSuppresion() throws Exception { + recordReader.addRecord(1, new MapRecord(personSchema, new HashMap<String,Object>() {{ + put("name", "John Doe"); + put("age", 48); + put("sport", null); + }})); + + List<Map<String, String>> attrs = new ArrayList<>(); + Map<String, String> attr = new HashMap<>(); + attr.put("doc_id", "1"); + attrs.add(attr); + + setupPut(); + testFetch(attrs); + + byte[] raw = FETCH_RUNNER.getContentAsByteArray(FETCH_RUNNER.getFlowFilesForRelationship(FetchElasticsearchHttp.REL_SUCCESS).get(0)); + String val = new String(raw); + + Map<String, Object> parsed = mapper.readValue(val, Map.class); + Assert.assertNotNull(parsed); + Map<String, Object> person = (Map)parsed.get("person"); + Assert.assertNotNull(person); + Assert.assertTrue(person.containsKey("sport")); + Assert.assertNull(person.get("sport")); + } + + private void sharedSuppressTest(SharedPostTest spt) throws Exception { + List<Map<String, String>> attrs = new ArrayList<>(); + Map<String, String> attr = new HashMap<>(); + attr.put("doc_id", "1"); + attrs.add(attr); + attr = new HashMap<>(); + attr.put("doc_id", "2"); + attrs.add(attr); + + setupPut(); + testFetch(attrs); + + List<MockFlowFile> flowFiles = FETCH_RUNNER.getFlowFilesForRelationship(FetchElasticsearchHttp.REL_SUCCESS); + String ff1 = new String(FETCH_RUNNER.getContentAsByteArray(flowFiles.get(0))); + String ff2 = new String(FETCH_RUNNER.getContentAsByteArray(flowFiles.get(1))); + Map<String, Object> ff1Map = mapper.readValue(ff1, Map.class); + Map<String, Object> ff2Map = mapper.readValue(ff2, Map.class); + Assert.assertNotNull(ff1Map); + Assert.assertNotNull(ff2Map); + Map<String, Object> p1 = (Map)ff1Map.get("person"); + Map<String, Object> p2 = (Map)ff2Map.get("person"); + Assert.assertNotNull(p1); + Assert.assertNotNull(p2); + + spt.run(p1, p2); + } + + @Test + public void testMissingRecord() throws Exception { + recordReader.addRecord(1, new MapRecord(personSchema, new HashMap<String,Object>() {{ + put("name", "John Doe"); + put("age", 48); + }})); + + recordReader.addRecord(2, new MapRecord(personSchema, new HashMap<String,Object>() {{ + put("name", "John Doe"); + put("age", 48); + put("sport", null); + }})); + + runner.setProperty(PutElasticsearchHttpRecord.SUPPRESS_NULLS, PutElasticsearchHttpRecord.SUPPRESS_MISSING); + + sharedSuppressTest((p1, p2) -> { + Assert.assertFalse(p1.containsKey("sport")); + Assert.assertTrue(p2.containsKey("sport")); + Assert.assertNull(p2.get("sport")); + }); + } + + @Test + public void testAlwaysSuppress() throws Exception { + recordReader.addRecord(1, new MapRecord(personSchema, new HashMap<String,Object>() {{ + put("name", "John Doe"); + put("age", 48); + }})); + + recordReader.addRecord(2, new MapRecord(personSchema, new HashMap<String,Object>() {{ + put("name", "John Doe"); + put("age", 48); + put("sport", null); + }})); + + runner.setProperty(PutElasticsearchHttpRecord.SUPPRESS_NULLS, PutElasticsearchHttpRecord.ALWAYS_SUPPRESS); + + sharedSuppressTest((p1, p2) -> { + Assert.assertFalse(p1.containsKey("sport")); + Assert.assertFalse(p2.containsKey("sport")); + }); + } + + private interface SharedPostTest { + void run(Map<String, Object> p1, Map<String, Object> p2); + } +}
