/*
* Copyright (c) 2013 Data Harmonisation Panel
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* Data Harmonisation Panel <http://www.dhpanel.eu>
*/
package eu.esdihumboldt.hale.io.json;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Iterator;
import javax.xml.namespace.QName;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.map.ObjectMapper;
import org.geotools.geojson.feature.FeatureJSON;
import org.geotools.geojson.geom.GeometryJSON;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.vividsolutions.jts.geom.Geometry;
import eu.esdihumboldt.hale.common.align.model.AlignmentUtil;
import eu.esdihumboldt.hale.common.align.model.impl.PropertyEntityDefinition;
import eu.esdihumboldt.hale.common.core.io.report.IOReporter;
import eu.esdihumboldt.hale.common.core.io.report.impl.IOMessageImpl;
import eu.esdihumboldt.hale.common.core.io.supplier.LocatableOutputSupplier;
import eu.esdihumboldt.hale.common.instance.geometry.GeometryFinder;
import eu.esdihumboldt.hale.common.instance.helper.DepthFirstInstanceTraverser;
import eu.esdihumboldt.hale.common.instance.helper.InstanceTraverser;
import eu.esdihumboldt.hale.common.instance.model.Group;
import eu.esdihumboldt.hale.common.instance.model.Instance;
import eu.esdihumboldt.hale.common.instance.model.InstanceCollection;
import eu.esdihumboldt.hale.common.instance.model.ResourceIterator;
import eu.esdihumboldt.hale.common.schema.geometry.GeometryProperty;
import eu.esdihumboldt.hale.common.schema.model.GroupPropertyDefinition;
import eu.esdihumboldt.hale.common.schema.model.constraint.property.ChoiceFlag;
import eu.esdihumboldt.util.Pair;
/**
* Transform Instances into JSON/GeoJSON using the Jackson-API and the Geotools
* GeoJSON-API
*
* @author Sebastian Reinhardt
*/
public class JacksonMapper {
private JsonGenerator jsonGen;
private GeometryJSON geometryJson;
/**
* Writes a collection of instances into JSON
*
* @param out the output supplier
* @param instances the collection of instances
* @param reporter the reporter
* @throws IOException if writing
*/
public void streamWriteCollection(LocatableOutputSupplier<? extends OutputStream> out,
InstanceCollection instances, IOReporter reporter) throws IOException {
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out.getOutput(),
Charset.forName("UTF-8")))) {
// initialize Jackson Json Streaming Api
JsonFactory jsonFactory = new JsonFactory();
// initialize GeoJSON Api
geometryJson = new GeometryJSON();
jsonGen = jsonFactory.createJsonGenerator(writer);
jsonGen.useDefaultPrettyPrinter();
jsonGen.writeStartArray();
// iterate through Instances
try (ResourceIterator<Instance> itInstance = instances.iterator()) {
while (itInstance.hasNext()) {
Instance instance = itInstance.next();
jsonGen.writeStartObject();
jsonGen.writeFieldName(instance.getDefinition().getName().getLocalPart());
streamWriteInstanceValue(instance, reporter);
jsonGen.writeEndObject();
}
}
jsonGen.writeEndArray();
jsonGen.flush();
}
// FIXME - rather move to a validator?!
// XXX cannot cope with GZiped file
// URI targetLoc = out.getLocation();
// if (targetLoc != null) {
// File file = new File(targetLoc);
// try (InputStream in = Files.newInputStream(file.toPath())) {
// isValidJSON(in, reporter);
// }
// }
}
/**
* Writes a collection of instances as GeoJSON
*
* @param out the output supplier
* @param instances the collection of instances
* @param config the default geometry configuration
* @param reporter the reporter
* @throws IOException if writing the instances fails
*/
public void streamWriteGeoJSONCollection(LocatableOutputSupplier<? extends OutputStream> out,
InstanceCollection instances, GeoJSONConfig config, IOReporter reporter)
throws IOException {
// TODO What about bbox & crs?
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out.getOutput(),
Charset.forName("UTF-8")))) {
JsonFactory jsonFactory = new JsonFactory();
geometryJson = new GeometryJSON();
jsonGen = jsonFactory.createJsonGenerator(writer);
jsonGen.useDefaultPrettyPrinter();
jsonGen.writeStartObject();
jsonGen.writeStringField("type", "FeatureCollection");
jsonGen.writeArrayFieldStart("features");
// iterate through Instances
try (ResourceIterator<Instance> itInstance = instances.iterator()) {
while (itInstance.hasNext()) {
Instance instance = itInstance.next();
streamWriteGeoJSONInstance(instance, config, reporter);
}
}
jsonGen.writeEndArray();
jsonGen.writeEndObject();
jsonGen.flush();
}
}
/**
* Writes a single instance as GeoJSON.
*
* @param instance the instance to write
* @param config the default geometry config
* @param reporter the reporter
* @throws IOException if writing the instance fails
*/
private void streamWriteGeoJSONInstance(Instance instance, GeoJSONConfig config,
IOReporter reporter) throws IOException {
jsonGen.writeStartObject();
jsonGen.writeStringField("type", "Feature");
PropertyEntityDefinition geomProperty = config.getDefaultGeometry(instance.getDefinition());
GeometryFinder geomFinder = new GeometryFinder(null);
// check whether a geometry property is set
if (geomProperty != null) {
// find all occurrences of the property
Collection<Object> values = AlignmentUtil.getValues(instance, geomProperty, false);
// find all geometries below any value (the values themselves might
// be geometries)
InstanceTraverser traverser = new DepthFirstInstanceTraverser(true);
for (Object value : values)
traverser.traverse(value, geomFinder);
}
Collection<GeometryProperty<?>> geometries = geomFinder.getGeometries();
if (!geometries.isEmpty()) {
// XXX It would be better to put CRS to each geometry.
// This is currently not possible because geotools doesn't support
// this.
GeometryProperty<?> geomProp = geometries.iterator().next();
if (geomProp.getCRSDefinition() != null) {
jsonGen.writeFieldName("crs");
jsonGen.writeRawValue(new FeatureJSON().toString(geomProp.getCRSDefinition()
.getCRS()));
}
}
jsonGen.writeFieldName("geometry");
if (geometries.isEmpty())
jsonGen.writeNull();
else if (geometries.size() == 1)
streamWriteGeometryValue(geometries.iterator().next().getGeometry());
else {
jsonGen.writeStartObject();
jsonGen.writeStringField("type", "GeometryCollection");
jsonGen.writeArrayFieldStart("geometries");
for (GeometryProperty<?> geom : geometries)
streamWriteGeometryValue(geom.getGeometry());
jsonGen.writeEndArray();
jsonGen.writeEndObject();
}
jsonGen.writeFieldName("properties");
jsonGen.writeStartObject();
jsonGen.writeStringField("_type", instance.getDefinition().getName().getLocalPart());
streamWriteProperties(instance, reporter);
jsonGen.writeEndObject();
jsonGen.writeEndObject();
}
/**
* Writes a single instance into JSON
*
* @param instance the instance to write
* @param reporter the reporter
* @throws JsonGenerationException if there was a problem generating the
* JSON
* @throws IOException if writing the file failed
*/
private void streamWriteInstanceValue(Instance instance, IOReporter reporter)
throws JsonGenerationException, IOException {
jsonGen.writeStartObject();
// check if the instance contains a value and write it down
Object value = instance.getValue();
if (value != null) {
// detect geometry collections (as occurring in XML schemas)
if (value instanceof Collection) {
Collection<?> col = (Collection<?>) value;
if (!col.isEmpty()) {
Object element = col.iterator().next();
if (element instanceof GeometryProperty<?> || element instanceof Geometry) {
// extract geometry
value = element;
if (col.size() > 1) {
// there are more geometry values
// XXX can we handle multiple here?
// XXX for now warn
reporter.warn(new IOMessageImpl(
"Ignoring multiple geometries in instance value", null));
}
}
}
}
jsonGen.writeFieldName("_value");
streamWriteValue(value);
}
streamWriteProperties(instance, reporter);
jsonGen.writeEndObject();
}
/**
* Writes a group into JSON
*
* @param group the group to write
* @param reporter the reporter
* @throws JsonGenerationException if there was a problem generating the
* JSON
* @throws IOException if writing the file failed
*/
private void streamWriteGroupValue(Group group, IOReporter reporter)
throws JsonGenerationException, IOException {
// write the Instance and name
jsonGen.writeStartObject();
streamWriteProperties(group, reporter);
jsonGen.writeEndObject();
}
/**
* Handles skipping choice groups.
*
* @param propertyName the start property name
* @param obj the object to inspect
* @param reporter the reporter
* @return a pair of property name and value to use
*/
private Pair<String, Object> skipChoice(String propertyName, Object obj, IOReporter reporter) {
if (obj instanceof Group) {
Group group = (Group) obj;
// For choices search for the (only!) child and skip the choice.
if (group.getDefinition() instanceof GroupPropertyDefinition) {
if (((GroupPropertyDefinition) group.getDefinition()).getConstraint(
ChoiceFlag.class).isEnabled()) {
Iterator<QName> childPropertyNames = group.getPropertyNames().iterator();
if (!childPropertyNames.hasNext()) {
reporter.warn(new IOMessageImpl("Found an empty choice.", null));
return null;
}
QName childPropertyName = childPropertyNames.next();
Object[] values = group.getProperty(childPropertyName);
Object value = values[0];
if (childPropertyNames.hasNext() || values.length > 1)
reporter.warn(new IOMessageImpl(
"Found a choice with multiple children. Using first.", null));
// delegate to only value
return skipChoice(childPropertyName.getLocalPart(), value, reporter);
}
}
}
return new Pair<>(propertyName, obj);
}
/**
* Writes the properties of a group into JSON
*
* @param group the group to write
* @param reporter the reporter
* @throws IOException if writing the file failed
*/
private void streamWriteProperties(Group group, IOReporter reporter) throws IOException {
// iterate over all properties
Iterator<QName> nameIt = group.getPropertyNames().iterator();
while (nameIt.hasNext()) {
QName name = nameIt.next();
Object[] values = group.getProperty(name);
// ... and over all values of each property
if (values != null && values.length > 0) {
// resolve choice groups
// XXX hope that the results don't "conflict" with anything.
Multimap<String, Object> valueMap = HashMultimap.create(1, values.length);
for (Object value : values) {
Pair<String, Object> useValue = skipChoice(name.getLocalPart(), value, reporter);
if (useValue != null)
valueMap.put(useValue.getFirst(), useValue.getSecond());
}
for (String propName : valueMap.keySet()) {
Collection<Object> realValues = valueMap.get(propName);
jsonGen.writeFieldName(propName);
if (realValues.size() == 1)
streamWritePropertyValue(realValues.iterator().next(), reporter);
else {
jsonGen.writeStartArray();
for (Object obj : realValues)
streamWritePropertyValue(obj, reporter);
jsonGen.writeEndArray();
}
}
}
}
}
/**
* Writes a single property of a group into JSON
*
* @param value the value
* @param reporter the reporter
* @throws IOException if writing the file failed
*/
private void streamWritePropertyValue(Object value, IOReporter reporter) throws IOException {
if (value instanceof Instance)
streamWriteInstanceValue((Instance) value, reporter);
else if (value instanceof Group)
streamWriteGroupValue((Group) value, reporter);
else
streamWriteValue(value);
}
/**
* Writes a flat value (no instance or group).
*
* @param value the value
* @throws IOException if writing the field fails
*/
private void streamWriteValue(Object value) throws IOException {
if (value instanceof Geometry)
streamWriteGeometryValue((Geometry) value);
else if (value instanceof GeometryProperty<?>)
streamWriteGeometryValue(((GeometryProperty<?>) value).getGeometry());
else if (value instanceof Number)
streamWriteNumeric((Number) value);
else {
// XXX use conversion service or something?
jsonGen.writeString(value.toString());
}
}
/**
* Writes a property numeric into json
*
* @param num the numeric
* @throws JsonGenerationException if there was a problem generating the
* JSON
* @throws IOException if writing the file failed
*/
private void streamWriteNumeric(Number num) throws JsonGenerationException, IOException {
if (num instanceof Integer)
jsonGen.writeNumber((Integer) num);
else if (num instanceof Float)
jsonGen.writeNumber((Float) num);
else if (num instanceof Double)
jsonGen.writeNumber((Double) num);
else if (num instanceof Long)
jsonGen.writeNumber((Long) num);
else if (num instanceof BigDecimal)
jsonGen.writeNumber((BigDecimal) num);
else if (num instanceof BigInteger)
jsonGen.writeNumber(new BigDecimal((BigInteger) num));
else {
// XXX this case is not particularly good ...
jsonGen.writeNumber(String.valueOf(num));
}
}
private void streamWriteGeometryValue(Geometry geom) throws IOException {
jsonGen.writeRawValue(geometryJson.toString(geom));
}
/**
* Validates a JSON stream
*
* @param in the JSON stream
* @param reporter the reporter
* @return true if valid, else false
*/
public boolean isValidJSON(final InputStream in, IOReporter reporter) {
boolean valid = false;
try {
final JsonParser parser = new ObjectMapper().getJsonFactory().createJsonParser(in);
while (parser.nextToken() != null) {
//
}
valid = true;
} catch (Exception e) {
reporter.error(new IOMessageImpl("Produced invalid JSON output", e));
}
return valid;
}
}