/*
* Copyright (c) 2012 Data Harmonisation Panel
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution. If not, see <http://www.gnu.org/licenses/>.
*
* Contributors:
* HUMBOLDT EU Integrated Project #030962
* Data Harmonisation Panel <http://www.dhpanel.eu>
*/
package eu.esdihumboldt.hale.io.gml.reader.internal.instance;
import static com.google.common.base.Preconditions.checkState;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import com.google.common.collect.Iterables;
import com.vividsolutions.jts.geom.Geometry;
import de.fhg.igd.slf4jplus.ALogger;
import de.fhg.igd.slf4jplus.ALoggerFactory;
import eu.esdihumboldt.hale.common.core.io.IOProvider;
import eu.esdihumboldt.hale.common.instance.geometry.CRSProvider;
import eu.esdihumboldt.hale.common.instance.geometry.DefaultGeometryProperty;
import eu.esdihumboldt.hale.common.instance.model.Instance;
import eu.esdihumboldt.hale.common.instance.model.MutableGroup;
import eu.esdihumboldt.hale.common.instance.model.MutableInstance;
import eu.esdihumboldt.hale.common.instance.model.impl.DefaultInstance;
import eu.esdihumboldt.hale.common.schema.geometry.CRSDefinition;
import eu.esdihumboldt.hale.common.schema.geometry.GeometryProperty;
import eu.esdihumboldt.hale.common.schema.model.ChildDefinition;
import eu.esdihumboldt.hale.common.schema.model.DefinitionGroup;
import eu.esdihumboldt.hale.common.schema.model.DefinitionUtil;
import eu.esdihumboldt.hale.common.schema.model.PropertyDefinition;
import eu.esdihumboldt.hale.common.schema.model.TypeDefinition;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.AugmentedValueFlag;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.HasValueFlag;
import eu.esdihumboldt.hale.io.gml.geometry.constraint.GeometryFactory;
import eu.esdihumboldt.hale.io.gml.internal.simpletype.SimpleTypeUtil;
import eu.esdihumboldt.hale.io.xsd.constraint.XmlAttributeFlag;
import eu.esdihumboldt.hale.io.xsd.constraint.XmlMixedFlag;
/**
* Utility methods for instances from {@link XMLStreamReader}s
*
* @author Simon Templer
* @partner 01 / Fraunhofer Institute for Computer Graphics Research
*/
public abstract class StreamGmlHelper {
private static final ALogger log = ALoggerFactory.getLogger(StreamGmlHelper.class);
/**
* Parses an instance with the given type from the given XML stream reader.
*
* @param reader the XML stream reader, the current event must be the start
* element of the instance
* @param type the definition of the instance type
* @param indexInStream the index of the instance in the stream or
* <code>null</code>
* @param strict if associating elements with properties should be done
* strictly according to the schema, otherwise a fall-back is
* used trying to populate values also on invalid property paths
* @param srsDimension the dimension of the instance or <code>null</code>
* @param crsProvider CRS provider in case no CRS is specified, may be
* <code>null</code>
* @param parentType the type of the topmost instance
* @param propertyPath the property path down from the topmost instance, may
* be <code>null</code>
* @param allowNull if a <code>null</code> result is allowed
* @param ignoreNamespaces if parsing of the XML instances should allow
* types and properties with namespaces that differ from those
* defined in the schema
* @param ioProvider the I/O Provider to get value
* @return the parsed instance, may be <code>null</code> if allowNull is
* <code>true</code>
* @throws XMLStreamException if parsing the instance failed
*/
public static Instance parseInstance(XMLStreamReader reader, TypeDefinition type,
Integer indexInStream, boolean strict, Integer srsDimension, CRSProvider crsProvider,
TypeDefinition parentType, List<QName> propertyPath, boolean allowNull,
boolean ignoreNamespaces, IOProvider ioProvider) throws XMLStreamException {
checkState(reader.getEventType() == XMLStreamConstants.START_ELEMENT);
if (propertyPath == null) {
propertyPath = Collections.emptyList();
}
if (srsDimension == null) {
String dim = reader.getAttributeValue(null, "srsDimension");
if (dim != null)
srsDimension = Integer.parseInt(dim);
}
MutableInstance instance;
if (indexInStream == null) {
// not necessary to associate data set
instance = new DefaultInstance(type, null);
}
else {
instance = new StreamGmlInstance(type, indexInStream);
}
boolean mixed = type.getConstraint(XmlMixedFlag.class).isEnabled();
if (!mixed) {
// mixed types are treated special (see else)
// check if xsi:nil attribute is there and set to true
String nilString = reader.getAttributeValue(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI,
"nil");
boolean isNil = nilString != null && "true".equalsIgnoreCase(nilString);
// instance properties
parseProperties(reader, instance, strict, srsDimension, crsProvider, parentType,
propertyPath, false, ignoreNamespaces, ioProvider);
// nil instance w/o properties
if (allowNull && isNil && Iterables.isEmpty(instance.getPropertyNames())) {
// no value should be created
/*
* XXX returning null here then results in problems during
* adding other properties to the parent group, as mandatory
* elements are expected to appear, and it will warn about
* possible invalid data loaded
*/
// return null;
}
// instance value
if (!hasElements(type)) {
/*
* Value can only be determined if there are no documents,
* because otherwise elements have already been processed in
* parseProperties and we are already past END_ELEMENT.
*/
if (type.getConstraint(HasValueFlag.class).isEnabled()) {
// try to get text value
String value = reader.getElementText();
if (!isNil && value != null) {
instance.setValue(convertSimple(type, value));
}
}
}
}
else {
/*
* XXX For a mixed type currently ignore elements and parse only
* attributes and text.
*/
// instance properties (attributes only)
parseProperties(reader, instance, strict, srsDimension, crsProvider, parentType,
propertyPath, true, ignoreNamespaces, ioProvider);
// combined text
String value = readText(reader);
if (value != null) {
instance.setValue(convertSimple(type, value));
}
}
// augmented value XXX should this be an else if?
if (type.getConstraint(AugmentedValueFlag.class).isEnabled()) {
// add geometry as a GeometryProperty value where applicable
GeometryFactory geomFactory = type.getConstraint(GeometryFactory.class);
Object geomValue = null;
// the default value for the srsDimension
int defaultValue = 2;
try {
if (srsDimension != null) {
geomValue = geomFactory.createGeometry(instance, srsDimension, ioProvider);
}
else {
// srsDimension is not set
geomValue = geomFactory.createGeometry(instance, defaultValue, ioProvider);
}
} catch (Exception e) {
/*
* Catch IllegalArgumentException that e.g. occurs if a linear
* ring has to few points. NullPointerExceptions may occur
* because an internal geometry could not be created.
*
* XXX a problem is that these messages will not appear in the
* report
*/
log.error("Error creating geometry", e);
}
if (geomValue != null && crsProvider != null && propertyPath != null) {
// check if CRS are set, and if not, try determining them using
// the CRS provider
Collection<?> values;
if (geomValue instanceof Collection) {
values = (Collection<?>) geomValue;
}
else {
values = Collections.singleton(geomValue);
}
List<Object> resultVals = new ArrayList<Object>();
for (Object value : values) {
if (value instanceof Geometry || (value instanceof GeometryProperty<?>
&& ((GeometryProperty<?>) value).getCRSDefinition() == null)) {
CRSDefinition crs = crsProvider.getCRS(parentType, propertyPath);
if (crs != null) {
Geometry geom = (value instanceof Geometry) ? ((Geometry) value)
: (((GeometryProperty<?>) value).getGeometry());
resultVals.add(new DefaultGeometryProperty<Geometry>(crs, geom));
continue;
}
}
resultVals.add(value);
}
if (resultVals.size() == 1) {
geomValue = resultVals.get(0);
}
else {
geomValue = resultVals;
}
}
if (geomValue != null) {
instance.setValue(geomValue);
}
}
return instance;
}
/**
* Read the text value of the current element from the stream. The stream is
* expected to be at {@link XMLStreamConstants#START_ELEMENT}. For mixed
* content elements the text content is concatenated and the elements
* ignored.
*
* FIXME different handling for mixed types?
*
* @param reader the XML stream reader
* @return the element text
* @throws XMLStreamException if an error occurs reading from the stream or
* the element ends prematurely
*/
private static String readText(XMLStreamReader reader) throws XMLStreamException {
checkState(reader.getEventType() == XMLStreamConstants.START_ELEMENT);
int eventType = reader.next();
StringBuilder content = new StringBuilder();
int openElements = 0;
while (openElements > 0 || eventType != XMLStreamConstants.END_ELEMENT) {
if (eventType == XMLStreamConstants.CHARACTERS || eventType == XMLStreamConstants.CDATA
|| eventType == XMLStreamConstants.SPACE
|| eventType == XMLStreamConstants.ENTITY_REFERENCE) {
content.append(reader.getText());
}
else if (eventType == XMLStreamConstants.PROCESSING_INSTRUCTION
|| eventType == XMLStreamConstants.COMMENT) {
// skipping
}
else if (eventType == XMLStreamConstants.END_DOCUMENT) {
throw new XMLStreamException(
"unexpected end of document when reading element text content");
}
else if (eventType == XMLStreamConstants.START_ELEMENT) {
openElements++;
}
else if (eventType == XMLStreamConstants.END_ELEMENT) {
openElements--;
}
else {
// ignore
// throw new XMLStreamException("Unexpected event type " + eventType, reader.getLocation());
}
eventType = reader.next();
}
checkState(reader.getEventType() == XMLStreamConstants.END_ELEMENT);
return content.toString();
}
/**
* Populates an instance or group with its properties based on the given XML
* stream reader.
*
* @param reader the XML stream reader
* @param group the group to populate with properties
* @param strict if associating elements with properties should be done
* strictly according to the schema, otherwise a fall-back is
* used trying to populate values also on invalid property paths
* @param srsDimension the dimension of the instance or <code>null</code>
* @param crsProvider CRS provider in case no CRS is specified, may be
* <code>null</code>
* @param parentType the type of the topmost instance
* @param propertyPath the property path down from the topmost instance
* @param onlyAttributes if only attributes should be parsed
* @param ignoreNamespaces if parsing of the XML instances should allow
* types and properties with namespaces that differ from those
* defined in the schema
* @param ioProvider the I/O Provider to get value
* @throws XMLStreamException if parsing the properties failed
*/
private static void parseProperties(XMLStreamReader reader, MutableGroup group, boolean strict,
Integer srsDimension, CRSProvider crsProvider, TypeDefinition parentType,
List<QName> propertyPath, boolean onlyAttributes, boolean ignoreNamespaces,
IOProvider ioProvider) throws XMLStreamException {
final MutableGroup topGroup = group;
// attributes (usually only present in Instances)
for (int i = 0; i < reader.getAttributeCount(); i++) {
QName propertyName = reader.getAttributeName(i);
// XXX might also be inside a group? currently every attribute group
// should be flattened
// for group support there would have to be some other kind of
// handling than for elements, cause order doesn't matter for
// attributes
ChildDefinition<?> child = GroupUtil.findChild(group.getDefinition(), propertyName,
ignoreNamespaces);
if (child != null && child.asProperty() != null) {
// add property value
addSimpleProperty(group, child.asProperty(), reader.getAttributeValue(i));
}
else {
// suppress warnings for xsi attributes (e.g. xsi:nil)
boolean suppress = XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI
.equals(propertyName.getNamespaceURI());
if (!suppress) {
log.warn(MessageFormat.format(
"No property ''{0}'' found in type ''{1}'', value is ignored",
propertyName, group.getDefinition().getIdentifier()));
}
}
}
Stack<MutableGroup> groups = new Stack<MutableGroup>();
groups.push(topGroup);
// elements
if (!onlyAttributes && hasElements(group.getDefinition())) {
int open = 1;
while (open > 0 && reader.hasNext()) {
int event = reader.next();
switch (event) {
case XMLStreamConstants.START_ELEMENT:
// determine property definition, allow fall-back to
// non-strict mode
GroupProperty gp = GroupUtil.determineProperty(groups, reader.getName(),
!strict, ignoreNamespaces);
if (gp != null) {
// update the stack from the path
groups = gp.getPath().getAllGroups(strict);
// get group object from stack
group = groups.peek();
PropertyDefinition property = gp.getProperty();
List<QName> path = new ArrayList<QName>(propertyPath);
path.add(property.getName());
if (hasElements(property.getPropertyType())) {
// use an instance as value
Instance inst = parseInstance(reader, property.getPropertyType(), null,
strict, srsDimension, crsProvider, parentType, path, true,
ignoreNamespaces, ioProvider);
if (inst != null) {
group.addProperty(property.getName(), inst);
}
}
else {
if (hasAttributes(property.getPropertyType())) {
// no elements but attributes
// use an instance as value, it will be assigned
// an instance value if possible
Instance inst = parseInstance(reader, property.getPropertyType(),
null, strict, srsDimension, crsProvider, parentType, path,
true, ignoreNamespaces, ioProvider);
if (inst != null) {
group.addProperty(property.getName(), inst);
}
}
else {
// no elements and no attributes
// use simple value
String value = readText(reader);
if (value != null) {
addSimpleProperty(group, property, value);
}
}
}
}
else {
log.warn(MessageFormat.format(
"No property ''{0}'' found in type ''{1}'', value is ignored",
reader.getLocalName(), topGroup.getDefinition().getIdentifier()));
}
if (reader.getEventType() != XMLStreamConstants.END_ELEMENT) {
// only increase open if the current event is not
// already the end element (because we used
// getElementText)
open++;
}
break;
case XMLStreamConstants.END_ELEMENT:
open--;
break;
}
}
}
}
/**
* Determines if the given type has properties that are represented as XML
* elements.
*
* @param group the type definition
* @return if the type is a complex type
*/
static boolean hasElements(DefinitionGroup group) {
return hasElementsOrAttributes(group, false, new HashSet<DefinitionGroup>());
}
private static boolean hasElementsOrAttributes(DefinitionGroup group, boolean attributes,
Set<DefinitionGroup> tested) {
if (tested.contains(group)) {
// prevent cycles
return false;
}
else {
tested.add(group);
}
Collection<? extends ChildDefinition<?>> children = DefinitionUtil.getAllChildren(group);
for (ChildDefinition<?> child : children) {
if (child.asProperty() != null) {
if (child.asProperty().getConstraint(XmlAttributeFlag.class).isEnabled()) {
if (attributes) {
return true;
}
}
else {
if (!attributes) {
return true;
}
}
}
else if (child.asGroup() != null) {
if (hasElementsOrAttributes(child.asGroup(), attributes, tested)) {
return true;
}
}
}
return false;
}
/**
* Determines if the given type has properties that are represented as XML
* attributes.
*
* @param group the type definition
* @return if the type has at least one XML attribute property
*/
static boolean hasAttributes(DefinitionGroup group) {
return hasElementsOrAttributes(group, true, new HashSet<DefinitionGroup>());
}
/**
* Adds a property value to the given instance. The property value will be
* converted appropriately.
*
* @param group the instance
* @param property the property
* @param value the property value as specified in the XML
*/
private static void addSimpleProperty(MutableGroup group, PropertyDefinition property,
String value) {
Object val = convertSimple(property.getPropertyType(), value);
group.addProperty(property.getName(), val);
}
/**
* Convert a string value from a XML simple type to the binding defined by
* the given type.
*
* @param type the type associated with the value
* @param value the value
* @return the converted object
*/
private static Object convertSimple(TypeDefinition type, String value) {
return SimpleTypeUtil.convertFromXml(value, type);
}
}