package org.geotoolkit.data.geojson.utils;
import org.opengis.util.GenericName;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.cql.CQL;
import org.geotoolkit.cql.CQLException;
import org.geotoolkit.factory.FactoryFinder;
import org.geotoolkit.factory.Hints;
import org.geotoolkit.factory.HintsPending;
import org.geotoolkit.feature.FeatureTypeUtilities;
import org.opengis.feature.AttributeType;
import org.opengis.feature.PropertyType;
import org.geotoolkit.feature.type.*;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.util.FactoryException;
import org.opengis.util.InternationalString;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.iso.SimpleInternationalString;
import org.geotoolkit.feature.AttributeDescriptorBuilder;
import org.geotoolkit.feature.type.FeatureTypeFactory;
import org.geotoolkit.util.NamesExt;
import org.geotoolkit.feature.FeatureTypeBuilder;
import org.geotoolkit.lang.Static;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* An utility class to handle read/write of FeatureType into a JSON schema file.
*
* Theses schema are inspired from <a href="http://json-schema.org/">JSON-Schema</a> specification.
* Changes are :
* - introducing of a {@code javatype} that define Java class used.
* - introducing of a {@code nillable} property for nullable attributes
* - introducing of a {@code userdata} Map property that contain previous user data information.
* - introducing of a {@code geometry} property to describe a geometry
* - introducing of a {@code crs} property to describe a geometry crs
*
* @author Quentin Boileau (Geomatys)
*/
public final class FeatureTypeUtils extends Static {
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.data.geojson.utils");
private static final FeatureTypeFactory FT_FACTORY = FeatureTypeFactory.INSTANCE;
private static final FilterFactory2 FF = (FilterFactory2) FactoryFinder.getFilterFactory(
new Hints(Hints.FILTER_FACTORY, FilterFactory2.class));
private static final String TITLE = "title";
private static final String TYPE = "type";
private static final String JAVA_TYPE = "javatype";
private static final String DESCRIPTION = "description";
private static final String PROPERTIES = "properties";
private static final String PRIMARY_KEY = "primaryKey";
private static final String RESTRICTION = "restriction";
private static final String REQUIRED = "required";
private static final String NILLABLE = "nillable";
private static final String MIN_ITEMS = "minItems";
private static final String MAX_ITEMS = "maxItems";
private static final String USER_DATA = "userdata";
private static final String GEOMETRY = "geometry";
private static final String GEOMETRY_ATT_NAME = "geometryName";
private static final String CRS = "crs";
private static final String OBJECT = "object";
private static final String ARRAY = "array";
private static final String INTEGER = "integer";
private static final String NUMBER = "number";
private static final String STRING = "string";
private static final String BOOLEAN = "boolean";
/**
* Write a FeatureType in output File.
* @param ft
* @param ouptut
* @throws IOException
*/
public static void writeFeatureType(FeatureType ft, File ouptut) throws IOException, DataStoreException {
ArgumentChecks.ensureNonNull("FeatureType", ft);
ArgumentChecks.ensureNonNull("outputFile", ouptut);
if (ft.getGeometryDescriptor() == null) {
throw new DataStoreException("No default Geometry in given FeatureType : "+ft);
}
JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(ouptut,
JsonEncoding.UTF8).useDefaultPrettyPrinter();
//start write feature collection.
writer.writeStartObject();
writer.writeStringField(TITLE, ft.getName().tip().toString());
writer.writeStringField(TYPE, OBJECT);
writer.writeStringField(JAVA_TYPE, "FeatureType");
if (ft.getDescription() != null) {
writer.writeStringField(DESCRIPTION, ft.getDescription().toString());
}
writeGeometryType(ft.getGeometryDescriptor(), writer);
writeProperties(ft, writer);
writer.writeEndObject();
writer.flush();
writer.close();
}
public static void writeFeatureTypes(List<FeatureType> fts, OutputStream output) throws IOException, DataStoreException {
ArgumentChecks.ensureNonNull("FeatureType", fts);
ArgumentChecks.ensureNonNull("outputStream", output);
if (fts.isEmpty()) return;
if (fts.size() > 1) {
JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(output, JsonEncoding.UTF8).useDefaultPrettyPrinter();
writer.writeStartArray();
for (FeatureType ft : fts) {
writeFeatureType(ft, output, writer);
}
writer.writeEndArray();
writer.flush();
writer.close();
} else {
writeFeatureType(fts.get(0), output);
}
}
/**
* Write a FeatureType in output File.
* @param ft
* @param output
* @throws IOException
*/
public static void writeFeatureType(FeatureType ft, OutputStream output) throws IOException, DataStoreException {
JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(output,JsonEncoding.UTF8).useDefaultPrettyPrinter();
writeFeatureType(ft, output, writer);
writer.flush();
writer.close();
}
private static void writeFeatureType(FeatureType ft, OutputStream output, JsonGenerator writer) throws IOException, DataStoreException {
ArgumentChecks.ensureNonNull("FeatureType", ft);
ArgumentChecks.ensureNonNull("outputStream", output);
if (ft.getGeometryDescriptor() == null) {
throw new DataStoreException("No default Geometry in given FeatureType : "+ft);
}
//start write feature collection.
writer.writeStartObject();
writer.writeStringField(TITLE, ft.getName().tip().toString());
writer.writeStringField(TYPE, OBJECT);
writer.writeStringField(JAVA_TYPE, "FeatureType");
if (ft.getDescription() != null) {
writer.writeStringField(DESCRIPTION, ft.getDescription().toString());
}
writeGeometryType(ft.getGeometryDescriptor(), writer);
writeProperties(ft, writer);
writer.writeEndObject();
}
private static void writeProperties(ComplexType ft, JsonGenerator writer) throws IOException {
writer.writeObjectFieldStart(PROPERTIES);
Collection<PropertyDescriptor> descriptors = ft.getDescriptors();
List<String> required = new ArrayList<>();
for (PropertyDescriptor descriptor : descriptors) {
PropertyType type = descriptor.getType();
boolean isRequired = false;
if (type instanceof ComplexType) {
isRequired = writeComplexType(descriptor, (ComplexType)type, writer);
} else if (type instanceof org.geotoolkit.feature.type.AttributeType) {
if (type instanceof GeometryType) {
// GeometryType geometryType = (GeometryType) type;
// isRequired = writeGeometryType(descriptor, geometryType, writer);
} else {
org.geotoolkit.feature.type.AttributeType att = (org.geotoolkit.feature.type.AttributeType) type;
isRequired = writeAttributeType(descriptor, att, writer);
}
}
if (isRequired) {
required.add(descriptor.getName().tip().toString());
}
}
if (!required.isEmpty()) {
writer.writeArrayFieldStart(REQUIRED);
for (String req : required) {
writer.writeString(req);
}
writer.writeEndArray();
}
writer.writeEndObject();
}
private static boolean writeComplexType(PropertyDescriptor descriptor, ComplexType complex, JsonGenerator writer)
throws IOException {
writer.writeObjectFieldStart(descriptor.getName().tip().toString());
writer.writeStringField(TYPE, OBJECT);
writer.writeStringField(JAVA_TYPE, "ComplexType");
if (complex.getDescription() != null) {
writer.writeStringField(DESCRIPTION, complex.getDescription().toString());
}
writeDescriptorAttributes(descriptor, complex, writer);
writeProperties(complex, writer);
writer.writeEndObject();
return complex.getMinimumOccurs() > 0;
}
private static boolean writeAttributeType(PropertyDescriptor descriptor, org.geotoolkit.feature.type.AttributeType att,
JsonGenerator writer)
throws IOException {
writer.writeObjectFieldStart(att.getName().tip().toString());
Class binding = att.getValueClass();
writer.writeStringField(TYPE, findType(binding));
writer.writeStringField(JAVA_TYPE, binding.getName());
if (att.getDescription() != null) {
writer.writeStringField(DESCRIPTION, att.getDescription().toString());
}
writeDescriptorAttributes(descriptor, att, writer);
List<Filter> restrictions = att.getRestrictions();
if (restrictions != null && !restrictions.isEmpty()) {
final Filter merged = FF.and(restrictions);
writer.writeStringField(RESTRICTION, CQL.write(merged));
}
writer.writeEndObject();
return att.getMinimumOccurs() > 0;
}
private static void writeDescriptorAttributes(PropertyDescriptor descriptor, AttributeType att, JsonGenerator writer)
throws IOException {
if (FeatureTypeUtilities.isPartOfPrimaryKey(descriptor)) {
writer.writeBooleanField(PRIMARY_KEY, true);
}
writer.writeBooleanField(NILLABLE, descriptor.isNillable());
writer.writeNumberField(MIN_ITEMS, descriptor.getMinOccurs());
writer.writeNumberField(MAX_ITEMS, descriptor.getMaxOccurs());
Map<Object, Object> userData = descriptor.getUserData();
if (!userData.isEmpty()) {
writeUserData(userData, writer);
}
}
private static void writeUserData(Map<Object, Object> userData, JsonGenerator writer) throws IOException {
writer.writeObjectFieldStart(USER_DATA);
for (Map.Entry<Object, Object> entry : userData.entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();
if (!(key instanceof Serializable) || !(value instanceof Serializable)) {
LOGGER.log(Level.WARNING, "User map entry not serializable "+entry);
} else {
writer.writeFieldName(key.toString());
GeoJSONUtils.writeValue(value, writer);
}
}
writer.writeEndObject();
}
private static String findType(Class binding) {
if (Integer.class.isAssignableFrom(binding)) {
return INTEGER;
} else if (Number.class.isAssignableFrom(binding)) {
return NUMBER;
} else if(Boolean.class.isAssignableFrom(binding)) {
return BOOLEAN;
} else if (binding.isArray()) {
return ARRAY;
} else {
//fallback
return STRING;
}
}
private static boolean writeGeometryType(GeometryDescriptor descriptor, JsonGenerator writer)
throws IOException {
GeometryType geometryType = descriptor.getType();
writer.writeObjectFieldStart(GEOMETRY);
writer.writeStringField(TYPE, OBJECT);
if (geometryType.getDescription() != null) {
writer.writeStringField(DESCRIPTION, geometryType.getDescription().toString());
}
writer.writeStringField(JAVA_TYPE, geometryType.getBinding().getCanonicalName());
CoordinateReferenceSystem crs = geometryType.getCoordinateReferenceSystem();
if (crs != null) {
String crsCode = GeoJSONUtils.toURN(crs);
writer.writeStringField(CRS, crsCode);
}
writer.writeStringField(GEOMETRY_ATT_NAME, descriptor.getLocalName());
writer.writeEndObject();
return true;
}
/**
* Read a FeatureType from an input File.
* @param input file to read
* @return FeatureType
* @throws IOException
*/
public static FeatureType readFeatureType(File input) throws IOException, DataStoreException {
JsonParser parser = GeoJSONParser.FACTORY.createParser(input);
final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
final AttributeDescriptorBuilder adb = new AttributeDescriptorBuilder();
parser.nextToken(); // {
String ftName = null;
InternationalString description = null;
List<PropertyDescriptor> propertyDescriptors = new ArrayList<>();
GeometryDescriptor geometryDescriptor = null;
while (parser.nextToken() != JsonToken.END_OBJECT) {
final String currName = parser.getCurrentName();
switch (currName) {
case TITLE:
ftName = parser.nextTextValue();
break;
case JAVA_TYPE:
String type = parser.nextTextValue();
if (!"FeatureType".equals(type)) {
throw new DataStoreException("Invalid JSON schema : " + input.getName());
}
break;
case PROPERTIES:
propertyDescriptors = readProperties(parser, ftb, adb);
break;
case GEOMETRY:
geometryDescriptor = readGeometry(parser, adb);
break;
case DESCRIPTION:
description = new SimpleInternationalString(parser.nextTextValue());
break;
}
}
if (ftName != null && geometryDescriptor != null) {
propertyDescriptors.add(geometryDescriptor);
ftb.reset();
ftb.setName(ftName);
ftb.setDescription(description);
ftb.setProperties(propertyDescriptors);
ftb.setDefaultGeometry(geometryDescriptor.getName());
} else {
throw new DataStoreException("FeatureType name or default geometry not found in JSON schema");
}
parser.close();
return ftb.buildFeatureType();
}
private static GeometryDescriptor readGeometry(JsonParser parser, AttributeDescriptorBuilder adb)
throws IOException, DataStoreException {
Class binding = null;
CoordinateReferenceSystem crs = null;
InternationalString description = null;
String geometryName = null;
parser.nextToken(); // {
while (parser.nextToken() != JsonToken.END_OBJECT) { // -> }
final String currName = parser.getCurrentName();
switch (currName) {
case JAVA_TYPE:
String javaTypeValue = parser.nextTextValue();
if (!"ComplexType".equals(javaTypeValue)) {
try {
binding = Class.forName(javaTypeValue);
} catch (ClassNotFoundException e) {
throw new DataStoreException("Geometry javatype " + javaTypeValue + " invalid : " + e.getMessage(), e);
}
}
break;
case CRS:
String crsCode = parser.nextTextValue();
try {
crs = org.geotoolkit.referencing.CRS.decode(crsCode);
} catch (FactoryException e) {
throw new DataStoreException("Geometry crs " + crsCode + " invalid : " + e.getMessage(), e);
}
break;
case DESCRIPTION:
description = new SimpleInternationalString(parser.nextTextValue());
break;
case GEOMETRY_ATT_NAME:
geometryName = parser.nextTextValue();
}
}
if (binding == null || crs == null) {
throw new DataStoreException("Geometry crs or binding not found.");
}
GenericName name = geometryName != null ? NamesExt.create(geometryName) : NamesExt.create(BasicFeatureTypes.GEOMETRY_ATTRIBUTE_NAME);
PropertyType prop = FT_FACTORY.createGeometryType(name, binding, crs, false, false, null, null, description);
return (GeometryDescriptor) adb.create((org.geotoolkit.feature.type.PropertyType) prop, name, crs, 1, 1, false, null);
}
private static List<PropertyDescriptor> readProperties(JsonParser parser, FeatureTypeBuilder ftb,
AttributeDescriptorBuilder adb)
throws IOException, DataStoreException {
List<PropertyDescriptor> propertyDescriptors = new ArrayList<>();
parser.nextToken(); // {
List<String> requiredList = null;
while (parser.nextToken() != JsonToken.END_OBJECT) { // -> }
final JsonToken currToken = parser.getCurrentToken();
if (currToken == JsonToken.FIELD_NAME) {
final String currName = parser.getCurrentName();
if (REQUIRED.equals(currName)) {
requiredList = parseRequiredArray(parser);
} else {
propertyDescriptors.add(parseProperty(parser, ftb, adb));
}
}
}
return propertyDescriptors;
}
private static PropertyDescriptor parseProperty(JsonParser parser, FeatureTypeBuilder ftb, AttributeDescriptorBuilder adb)
throws IOException, DataStoreException {
final String attributeName = parser.getCurrentName();
Class binding = String.class;
boolean nillable = true;
boolean primaryKey = false;
int minOccurs = 0;
int maxOccurs = 1;
String description = null;
String restrictionCQL = null;
Map<Object, Object> userData = null;
List<PropertyDescriptor> descs = new ArrayList<>();
parser.nextToken(); // {
while (parser.nextToken() != JsonToken.END_OBJECT) { // -> }
final String currName = parser.getCurrentName();
switch (currName) {
case JAVA_TYPE:
String javaTypeValue = parser.nextTextValue();
if (!"ComplexType".equals(javaTypeValue)) {
try {
binding = Class.forName(javaTypeValue);
} catch (ClassNotFoundException e) {
throw new DataStoreException("Attribute " + attributeName + " invalid : " + e.getMessage(), e);
}
}
break;
case NILLABLE:
nillable = parser.nextBooleanValue();
break;
case MIN_ITEMS:
minOccurs = parser.nextIntValue(0);
break;
case MAX_ITEMS:
maxOccurs = parser.nextIntValue(1);
break;
case PRIMARY_KEY:
primaryKey = parser.nextBooleanValue();
break;
case RESTRICTION:
restrictionCQL = parser.nextTextValue();
break;
case USER_DATA:
userData = parseUserDataMap(parser);
break;
case PROPERTIES:
descs = readProperties(parser, ftb, adb);
break;
case DESCRIPTION:
description = parser.nextTextValue();
break;
}
}
if (primaryKey) {
if (userData == null) userData = new HashMap<>();
userData.put(HintsPending.PROPERTY_IS_IDENTIFIER, Boolean.TRUE);
}
GenericName name = NamesExt.valueOf(attributeName);
InternationalString desc = description != null ? new SimpleInternationalString(description) : null;
if (descs.isEmpty()) {
//build AttributeDescriptor
if (binding == null) {
throw new DataStoreException("Empty javatype for attribute "+attributeName);
}
List<Filter> restrictions = null;
if (restrictionCQL != null) {
try {
restrictions = new ArrayList<>();
restrictions.add(CQL.parseFilter(restrictionCQL, FF));
} catch (CQLException e) {
LOGGER.log(Level.WARNING, "Can't parse restriction filter : "+restrictionCQL, e);
}
}
PropertyType prop = FT_FACTORY.createAttributeType(name, binding, false, false, restrictions, null, desc);
return adb.create((org.geotoolkit.feature.type.PropertyType) prop, name, minOccurs, maxOccurs, nillable, userData);
} else {
//build ComplexType
ftb.reset();
ftb.setName(name);
for (PropertyDescriptor property : descs) {
ftb.add(property);
}
ftb.setDescription(desc);
final ComplexType complexType = ftb.buildType();
return adb.create(complexType, name, minOccurs, maxOccurs, nillable, userData);
}
}
private static Map<Object, Object> parseUserDataMap(JsonParser parser) throws IOException {
Map<Object, Object> map = new HashMap<>();
parser.nextToken(); // {
while (parser.nextToken() != JsonToken.END_OBJECT) {
Object key = parser.getCurrentName();
JsonToken next = parser.nextToken();
map.put(key, GeoJSONParser.getValue(next, parser));
}
return map;
}
private static List<String> parseRequiredArray(JsonParser parser) throws IOException {
List<String> requiredList = new ArrayList<>();
parser.nextToken(); // [
while (parser.nextToken() != JsonToken.END_ARRAY) { // -> ]
requiredList.add(parser.getValueAsString());
}
return requiredList;
}
}