Skip to content

Commit

Permalink
[GEOS-9214] GeoJSON complex encoder encodes separate FeaturePropertyT…
Browse files Browse the repository at this point in the history
…ype as a single array
  • Loading branch information
aaime committed May 15, 2019
1 parent 9328740 commit 5e4489f
Show file tree
Hide file tree
Showing 4 changed files with 94 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ public void testGetGeoJsonResponseWfs20() throws Exception {
assertThat(phone.size(), is(1));
assertFalse(phone.has("value"));
assertThat(phone.get("@timeZone"), is(""));
// check the linked features have been kept separate despite the shared element type
// A and B have max multiplicity > 1, C is ensured to be single
JSONObject featureLinkA = station.getJSONArray("featureLinkA").getJSONObject(0);
assertEquals("http://www.geoserver.org/featureA", featureLinkA.getString("@href"));
JSONObject featureLinkB = station.getJSONArray("featureLinkB").getJSONObject(0);
assertEquals("http://www.geoserver.org/featureB", featureLinkB.getString("@href"));
JSONObject featureLinkC = station.getJSONObject("featureLinkC");
assertEquals("http://www.geoserver.org/featureC", featureLinkC.getString("@href"));
}

/** Helper method that station 1 exists and was correctly encoded in the GeoJSON response. */
Expand Down Expand Up @@ -138,7 +146,6 @@ private void checkStation1Exists(JSON geoJson) {
public void testSimpleContentTimeEncoding() throws Exception {
String path = "wfs?request=GetFeature&typename=gsmlbh:Borehole&outputFormat=json";
JSON json = getAsJSON(path);
print(json);
JSONObject properties = getFeaturePropertiesById(json, "borehole.GA.17322");
assertThat(properties, is(notNullValue()));
JSONObject timeInstant =
Expand All @@ -160,7 +167,6 @@ public void testSimpleContentTimeEncoding() throws Exception {
public void testOneDimensionalEncoding() throws Exception {
String path = "wfs?request=GetFeature&typename=gsmlbh:Borehole&outputFormat=json";
JSON json = getAsJSON(path);
print(json);
JSONObject properties = getFeaturePropertiesById(json, "borehole.GA.17322");
assertThat(properties, is(notNullValue()));
JSONObject samplingLocation =
Expand All @@ -180,7 +186,6 @@ public void testOneDimensionalEncoding() throws Exception {
public void testNestedFeatureEncoding() throws Exception {
String path = "wfs?request=GetFeature&typename=gsml:Borehole&outputFormat=json";
JSON json = getAsJSON(path);
print(json);
JSONObject properties = getFeaturePropertiesById(json, "BOREHOLE.WTB5");
assertThat(properties, is(notNullValue()));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,51 @@
<value>strConcat('http://www.stations.org/', ID)</value>
</ClientProperty>
</AttributeMapping>
<AttributeMapping>
<targetAttribute>st_${GML_PREFIX}:featureLinkA</targetAttribute>
<ClientProperty>
<name>xlink:role</name>
<value>'http://www.geosciml.org/geosciml/2.0/doc/GeoSciML/GeologicUnit/GeologicUnit.html'</value>
</ClientProperty>
<ClientProperty>
<name>xlink:href</name>
<value>'http://www.geoserver.org/featureA'</value>
</ClientProperty>
<ClientProperty>
<name>xlink:title</name>
<value>'The A feature'</value>
</ClientProperty>
</AttributeMapping>
<AttributeMapping>
<targetAttribute>st_${GML_PREFIX}:featureLinkB</targetAttribute>
<ClientProperty>
<name>xlink:role</name>
<value>'http://www.geosciml.org/geosciml/2.0/doc/GeoSciML/GeologicUnit/GeologicUnit.html'</value>
</ClientProperty>
<ClientProperty>
<name>xlink:href</name>
<value>'http://www.geoserver.org/featureB'</value>
</ClientProperty>
<ClientProperty>
<name>xlink:title</name>
<value>'The B feature'</value>
</ClientProperty>
</AttributeMapping>
<AttributeMapping>
<targetAttribute>st_${GML_PREFIX}:featureLinkC</targetAttribute>
<ClientProperty>
<name>xlink:role</name>
<value>'http://www.geosciml.org/geosciml/2.0/doc/GeoSciML/GeologicUnit/GeologicUnit.html'</value>
</ClientProperty>
<ClientProperty>
<name>xlink:href</name>
<value>'http://www.geoserver.org/featureC'</value>
</ClientProperty>
<ClientProperty>
<name>xlink:title</name>
<value>'The C feature'</value>
</ClientProperty>
</AttributeMapping>
</attributeMappings>
</FeatureTypeMapping>
</typeMappings>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
<xs:element name="location" type="gml:GeometryPropertyType"/>
<xs:element maxOccurs="unbounded" minOccurs="0" name="measurements"
type="st:MeasurementPropertyType"/>
<xs:element name="featureLinkA" type="gml:FeaturePropertyType" maxOccurs="unbounded" nillable="true"/>
<xs:element name="featureLinkB" type="gml:FeaturePropertyType" maxOccurs="unbounded" nillable="true"/>
<xs:element name="featureLinkC" type="gml:FeaturePropertyType" nillable="true"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
Expand Down
100 changes: 38 additions & 62 deletions src/wfs/src/main/java/org/geoserver/wfs/json/ComplexGeoJsonWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.StreamSupport;
import org.checkerframework.checker.units.qual.K;
import org.geotools.data.DataStoreFinder;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
Expand All @@ -28,7 +30,6 @@
import org.opengis.feature.Feature;
import org.opengis.feature.Property;
import org.opengis.feature.type.AttributeType;
import org.opengis.feature.type.ComplexType;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.feature.type.Name;
import org.opengis.feature.type.PropertyDescriptor;
Expand Down Expand Up @@ -196,31 +197,31 @@ private Property encodeGeometry(Feature feature) {
private void encodeProperties(
Property geometryAttribute, PropertyType parentType, Collection<Property> properties) {
// index all the feature available properties by their type
Map<PropertyType, List<Property>> index =
indexPropertiesByType(geometryAttribute, properties);
for (Map.Entry<PropertyType, List<Property>> entry : index.entrySet()) {
Map<PropertyDescriptor, List<Property>> index =
indexPropertiesByDescriptor(geometryAttribute, properties);
for (Map.Entry<PropertyDescriptor, List<Property>> entry : index.entrySet()) {
// encode properties per type
encodePropertiesByType(parentType, entry.getKey(), entry.getValue());
}
}

/** Index the provided properties by their type, geometry property will be ignored. */
private Map<PropertyType, List<Property>> indexPropertiesByType(
private Map<PropertyDescriptor, List<Property>> indexPropertiesByDescriptor(
Property geometryAttribute, Collection<Property> properties) {
Map<PropertyType, List<Property>> index = new HashMap<>();
Map<PropertyDescriptor, List<Property>> index = new LinkedHashMap<>();
for (Property property : properties) {
if (geometryAttribute != null && geometryAttribute.equals(property)) {
// ignore the geometry attribute that should have been encoded already
continue;
}
// update the index with the current property
List<Property> propertiesWithSameType = index.get(property.getType());
if (propertiesWithSameType == null) {
List<Property> propertiesWithSameDescriptor = index.get(property.getDescriptor());
if (propertiesWithSameDescriptor == null) {
// first time we see a property fo this type
propertiesWithSameType = new ArrayList<>();
index.put(property.getType(), propertiesWithSameType);
propertiesWithSameDescriptor = new ArrayList<>();
index.put(property.getDescriptor(), propertiesWithSameDescriptor);
}
propertiesWithSameType.add(property);
propertiesWithSameDescriptor.add(property);
}
return index;
}
Expand All @@ -230,43 +231,44 @@ private Map<PropertyType, List<Property>> indexPropertiesByType(
* properties should be encoded as a list or as elements that appear multiple times.
*/
private void encodePropertiesByType(
PropertyType parentType, PropertyType type, List<Property> properties) {
PropertyDescriptor multipleType = isMultipleType(parentType, type);
if (multipleType == null) {
// simple JSON objects
properties.forEach(this::encodeProperty);
} else {
// possible chained features that need to be encoded as a list
List<Feature> chainedFeatures = getChainedFeatures(properties);
if (chainedFeatures == null || chainedFeatures.isEmpty()) {
// let's check if we are in the presence of linked features
List<Map<NameImpl, String>> linkedFeatures = getLinkedFeatures(properties);
if (!linkedFeatures.isEmpty()) {
// encode linked features
encodeLinkedFeatures(multipleType.getName().getLocalPart(), linkedFeatures);
} else {
// no chained or linked features just encode each property
properties.forEach(this::encodeProperty);
}
PropertyType parentType, PropertyDescriptor descriptor, List<Property> properties) {
// possible chained features that need to be encoded as a list
List<Feature> chainedFeatures = getChainedFeatures(properties);
if (chainedFeatures == null
|| chainedFeatures.isEmpty()
|| descriptor.getMaxOccurs() == 1) {
// let's check if we are in the presence of linked features
List<Map<NameImpl, String>> linkedFeatures = getLinkedFeatures(properties);
if (!linkedFeatures.isEmpty()) {
// encode linked features
encodeLinkedFeatures(descriptor, linkedFeatures);
} else {
// chained features so we need to encode the chained features as an array
encodeChainedFeatures(multipleType.getName().getLocalPart(), chainedFeatures);
// no chained or linked features just encode each property
properties.forEach(this::encodeProperty);
}
} else {
// chained features so we need to encode the chained features as an array
encodeChainedFeatures(descriptor.getName().getLocalPart(), chainedFeatures);
}
}

/** Encodes linked features as a JSON array. */
private void encodeLinkedFeatures(
String attributeName, List<Map<NameImpl, String>> linkedFeatures) {
PropertyDescriptor descriptor, List<Map<NameImpl, String>> linkedFeatures) {
// start the JSON object
jsonWriter.key(attributeName);
jsonWriter.array();
jsonWriter.key(descriptor.getName().getLocalPart());
// is it multiple or single?
if (descriptor.getMaxOccurs() > 1) {
jsonWriter.array();
}
// encode each linked feature
for (Map<NameImpl, String> feature : linkedFeatures) {
encodeAttributesAsObject(feature);
}
// end the linked features JSON array
jsonWriter.endArray();
if (descriptor.getMaxOccurs() > 1) {
// end the linked features JSON array
jsonWriter.endArray();
}
}

/** Encodes a list of features (chained features) as a JSON array. */
Expand All @@ -289,32 +291,6 @@ private void encodeChainedFeatures(String attributeName, List<Feature> chainedFe
jsonWriter.endArray();
}

/** Check if a property type should appear multiple times or be encoded as a list. */
private PropertyDescriptor isMultipleType(PropertyType parentType, PropertyType type) {
if (!(parentType instanceof ComplexType)) {
// only properties that belong to a complex type can be chained features
return null;
}
// search the current type on the parent properties
ComplexType complexType = (ComplexType) parentType;
PropertyDescriptor foundType = null;
for (PropertyDescriptor descriptor : complexType.getDescriptors()) {
if (descriptor.getType().equals(type)) {
// found our type
foundType = descriptor;
}
}
// if the found type can appear multiples time is not a chained feature
if (foundType == null) {
return null;
}
if (foundType.getMaxOccurs() > 1) {
// this type can appear more than once so it should not be encoded as a list
return foundType;
}
return null;
}

/**
* Get a list of chained features, NULL will be returned if this properties are not chained
* features.
Expand Down

0 comments on commit 5e4489f

Please sign in to comment.