Skip to content

Commit

Permalink
Add simple JSON encoder
Browse files Browse the repository at this point in the history
Make `application/json` output format serve simple JSON instead of
GeoJSON.

```json
    {
        "numberMatched":16,
        "numberReturned":2,
        "records":[
            {
                "@id":"1",
                "city":" Trento",
                "number":140,
                "year":2002
            },
            {
                "@id":"10",
                "city":" Barcelona",
                "number":914,
                "year":2010
            }
         ],
         "links":[
             {
                 "href":"http://localhost:8080/ogcapi/collections/locations/items?f=json&offset=0&limit=2",
                 "rel":"self",
                 "type":"application/json",
                 "title":"This document"
             },
             {
                 "href":"http://localhost:8080/ogcapi/collections/locations/items?f=json&offset=2&limit=2",
                 "rel":"next",
                 "type":"application/json",
                 "title":"Next page"
             },
             {
                 "href":"http://localhost:8080/ogcapi/collections/locations/items?offset=0&limit=2&f=geojson",
                 "rel":"alternate",
                 "type":"application/geo+json",
                 "title":"This document as GeoJSON"
             },
             {
                 "href":"http://localhost:8080/ogcapi/collections/locations/items?offset=0&limit=2&f=shapefile",
                 "rel":"alternate",
                 "type":"application/x-shapefile",
                 "title":"This document as Esri Shapefile"
             },
             {
                 "href":"http://localhost:8080/ogcapi/collections/locations/items?offset=0&limit=2&f=csv",
                 "rel":"alternate",
                 "type":"text/csv;charset=UTF-8",
                 "title":"This document as Comma Separated Values"
             },
             {
                 "href":"http://localhost:8080/ogcapi/collections/locations/items?offset=0&limit=2&f=ooxml",
                 "rel":"alternate",
                 "type":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
                 "title":"This document as Excel 2007 / OOXML"
             }
         ]
    }

```
  • Loading branch information
groldan committed Dec 12, 2023
1 parent 4e30aee commit 7135c00
Show file tree
Hide file tree
Showing 8 changed files with 492 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.camptocamp.opendata.ogc.features.autoconfigure.geotools.SampleDataBackendAutoConfiguration;
import com.camptocamp.opendata.ogc.features.http.codec.MimeTypes;
import com.camptocamp.opendata.ogc.features.http.codec.csv.CsvFeatureCollectionHttpMessageConverter;
import com.camptocamp.opendata.ogc.features.http.codec.json.SimpleJsonFeatureCollectionHttpMessageConverter;
import com.camptocamp.opendata.ogc.features.http.codec.shp.ShapefileFeatureCollectionHttpMessageConverter;
import com.camptocamp.opendata.ogc.features.http.codec.xls.Excel2007FeatureCollectionHttpMessageConverter;
import com.camptocamp.opendata.ogc.features.repository.CollectionRepository;
Expand Down Expand Up @@ -68,11 +69,17 @@ CollectionsApiController collectionsApiController(CollectionsApiDelegate delegat
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(simpleJsonFeatureCollectionHttpMessageConverter());
converters.add(csvFeatureCollectionHttpMessageConverter());
converters.add(shapefileFeatureCollectionHttpMessageConverter());
converters.add(excel2007FeatureCollectionHttpMessageConverter());
}

@Bean
SimpleJsonFeatureCollectionHttpMessageConverter simpleJsonFeatureCollectionHttpMessageConverter() {
return new SimpleJsonFeatureCollectionHttpMessageConverter();
}

