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 com.vividsolutions.jts.geom.Geometry; 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.opengis.feature.AttributeType; import org.opengis.feature.PropertyType; 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.util.NamesExt; import org.geotoolkit.lang.Static; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import static java.nio.file.StandardOpenOption.*; import org.apache.sis.feature.SingleAttributeTypeBuilder; import org.apache.sis.feature.DefaultAssociationRole; import org.apache.sis.feature.DefaultAttributeType; import org.apache.sis.feature.FeatureExt; import org.apache.sis.feature.FeatureTypeExt; import org.apache.sis.feature.builder.AttributeRole; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.opengis.feature.FeatureAssociationRole; import org.opengis.feature.FeatureType; /** * 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 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 output * @throws IOException */ @Deprecated public static void writeFeatureType(FeatureType ft, File output) throws IOException, DataStoreException { writeFeatureType(ft, output.toPath()); } /** * Write a FeatureType in output File. * @param ft * @param output * @throws IOException */ public static void writeFeatureType(FeatureType ft, Path output) throws IOException, DataStoreException { ArgumentChecks.ensureNonNull("FeatureType", ft); ArgumentChecks.ensureNonNull("outputFile", output); if (FeatureExt.getDefaultGeometryAttribute(ft) == null) { throw new DataStoreException("No default Geometry in given FeatureType : "+ft); } try (OutputStream outStream = Files.newOutputStream(output, CREATE, WRITE, TRUNCATE_EXISTING); JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(outStream, JsonEncoding.UTF8)) { writer.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(FeatureExt.getDefaultGeometryAttribute(ft), writer); writeProperties(ft, writer); writer.writeEndObject(); writer.flush(); } } 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 (FeatureExt.getDefaultGeometryAttribute(ft) == 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(FeatureExt.getDefaultGeometryAttribute(ft), writer); writeProperties(ft, writer); writer.writeEndObject(); } private static void writeProperties(FeatureType ft, JsonGenerator writer) throws IOException { writer.writeObjectFieldStart(PROPERTIES); Collection<? extends PropertyType> descriptors = ft.getProperties(true); List<String> required = new ArrayList<>(); for (PropertyType type : descriptors) { boolean isRequired = false; if (type instanceof FeatureAssociationRole) { isRequired = writeComplexType((FeatureAssociationRole) type, ((FeatureAssociationRole)type).getValueType(), writer); } else if (type instanceof AttributeType) { if(Geometry.class.isAssignableFrom( ((AttributeType) type).getValueClass())){ // GeometryType geometryType = (GeometryType) type; // isRequired = writeGeometryType(descriptor, geometryType, writer); } else { isRequired = writeAttributeType(ft, (AttributeType)type, writer); } } if (isRequired) { required.add(type.getName().tip().toString()); } } if (!required.isEmpty()) { writer.writeArrayFieldStart(REQUIRED); for (String req : required) { writer.writeString(req); } writer.writeEndArray(); } writer.writeEndObject(); } private static boolean writeComplexType(FeatureAssociationRole descriptor, FeatureType 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()); } writer.writeNumberField(MIN_ITEMS, descriptor.getMinimumOccurs()); writer.writeNumberField(MAX_ITEMS, descriptor.getMaximumOccurs()); writeProperties(complex, writer); writer.writeEndObject(); return descriptor.getMinimumOccurs() > 0; } private static boolean writeAttributeType(FeatureType featureType, 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()); } if (FeatureTypeExt.isPartOfPrimaryKey(featureType,att.getName().toString())) { writer.writeBooleanField(PRIMARY_KEY, true); } writer.writeNumberField(MIN_ITEMS, att.getMinimumOccurs()); writer.writeNumberField(MAX_ITEMS, att.getMaximumOccurs()); // 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 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(AttributeType geometryType, JsonGenerator writer) throws IOException { writer.writeObjectFieldStart(GEOMETRY); writer.writeStringField(TYPE, OBJECT); if (geometryType.getDescription() != null) { writer.writeStringField(DESCRIPTION, geometryType.getDescription().toString()); } writer.writeStringField(JAVA_TYPE, geometryType.getValueClass().getCanonicalName()); CoordinateReferenceSystem crs = FeatureExt.getCRS(geometryType); String crsCode = GeoJSONUtils.toURN(crs); writer.writeStringField(CRS, crsCode); writer.writeStringField(GEOMETRY_ATT_NAME, geometryType.getName().tip().toString()); writer.writeEndObject(); return true; } /** * Read a FeatureType from an input File. * @param input file to read * @return FeatureType * @throws IOException */ @Deprecated public static FeatureType readFeatureType(File input) throws IOException, DataStoreException { return readFeatureType(input.toPath()); } /** * Read a FeatureType from an input File. * @param input file to read * @return FeatureType * @throws IOException */ public static FeatureType readFeatureType(Path input) throws IOException, DataStoreException { try (InputStream stream = Files.newInputStream(input); JsonParser parser = GeoJSONParser.FACTORY.createParser(stream)) { final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); parser.nextToken(); while (parser.nextToken() != JsonToken.END_OBJECT) { final String currName = parser.getCurrentName(); switch (currName) { case TITLE: ftb.setName(parser.nextTextValue()); break; case JAVA_TYPE: String type = parser.nextTextValue(); if (!"FeatureType".equals(type)) { throw new DataStoreException("Invalid JSON schema : " + input.getFileName().toString()); } break; case PROPERTIES: for(PropertyType pt : readProperties(parser)){ if(pt instanceof AttributeType){ ftb.addAttribute((AttributeType) pt); }else if(pt instanceof FeatureAssociationRole){ ftb.addAssociation((FeatureAssociationRole) pt); } } break; case GEOMETRY: AttributeType geomAtt = readGeometry(parser); ftb.addAttribute(geomAtt).addRole(AttributeRole.DEFAULT_GEOMETRY); break; case DESCRIPTION: ftb.setDescription(parser.nextTextValue()); break; } } try{ return ftb.build(); }catch(IllegalStateException ex){ throw new DataStoreException("FeatureType name or default geometry not found in JSON schema\n"+ex.getMessage(),ex); } } } private static AttributeType readGeometry(JsonParser parser) 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.apache.sis.referencing.CRS.forCode(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."); } final GenericName name = geometryName != null ? NamesExt.create(geometryName) : NamesExt.create("geometry"); final SingleAttributeTypeBuilder atb = new SingleAttributeTypeBuilder(); atb.setName(name); atb.setCRS(crs); atb.setValueClass(binding); atb.setMinimumOccurs(1); atb.setMaximumOccurs(1); return atb.build(); } private static List<PropertyType> readProperties(JsonParser parser) throws IOException, DataStoreException { List<PropertyType> 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)); } } } return propertyDescriptors; } private static PropertyType parseProperty(JsonParser parser) throws IOException, DataStoreException { final String attributeName = parser.getCurrentName(); Class binding = String.class; boolean primaryKey = false; int minOccurs = 0; int maxOccurs = 1; String description = null; String restrictionCQL = null; Map<Object, Object> userData = null; List<PropertyType> 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 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); 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); } } final SingleAttributeTypeBuilder atb = new SingleAttributeTypeBuilder(); atb.setName(name); atb.setDescription(desc); atb.setValueClass(binding); atb.setMinimumOccurs(minOccurs); atb.setMaximumOccurs(maxOccurs); return atb.build(); } else { //build ComplexType final FeatureTypeBuilder ftb = new FeatureTypeBuilder(); ftb.setName(name); for (PropertyType pt : descs) { if(pt instanceof AttributeType){ ftb.addAttribute((AttributeType) pt); }else if(pt instanceof FeatureAssociationRole){ ftb.addAssociation((FeatureType) pt); } } ftb.setDescription(desc); final FeatureType complexType = ftb.build(); return new DefaultAssociationRole(Collections.singletonMap("name", name), complexType, minOccurs, maxOccurs); } } 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; } }