/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2002-2015, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.gml2.bindings; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.xml.XMLConstants; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.eclipse.xsd.XSDComplexTypeDefinition; import org.eclipse.xsd.XSDCompositor; import org.eclipse.xsd.XSDDerivationMethod; import org.eclipse.xsd.XSDElementDeclaration; import org.eclipse.xsd.XSDFactory; import org.eclipse.xsd.XSDModelGroup; import org.eclipse.xsd.XSDParticle; import org.eclipse.xsd.XSDTypeDefinition; import org.eclipse.xsd.util.XSDConstants; import org.geotools.feature.NameImpl; import org.geotools.gml2.GMLConfiguration; import org.geotools.util.logging.Logging; import org.geotools.xlink.XLINK; import org.geotools.xml.Configuration; import org.geotools.xml.SchemaIndex; import org.geotools.xml.Schemas; import org.geotools.xml.XSD; import org.geotools.xs.XS; import org.opengis.feature.ComplexAttribute; import org.opengis.feature.Feature; import org.opengis.feature.GeometryAttribute; import org.opengis.feature.Property; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.ComplexType; import org.opengis.feature.type.FeatureType; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.feature.type.Name; import org.opengis.geometry.BoundingBox; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.w3c.dom.Document; import org.xml.sax.Attributes; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; /** * * * @source $URL$ */ public class GMLEncodingUtils { /** logging instance */ static Logger LOGGER = Logging.getLogger( "org.geotools.gml"); XSD gml; public GMLEncodingUtils(XSD gml) { this.gml = gml; } @SuppressWarnings({ "rawtypes", "unchecked" }) public List AbstractFeatureType_getProperties(Object object, XSDElementDeclaration element, SchemaIndex schemaIndex, Set<String> toFilter, Configuration configuration) { Feature feature = (Feature) object; //check if this was a resolved feature, if so dont return anything // TODO: this is just a hack for our lame xlink implementation if (feature.getUserData().get("xlink:id") != null) { return Collections.EMPTY_LIST; } FeatureType featureType = feature.getType(); String namespace = featureType.getName().getNamespaceURI(); if (namespace == null) { namespace = element.getTargetNamespace(); } String typeName = featureType.getName().getLocalPart(); QName qualifiedTypeName = new QName(namespace, typeName); //find the type in the schema XSDTypeDefinition type = schemaIndex.getTypeDefinition(qualifiedTypeName); if (type == null) { //type not found, do a check for an element, and use its type XSDElementDeclaration e = schemaIndex.getElementDeclaration(qualifiedTypeName); if (e != null) { type = e.getTypeDefinition(); } } if (type == null) { if (featureType instanceof SimpleFeatureType) { // could not find the feature type in the schema, create a mock one LOGGER.fine("Could find type for " + typeName + " in the schema, generating type from feature."); type = createXmlTypeFromFeatureType((SimpleFeatureType) featureType, schemaIndex, toFilter); } else { // look for an element declaration smuggled in the UserData map. XSDElementDeclaration e = (XSDElementDeclaration) feature.getDescriptor() .getUserData().get(XSDElementDeclaration.class); if (e != null) { type = e.getTypeDefinition(); } else if (element != null) { // as a last resort, use type definition from element declaration XSDTypeDefinition elementTypeDef = element.getTypeDefinition(); QName qualifiedElementTypeName = new QName( elementTypeDef.getTargetNamespace(), elementTypeDef.getName()); if (qualifiedTypeName.equals(qualifiedElementTypeName)) { type = elementTypeDef; } } } } if (type == null) { throw new RuntimeException("Could not find type for " + qualifiedTypeName + " in schema"); } List particles = Schemas.getChildElementParticles(type, true); List properties = new ArrayList(); Set<Name> unsubstPropertyNames = null; O: for (int i = 0; i < particles.size(); i++) { XSDParticle particle = (XSDParticle) particles.get(i); XSDElementDeclaration attribute = (XSDElementDeclaration) particle.getContent(); if (attribute.isElementDeclarationReference()) { attribute = attribute.getResolvedElementDeclaration(); } if (gml.qName("boundedBy") .equals(new QName(attribute.getTargetNamespace(), attribute.getName()))) { BoundingBox bounds = getBoundedBy(feature, configuration); if (bounds != null) { properties.add(new Object[] { particle, bounds }); } } else if (featureType instanceof SimpleFeatureType) { // first simple feature hack, if the schema "overrides" gml attributes like // name and description, ignore the gml version boolean skip = false; if (gml.getNamespaceURI().equals(attribute.getTargetNamespace())) { for (int j = i+1; j < particles.size(); j++) { XSDParticle particle2 = (XSDParticle) particles.get(j); XSDElementDeclaration attribute2 = (XSDElementDeclaration) particle2.getContent(); if (attribute2.isElementDeclarationReference()) { attribute2 = attribute2.getResolvedElementDeclaration(); } if (attribute2.getName().equals(attribute.getName())) { skip = true; break; } } } if (skip) { continue; } // simple feature brain damage: discard namespace // make sure the feature type has an element if (!isValidDescriptor(featureType, new NameImpl(attribute.getName()))) { continue; } // get the value Object attributeValue = ((SimpleFeature) feature).getAttribute(attribute.getName()); if (attributeValue != null && attributeValue instanceof Geometry) { Object obj = ((Geometry) attributeValue).getUserData(); Map<Object, Object> userData = new HashMap<Object, Object>(); if (obj != null && obj instanceof Map) { userData.putAll((Map) obj); } userData.put(CoordinateReferenceSystem.class, featureType .getCoordinateReferenceSystem()); ((Geometry) attributeValue).setUserData(userData); } properties.add(new Object[] { particle, attributeValue }); } else { // namespaces matter for non-simple feature types Name propertyName = new NameImpl(attribute.getTargetNamespace(), attribute .getName()); // make sure the feature type has an element if (!isValidDescriptor(featureType, propertyName)) { continue; } Collection<Property> featureProperties = feature.getProperties(propertyName); //if no feature properties are found for this element check substitution groups if (featureProperties.size() == 0) { if (unsubstPropertyNames == null) { // lazy initialisation of a set of all property names that // will be obtained without considering substitution groups unsubstPropertyNames = (Set<Name>) particles.stream().map(new Function() { @Override public Object apply(Object particle) { XSDElementDeclaration attr = (XSDElementDeclaration) ((XSDParticle) particle) .getContent(); if (attr.isElementDeclarationReference()) { attr = attr.getResolvedElementDeclaration(); } return new NameImpl(attr.getTargetNamespace(), attr.getName()); } }).collect(Collectors.toSet()); } for (XSDElementDeclaration xsdElementDeclaration : attribute .getSubstitutionGroup()) { Name substPropertyName = new NameImpl( xsdElementDeclaration.getTargetNamespace(), xsdElementDeclaration.getName()); if (!unsubstPropertyNames.contains(substPropertyName)) { featureProperties = feature.getProperties(substPropertyName); if (featureProperties.size() > 0) { // the particle is used outside this class, replace // the particle with the correct substituted element particle = (XSDParticle) particle.cloneConcreteComponent(true, false); particle.setContent(xsdElementDeclaration); break; } } } } // get the value (might be multiple) for (Property property : featureProperties) { Object value; if (property instanceof ComplexAttribute) { // do not unpack complex attributes as these may have their own bindings, which // will be applied by the encoder value = property; } else if (property instanceof GeometryAttribute) { value = property.getValue(); if (value != null) { // ensure CRS is passed to the Geometry object Geometry geometry = (Geometry) value; CoordinateReferenceSystem crs = ((GeometryAttribute) property) .getDescriptor().getCoordinateReferenceSystem(); Map<Object, Object> userData = new HashMap<Object, Object>(); Object obj = geometry.getUserData(); if (obj != null && obj instanceof Map) { userData.putAll((Map) obj); } userData.put(CoordinateReferenceSystem.class, crs); geometry.setUserData(userData); } } else { // non-complex bindings are unpacked as for simple feature case value = property.getValue(); } properties.add(new Object[] { particle, value }); } } } return properties; } public XSDTypeDefinition createXmlTypeFromFeatureType(SimpleFeatureType featureType, SchemaIndex schemaIndex, Set<String> toFilter) { XSDFactory f = XSDFactory.eINSTANCE; Document dom; try { dom = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); } catch (ParserConfigurationException e) { throw new RuntimeException( e ); } XSDComplexTypeDefinition type = f.createXSDComplexTypeDefinition(); type.setTargetNamespace( featureType.getName().getNamespaceURI() ); type.setName( featureType.getTypeName() + "Type" ); type.setDerivationMethod(XSDDerivationMethod.EXTENSION_LITERAL); type.setBaseTypeDefinition(schemaIndex.getTypeDefinition(gml.qName("AbstractFeatureType"))); XSDModelGroup group = f.createXSDModelGroup(); group.setCompositor(XSDCompositor.SEQUENCE_LITERAL); List attributes = featureType.getAttributeDescriptors(); for (int i = 0; i < attributes.size(); i++) { AttributeDescriptor attribute = (AttributeDescriptor) attributes.get(i); if ( toFilter.contains( attribute.getLocalName() ) ) { continue; } XSDElementDeclaration element = f.createXSDElementDeclaration(); element.setName(attribute.getLocalName()); element.setNillable(attribute.isNillable()); //check for geometry if ( attribute instanceof GeometryDescriptor ) { Class binding = attribute.getType().getBinding(); if ( Point.class.isAssignableFrom( binding ) ) { element.setTypeDefinition(schemaIndex.getTypeDefinition(gml .qName("PointPropertyType"))); } else if ( LineString.class.isAssignableFrom( binding ) ) { // check both GML 3.1 and GML 3.2 types names element.setTypeDefinition(searchType(schemaIndex, "LineStringPropertyType", "CurvePropertyType")); } else if ( Polygon.class.isAssignableFrom( binding) ) { // check both GML 3.1 and GML 3.2 types names element.setTypeDefinition(searchType(schemaIndex, "PolygonPropertyType", "SurfacePropertyType")); } else if ( MultiPoint.class.isAssignableFrom( binding ) ) { element.setTypeDefinition(schemaIndex.getTypeDefinition(gml .qName("MultiPointPropertyType"))); } else if ( MultiLineString.class.isAssignableFrom( binding ) ) { // check both GML 3.1 and GML 3.2 types names element.setTypeDefinition(searchType(schemaIndex, "MultiLineStringPropertyType", "MultiCurvePropertyType")); } else if ( MultiPolygon.class.isAssignableFrom( binding) ) { // check both GML 3.1 and GML 3.2 types names element.setTypeDefinition(searchType(schemaIndex, "MultiPolygonPropertyType", "MultiSurfacePropertyType")); } else { element.setTypeDefinition(schemaIndex.getTypeDefinition(gml .qName("GeometryPropertyType"))); } } else { //TODO: do a proper mapping element.setTypeDefinition(schemaIndex.getTypeDefinition(XS.STRING)); } element.setTargetNamespace(featureType.getName().getNamespaceURI()); XSDParticle particle = f.createXSDParticle(); particle.setMinOccurs(attribute.getMinOccurs()); particle.setMaxOccurs(attribute.getMaxOccurs()); particle.setContent(element); particle.setElement( dom.createElementNS( XSDConstants.SCHEMA_FOR_SCHEMA_URI_2001, "element" ) ); group.getContents().add(particle); } XSDParticle particle = f.createXSDParticle(); particle.setContent(group); particle.setElement( dom.createElementNS( XSDConstants.SCHEMA_FOR_SCHEMA_URI_2001, "sequence") ); type.setContent(particle); return type; } /** * Return the first XSD type definition found in the schema index for * the provided GML types names. NULL is returned if no XSD type * definition is found. */ private XSDTypeDefinition searchType(SchemaIndex schemaIndex, String... typesNames) { for (String typeName : typesNames) { XSDTypeDefinition type = schemaIndex.getTypeDefinition(gml.qName(typeName)); if (type != null) { // we found a matching XSD type return type; } } // no matching XSD type found LOGGER.fine(String.format("No type definition found for types [%s].", Arrays.stream(typesNames).collect(Collectors.joining(", ")))); return null; } /** * Return true if name is the name of a descriptor of the type or of an ancestor type. * * @param type type to test * @param name name of descriptor * @return true if the type or an ancestor has a descriptor of this name */ private boolean isValidDescriptor(ComplexType type, Name name) { if (type.getDescriptor(name) != null) { return true; } else if (type.getSuper() instanceof ComplexType) { return isValidDescriptor((ComplexType) type.getSuper(), name); } else { return false; } } /** * Return gml:boundedBy property if wanted. * * @param feature * feature for which bounds might be required * @param configuration * encoder configuration, used to suppress feature bounds * @return the feature bounds, or null if none or unwanted */ private BoundingBox getBoundedBy(Feature feature, Configuration configuration) { // check for flag not to include bounds if (configuration.hasProperty(GMLConfiguration.NO_FEATURE_BOUNDS)) { return null; } else { BoundingBox bounds = feature.getBounds(); // do a check for the case where the feature has no geometry properties if (bounds.isEmpty() && (feature.getDefaultGeometryProperty() == null || feature .getDefaultGeometryProperty().getValue() == null)) { return null; } else { return bounds; } } } public Object GeometryPropertyType_getProperty(Geometry geometry, QName name) { return GeometryPropertyType_getProperty(geometry, name, true, false); } public Object GeometryPropertyType_getProperty(Geometry geometry, QName name, boolean includeAbstractGeometry) { return GeometryPropertyType_getProperty(geometry, name, includeAbstractGeometry, false); } public Object GeometryPropertyType_getProperty(Geometry geometry, QName name, boolean includeAbstractGeometry, boolean makeEmpty) { if (name.equals(gml.qName("Point")) || name.equals(gml.qName("LineString")) || name.equals(gml.qName("Polygon")) || name.equals(gml.qName("MultiPoint")) || name.equals(gml.qName("MultiLineString")) || name.equals(gml.qName("MultiPolygon")) || name.equals(gml.qName("MultiSurface")) || name.equals(gml.qName("AbstractSurface")) || name.equals(gml.qName("_Surface")) || name.equals(gml.qName("_Curve")) || name.equals(gml.qName("AbstractCurve")) || name.equals(gml.qName("MultiCurve")) || (includeAbstractGeometry && (name.equals(gml.qName("_Geometry")) || name.equals(gml.qName("AbstractGeometry"))))) { // if the geometry is null, return null if (isEmpty(geometry) || makeEmpty) { return null; } return geometry; } if (geometry.getUserData() instanceof Map) { Map<Name, Object> clientProperties = (Map<Name, Object>) ((Map) geometry.getUserData()) .get(Attributes.class); Name cname = toTypeName(name); if (clientProperties != null && clientProperties.keySet().contains(cname)) return clientProperties.get(cname); } if (XLINK.HREF.equals(name)) { // only process if geometry is empty and ID exists String id = getID(geometry); if ((makeEmpty || isEmpty(geometry)) && id != null) { return "#" + id; } } return null; } /** * Convert a {@link QName} to a {@link Name}. * * @param name * @return */ private static Name toTypeName(QName name) { if (XMLConstants.NULL_NS_URI.equals(name.getNamespaceURI())) { return new NameImpl(name.getLocalPart()); } else { return new NameImpl(name.getNamespaceURI(), name.getLocalPart()); } } public List GeometryPropertyType_getProperties(Geometry geometry) { return null; } public static boolean isEmpty( Geometry geometry ) { if ( geometry.isEmpty() ) { //check for case of multi geometry, if it has > 0 goemetries // we consider this to be not empty if ( geometry instanceof GeometryCollection ) { if ( ((GeometryCollection) geometry).getNumGeometries() != 0 ) { return false; } } return true; } return false; } /** * Determines the identifier (gml:id) of the geometry by checking * {@link Geometry#getUserData()}. * <p> * This method returns <code>null</code> when no id can be found. * </p> */ public String getID(Geometry g) { return getMetadata( g, "gml:id" ); } /** * Set the identifier (gml:id) of the geometry as a key in the user data map * {@link Geometry#getUserData()} (creating it with{@link Geometry#getUserData()} * if it does not already exist). If the user data exists and is not a * {@link Map}, this method has no effect. * * @param g the geometry * @param id the gml:id to be set */ public void setID(Geometry g, String id) { setMetadata(g, "gml:id", id); } /** * Determines the description (gml:description) of the geometry by checking * {@link Geometry#getUserData()}. * <p> * This method returns <code>null</code> when no name can be found. * </p> */ public String getName(Geometry g) { return getMetadata( g, "gml:name" ); } /** * Set the name (gml:name) of the geometry as a key in the user data map * {@link Geometry#getUserData()} (creating it with{@link Geometry#getUserData()} * if it does not already exist). If the user data exists and is not a * {@link Map}, this method has no effect. * * @param g the geometry * @param name the gml:name to be set */ public void setName(Geometry g, String name) { setMetadata(g, "gml:name", name); } /** * Determines the name (gml:name) of the geometry by checking * {@link Geometry#getUserData()}. * <p> * This method returns <code>null</code> when no description can be found. * </p> */ public String getDescription(Geometry g) { return getMetadata( g, "gml:description" ); } /** * Set the description (gml:description) of the geometry as a key in the user data map * {@link Geometry#getUserData()} (creating it with{@link Geometry#getUserData()} * if it does not already exist). If the user data exists and is not a * {@link Map}, this method has no effect. * * @param g the geometry * @param description the gml:description to be set */ public void setDescription(Geometry g, String description) { setMetadata(g, "gml:description", description); } @SuppressWarnings("rawtypes") String getMetadata(Geometry g, String metadata) { if (g.getUserData() instanceof Map) { Map userData = (Map) g.getUserData(); return (String) userData.get(metadata); } return null; } @SuppressWarnings({ "unchecked", "rawtypes" }) void setMetadata(Geometry g, String metadata, String value) { if (g.getUserData() == null) { g.setUserData(new HashMap()); } if (g.getUserData() instanceof Map) { ((Map) g.getUserData()).put(metadata, value); } } /** * Checks if a feature is a joined one * * @param obj * @return */ public static boolean isJoinedFeature(Object obj) { if (!(obj instanceof SimpleFeature)) { return false; } SimpleFeature feature = (SimpleFeature) obj; for (Object att : feature.getAttributes()) { if (att != null && att instanceof SimpleFeature) { return true; } } return false; } /** * Splits a joined feature into its components * * @param obj * @return */ public static SimpleFeature[] splitJoinedFeature(Object obj) { SimpleFeature feature = (SimpleFeature) obj; List features = new ArrayList(); features.add(feature); for (int i = 0; i < feature.getAttributeCount(); i++) { Object att = feature.getAttribute(i); if (att != null && att instanceof SimpleFeature) { features.add(att); // TODO: come up with a better approcach user, use user data or something to mark // the attribute as encoded feature.setAttribute(i, null); } } return (SimpleFeature[]) features.toArray(new SimpleFeature[features.size()]); } }