From 7ed3aa4387d0fd36822c7f4e895185f9d031ed53 Mon Sep 17 00:00:00 2001 From: Simon Templer Date: Wed, 16 Aug 2023 16:45:26 +0200 Subject: [PATCH] feat(json): basic support for reading complex properties ING-3128 --- .../instance/groovy/InstanceBuilder.groovy | 10 +- .../io/json/test/JsonToInstanceTest.groovy | 189 +++++++++++++++++- .../json/internal/JsonInstanceBuilder.groovy | 46 ++++- 3 files changed, 236 insertions(+), 9 deletions(-) diff --git a/common/plugins/eu.esdihumboldt.hale.common.instance.groovy/src/eu/esdihumboldt/hale/common/instance/groovy/InstanceBuilder.groovy b/common/plugins/eu.esdihumboldt.hale.common.instance.groovy/src/eu/esdihumboldt/hale/common/instance/groovy/InstanceBuilder.groovy index 31a3ee4298..0942a3aec7 100644 --- a/common/plugins/eu.esdihumboldt.hale.common.instance.groovy/src/eu/esdihumboldt/hale/common/instance/groovy/InstanceBuilder.groovy +++ b/common/plugins/eu.esdihumboldt.hale.common.instance.groovy/src/eu/esdihumboldt/hale/common/instance/groovy/InstanceBuilder.groovy @@ -393,8 +393,14 @@ class InstanceBuilder extends BuilderBase { value = params[0] } - // create instance - return createInstanceAndValue(property.propertyType, value) + if (value instanceof Instance) { + // allow providing an instance as value + return value + } + else { + // create instance + return createInstanceAndValue(property.propertyType, value) + } } else { // normal property value diff --git a/io/plugins/eu.esdihumboldt.hale.io.json.test/src/eu/esdihumboldt/hale/io/json/test/JsonToInstanceTest.groovy b/io/plugins/eu.esdihumboldt.hale.io.json.test/src/eu/esdihumboldt/hale/io/json/test/JsonToInstanceTest.groovy index ed3c1a7617..2a092d84b8 100644 --- a/io/plugins/eu.esdihumboldt.hale.io.json.test/src/eu/esdihumboldt/hale/io/json/test/JsonToInstanceTest.groovy +++ b/io/plugins/eu.esdihumboldt.hale.io.json.test/src/eu/esdihumboldt/hale/io/json/test/JsonToInstanceTest.groovy @@ -451,11 +451,196 @@ class JsonToInstanceTest { } as Consumer) } + /** + * Test reading a Json object with nested properties. + */ + @Test + void testReadComplex() { + TypeDefinition type + Schema schema = new SchemaBuilder().schema { + def nameType = NameType { + en(String) + de(String) + } + + type = TestType { + id(String) + name(nameType) + } + } + + def json = '''{ + "id": "test", + "name": { + "en": "corn", + "de": "Mais" + } +}''' + + def instance = readInstance(json, type, null, true) + + def id = instance.p.id.value() + assertThat(id).isEqualTo('test') + + def name = instance.p.name.first() + assertThat(name).isInstanceOf(Instance) + + assertThat(name.p.en.value()).isEqualTo('corn') + assertThat(name.p.de.value()).isEqualTo('Mais') + } + + /** + * Test reading a Json object with multiple layers of nested properties. + */ + @Test + void testReadComplexNested() { + TypeDefinition type + Schema schema = new SchemaBuilder().schema { + type = TestType { + product { + name { + en(String) + de(String) + } + } + } + } + + def json = '''{ + "product": { + "name": { + "en": "corn", + "de": "Mais" + } + } +}''' + + def instance = readInstance(json, type, null, true) + + def product = instance.p.product.first() + assertThat(product).isInstanceOf(Instance) + + def name = product.p.name.first() + assertThat(name).isInstanceOf(Instance) + + assertThat(name.p.en.value()).isEqualTo('corn') + assertThat(name.p.de.value()).isEqualTo('Mais') + } + + /** + * Test reading a Json object with a simple array property. + */ + @Test + void testReadSimpleArray() { + TypeDefinition type + Schema schema = new SchemaBuilder().schema { + type = TestType { + item(cardinality: '1..n', String) + } + } + + def json = '''{ + "item": [ + "foo", + "bar" + ] +}''' + + def instance = readInstance(json, type, null, true) + + List values = instance.p.item.values() + assertThat(values).containsExactly('foo', 'bar') + } + + /** + * Test reading a Json object with multiple layers of nested properties. + */ + @Test + void testReadArrayNested() { + TypeDefinition type + Schema schema = new SchemaBuilder().schema { + type = TestType { + product(cardinality: '0..n') { + name { + en(String) + de(String) + } + } + } + } + + def json = '''{ + "product": [ + { + "name": { + "en": "corn", + "de": "Mais" + } + }, + { + "name": { + "en": "cucumber", + "de": "Grüne Banane" + } + } + ] +}''' + + def instance = readInstance(json, type, null, true) + + List products = instance.p.product.list() + assertThat(products).hasSize(2) + + assertThat(products[0].p.name.en.value()).isEqualTo('corn') + assertThat(products[0].p.name.de.value()).isEqualTo('Mais') + + assertThat(products[1].p.name.en.value()).isEqualTo('cucumber') + assertThat(products[1].p.name.de.value()).isEqualTo('Grüne Banane') + } + + /** + * Test reading a Json object with multiple layers of nested properties. + */ + @Test + void testReadSimpleChoice() { + TypeDefinition type + Schema schema = new SchemaBuilder().schema { + type = TestType { + product(cardinality: '0..n') { + _(choice: true) { + name_en(String) + name_de(String) + } + } + } + } + + def json = '''{ + "product": [ + { + "name_de": "Mais" + }, + { + "name_en": "cucumber" + } + ] +}''' + + def instance = readInstance(json, type, null, true) + + List products = instance.p.product.list() + assertThat(products).hasSize(2) + + assertThat(products[0].p.name_de.value()).isEqualTo('Mais') + assertThat(products[0].p.name_en.value()).isNull() + + assertThat(products[1].p.name_de.value()).isNull() + assertThat(products[1].p.name_en.value()).isEqualTo('cucumber') + } + /* * TODO add additional tests for specific cases: - * - complex properties / arrays * - any specific bindings that should be supported? - * Later: */ // helper methods diff --git a/io/plugins/eu.esdihumboldt.hale.io.json/src/eu/esdihumboldt/hale/io/json/internal/JsonInstanceBuilder.groovy b/io/plugins/eu.esdihumboldt.hale.io.json/src/eu/esdihumboldt/hale/io/json/internal/JsonInstanceBuilder.groovy index ae3297c6fe..1287c2249e 100644 --- a/io/plugins/eu.esdihumboldt.hale.io.json/src/eu/esdihumboldt/hale/io/json/internal/JsonInstanceBuilder.groovy +++ b/io/plugins/eu.esdihumboldt.hale.io.json/src/eu/esdihumboldt/hale/io/json/internal/JsonInstanceBuilder.groovy @@ -14,6 +14,8 @@ */ package eu.esdihumboldt.hale.io.json.internal +import java.util.Map.Entry + import javax.xml.namespace.QName import org.geotools.geojson.geom.GeometryJSON @@ -65,7 +67,11 @@ class JsonInstanceBuilder { this.geometryJson = new GeometryJSON() this.defaultCrs = defaultCrs - builder = new InstanceBuilder( + builder = createBuilder() + } + + protected InstanceBuilder createBuilder() { + new InstanceBuilder( strictBinding: false, strictValueFlags: false // allow setting values even if no value is expected (mainly for use with XML schema and geometries) ) @@ -81,6 +87,19 @@ class JsonInstanceBuilder { */ public Instance buildInstance(TypeDefinition type, ObjectNode geom, Map properties) { + buildInstance(builder, type, geom, properties) + } + + /** + * Build an instance of the given type. + * + * @param type the type definition or null if without schema + * @param geom a dedicated (GeoJson) geometry object or null + * @param properties map of properties or null + * @return the created instance + */ + protected Instance buildInstance(InstanceBuilder builder, TypeDefinition type, ObjectNode geom, + Map properties) { if (type == null) { // TODO implement schema-less mode throw new UnsupportedOperationException("Schema-less mode not implemented yet"); @@ -123,7 +142,7 @@ class JsonInstanceBuilder { else { if (value.isArray()) { // add each value - def iterator = value.getElements() + def iterator = value.elements() while (iterator.hasNext()) { JsonNode item = iterator.next() def itemValue = translateValue(item, property) @@ -221,9 +240,26 @@ class JsonInstanceBuilder { } } else { - //FIXME handle complex properties? - //XXX for now ignore - null + // handle complex properties + + ObjectNode fields = (value != null && value.isObject()) ? (ObjectNode) value + : null; + Map properties = new HashMap<>(); + if (fields != null) { + Iterator> it = fields.fields(); + while (it.hasNext()) { + Entry entry = it.next(); + properties.put(entry.getKey(), entry.getValue()); + } + + // build instance with new builder instance + buildInstance(createBuilder(), property.propertyType, null, properties) + } + else { + // not an object + //TODO throw/report error if not null? + null + } } }