/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 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.simple;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.xml.namespace.QName;
import org.eclipse.xsd.XSDElementDeclaration;
import org.eclipse.xsd.XSDFactory;
import org.eclipse.xsd.XSDParticle;
import org.eclipse.xsd.XSDTypeDefinition;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.geometry.jts.MultiCurve;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.gml2.GML;
import org.geotools.gml2.GMLConfiguration;
import org.geotools.gml2.bindings.GML2EncodingUtils;
import org.geotools.gml2.bindings.GMLEncodingUtils;
import org.geotools.util.Converters;
import org.geotools.xml.Binding;
import org.geotools.xml.Encoder;
import org.geotools.xml.EncoderDelegate;
import org.geotools.xml.SimpleBinding;
import org.geotools.xml.impl.BindingLoader;
import org.geotools.xs.bindings.XSStringBinding;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.NamespaceSupport;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.MultiLineString;
/**
* Base class for feature collection optimized GML encoder delegates
*
* @author Justin Deoliveira, OpenGeo
* @author Andrea Aime, GeoSolutions
*/
public abstract class FeatureCollectionEncoderDelegate implements EncoderDelegate {
Encoder encoder;
GMLDelegate gml;
SimpleFeatureCollection features;
HashMap<Class, GeometryEncoder> geometryEncoders;
NamespaceSupport namespaces;
QName boundedBy;
QName name;
protected FeatureCollectionEncoderDelegate(SimpleFeatureCollection features, Encoder encoder,
GMLDelegate gml) {
this.features = features;
this.gml = gml;
this.encoder = encoder;
this.namespaces = encoder.getNamespaces();
this.encoder = encoder;
this.geometryEncoders = new HashMap<Class, GeometryEncoder>();
this.boundedBy = gml.getSchema().qName("boundedBy");
this.name = gml.getSchema().qName("name");
gml.registerGeometryEncoders(geometryEncoders, encoder);
}
public void encode(ContentHandler handler) throws Exception {
GMLWriter output = new GMLWriter(handler, namespaces, gml.getNumDecimals(),
gml.forceDecimalEncoding(),
gml.getGmlPrefix());
boolean featureBounds = !encoder.getConfiguration().hasProperty(
GMLConfiguration.NO_FEATURE_BOUNDS);
try (SimpleFeatureIterator fi = features.features()) {
if (!fi.hasNext()) {
return;
}
ObjectEncoder ee = gml.createEnvelopeEncoder(encoder);
ee = gml.createEnvelopeEncoder(encoder);
gml.startFeatures(output);
AttributesImpl idatts = new AttributesImpl();
gml.initFidAttribute(idatts);
// scroll over the features, encode each of them
SimpleFeature f = fi.next();
FeatureTypeContextCache ftCache = new FeatureTypeContextCache();
while (f != null) {
// special handling for joined features, they need to be split and
// encoded inside a tuple
if (GMLEncodingUtils.isJoinedFeature(f) && gml.supportsTuples()) {
SimpleFeature[] splitted = GMLEncodingUtils.splitJoinedFeature(f);
gml.startTuple(output);
for (SimpleFeature feature : splitted) {
encodeFeature(output, featureBounds, ee, idatts, feature, ftCache);
}
gml.endTuple(output);
} else {
encodeFeature(output, featureBounds, ee, idatts, f, ftCache);
}
if (fi.hasNext()) {
f = fi.next();
} else {
f = null;
}
}
gml.endFeatures(output);
}
}
/**
* Encodes a single feature
*/
private void encodeFeature(GMLWriter output, boolean featureBounds, ObjectEncoder ee,
AttributesImpl idatts, SimpleFeature f, FeatureTypeContextCache ftCache)
throws SAXException, Exception {
gml.startFeature(output);
FeatureTypeContext ftContext = ftCache.getFeatureTypeContext(f);
idatts.setValue(0, f.getID());
output.startElement(ftContext.featureQualifiedName, idatts);
for (AttributeContext attribute : ftContext.attributes) {
QualifiedName name = attribute.name;
Object value1 = null;
AttributeDescriptor ad = null;
if (boundedBy.equals(name) && featureBounds) {
value1 = f.getBounds();
} else {
int idx = attribute.attributeIndex;
value1 = f.getAttribute(idx);
ad = f.getFeatureType().getDescriptor(idx);
}
Object value = value1;
if (value == null) {
continue;
}
encodeValue(output, ee, value, attribute);
}
output.endElement(ftContext.featureQualifiedName);
gml.endFeature(output);
}
private void encodeValue(GMLWriter output, ObjectEncoder ee, Object value,
AttributeContext attribute) throws SAXException, Exception {
output.startElement(attribute.name, null);
if (value instanceof Geometry) {
Geometry g = (Geometry) value;
Integer dimension = GML2EncodingUtils.getGeometryDimension(g,
encoder.getConfiguration());
AttributesImpl atts = buildSrsAttributes(
((GeometryDescriptor) attribute.descriptor).getCoordinateReferenceSystem(),
dimension);
GeometryEncoder geometryEncoder = getGeometryEncoder(value, attribute);
geometryEncoder.encode(g, atts, output);
} else if (value instanceof Envelope) {
ReferencedEnvelope e = (ReferencedEnvelope) value;
Integer dimension = GML2EncodingUtils.getEnvelopeDimension(e,
encoder.getConfiguration());
AttributesImpl atts = buildSrsAttributes(e.getCoordinateReferenceSystem(), dimension);
ee.encode(e, atts, output);
} else if (attribute.binding instanceof SimpleBinding) {
encodeSimpleBinding(output, value, attribute.binding);
} else {
// just encode string value
output.characters(value.toString());
}
output.endElement(attribute.name);
}
private GeometryEncoder getGeometryEncoder(Object value, AttributeContext attribute) {
Class<? extends Object> clazz = value.getClass();
if(MultiLineString.class.equals(clazz)) {
// we have a wrinkle with curve support, were we supposed to encode the
// multi line string as a curve or not?
if(attribute.binding.getTarget().getLocalPart().startsWith("MultiCurve")) {
clazz = MultiCurve.class;
}
}
GeometryEncoder encoder = geometryEncoders.get(clazz);
while (encoder == null && clazz.getSuperclass() != null) {
clazz = clazz.getSuperclass();
encoder = geometryEncoders.get(clazz);
}
if (encoder == null) {
throw new RuntimeException("Failed to find an appropriate geometry encoder for class "
+ value.getClass());
} else {
return encoder;
}
}
private AttributesImpl buildSrsAttributes(CoordinateReferenceSystem crs, Integer dimension) {
AttributesImpl atts = null;
if (crs != null || dimension != null) {
atts = new AttributesImpl();
if (crs != null) {
gml.setSrsNameAttribute(atts, crs);
}
if (dimension != null) {
gml.setGeometryDimensionAttribute(atts, dimension);
}
}
return atts;
}
private void encodeSimpleBinding(GMLWriter output, Object value, Binding binding)
throws Exception, SAXException {
if (!binding.getType().isInstance(value)) {
Object converted = Converters.convert(value, binding.getType());
if (converted != null) {
value = converted;
}
}
String encoded = ((SimpleBinding) binding).encode(value, null);
if (encoded != null) {
output.characters(encoded);
}
}
/**
* Encoding context for a single attribute, contains all the information we need repeatedly, so
* that we don't need to look it up over and over
*
* @author Andrea Aime - GeoSolutions
*
*/
static final class AttributeContext {
QualifiedName name;
int attributeIndex;
Binding binding;
AttributeDescriptor descriptor;
public AttributeContext(QualifiedName name) {
this.name = name;
}
}
/**
* Encoding context for a feature type, contains all the information we need repeatedly, so that
* we don't need to look it up over and over
*/
final class FeatureTypeContext {
private SimpleFeatureType featureType;
private List<AttributeContext> attributes;
private QualifiedName featureQualifiedName;
public FeatureTypeContext(SimpleFeature f, GMLDelegate gml) {
this.featureType = f.getFeatureType();
QName featureName = new QName(featureType.getName().getNamespaceURI(), featureType
.getName().getLocalPart());
// look up the element in the schema
XSDElementDeclaration element = encoder.getSchemaIndex().getElementDeclaration(
featureName);
if (element == null) {
// create one
element = XSDFactory.eINSTANCE.createXSDElementDeclaration();
element.setName(featureType.getName().getLocalPart());
element.setTargetNamespace(featureType.getName().getNamespaceURI());
element.setTypeDefinition(encoder.getSchemaIndex().getTypeDefinition(
GML.AbstractFeatureType));
}
// look up all the bindings for each property
BindingLoader bindingLoader = encoder.getBindingLoader();
// get all the properties
List properties = gml.getFeatureProperties(f, element, encoder);
attributes = setupAttributeContexts(properties, featureType, bindingLoader);
featureQualifiedName = getFeatureQualifiedName(featureName);
}
/**
* Builds the list of {@link AttributeContext} for each attribute to be encoded
*
* @param properties
* @param schema
* @param bindingLoader
* @return
*/
private List<AttributeContext> setupAttributeContexts(List properties,
SimpleFeatureType schema,
BindingLoader bindingLoader) {
ArrayList<AttributeContext> attributes = new ArrayList<AttributeContext>(
properties.size());
List<AttributeDescriptor> attributeDescriptors = schema.getAttributeDescriptors();
for (Iterator p = properties.iterator(); p.hasNext();) {
Object[] o = (Object[]) p.next();
XSDParticle particle = (XSDParticle) o[0];
XSDElementDeclaration content = (XSDElementDeclaration) particle.getContent();
if (content.isElementDeclarationReference()) {
content = content.getResolvedElementDeclaration();
}
String prefix = namespaces.getPrefix(content.getTargetNamespace());
QualifiedName contentName;
if (prefix != null) {
contentName = QualifiedName.build(content.getTargetNamespace(),
content.getName(), prefix);
} else {
contentName = new QualifiedName(content.getTargetNamespace(), content.getName());
}
AttributeContext attribute = new AttributeContext(contentName);
attributes.add(attribute);
int idx = getNameIndex(content.getName(), attributeDescriptors);
attribute.attributeIndex = idx;
if (idx != -1) {
attribute.descriptor = attributeDescriptors.get(idx);
}
if (name.equals(contentName)) {
// gml:name is a code type which is actually complex, but since we don't
// support code types for simple features, we just use xs:string
attribute.binding = new XSStringBinding();
} else if (boundedBy.equals(contentName)) {
// no need for a binding here
} else {
XSDTypeDefinition contentType = content.getTypeDefinition();
if (contentType.getName() == null) {
// move up to a parent which is not null
while (contentType != null && contentType.getName() == null) {
XSDTypeDefinition baseType = contentType.getBaseType();
if (contentType.equals(baseType)) {
contentType = null;
continue;
}
contentType = baseType;
}
}
if (contentType == null || content.getName() == null) {
throw new IllegalArgumentException("Could not find non annonymous type");
}
QName contentTypeName = new QName(contentType.getTargetNamespace(),
contentType.getName());
Binding binding = bindingLoader.loadBinding(contentTypeName,
encoder.getContext());
attribute.binding = binding;
}
}
return attributes;
}
private int getNameIndex(String name, List<AttributeDescriptor> attributeDescriptors) {
for (int i = 0; i < attributeDescriptors.size(); i++) {
if (name.equals(attributeDescriptors.get(i).getLocalName())) {
return i;
}
}
return -1;
}
private QualifiedName getFeatureQualifiedName(QName featureName) {
String featureNamespaceURI = featureName.getNamespaceURI();
String featureLocalName = featureName.getLocalPart();
String featurePrefix = namespaces.getPrefix(featureNamespaceURI);
QualifiedName featureQualifiedName = QualifiedName.build(featureNamespaceURI,
featureLocalName, featurePrefix);
return featureQualifiedName;
}
public boolean isCompatible(SimpleFeature sf) {
SimpleFeatureType schema = sf.getFeatureType();
return this.featureType == schema || this.featureType.equals(schema);
}
}
/**
* A cache for feature type contexts, to avoid rebuilding them in case a single feature
* collection contains multiple feature types (wrong, but used in CompositeFeatureCollection in
* GeoServer) and for joined results which makes us encode 2 or more features for each "member"
* in the result
*/
final class FeatureTypeContextCache {
FeatureTypeContext last;
Map<SimpleFeatureType, FeatureTypeContext> featureTypeContexts = new IdentityHashMap<>();
public FeatureTypeContext getFeatureTypeContext(SimpleFeature f) {
if (last != null && last.isCompatible(f)) {
return last;
} else {
FeatureTypeContext result = featureTypeContexts.get(f.getFeatureType());
if (result == null) {
result = new FeatureTypeContext(f, gml);
featureTypeContexts.put(f.getFeatureType(), result);
}
return result;
}
}
}
}