@Bean
Excel2007FeatureCollectionHttpMessageConverter excel2007FeatureCollectionHttpMessageConverter() {
return new Excel2007FeatureCollectionHttpMessageConverter();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.camptocamp.opendata.ogc.features.http.codec.json;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;

import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractGenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.util.MimeType;

import com.camptocamp.opendata.model.GeodataRecord;
import com.camptocamp.opendata.ogc.features.http.codec.MimeTypes;
import com.camptocamp.opendata.ogc.features.model.FeatureCollection;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
* Simpler, geometry-less, JSON encoder for {@link FeatureCollection} and
* {@link GeodataRecord}.
*
* <p>
* Sample output:
*
* <pre>
* <code>
* {
* "numberMatched":16,
* "numberReturned":2,
* "records":[
* {
* "@id":"1",
* "city":" Trento",
* "number":140,
* "year":2002
* },
* ...
* ],
* "links":[
* {
* "href":"http://localhost:8080/ogcapi/collections/locations/items?f=json&offset=0&limit=2",
* "rel":"self",
* "type":"application/json",
* "title":"This document"
* },
* ...
* ]
* }
* </code>
* </pre>
*
*/
public class SimpleJsonFeatureCollectionHttpMessageConverter
extends AbstractGenericHttpMessageConverter<FeatureCollection> {

private static final MimeType MIME_TYPE = MimeTypes.JSON.getMimeType();
private static final MediaType MEDIA_TYPE = new MediaType(MIME_TYPE);

private ObjectMapper mapper;

public SimpleJsonFeatureCollectionHttpMessageConverter() {
super(MEDIA_TYPE);
mapper = new ObjectMapper();
Jackson2ObjectMapperBuilder.json().configure(mapper);
mapper.registerModule(new SimpleJsonModule());
}

/**
* {@inheritDoc}
*/
protected @Override boolean supports(Class<?> clazz) {
return FeatureCollection.class.isAssignableFrom(clazz);
}

/**
* {@inheritDoc}
*/
protected @Override MediaType getDefaultContentType(FeatureCollection message) {
return MEDIA_TYPE;
}

protected @Override void writeInternal(FeatureCollection message, Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {

OutputStream body = outputMessage.getBody();
mapper.writeValue(body, message);
body.flush();
}

protected @Override FeatureCollection readInternal(Class<? extends FeatureCollection> clazz,
HttpInputMessage inputMessage) {
throw new UnsupportedOperationException();
}

@Override
public FeatureCollection read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) {
throw new UnsupportedOperationException();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.camptocamp.opendata.ogc.features.http.codec.json;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;

import com.camptocamp.opendata.model.GeodataRecord;
import com.camptocamp.opendata.model.GeometryProperty;
import com.camptocamp.opendata.model.SimpleProperty;
import com.camptocamp.opendata.ogc.features.model.FeatureCollection;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.core.type.WritableTypeId;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

@SuppressWarnings("serial")
class SimpleJsonModule extends SimpleModule {

private static final Version VERSION = new Version(1, 0, 0, null, null, null);

public SimpleJsonModule() {
super(SimpleJsonModule.class.getSimpleName(), VERSION);

addSerializer(new FeatureCollectionSerializer());
addSerializer(new GeodataRecordSerializer());
}

static class FeatureCollectionSerializer extends StdSerializer<FeatureCollection> {

protected FeatureCollectionSerializer() {
super(FeatureCollection.class);
}

@Override
public void serializeWithType(FeatureCollection collection, JsonGenerator gen, SerializerProvider serializers,
TypeSerializer typeSer) throws IOException {

WritableTypeId typeIdDef = typeSer.writeTypePrefix(gen, typeSer.typeId(collection, JsonToken.START_OBJECT));

serializeContent(collection, gen);

typeSer.writeTypeSuffix(gen, typeIdDef);
}

@Override
public void serialize(FeatureCollection collection, JsonGenerator generator, SerializerProvider serializers)
throws IOException {

if (collection == null) {
generator.writeNull();
return;
}
generator.writeStartObject();
serializeContent(collection, generator);
generator.writeEndObject();
}

private void serializeContent(FeatureCollection collection, JsonGenerator generator) throws IOException {
generator.writeNumberField("numberMatched", collection.getNumberMatched());
generator.writeNumberField("numberReturned", collection.getNumberReturned());

generator.writeFieldName("records");
generator.writeStartArray();
collection.getFeatures().forEach(rec -> write(rec, generator));
generator.writeEndArray();

generator.writeFieldName("links");
generator.writeStartArray();
collection.getLinks().forEach(link -> write(link, generator));
generator.writeEndArray();
}

private void write(Object obj, JsonGenerator generator) {
try {
generator.writeObject(obj);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

}

static class GeodataRecordSerializer extends StdSerializer<GeodataRecord> {

public GeodataRecordSerializer() {
super(GeodataRecord.class);
}

@Override
public void serializeWithType(GeodataRecord rec, JsonGenerator gen, SerializerProvider serializers,
TypeSerializer typeSer) throws IOException {

WritableTypeId typeIdDef = typeSer.writeTypePrefix(gen, typeSer.typeId(rec, JsonToken.START_OBJECT));

serializeContent(rec, gen);

typeSer.writeTypeSuffix(gen, typeIdDef);
}

@Override
public void serialize(GeodataRecord rec, JsonGenerator generator, SerializerProvider serializers)
throws IOException {

if (rec == null) {
generator.writeNull();
return;
}
generator.writeStartObject();
serializeContent(rec, generator);
generator.writeEndObject();
}

private void serializeContent(GeodataRecord rec, JsonGenerator generator) throws IOException {
if (null != rec.getId()) {
generator.writeStringField("@id", rec.getId());
}
writeProperties(generator, rec.getProperties());
}

private void writeProperties(JsonGenerator generator, List<? extends SimpleProperty<?>> properties)
throws IOException {
for (SimpleProperty<?> p : properties) {
if (!(p instanceof GeometryProperty)) {
generator.writeObjectField(p.getName(), p.getValue());
}
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,66 @@ components:
$ref: "#/components/schemas/numberMatched"
numberReturned:
$ref: "#/components/schemas/numberReturned"
example:
{
"type":"FeatureCollection",
"numberMatched":16,
"numberReturned":2,
"features":[
{
"type":"Feature",
"@typeName":"locations",
"@id":"1",
"geometry":{"type":"Point","@name":"geom","@srs":"EPSG:4326","coordinates":[11.116667,46.066667]},
"properties":{"city":" Trento","number":140,"year":2002}
},
{
"type":"Feature",
"@typeName":"locations",
"@id":"10",
"geometry":{"type":"Point","@name":"geom","@srs":"EPSG:4326","coordinates":[2.183333,41.383333]},
"properties":{"city":" Barcelona","number":914,"year":2010}
}
],
"links":[
{
"href":"http://localhost:<port>/ogcapi/collections/locations/items?f=geojson&offset=0&limit=2",
"rel":"self",
"type":"application/geo+json",
"title":"This document"
},
{
"href":"http://localhost:<port>/ogcapi/collections/locations/items?f=geojson&offset=2&limit=2",
"rel":"next",
"type":"application/geo+json",
"title":"Next page"
},
{
"href":"http://localhost:<port>/ogcapi/collections/locations/items?offset=0&limit=2&f=json",
"rel":"alternate",
"type":"application/json",
"title":"This document as JSON"
},
{
"href":"http://localhost:<port>/ogcapi/collections/locations/items?offset=0&limit=2&f=shapefile",
"rel":"alternate",
"type":"application/x-shapefile",
"title":"This document as Esri Shapefile"
},
{
"href":"http://localhost:<port>/ogcapi/collections/locations/items?offset=0&limit=2&f=csv",
"rel":"alternate",
"type":"text/csv;charset=UTF-8",
"title":"This document as Comma Separated Values"
},
{
"href":"http://localhost:<port>/ogcapi/collections/locations/items?offset=0&limit=2&f=ooxml",
"rel":"alternate",
"type":"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"title":"This document as Excel 2007 / OOXML"
}
]
}
featureGeoJSON:
type: object
required:
Expand Down Expand Up @@ -968,48 +1028,12 @@ components:
returned features (`numberMatched` and `numberReturned`) as well as
links to support paging (link relation `next`).
content:
application/json:
schema:
$ref: '#/components/schemas/featureCollectionGeoJSON'
application/geo+json:
schema:
$ref: '#/components/schemas/featureCollectionGeoJSON'
example:
type: FeatureCollection
links:
- href: 'http://data.example.com/collections/buildings/items.json'
rel: self
type: application/geo+json
title: this document
- href: 'http://data.example.com/collections/buildings/items.html'
rel: alternate
type: text/html
title: this document as HTML
- href: 'http://data.example.com/collections/buildings/items.json&offset=10&limit=2'
rel: next
type: application/geo+json
title: next page
timeStamp: '2018-04-03T14:52:23Z'
numberMatched: 123
numberReturned: 2
features:
- type: Feature
id: '123'
geometry:
type: Polygon
coordinates:
- ...
properties:
function: residential
floors: '2'
lastUpdate: '2015-08-01T12:34:56Z'
- type: Feature
id: '132'
geometry:
type: Polygon
coordinates:
- ...
properties:
function: public use
floors: '10'
lastUpdate: '2013-12-03T10:15:37Z'
'text/csv':
schema:
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

import lombok.Cleanup;

public abstract class AbstractCollectionsApiImplIT {
public abstract class AbstractCollectionsApiImplTest {

protected @Autowired CollectionsApiImpl collectionsApi;

Expand Down
Loading

0 comments on commit 7135c00

Please sign in to comment.