/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2012, 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.data.wfs.internal.parsers;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.xml.namespace.QName;
import org.geotools.data.DataSourceException;
import org.geotools.data.complex.ComplexFeatureConstants;
import org.geotools.data.complex.config.Types;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.AttributeBuilder;
import org.geotools.feature.AttributeImpl;
import org.geotools.feature.LenientFeatureFactoryImpl;
import org.geotools.feature.NameImpl;
import org.geotools.feature.ComplexFeatureBuilder;
import org.geotools.feature.type.AttributeDescriptorImpl;
import org.geotools.gml3.GML;
import org.opengis.feature.Attribute;
import org.opengis.feature.ComplexAttribute;
import org.opengis.feature.Feature;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.feature.Property;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.AttributeType;
import org.opengis.feature.type.ComplexType;
import org.opengis.feature.type.FeatureType;
import org.opengis.feature.type.GeometryType;
import org.opengis.feature.type.Name;
import org.opengis.feature.type.PropertyDescriptor;
import org.opengis.feature.type.PropertyType;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
/**
* Parses complex features from a WFS response input stream.
*
* @author Adam Brown (Curtin University of Technology)
*/
public class XmlComplexFeatureParser extends
XmlFeatureParser<FeatureType, Feature> {
/**
* The feature builder used to construct the features.
*/
private final ComplexFeatureBuilder featureBuilder;
/**
* This is a mapping which links string gml:ids to an attribute. It's used
* to keep track of an id'd attributes so that we can refer back to them if
* they're referred to further down the input stream by a href.
*/
private Map<String, Attribute> discoveredComplexAttributes = new HashMap<String, Attribute>();
/**
* The placeholder complex attributes object maintains a record of
* incomplete attributes that relate to a particular hrefed id.
*/
private Map<String, ArrayList<Attribute>> placeholderComplexAttributes = new HashMap<String, ArrayList<Attribute>>();
private Filter filter;
/**
* Initialises a new instance of the XmlComplexFeature class.
*
* @param getFeatureResponseStream
* the input stream of the WFS response.
* @param targetType
* The feature type of the WFS response.
* @param featureDescriptorName
* The name of the feature descriptor.
* @throws IOException
*/
public XmlComplexFeatureParser(InputStream getFeatureResponseStream,
FeatureType targetType, QName featureDescriptorName)
throws IOException {
super(getFeatureResponseStream, targetType, featureDescriptorName);
this.featureBuilder = new ComplexFeatureBuilder(this.targetType);
}
/**
* Initialises a new instance of the XmlComplexFeature class.
*
* @param getFeatureResponseStream
* the input stream of the WFS response.
* @param targetType
* The feature type of the WFS response.
* @param featureDescriptorName
* The name of the feature descriptor.
* @param filter
* Filter to apply to the features.
* @throws IOException
*/
public XmlComplexFeatureParser(InputStream getFeatureResponseStream,
FeatureType targetType, QName featureDescriptorName, Filter filter)
throws IOException {
super(getFeatureResponseStream, targetType, featureDescriptorName);
this.featureBuilder = new ComplexFeatureBuilder(this.targetType);
this.filter = filter;
}
/**
* Search for and parse the next feature.
*/
@Override
public Feature parse() throws IOException {
final String fid;
try {
// Get the feature id or return null if there isn't one:
if ((fid = seekFeature()) == null) {
return null;
}
ReturnAttribute nextAttribute;
// Loop over the document, continually getting the next attribute until
// there are none left.
while ((nextAttribute = parseNextAttribute(this.targetType)) != null) {
if (!Property.class.isAssignableFrom(nextAttribute.value.getClass())) {
featureBuilder.append(nextAttribute.name, new AttributeImpl(nextAttribute.value, (AttributeDescriptor) this.targetType.getDescriptor(nextAttribute.name), null));
} else {
featureBuilder.append(nextAttribute.name,
(Property) nextAttribute.value);
}
}
} catch (XmlPullParserException e) {
throw new DataSourceException(e);
}
Feature feature = featureBuilder.buildFeature(fid);
if (filter == null || filter.evaluate(feature)) {
return feature;
}
return null;
}
/**
* Register the target of any hrefs' with the target id specified,
* to the attribute provided.
* This has two effects: it puts the id and value in the discoveredComplexAttributes object
* so that if we come across a href pointing to this id in the future, we'll be able to
* just use the attribute provided as the value for that href.
* Secondly: it loops over any placeholderComplexAttributes to see if any of them were waiting
* for this target, if their are any they will have their values changed from the placeholder value
* to the value provided.
* @param id
* The id of the gml target.
* @param value
* The parsed attribute value for this id.
*/
private void registerGmlTarget(String id, Attribute value) {
// Add the value to the discoveredComplexAttributes object:
discoveredComplexAttributes.put(id, value);
// Check whether anything is waiting for this attribute and, if so,
// populate them.
if (placeholderComplexAttributes.containsKey(id)) {
for (Attribute placeholderComplexAttribute : this.placeholderComplexAttributes
.get(id)) {
placeholderComplexAttribute.setValue(value.getValue());
}
}
}
/**
* Given a href and an expected type, return either the actual manifestation
* of that href's target or a placeholder object. The real instance will
* be returned if it's already been parsed, otherwise the placeholder will be
* returned. The placeholder will automatically be replaced upon calling
* RegisterGmlTarget(...) once the actual object is parsed.
* @param href
* The href that you wish to resolve.
* @param expectedType
* The attribute type that you expect the href to point to.
* @return
* An attribute of the type specified, either the actual attribute or a
* placeholder.
*/
private Attribute resolveHref(String href, AttributeType expectedType) {
// See what kind of href it is:
if (href.startsWith("#")) {
String hrefId = href.substring(1);
// Does the target of this href already exist in the
// discoveredComplexAttributes object?
if (discoveredComplexAttributes.containsKey(hrefId)) {
// If it does, we can just return that.
return discoveredComplexAttributes.get(hrefId);
} else {
// If not, then we create a placeholderComplexAttribute instead:
Attribute placeholderComplexAttribute = new AttributeImpl(
Collections.<Property> emptyList(), expectedType, null);
// I must maintain a reference back to this object so that I can
// change it once its target is found:
if (!placeholderComplexAttributes.containsKey(hrefId)) {
placeholderComplexAttributes.put(hrefId,
new ArrayList<Attribute>());
}
// Adding it to a list allows us to have multiple hrefs pointing
// to the same target.
placeholderComplexAttributes.get(hrefId).add(
placeholderComplexAttribute);
return placeholderComplexAttribute;
}
} else {
// NOTE: You could modify this to make it handle remote hrefs if
// need be.
// This is temporary code to get things to work:
Attribute placeholderComplexAttribute = new AttributeImpl(
Collections.<Property> emptyList(), expectedType, null);
return placeholderComplexAttribute;
}
}
/**
* Get base (non-collection) type of simple content.
*
* @param type
* @return
*/
static AttributeType getSimpleContentType(AttributeType type) {
Class<?> binding = type.getBinding();
if (binding == Collection.class) {
return getSimpleContentType(type.getSuper());
} else {
return type;
}
}
/**
* This is a recursive method that returns any object that belongs to the
* complexType specified. The return object is wrapped in a ReturnAttribute
* which carries through some values related to the object. They are: its
* GML Id and its name.
*
* @param complexType
* The complex type that the CALLER is trying to build. NB: this
* is NOT the type that the method will build, it's the type that
* the caller wants.
* @return A ReturnAttribute object which groups a (Name) name, (String) id,
* and (Object) value that represent an attribute that belongs in
* the complexType specified. Returns null once there are no more
* elements in the complex type you're trying to parse.
* @throws XmlPullParserException
* @throws IOException
*/
private ReturnAttribute parseNextAttribute(ComplexType complexType)
throws XmlPullParserException, IOException {
// 1. Read through the XML until you come across a start tag, end tag or
// the end of the document:
int tagType;
do {
tagType = parser.next();
} while (tagType != XmlPullParser.START_TAG
&& tagType != XmlPullParser.END_TAG
&& tagType != XmlPullParser.END_DOCUMENT);
// 2. We'll take an action depending on the type of tag we got.
if (tagType == XmlPullParser.START_TAG) {
// 2a. A start tag has been found; if it belongs to the complexType
// then we should parse it and return it.
// 3. Convert the tag's name into a NameImpl and then see if
// there's a descriptor by that name in the type:
Name currentTagName = new NameImpl(parser.getNamespace(),
parser.getName());
PropertyDescriptor descriptor = complexType
.getDescriptor(currentTagName);
if (descriptor != null) {
// 3a. We've found a descriptor for the tag's name in the
// complexType.
// Get the type that the descriptor relates to, and get the GML
// Id if it's set:
PropertyType type = descriptor.getType();
String id = parser.getAttributeValue(GML.id.getNamespaceURI(),
GML.id.getLocalPart());
// Is it defined by an xlink?
String href = parser.getAttributeValue(
"http://www.w3.org/1999/xlink", "href");
// 4. Parse the tag's contents based on whether it's a:
if (href != null) {
// Resolve the href:
Attribute hrefAttribute = resolveHref(href,
(AttributeType) type);
// We've got the attribute but the parser is still
// pointing at this tag so
// we have to advance it till we get to the end tag.
while (parser.next() != XmlPullParser.END_TAG)
;
return new ReturnAttribute(id, currentTagName,
hrefAttribute);
}
// ComplexType or an AttributeType.
else if (type instanceof ComplexType) {
// 4a. The element is a complex type so we must loop through
// each of its internal elements and construct a complex
// attribute.
// The attribute that we get from parsing the next
// attribute.
ReturnAttribute innerAttribute;
// Configure the attribute builder to help build the complex
// attribute.
AttributeBuilder attributeBuilder = new AttributeBuilder(
new LenientFeatureFactoryImpl());
attributeBuilder.setType((AttributeType) type);
if (type.getBinding() == Collection.class
&& Types.isSimpleContentType(type)) {
// Get the value
// I'm calling 'next()' to move the cursor off the tag
// and into its body, otherwise getText() pulls the
// wrong part.
parser.next();
Object value = parser.getText();
// create an empty list
ArrayList<Property> list = new ArrayList<Property>();
// Add the value to the list if it's not null or if
// nulls are allowed by the descriptor.
if (value != null || descriptor.isNillable()) {// add
// the
// result
// of
// buildSimpleContent(type,
// value)
// to
// the
// list
// and
// return
// it.
AttributeType simpleContentType = getSimpleContentType((AttributeType) type);
FilterFactory ff = CommonFactoryFinder
.getFilterFactory(null);
Object convertedValue = ff.literal(value).evaluate(
value, simpleContentType.getBinding());
AttributeDescriptor simpleContentDescriptor = new AttributeDescriptorImpl(
simpleContentType,
ComplexFeatureConstants.SIMPLE_CONTENT, 1,
1, true, (Object) null);
list.add(new AttributeImpl(convertedValue,
simpleContentDescriptor, null));
}
// We've got the attribute but the parser is still
// pointing at this tag so
// we have to advance it till we get to the end tag.
while (parser.next() != XmlPullParser.END_TAG)
;
return new ReturnAttribute(id, currentTagName, list);
}
// 5. Loop over and parse all the attributes in this complex
// feature.
while ((innerAttribute = parseNextAttribute((ComplexType) type)) != null) {
// 6. Check the type of the parsed attribute.
if (ComplexAttribute.class
.isAssignableFrom(innerAttribute.value
.getClass())) {
// 6a. If it's a Property then we must add it to
// a list before sending it to the builder.
ArrayList<Property> properties = new ArrayList<Property>();
properties.add((Property) innerAttribute.value);
attributeBuilder.add(innerAttribute.id, properties,
innerAttribute.name);
} else {
// 6b. If the parsed attribute is actually just
// an object then it must belong to a simple
// type in which case we can just add it to the
// builder as is.
attributeBuilder.add(innerAttribute.id,
getValue(innerAttribute), innerAttribute.name);
}
}
Attribute attribteValue;
if (type instanceof FeatureType) {
attribteValue = attributeBuilder.build(id);
} else {
attribteValue = attributeBuilder.build();
}
// If this item has an id we'll register it in case
// anything else points to it with an xlink:
if (id != null) {
this.registerGmlTarget(id,
(ComplexAttribute) attribteValue);
}
return new ReturnAttribute(id, currentTagName,
attribteValue);
} else if (type instanceof AttributeType
|| type instanceof GeometryType) {
// 4b. It's a simple type so we can use super's
// parseAttributeValue method.
Object attributeValue = super
.parseAttributeValue((AttributeDescriptor) descriptor);
return new ReturnAttribute(id, currentTagName,
attributeValue);
}
} else {
// 3b. If the tag name doesn't belong to this type then
// something is wrong.
throw new RuntimeException(
String.format(
"WFS response structure unexpected. Could not find descriptor in type '%s' for '%s'.",
complexType, currentTagName));
}
} else if (tagType == XmlPullParser.END_DOCUMENT) {
// 2b. Close the parser if we're at the end of the document.
close();
}
// We don't need any special action if the tagType was END_TAG.
return null;
}
private Object getValue(ReturnAttribute innerAttribute) {
if (innerAttribute.value instanceof AttributeImpl) {
return ((AttributeImpl)innerAttribute.value).getValue();
}
return innerAttribute.value;
}
private class ReturnAttribute {
public final String id;
public final Name name;
public final Object value;
public ReturnAttribute(String id, Name name, Object value) {
this.id = id;
this.name = name;
this.value = value;
}
}
}