diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/pom.xml index 4d47701ff6..0c54b7c1b1 100755 --- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/pom.xml +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/pom.xml @@ -115,6 +115,10 @@ src/test/resources/json/bank-account-array-different-schemas.json src/test/resources/json/bank-account-array-optional-balance.json src/test/resources/json/bank-account-array.json + src/test/resources/json/bank-account-mixed.json + src/test/resources/json/bank-account-multiarray.json + src/test/resources/json/bank-account-multiline.json + src/test/resources/json/bank-account-oneline.json src/test/resources/json/json-with-unicode.json src/test/resources/json/primitive-type-array.json src/test/resources/json/single-bank-account.json diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/AbstractJsonRowRecordReader.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/AbstractJsonRowRecordReader.java index 663b8371d2..cc08d346f0 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/AbstractJsonRowRecordReader.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/AbstractJsonRowRecordReader.java @@ -84,10 +84,6 @@ public abstract class AbstractJsonRowRecordReader implements RecordReader { @Override public Record nextRecord(final boolean coerceTypes, final boolean dropUnknownFields) throws IOException, MalformedRecordException { - if (firstObjectConsumed && !array) { - return null; - } - final JsonNode nextNode = getNextJsonNode(); final RecordSchema schema = getSchema(); try { @@ -197,7 +193,8 @@ public abstract class AbstractJsonRowRecordReader implements RecordReader { return jsonParser.readValueAsTree(); case END_ARRAY: case START_ARRAY: - return null; + continue; + default: throw new MalformedRecordException("Expected to get a JSON Object but got a token of type " + token.name()); } diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonPathReader.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonPathReader.java index f8f1ea2b4e..d8f21ca8e4 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonPathReader.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonPathReader.java @@ -48,15 +48,16 @@ import org.apache.nifi.serialization.record.RecordSchema; import com.jayway.jsonpath.JsonPath; @Tags({"json", "jsonpath", "record", "reader", "parser"}) -@CapabilityDescription("Parses JSON records and evaluates user-defined JSON Path's against each JSON object. The root element may be either " - + "a single JSON object or a JSON array. If a JSON array is found, each JSON object within that array is treated as a separate record. " - + "User-defined properties define the fields that should be extracted from the JSON in order to form the fields of a Record. Any JSON field " - + "that is not extracted via a JSONPath will not be returned in the JSON Records.") +@CapabilityDescription("Parses JSON records and evaluates user-defined JSON Path's against each JSON object. While the reader expects each record " + + "to be well-formed JSON, the content of a FlowFile may consist of many records, each as a well-formed JSON array or JSON object with " + + "optional whitespace between them, such as the common 'JSON-per-line' format. If an array is encountered, each element in that array will " + + "be treated as a separate record. User-defined properties define the fields that should be extracted from the JSON in order to form the " + + "fields of a Record. Any JSON field that is not extracted via a JSONPath will not be returned in the JSON Records.") @SeeAlso(JsonTreeReader.class) @DynamicProperty(name = "The field name for the record.", value="A JSONPath Expression that will be evaluated against each JSON record. The result of the JSONPath will be the value of the " + "field whose name is the same as the property name.", - description="User-defined properties identifiy how to extract specific fields from a JSON object in order to create a Record", + description="User-defined properties identify how to extract specific fields from a JSON object in order to create a Record", expressionLanguageScope=ExpressionLanguageScope.NONE) public class JsonPathReader extends SchemaRegistryService implements RecordReaderFactory { diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonRecordSetWriter.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonRecordSetWriter.java index c5cb82a403..078a15d3bf 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonRecordSetWriter.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonRecordSetWriter.java @@ -20,6 +20,7 @@ package org.apache.nifi.json; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.apache.nifi.annotation.documentation.CapabilityDescription; @@ -27,6 +28,8 @@ import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.annotation.lifecycle.OnEnabled; import org.apache.nifi.components.AllowableValue; import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; import org.apache.nifi.controller.ConfigurationContext; import org.apache.nifi.expression.ExpressionLanguageScope; import org.apache.nifi.logging.ComponentLog; @@ -37,8 +40,8 @@ import org.apache.nifi.serialization.RecordSetWriterFactory; import org.apache.nifi.serialization.record.RecordSchema; @Tags({"json", "resultset", "writer", "serialize", "record", "recordset", "row"}) -@CapabilityDescription("Writes the results of a RecordSet as a JSON Array. Even if the RecordSet " - + "consists of a single row, it will be written as an array with a single element.") +@CapabilityDescription("Writes the results of a RecordSet as either a JSON Array or one JSON object per line. If using Array output, then even if the RecordSet " + + "consists of a single row, it will be written as an array with a single element. If using One Line Per Object output, the JSON objects cannot be pretty-printed.") public class JsonRecordSetWriter extends DateTimeTextRecordSetWriter implements RecordSetWriterFactory { static final AllowableValue ALWAYS_SUPPRESS = new AllowableValue("always-suppress", "Always Suppress", @@ -48,6 +51,11 @@ public class JsonRecordSetWriter extends DateTimeTextRecordSetWriter implements 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 AllowableValue OUTPUT_ARRAY = new AllowableValue("output-array", "Array", + "Output records as a JSON array"); + static final AllowableValue OUTPUT_ONELINE = new AllowableValue("output-oneline", "One Line Per Object", + "Output records with one JSON object per line, delimited by a newline character"); + static final PropertyDescriptor SUPPRESS_NULLS = new PropertyDescriptor.Builder() .name("suppress-nulls") .displayName("Suppress Null Values") @@ -64,18 +72,40 @@ public class JsonRecordSetWriter extends DateTimeTextRecordSetWriter implements .defaultValue("false") .required(true) .build(); + static final PropertyDescriptor OUTPUT_GROUPING = new PropertyDescriptor.Builder() + .name("output-grouping") + .displayName("Output Grouping") + .description("Specifies how the writer should output the JSON records (as an array or one object per line, e.g.) Note that if 'One Line Per Object' is " + + "selected, then Pretty Print JSON must be false.") + .allowableValues(OUTPUT_ARRAY, OUTPUT_ONELINE) + .defaultValue(OUTPUT_ARRAY.getValue()) + .required(true) + .build(); private volatile boolean prettyPrint; private volatile NullSuppression nullSuppression; + private volatile OutputGrouping outputGrouping; @Override protected List getSupportedPropertyDescriptors() { final List properties = new ArrayList<>(super.getSupportedPropertyDescriptors()); properties.add(PRETTY_PRINT_JSON); properties.add(SUPPRESS_NULLS); + properties.add(OUTPUT_GROUPING); return properties; } + @Override + protected Collection customValidate(ValidationContext context) { + final List problems = new ArrayList<>(super.customValidate(context)); + // Don't allow Pretty Print if One Line Per Object is selected + if (context.getProperty(PRETTY_PRINT_JSON).asBoolean() && context.getProperty(OUTPUT_GROUPING).getValue().equals(OUTPUT_ONELINE.getValue())) { + problems.add(new ValidationResult.Builder().input("Pretty Print").valid(false) + .explanation("Pretty Print JSON must be false when 'Output Grouping' is set to 'One Line Per Object'").build()); + } + return problems; + } + @OnEnabled public void onEnabled(final ConfigurationContext context) { prettyPrint = context.getProperty(PRETTY_PRINT_JSON).asBoolean(); @@ -90,11 +120,20 @@ public class JsonRecordSetWriter extends DateTimeTextRecordSetWriter implements suppression = NullSuppression.NEVER_SUPPRESS; } this.nullSuppression = suppression; + + String outputGroupingValue = context.getProperty(OUTPUT_GROUPING).getValue(); + final OutputGrouping grouping; + if(OUTPUT_ONELINE.getValue().equals(outputGroupingValue)) { + grouping = OutputGrouping.OUTPUT_ONELINE; + } else { + grouping = OutputGrouping.OUTPUT_ARRAY; + } + this.outputGrouping = grouping; } @Override public RecordSetWriter createWriter(final ComponentLog logger, final RecordSchema schema, final OutputStream out) throws SchemaNotFoundException, IOException { - return new WriteJsonResult(logger, schema, getSchemaAccessWriter(schema), out, prettyPrint, nullSuppression, + return new WriteJsonResult(logger, schema, getSchemaAccessWriter(schema), out, prettyPrint, nullSuppression, outputGrouping, getDateFormat().orElse(null), getTimeFormat().orElse(null), getTimestampFormat().orElse(null)); } diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonTreeReader.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonTreeReader.java index 829028436a..27833ad1ed 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonTreeReader.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/JsonTreeReader.java @@ -38,12 +38,12 @@ import org.apache.nifi.serialization.RecordReaderFactory; import org.apache.nifi.serialization.SchemaRegistryService; @Tags({"json", "tree", "record", "reader", "parser"}) -@CapabilityDescription("Parses JSON into individual Record objects. The Record that is produced will contain all top-level " - + "elements of the corresponding JSON Object. " - + "The root JSON element can be either a single element or an array of JSON elements, and each " - + "element in that array will be treated as a separate record. " - + "If the schema that is configured contains a field that is not present in the JSON, a null value will be used. If the JSON contains " - + "a field that is not present in the schema, that field will be skipped. " +@CapabilityDescription("Parses JSON into individual Record objects. While the reader expects each record " + + "to be well-formed JSON, the content of a FlowFile may consist of many records, each as a well-formed " + + "JSON array or JSON object with optional whitespace between them, such as the common 'JSON-per-line' format. " + + "If an array is encountered, each element in that array will be treated as a separate record. " + + "If the schema that is configured contains a field that is not present in the JSON, a null value will be used. If the JSON contains " + + "a field that is not present in the schema, that field will be skipped. " + "See the Usage of the Controller Service for more information and examples.") @SeeAlso(JsonPathReader.class) public class JsonTreeReader extends SchemaRegistryService implements RecordReaderFactory { diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/OutputGrouping.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/OutputGrouping.java new file mode 100644 index 0000000000..9c0aff5c5b --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/OutputGrouping.java @@ -0,0 +1,23 @@ +/* + * 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.json; + +public enum OutputGrouping { + OUTPUT_ARRAY, + OUTPUT_ONELINE +} diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/WriteJsonResult.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/WriteJsonResult.java index 41a72c7552..c91768e72b 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/WriteJsonResult.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/json/WriteJsonResult.java @@ -46,6 +46,7 @@ import org.apache.nifi.serialization.record.util.DataTypeUtils; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.util.MinimalPrettyPrinter; public class WriteJsonResult extends AbstractRecordSetWriter implements RecordSetWriter, RawRecordWriter { private final ComponentLog logger; @@ -53,19 +54,23 @@ public class WriteJsonResult extends AbstractRecordSetWriter implements RecordSe private final RecordSchema recordSchema; private final JsonFactory factory = new JsonFactory(); private final JsonGenerator generator; + private final OutputStream out; private final NullSuppression nullSuppression; + private final OutputGrouping outputGrouping; private final Supplier LAZY_DATE_FORMAT; private final Supplier LAZY_TIME_FORMAT; private final Supplier LAZY_TIMESTAMP_FORMAT; public WriteJsonResult(final ComponentLog logger, final RecordSchema recordSchema, final SchemaAccessWriter schemaAccess, final OutputStream out, final boolean prettyPrint, - final NullSuppression nullSuppression, final String dateFormat, final String timeFormat, final String timestampFormat) throws IOException { + final NullSuppression nullSuppression, final OutputGrouping outputGrouping, final String dateFormat, final String timeFormat, final String timestampFormat) throws IOException { super(out); this.logger = logger; this.recordSchema = recordSchema; this.schemaAccess = schemaAccess; + this.out = out; this.nullSuppression = nullSuppression; + this.outputGrouping = outputGrouping; final DateFormat df = dateFormat == null ? null : DataTypeUtils.getDateFormat(dateFormat); final DateFormat tf = timeFormat == null ? null : DataTypeUtils.getDateFormat(timeFormat); @@ -78,6 +83,9 @@ public class WriteJsonResult extends AbstractRecordSetWriter implements RecordSe this.generator = factory.createJsonGenerator(out); if (prettyPrint) { generator.useDefaultPrettyPrinter(); + } else if (OutputGrouping.OUTPUT_ONELINE.equals(outputGrouping)) { + // Use a minimal pretty printer with a newline object separator, will output one JSON object per line + generator.setPrettyPrinter(new MinimalPrettyPrinter("\n")); } } @@ -87,12 +95,16 @@ public class WriteJsonResult extends AbstractRecordSetWriter implements RecordSe final OutputStream out = getOutputStream(); schemaAccess.writeHeader(recordSchema, out); - generator.writeStartArray(); + if (outputGrouping == OutputGrouping.OUTPUT_ARRAY) { + generator.writeStartArray(); + } } @Override protected Map onFinishRecordSet() throws IOException { - generator.writeEndArray(); + if (outputGrouping == OutputGrouping.OUTPUT_ARRAY) { + generator.writeEndArray(); + } return schemaAccess.getAttributes(recordSchema); } @@ -191,8 +203,6 @@ public class WriteJsonResult extends AbstractRecordSetWriter implements RecordSe } } - - endTask.apply(generator); } catch (final Exception e) { logger.error("Failed to write {} with schema {} as a JSON Object due to {}", new Object[] {record, record.getSchema(), e.toString(), e}); diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestJsonPathRowRecordReader.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestJsonPathRowRecordReader.java index 11e2828820..5e0da9176f 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestJsonPathRowRecordReader.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestJsonPathRowRecordReader.java @@ -118,6 +118,32 @@ public class TestJsonPathRowRecordReader { } } + @Test + public void testReadOneLine() throws IOException, MalformedRecordException { + final RecordSchema schema = new SimpleRecordSchema(getDefaultFields()); + + try (final InputStream in = new FileInputStream(new File("src/test/resources/json/bank-account-oneline.json")); + final JsonPathRowRecordReader reader = new JsonPathRowRecordReader(allJsonPaths, schema, in, Mockito.mock(ComponentLog.class), dateFormat, timeFormat, timestampFormat)) { + + final List fieldNames = schema.getFieldNames(); + final List expectedFieldNames = Arrays.asList(new String[] {"id", "name", "balance", "address", "city", "state", "zipCode", "country"}); + assertEquals(expectedFieldNames, fieldNames); + + final List dataTypes = schema.getDataTypes().stream().map(dt -> dt.getFieldType()).collect(Collectors.toList()); + final List expectedTypes = Arrays.asList(new RecordFieldType[] {RecordFieldType.INT, RecordFieldType.STRING, + RecordFieldType.DOUBLE, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING}); + assertEquals(expectedTypes, dataTypes); + + final Object[] firstRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {1, "John Doe", 4750.89, "123 My Street", "My City", "MS", "11111", "USA"}, firstRecordValues); + + final Object[] secondRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {2, "Jane Doe", 4820.09, "321 Your Street", "Your City", "NY", "33333", "USA"}, secondRecordValues); + + assertNull(reader.nextRecord()); + } + } + @Test public void testSingleJsonElement() throws IOException, MalformedRecordException { final RecordSchema schema = new SimpleRecordSchema(getDefaultFields()); diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestJsonTreeRowRecordReader.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestJsonTreeRowRecordReader.java index e898edde3b..73abdff418 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestJsonTreeRowRecordReader.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestJsonTreeRowRecordReader.java @@ -161,6 +161,123 @@ public class TestJsonTreeRowRecordReader { } } + @Test + public void testReadOneLinePerJSON() throws IOException, MalformedRecordException { + final RecordSchema schema = new SimpleRecordSchema(getDefaultFields()); + + try (final InputStream in = new FileInputStream(new File("src/test/resources/json/bank-account-oneline.json")); + final JsonTreeRowRecordReader reader = new JsonTreeRowRecordReader(in, Mockito.mock(ComponentLog.class), schema, dateFormat, timeFormat, timestampFormat)) { + + final List fieldNames = schema.getFieldNames(); + final List expectedFieldNames = Arrays.asList(new String[] {"id", "name", "balance", "address", "city", "state", "zipCode", "country"}); + assertEquals(expectedFieldNames, fieldNames); + + final List dataTypes = schema.getDataTypes().stream().map(dt -> dt.getFieldType()).collect(Collectors.toList()); + final List expectedTypes = Arrays.asList(new RecordFieldType[] {RecordFieldType.INT, RecordFieldType.STRING, + RecordFieldType.DOUBLE, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING}); + assertEquals(expectedTypes, dataTypes); + + final Object[] firstRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {1, "John Doe", 4750.89, "123 My Street", "My City", "MS", "11111", "USA"}, firstRecordValues); + + final Object[] secondRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {2, "Jane Doe", 4820.09, "321 Your Street", "Your City", "NY", "33333", "USA"}, secondRecordValues); + + assertNull(reader.nextRecord()); + } + } + + @Test + public void testReadMultilineJSON() throws IOException, MalformedRecordException { + final RecordSchema schema = new SimpleRecordSchema(getDefaultFields()); + + try (final InputStream in = new FileInputStream(new File("src/test/resources/json/bank-account-multiline.json")); + final JsonTreeRowRecordReader reader = new JsonTreeRowRecordReader(in, Mockito.mock(ComponentLog.class), schema, dateFormat, timeFormat, timestampFormat)) { + + final List fieldNames = schema.getFieldNames(); + final List expectedFieldNames = Arrays.asList(new String[] {"id", "name", "balance", "address", "city", "state", "zipCode", "country"}); + assertEquals(expectedFieldNames, fieldNames); + + final List dataTypes = schema.getDataTypes().stream().map(dt -> dt.getFieldType()).collect(Collectors.toList()); + final List expectedTypes = Arrays.asList(new RecordFieldType[] {RecordFieldType.INT, RecordFieldType.STRING, + RecordFieldType.DOUBLE, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING}); + assertEquals(expectedTypes, dataTypes); + + final Object[] firstRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {1, "John Doe", 4750.89, "123 My Street", "My City", "MS", "11111", "USA"}, firstRecordValues); + + final Object[] secondRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {2, "Jane Doe", 4820.09, "321 Your Street", "Your City", "NY", "33333", "USA"}, secondRecordValues); + + assertNull(reader.nextRecord()); + } + } + + @Test + public void testReadMultilineArrays() throws IOException, MalformedRecordException { + final RecordSchema schema = new SimpleRecordSchema(getDefaultFields()); + + try (final InputStream in = new FileInputStream(new File("src/test/resources/json/bank-account-multiarray.json")); + final JsonTreeRowRecordReader reader = new JsonTreeRowRecordReader(in, Mockito.mock(ComponentLog.class), schema, dateFormat, timeFormat, timestampFormat)) { + + final List fieldNames = schema.getFieldNames(); + final List expectedFieldNames = Arrays.asList(new String[] {"id", "name", "balance", "address", "city", "state", "zipCode", "country"}); + assertEquals(expectedFieldNames, fieldNames); + + final List dataTypes = schema.getDataTypes().stream().map(dt -> dt.getFieldType()).collect(Collectors.toList()); + final List expectedTypes = Arrays.asList(new RecordFieldType[] {RecordFieldType.INT, RecordFieldType.STRING, + RecordFieldType.DOUBLE, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING}); + assertEquals(expectedTypes, dataTypes); + + final Object[] firstRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {1, "John Doe", 4750.89, "123 My Street", "My City", "MS", "11111", "USA"}, firstRecordValues); + + final Object[] secondRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {2, "Jane Doe", 4820.09, "321 Your Street", "Your City", "NY", "33333", "USA"}, secondRecordValues); + + final Object[] thirdRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {3, "Maria Doe", 4750.89, "123 My Street", "My City", "ME", "11111", "USA"}, thirdRecordValues); + + final Object[] fourthRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {4, "Xi Doe", 4820.09, "321 Your Street", "Your City", "NV", "33333", "USA"}, fourthRecordValues); + + assertNull(reader.nextRecord()); + } + } + + @Test + public void testReadMixedJSON() throws IOException, MalformedRecordException { + final RecordSchema schema = new SimpleRecordSchema(getDefaultFields()); + + try (final InputStream in = new FileInputStream(new File("src/test/resources/json/bank-account-mixed.json")); + final JsonTreeRowRecordReader reader = new JsonTreeRowRecordReader(in, Mockito.mock(ComponentLog.class), schema, dateFormat, timeFormat, timestampFormat)) { + + final List fieldNames = schema.getFieldNames(); + final List expectedFieldNames = Arrays.asList(new String[] {"id", "name", "balance", "address", "city", "state", "zipCode", "country"}); + assertEquals(expectedFieldNames, fieldNames); + + final List dataTypes = schema.getDataTypes().stream().map(dt -> dt.getFieldType()).collect(Collectors.toList()); + final List expectedTypes = Arrays.asList(new RecordFieldType[] {RecordFieldType.INT, RecordFieldType.STRING, + RecordFieldType.DOUBLE, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING, RecordFieldType.STRING}); + assertEquals(expectedTypes, dataTypes); + + final Object[] firstRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {1, "John Doe", 4750.89, "123 My Street", "My City", "MS", "11111", "USA"}, firstRecordValues); + + final Object[] secondRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {2, "Jane Doe", 4820.09, "321 Your Street", "Your City", "NY", "33333", "USA"}, secondRecordValues); + + final Object[] thirdRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {3, "Maria Doe", 4750.89, "123 My Street", "My City", "ME", "11111", "USA"}, thirdRecordValues); + + final Object[] fourthRecordValues = reader.nextRecord().getValues(); + Assert.assertArrayEquals(new Object[] {4, "Xi Doe", 4820.09, "321 Your Street", "Your City", "NV", "33333", "USA"}, fourthRecordValues); + + + assertNull(reader.nextRecord()); + } + } + @Test public void testReadRawRecordIncludesFieldsNotInSchema() throws IOException, MalformedRecordException { final List fields = new ArrayList<>(); diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestWriteJsonResult.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestWriteJsonResult.java index af19394d6f..88f3ed94ff 100644 --- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestWriteJsonResult.java +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/json/TestWriteJsonResult.java @@ -104,7 +104,8 @@ public class TestWriteJsonResult { final RecordSet rs = RecordSet.of(schema, record); try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, true, - NullSuppression.NEVER_SUPPRESS, RecordFieldType.DATE.getDefaultFormat(), RecordFieldType.TIME.getDefaultFormat(), RecordFieldType.TIMESTAMP.getDefaultFormat())) { + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, RecordFieldType.DATE.getDefaultFormat(), + RecordFieldType.TIME.getDefaultFormat(), RecordFieldType.TIMESTAMP.getDefaultFormat())) { writer.write(rs); } @@ -141,7 +142,8 @@ public class TestWriteJsonResult { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, true, - NullSuppression.NEVER_SUPPRESS, RecordFieldType.DATE.getDefaultFormat(), RecordFieldType.TIME.getDefaultFormat(), RecordFieldType.TIMESTAMP.getDefaultFormat())) { + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, RecordFieldType.DATE.getDefaultFormat(), + RecordFieldType.TIME.getDefaultFormat(), RecordFieldType.TIMESTAMP.getDefaultFormat())) { writer.write(rs); } @@ -172,7 +174,8 @@ public class TestWriteJsonResult { final RecordSet rs = RecordSet.of(schema, record); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.NEVER_SUPPRESS, null, null, null)) { + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.write(rs); } @@ -196,7 +199,8 @@ public class TestWriteJsonResult { final Record record = new MapRecord(schema, values); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.NEVER_SUPPRESS, null, null, null)) { + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.writeRecord(record); writer.finishRecordSet(); @@ -222,7 +226,8 @@ public class TestWriteJsonResult { final Record record = new MapRecord(schema, values); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.NEVER_SUPPRESS, null, null, null)) { + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.writeRawRecord(record); writer.finishRecordSet(); @@ -248,7 +253,8 @@ public class TestWriteJsonResult { final Record record = new MapRecord(schema, values); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.NEVER_SUPPRESS, null, null, null)) { + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.writeRecord(record); writer.finishRecordSet(); @@ -274,7 +280,8 @@ public class TestWriteJsonResult { final Record record = new MapRecord(schema, values); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.NEVER_SUPPRESS, null, null, null)) { + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.writeRawRecord(record); writer.finishRecordSet(); @@ -301,7 +308,8 @@ public class TestWriteJsonResult { final Record record = new MapRecord(schema, values); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.NEVER_SUPPRESS, null, null, null)) { + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.writeRecord(record); writer.finishRecordSet(); @@ -328,7 +336,8 @@ public class TestWriteJsonResult { final Record record = new MapRecord(schema, values); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.NEVER_SUPPRESS, null, null, null)) { + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.writeRawRecord(record); writer.finishRecordSet(); @@ -354,7 +363,8 @@ public class TestWriteJsonResult { final Record recordWithMissingName = new MapRecord(schema, values); final ByteArrayOutputStream baos = new ByteArrayOutputStream(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.NEVER_SUPPRESS, null, null, null)) { + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.write(recordWithMissingName); writer.finishRecordSet(); @@ -364,7 +374,8 @@ public class TestWriteJsonResult { baos.reset(); try ( - final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.ALWAYS_SUPPRESS, null, null, null)) { + final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.ALWAYS_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.write(recordWithMissingName); writer.finishRecordSet(); @@ -373,7 +384,8 @@ public class TestWriteJsonResult { assertEquals("[{\"id\":\"1\"}]", new String(baos.toByteArray(), StandardCharsets.UTF_8)); baos.reset(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.SUPPRESS_MISSING, null, null, + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.SUPPRESS_MISSING, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.write(recordWithMissingName); @@ -387,7 +399,8 @@ public class TestWriteJsonResult { final Record recordWithNullValue = new MapRecord(schema, values); baos.reset(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.NEVER_SUPPRESS, null, null, null)) { + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.write(recordWithNullValue); writer.finishRecordSet(); @@ -397,7 +410,8 @@ public class TestWriteJsonResult { baos.reset(); try ( - final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.ALWAYS_SUPPRESS, null, null, null)) { + final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.ALWAYS_SUPPRESS, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.write(recordWithNullValue); writer.finishRecordSet(); @@ -406,7 +420,8 @@ public class TestWriteJsonResult { assertEquals("[{\"id\":\"1\"}]", new String(baos.toByteArray(), StandardCharsets.UTF_8)); baos.reset(); - try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, NullSuppression.SUPPRESS_MISSING, null, null, + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.SUPPRESS_MISSING, OutputGrouping.OUTPUT_ARRAY, null, null, null)) { writer.beginRecordSet(); writer.write(recordWithNullValue); @@ -416,4 +431,44 @@ public class TestWriteJsonResult { assertEquals("[{\"id\":\"1\",\"name\":null}]", new String(baos.toByteArray(), StandardCharsets.UTF_8)); } + + @Test + public void testOnelineOutput() throws IOException { + final Map values1 = new HashMap<>(); + values1.put("timestamp", new java.sql.Timestamp(37293723L)); + values1.put("time", new java.sql.Time(37293723L)); + values1.put("date", new java.sql.Date(37293723L)); + + final List fields1 = new ArrayList<>(); + fields1.add(new RecordField("timestamp", RecordFieldType.TIMESTAMP.getDataType())); + fields1.add(new RecordField("time", RecordFieldType.TIME.getDataType())); + fields1.add(new RecordField("date", RecordFieldType.DATE.getDataType())); + + final RecordSchema schema = new SimpleRecordSchema(fields1); + + final Record record1 = new MapRecord(schema, values1); + + final Map values2 = new HashMap<>(); + values2.put("timestamp", new java.sql.Timestamp(37293999L)); + values2.put("time", new java.sql.Time(37293999L)); + values2.put("date", new java.sql.Date(37293999L)); + + + final Record record2 = new MapRecord(schema, values2); + + final RecordSet rs = RecordSet.of(schema, record1, record2); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (final WriteJsonResult writer = new WriteJsonResult(Mockito.mock(ComponentLog.class), schema, new SchemaNameAsAttribute(), baos, false, + NullSuppression.NEVER_SUPPRESS, OutputGrouping.OUTPUT_ONELINE, null, null, null)) { + writer.write(rs); + } + + final byte[] data = baos.toByteArray(); + + final String expected = "{\"timestamp\":37293723,\"time\":37293723,\"date\":37293723}\n{\"timestamp\":37293999,\"time\":37293999,\"date\":37293999}"; + + final String output = new String(data, StandardCharsets.UTF_8); + assertEquals(expected, output); + } } diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-mixed.json b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-mixed.json new file mode 100644 index 0000000000..6c6def4463 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-mixed.json @@ -0,0 +1,32 @@ +[ + { + "id": 1, + "name": "John Doe", + "balance": 4750.89, + "address": "123 My Street", + "city": "My City", + "state": "MS", + "zipCode": "11111", + "country": "USA" + }, { + "id": 2, + "name": "Jane Doe", + "balance": 4820.09, + "address": "321 Your Street", + "city": "Your City", + "state": "NY", + "zipCode": "33333", + "country": "USA" + } +] +{ "id": 3, "name": "Maria Doe", "balance": 4750.89, "address": "123 My Street", "city": "My City", "state": "ME", "zipCode": "11111", "country": "USA" } +{ + "id": 4, + "name": "Xi Doe", + "balance": 4820.09, + "address": "321 Your Street", + "city": "Your City", + "state": "NV", + "zipCode": "33333", + "country": "USA" +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-multiarray.json b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-multiarray.json new file mode 100644 index 0000000000..a77dfa6a1b --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-multiarray.json @@ -0,0 +1,42 @@ +[ + { + "id": 1, + "name": "John Doe", + "balance": 4750.89, + "address": "123 My Street", + "city": "My City", + "state": "MS", + "zipCode": "11111", + "country": "USA" + }, { + "id": 2, + "name": "Jane Doe", + "balance": 4820.09, + "address": "321 Your Street", + "city": "Your City", + "state": "NY", + "zipCode": "33333", + "country": "USA" + } +] +[ + { + "id": 3, + "name": "Maria Doe", + "balance": 4750.89, + "address": "123 My Street", + "city": "My City", + "state": "ME", + "zipCode": "11111", + "country": "USA" + }, { + "id": 4, + "name": "Xi Doe", + "balance": 4820.09, + "address": "321 Your Street", + "city": "Your City", + "state": "NV", + "zipCode": "33333", + "country": "USA" +} +] \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-multiline.json b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-multiline.json new file mode 100644 index 0000000000..472a0a0e20 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-multiline.json @@ -0,0 +1,19 @@ + { + "id": 1, + "name": "John Doe", + "balance": 4750.89, + "address": "123 My Street", + "city": "My City", + "state": "MS", + "zipCode": "11111", + "country": "USA" + } { + "id": 2, + "name": "Jane Doe", + "balance": 4820.09, + "address": "321 Your Street", + "city": "Your City", + "state": "NY", + "zipCode": "33333", + "country": "USA" + } diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-oneline.json b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-oneline.json new file mode 100644 index 0000000000..6a0dbd8b81 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/resources/json/bank-account-oneline.json @@ -0,0 +1,2 @@ +{ "id": 1, "name": "John Doe", "balance": 4750.89, "address": "123 My Street", "city": "My City", "state": "MS", "zipCode": "11111", "country": "USA" } +{ "id": 2, "name": "Jane Doe", "balance": 4820.09, "address": "321 Your Street", "city": "Your City", "state": "NY", "zipCode": "33333", "country": "USA"}