/*
* 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.writer.internal;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.Nullable;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.geotools.gml3.GML;
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.core.io.IOProviderConfigurationException;
import eu.esdihumboldt.hale.common.core.io.ProgressIndicator;
import eu.esdihumboldt.hale.common.core.io.Value;
import eu.esdihumboldt.hale.common.core.io.impl.AbstractIOProvider;
import eu.esdihumboldt.hale.common.core.io.impl.SubtaskProgressIndicator;
import eu.esdihumboldt.hale.common.core.io.report.IOReport;
import eu.esdihumboldt.hale.common.core.io.report.IOReporter;
import eu.esdihumboldt.hale.common.core.io.report.impl.IOMessageImpl;
import eu.esdihumboldt.hale.common.core.io.supplier.DefaultInputSupplier;
import eu.esdihumboldt.hale.common.core.io.supplier.Locatable;
import eu.esdihumboldt.hale.common.instance.io.impl.AbstractGeoInstanceWriter;
import eu.esdihumboldt.hale.common.instance.io.impl.AbstractInstanceWriter;
import eu.esdihumboldt.hale.common.instance.io.util.EnumWindingOrderTypes;
import eu.esdihumboldt.hale.common.instance.model.Group;
import eu.esdihumboldt.hale.common.instance.model.Instance;
import eu.esdihumboldt.hale.common.instance.model.InstanceCollection;
import eu.esdihumboldt.hale.common.instance.model.ResourceIterator;
import eu.esdihumboldt.hale.common.schema.geometry.CRSDefinition;
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.GroupPropertyDefinition;
import eu.esdihumboldt.hale.common.schema.model.PropertyDefinition;
import eu.esdihumboldt.hale.common.schema.model.Schema;
import eu.esdihumboldt.hale.common.schema.model.SchemaSpace;
import eu.esdihumboldt.hale.common.schema.model.TypeDefinition;
import eu.esdihumboldt.hale.common.schema.model.constraint.property.Cardinality;
import eu.esdihumboldt.hale.common.schema.model.constraint.property.ChoiceFlag;
import eu.esdihumboldt.hale.common.schema.model.constraint.property.NillableFlag;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.AbstractFlag;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.Binding;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.ElementType;
import eu.esdihumboldt.hale.common.schema.model.constraint.type.HasValueFlag;
import eu.esdihumboldt.hale.io.gml.geometry.GMLConstants;
import eu.esdihumboldt.hale.io.gml.internal.simpletype.SimpleTypeUtil;
import eu.esdihumboldt.hale.io.gml.writer.XmlWrapper;
import eu.esdihumboldt.hale.io.gml.writer.XmlWriterBase;
import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.AbstractTypeMatcher;
import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.DefinitionPath;
import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.Descent;
import eu.esdihumboldt.hale.io.gml.writer.internal.geometry.StreamGeometryWriter;
import eu.esdihumboldt.hale.io.xsd.constraint.XmlAttributeFlag;
import eu.esdihumboldt.hale.io.xsd.constraint.XmlElements;
import eu.esdihumboldt.hale.io.xsd.model.XmlElement;
import eu.esdihumboldt.hale.io.xsd.model.XmlIndex;
import eu.esdihumboldt.hale.io.xsd.reader.XmlSchemaReader;
import eu.esdihumboldt.util.Pair;
import eu.esdihumboldt.util.geometry.NumberFormatter;
/**
* Writes GML/XML using a {@link XMLStreamWriter}
*
* @author Simon Templer
* @partner 01 / Fraunhofer Institute for Computer Graphics Research
*/
public class StreamGmlWriter extends AbstractGeoInstanceWriter
implements XmlWriterBase, GMLConstants {
/**
* Schema instance namespace (for specifying schema locations)
*/
public static final String SCHEMA_INSTANCE_NS = XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI; // $NON-NLS-1$
private static final ALogger log = ALoggerFactory.getLogger(StreamGmlWriter.class);
/**
* The parameter name for the flag specifying if a geometry should be
* simplified before writing it, if possible. Defaults to true.
*/
public static final String PARAM_SIMPLIFY_GEOMETRY = "gml.geometry.simplify";
/**
* The parameter name for the flag specifying if the output should be
* indented. Defaults to <code>false</code>.
*/
public static final String PARAM_PRETTY_PRINT = "xml.pretty";
/**
* The parameter name for the flag specifying an identifier (XML ID) for the
* container.
*/
public static final String PARAM_CONTAINER_ID = "xml.containerId";
/**
* The parameter name of the flag specifying if nilReason attributes should
* be omitted if an element is not nil.
*/
public static final String PARAM_OMIT_NIL_REASON = "xml.notNil.omitNilReason";
/**
* The parameter name for the flag specifying if the output of geometry
* coordinates should be formatted.
*/
public static final String PARAM_GEOMETRY_FORMAT = "geometry.write.decimalFormat";
/**
* The XML stream writer
*/
private XMLStreamWriter writer;
/**
* The GML namespace
*/
private String gmlNs;
// /**
// * The type index
// */
// private TypeIndex types;
/**
* The geometry writer
*/
private StreamGeometryWriter geometryWriter;
/**
* Additional schemas included in the document
*/
private final Map<String, Locatable> additionalSchemas = new HashMap<>();
private final Map<String, String> additionalSchemaPrefixes = new HashMap<>();
/**
* States if a feature collection shall be used
*/
private final boolean useFeatureCollection;
private XmlIndex targetIndex;
private XmlWrapper documentWrapper;
/**
* Create a GML writer
*
* @param useFeatureCollection if a GML feature collection shall be used to
* store the instances (if possible)
*/
public StreamGmlWriter(boolean useFeatureCollection) {
super();
this.useFeatureCollection = useFeatureCollection;
addSupportedParameter(PARAM_ROOT_ELEMENT_NAMESPACE);
addSupportedParameter(PARAM_ROOT_ELEMENT_NAME);
addSupportedParameter(PARAM_SIMPLIFY_GEOMETRY);
}
/**
* @return the document wrapper
*/
@Nullable
public XmlWrapper getDocumentWrapper() {
return documentWrapper;
}
/**
* @param documentWrapper the document wrapper to set
*/
public void setDocumentWrapper(@Nullable XmlWrapper documentWrapper) {
this.documentWrapper = documentWrapper;
}
@Override
public List<? extends Locatable> getValidationSchemas() {
List<Locatable> result = new ArrayList<Locatable>();
result.addAll(super.getValidationSchemas());
for (Locatable schema : additionalSchemas.values()) {
result.add(schema);
}
return result;
}
/**
* Add a schema that should be included for validation. Should be called
* before or in
* {@link #write(InstanceCollection, ProgressIndicator, IOReporter)} prior
* to writing the schema locations, but after {@link #init(IOReporter)}
*
* @param namespace the schema namespace
* @param schema the schema location
* @param prefix the desired namespace prefix, may be <code>null</code>
*/
protected void addValidationSchema(String namespace, Locatable schema,
@Nullable String prefix) {
additionalSchemas.put(namespace, schema);
if (prefix != null) {
additionalSchemaPrefixes.put(namespace, prefix);
}
}
/**
* @see AbstractIOProvider#execute(ProgressIndicator, IOReporter)
*/
@Override
protected IOReport execute(ProgressIndicator progress, IOReporter reporter)
throws IOProviderConfigurationException, IOException {
OutputStream out;
try {
out = init(reporter);
} catch (XMLStreamException e) {
throw new IOException("Creating the XML stream writer failed", e);
}
try {
write(getInstances(), progress, reporter);
reporter.setSuccess(reporter.getErrors().isEmpty());
} catch (Exception e) {
reporter.error(new IOMessageImpl(e.getLocalizedMessage(), e));
reporter.setSuccess(false);
} finally {
progress.end();
out.close();
}
return reporter;
}
// FIXME
// /**
// * @see AbstractInstanceWriter#getValidationSchemas()
// */
// @Override
// public List<Schema> getValidationSchemas() {
// List<Schema> result = new ArrayList<Schema>(super.getValidationSchemas());
// result.addAll(additionalSchemas);
// return result;
// }
@Override
public boolean isPassthrough() {
return true;
}
/**
* @see AbstractInstanceWriter#validate()
*/
@Override
public void validate() throws IOProviderConfigurationException {
super.validate();
if (getXMLIndex() == null) {
fail("No XML target schema");
}
}
/**
* @see AbstractInstanceWriter#checkCompatibility()
*/
@Override
public void checkCompatibility() throws IOProviderConfigurationException {
super.checkCompatibility();
XmlIndex xmlIndex = getXMLIndex();
if (xmlIndex == null) {
fail("No XML target schema");
}
if (requiresDefaultContainer()) {
XmlElement element;
try {
element = findDefaultContainter(xmlIndex, null);
} catch (Exception e) {
// ignore
element = null;
}
if (element == null) {
fail("Cannot find container element in schema.");
}
}
}
/**
* States if the instance writer in all cases requires that the default
* container is being found.
*
* @return if the default container must be present in the target schema
*/
protected boolean requiresDefaultContainer() {
return false; // not needed, we allow specifying it through a parameter
}
/**
* Get the XML type index.
*
* @return the target type index
*/
protected XmlIndex getXMLIndex() {
if (targetIndex == null) {
targetIndex = getXMLIndex(getTargetSchema());
}
return targetIndex;
}
/**
* Get the XML index from the given schema space
*
* @param schemas the schema space
* @return the XML index or <code>null</code>
*/
public static XmlIndex getXMLIndex(SchemaSpace schemas) {
// XXX respect a container, types?
for (Schema schema : schemas.getSchemas()) {
if (schema instanceof XmlIndex) {
// TODO respect root element for schema selection?
return (XmlIndex) schema;
}
}
return null;
}
/**
* Create and setup the stream writer, the type index and the GML namespace
* (Initializes {@link #writer}, {@link #gmlNs} and {@link #targetIndex},
* resets {@link #geometryWriter} and {@link #additionalSchemas}).
*
* @param reporter the reporter for any errors
*
* @return the opened output stream
*
* @throws XMLStreamException if creating the {@link XMLStreamWriter} fails
* @throws IOException if creating the output stream fails
*/
private OutputStream init(IOReporter reporter) throws XMLStreamException, IOException {
// reset target index
targetIndex = null;
// reset geometry writer
geometryWriter = null;
// reset additional schemas
additionalSchemas.clear();
additionalSchemaPrefixes.clear();
// create and set-up a writer
XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
// will set namespaces if these not set explicitly
outputFactory.setProperty("javax.xml.stream.isRepairingNamespaces", //$NON-NLS-1$
Boolean.valueOf(true));
// create XML stream writer with UTF-8 encoding
OutputStream outStream = getTarget().getOutput();
XMLStreamWriter tmpWriter = outputFactory.createXMLStreamWriter(outStream,
getCharset().name()); // $NON-NLS-1$
String defNamespace = null;
XmlIndex index = getXMLIndex();
// read the namespaces from the map containing namespaces
if (index.getPrefixes() != null) {
for (Entry<String, String> entry : index.getPrefixes().entrySet()) {
if (entry.getValue().isEmpty()) {
// XXX don't use a default namespace, as this results in
// problems with schemas w/o elementFormQualified=true
// defNamespace = entry.getKey();
}
else {
tmpWriter.setPrefix(entry.getValue(), entry.getKey());
}
}
}
GmlWriterUtil.addNamespace(tmpWriter, SCHEMA_INSTANCE_NS, "xsi"); //$NON-NLS-1$
// determine default namespace
// if (defNamespace == null) {
// XXX don't use a default namespace, as this results in problems
// with schemas w/o elementFormQualified=true
// defNamespace = index.getNamespace();
// TODO remove prefix for target schema namespace?
// }
tmpWriter.setDefaultNamespace(defNamespace);
if (documentWrapper != null) {
documentWrapper.configure(tmpWriter, reporter);
}
// prettyPrint if enabled
if (isPrettyPrint()) {
writer = new IndentingXMLStreamWriter(tmpWriter);
}
else {
writer = tmpWriter;
}
// determine GML namespace from target schema
String gml = null;
if (index.getPrefixes() != null) {
Set<String> candidates = new TreeSet<>();
for (String ns : index.getPrefixes().keySet()) {
if (ns.startsWith(GML_NAMESPACE_CORE)) { // $NON-NLS-1$
candidates.add(ns);
}
}
if (!candidates.isEmpty()) {
if (candidates.size() == 1) {
gml = candidates.iterator().next();
}
else {
log.warn("Multiple candidates for GML namespace found");
// TODO how to choose the right one?
// prefer known GML namespaces
if (candidates.contains(NS_GML_32)) {
gml = NS_GML_32;
}
else if (candidates.contains(NS_GML)) {
gml = NS_GML;
}
else {
// fall back to first namespace
gml = candidates.iterator().next();
}
}
}
}
if (gml == null) {
// default to GML 2/3 namespace
gml = GML.NAMESPACE;
}
gmlNs = gml;
if (log.isDebugEnabled()) {
log.debug("GML namespace is " + gmlNs); //$NON-NLS-1$
}
return outStream;
}
/**
* @return if the output should be pretty printed
*/
public boolean isPrettyPrint() {
return getParameter(PARAM_PRETTY_PRINT).as(Boolean.class, false);
}
/**
* Set if the output should be pretty printed.
*
* @param prettyPrint <code>true</code> if the output should be indented,
* <code>false</code> otherwise
*/
public void setPrettyPrint(boolean prettyPrint) {
setParameter(PARAM_PRETTY_PRINT, Value.of(prettyPrint));
}
/**
* @return geometry write format
*/
public String getGeometryWriteFormat() {
return getParameter(PARAM_GEOMETRY_FORMAT).as(String.class);
}
/**
* Set geometry write format
*
* @param format pattern in which geometry coordinates would be formatted
*/
public void setGeometryWriteFormat(String format) {
setParameter(PARAM_GEOMETRY_FORMAT, Value.of(format));
}
/**
* @see IOProvider#isCancelable()
*/
@Override
public boolean isCancelable() {
return true;
}
/**
* @see AbstractIOProvider#getDefaultTypeName()
*/
@Override
protected String getDefaultTypeName() {
return "GML/XML";
}
/**
* Write the given instances.
*
* @param instances the instance collection
* @param reporter the reporter
* @param progress the progress
* @throws XMLStreamException if writing the feature collection fails
*/
public void write(InstanceCollection instances, ProgressIndicator progress, IOReporter reporter)
throws XMLStreamException {
final SubtaskProgressIndicator sub = new SubtaskProgressIndicator(progress) {
@Override
protected String getCombinedTaskName(String taskName, String subtaskName) {
return taskName + " (" + subtaskName + ")";
}
};
progress = sub;
progress.begin(getTaskName(), instances.size());
XmlElement container = findDefaultContainter(targetIndex, reporter);
TypeDefinition containerDefinition = (container == null) ? (null) : (container.getType());
QName containerName = (container == null) ? (null) : (container.getName());
if (containerDefinition == null) {
XmlElement containerElement = getConfiguredContainerElement(this, getXMLIndex());
containerDefinition = containerElement.getType();
containerName = containerElement.getName();
}
if (containerDefinition == null || containerName == null) {
throw new IllegalStateException("No root element/container found");
}
/*
* Add schema for container to validation schemas, if the namespace
* differs from the main namespace or additional schemas.
*
* Needed for validation based on schemaLocation attribute.
*/
if (!containerName.getNamespaceURI().equals(targetIndex.getNamespace())
&& !additionalSchemas.containsKey(containerName.getNamespaceURI())) {
try {
@SuppressWarnings("null")
final URI containerSchemaLoc = stripFragment(container.getLocation());
if (containerSchemaLoc != null) {
addValidationSchema(containerName.getNamespaceURI(), new Locatable() {
@Override
public URI getLocation() {
return containerSchemaLoc;
}
}, null);
}
} catch (Exception e) {
reporter.error(new IOMessageImpl(
"Could not determine location of container definition", e));
}
}
// additional schema namespace prefixes
for (Entry<String, String> schemaNs : additionalSchemaPrefixes.entrySet()) {
GmlWriterUtil.addNamespace(writer, schemaNs.getKey(), schemaNs.getValue());
}
writer.writeStartDocument();
if (documentWrapper != null) {
documentWrapper.startWrap(writer, reporter);
}
GmlWriterUtil.writeStartElement(writer, containerName);
// generate mandatory id attribute (for feature collection)
String containerId = getParameter(PARAM_CONTAINER_ID).as(String.class);
GmlWriterUtil.writeID(writer, containerDefinition, null, false, containerId);
// write schema locations
StringBuffer locations = new StringBuffer();
locations.append(targetIndex.getNamespace());
locations.append(" "); //$NON-NLS-1$
locations.append(targetIndex.getLocation().toString());
for (Entry<String, Locatable> schema : additionalSchemas.entrySet()) {
locations.append(" "); //$NON-NLS-1$
locations.append(schema.getKey());
locations.append(" "); //$NON-NLS-1$
locations.append(schema.getValue().getLocation().toString());
}
writer.writeAttribute(SCHEMA_INSTANCE_NS, "schemaLocation", locations.toString()); //$NON-NLS-1$
writeAdditionalElements(writer, containerDefinition, reporter);
// write the instances
ResourceIterator<Instance> itInstance = instances.iterator();
try {
Map<TypeDefinition, DefinitionPath> paths = new HashMap<TypeDefinition, DefinitionPath>();
long lastUpdate = 0;
int count = 0;
Descent lastDescent = null;
while (itInstance.hasNext() && !progress.isCanceled()) {
Instance instance = itInstance.next();
TypeDefinition type = instance.getDefinition();
/*
* Skip all objects that are no features when writing to a GML
* feature collection.
*/
boolean skip = useFeatureCollection && !GmlWriterUtil.isFeatureType(type);
if (skip) {
progress.advance(1);
continue;
}
// get stored definition path for the type
DefinitionPath defPath;
if (paths.containsKey(type)) {
// get the stored path, may be null
defPath = paths.get(type);
}
else {
// determine a valid definition path in the container
defPath = findMemberAttribute(containerDefinition, containerName, type);
// store path (may be null)
paths.put(type, defPath);
}
if (defPath != null) {
// write the feature
lastDescent = Descent.descend(writer, defPath, lastDescent, false);
writeMember(instance, type, reporter);
}
else {
reporter.warn(
new IOMessageImpl(
MessageFormat.format(
"No compatible member attribute for type {0} found in root element {1}, one instance was skipped",
type.getDisplayName(), containerName.getLocalPart()),
null));
}
progress.advance(1);
count++;
long now = System.currentTimeMillis();
// only update every 100 milliseconds
if (now - lastUpdate > 100 || !itInstance.hasNext()) {
lastUpdate = now;
sub.subTask(String.valueOf(count) + " instances");
}
}
if (lastDescent != null) {
lastDescent.close();
}
} finally {
itInstance.close();
}
writer.writeEndElement(); // FeatureCollection
if (documentWrapper != null) {
documentWrapper.endWrap(writer, reporter);
}
writer.writeEndDocument();
writer.close();
}
/**
* Strip the fragment from a location (as it usually represents line and
* column numbers)
*
* @param location the location
* @return the location w/o fragment
* @throws URISyntaxException if the URI cannot be recreated properly
*/
private URI stripFragment(URI location) throws URISyntaxException {
return new URI(location.getScheme(), location.getUserInfo(), location.getHost(),
location.getPort(), location.getPath(), location.getQuery(), null);
}
/**
* @return the execution task name
*/
protected String getTaskName() {
return "Generating " + getTypeName();
}
/**
* This method is called after the container element is started and filled
* with needed attributes. The default implementation ensures that a
* mandatory boundedBy of GML 2 FeatureCollection is written.
*
* @param writer the XML stream writer
* @param containerDefinition the container type definition
* @param reporter the reporter
* @throws XMLStreamException if writing anything fails
*/
protected void writeAdditionalElements(XMLStreamWriter writer,
TypeDefinition containerDefinition, IOReporter reporter) throws XMLStreamException {
// boundedBy is needed for GML 2 FeatureCollections
// XXX working like this - getting the child with only a local name?
ChildDefinition<?> boundedBy = containerDefinition.getChild(new QName("boundedBy")); //$NON-NLS-1$
if (boundedBy != null && boundedBy.asProperty() != null
&& boundedBy.asProperty().getConstraint(Cardinality.class).getMinOccurs() > 0) {
writer.writeStartElement(boundedBy.getName().getNamespaceURI(),
boundedBy.getName().getLocalPart());
writer.writeStartElement(gmlNs, "null"); //$NON-NLS-1$
writer.writeCharacters("missing"); //$NON-NLS-1$
writer.writeEndElement();
writer.writeEndElement();
}
}
/**
* Get the for an I/O provider configured target container element, assuming
* the I/O provider uses the {@link #PARAM_ROOT_ELEMENT_NAMESPACE} and
* {@value #PARAM_ROOT_ELEMENT_NAME} parameters for this.
*
* @param provider the I/O provider
* @param targetIndex the target XML index
* @return the container element or <code>null</code> if it was not found
*/
public static XmlElement getConfiguredContainerElement(IOProvider provider,
XmlIndex targetIndex) {
// no container defined, try to use a custom root element
String namespace = provider.getParameter(PARAM_ROOT_ELEMENT_NAMESPACE).as(String.class);
// determine target namespace
if (namespace == null) {
// default to target namespace
namespace = targetIndex.getNamespace();
}
String elementName = provider.getParameter(PARAM_ROOT_ELEMENT_NAME).as(String.class);
// find root element
XmlElement containerElement = null;
if (elementName != null) {
QName name = new QName(namespace, elementName);
containerElement = targetIndex.getElements().get(name);
}
return containerElement;
}
/**
* Find the default container element.
*
* @param targetIndex the target type index
* @param reporter the reporter, may be <code>null</code>
* @return the container XML element or <code>null</code>
*/
protected XmlElement findDefaultContainter(XmlIndex targetIndex, IOReporter reporter) {
if (useFeatureCollection) {
// try to find FeatureCollection element
Iterator<XmlElement> it = targetIndex.getElements().values().iterator();
Collection<XmlElement> fcElements = new HashSet<XmlElement>();
while (it.hasNext()) {
XmlElement el = it.next();
if (isFeatureCollection(el)) {
fcElements.add(el);
}
}
if (fcElements.isEmpty() && gmlNs != null && gmlNs.equals(NS_GML)) { // $NON-NLS-1$
// no FeatureCollection defined and "old" namespace -> GML 2
// include WFS 1.0.0 for the FeatureCollection element
try {
URI location = StreamGmlWriter.class
.getResource("/schemas/wfs/1.0.0/WFS-basic.xsd").toURI(); //$NON-NLS-1$
XmlSchemaReader schemaReader = new XmlSchemaReader();
schemaReader.setSource(new DefaultInputSupplier(location));
// FIXME to work with the extra schema it must be integrated
// with the main schema
// schemaReader.setSharedTypes(sharedTypes);
IOReport report = schemaReader.execute(null);
if (report.isSuccess()) {
XmlIndex wfsSchema = schemaReader.getSchema();
// look for FeatureCollection element
for (XmlElement el : wfsSchema.getElements().values()) {
if (isFeatureCollection(el)) {
fcElements.add(el);
}
}
// add as additional schema, replace location for
// verification
additionalSchemas.put(wfsSchema.getNamespace(),
new SchemaDecorator(wfsSchema) {
@Override
public URI getLocation() {
return URI.create(
"http://schemas.opengis.net/wfs/1.0.0/WFS-basic.xsd");
}
});
// add namespace
GmlWriterUtil.addNamespace(writer, wfsSchema.getNamespace(), "wfs"); //$NON-NLS-1$
}
} catch (Exception e) {
log.warn("Using WFS schema for the FeatureCollection definition failed", e); //$NON-NLS-1$
}
}
if (fcElements.isEmpty() && reporter != null) {
reporter.warn(
new IOMessageImpl("No element describing a FeatureCollection found", null)); //$NON-NLS-1$
}
else {
// select fc element TODO priorized selection (root element
// parameters)
XmlElement fcElement = fcElements.iterator().next();
log.info("Found " + fcElements.size() + " possible FeatureCollection elements" + //$NON-NLS-1$ //$NON-NLS-2$
", using element " + fcElement.getName()); //$NON-NLS-1$
return fcElement;
}
}
return null;
}
/**
* Find a matching attribute for the given member type in the given
* container type
*
* @param container the container type
* @param containerName the container element name
* @param memberType the member type
*
* @return the attribute definition or <code>null</code>
*/
protected DefinitionPath findMemberAttribute(TypeDefinition container, QName containerName,
final TypeDefinition memberType) {
// XXX not working if property is no substitution of the property type -
// use matching instead
// for (PropertyDefinition property : GmlWriterUtil.collectProperties(container.getChildren())) {
// // direct match -
// if (property.getPropertyType().equals(memberType)) {
// long max = property.getConstraint(Cardinality.class).getMaxOccurs();
// return new DefinitionPath(
// property.getPropertyType(),
// property.getName(),
// max != Cardinality.UNBOUNDED && max <= 1);
// }
// }
AbstractTypeMatcher<TypeDefinition> matcher = new AbstractTypeMatcher<TypeDefinition>() {
@Override
protected DefinitionPath matchPath(TypeDefinition type, TypeDefinition matchParam,
DefinitionPath path) {
if (type.equals(memberType)) {
return path;
}
// XXX special case: FeatureCollection from foreign schema
Collection<? extends XmlElement> elements = matchParam
.getConstraint(XmlElements.class).getElements();
Collection<? extends XmlElement> containerElements = type
.getConstraint(XmlElements.class).getElements();
if (!elements.isEmpty() && !containerElements.isEmpty()) {
TypeDefinition parent = matchParam.getSuperType();
while (parent != null) {
if (parent.equals(type)) {
// FIXME will not work with separately loaded
// schemas because e.g. the choice allowing the
// specific type is missing
// FIXME add to path
// return new DefinitionPath(path).addSubstitution(elements.iterator().next());
}
parent = parent.getSuperType();
}
}
return null;
}
};
// candidate match (go down at maximum ten levels)
List<DefinitionPath> candidates = matcher.findCandidates(container, containerName, true,
memberType, 10);
if (candidates != null && !candidates.isEmpty()) {
return candidates.get(0); // TODO notification? FIXME will this
// work? possible problem: attribute is
// selected even though better candidate
// is in other attribute
}
return null;
}
private boolean isFeatureCollection(XmlElement el) {
// TODO improve condition?
// FIXME working like this?!
return el.getName().getLocalPart().contains("FeatureCollection") && //$NON-NLS-1$
!el.getType().getConstraint(AbstractFlag.class).isEnabled()
&& hasChild(el.getType(), "featureMember"); //$NON-NLS-1$
}
private boolean hasChild(TypeDefinition type, String localName) {
for (ChildDefinition<?> child : DefinitionUtil.getAllProperties(type)) {
if (localName.equals(child.getName().getLocalPart())) {
return true;
}
}
return false;
}
/**
* Write a given instance
*
* @param instance the instance to writer
* @param type the feature type definition
* @param report the reporter
* @throws XMLStreamException if writing the feature fails
*/
protected void writeMember(Instance instance, TypeDefinition type, IOReporter report)
throws XMLStreamException {
// Name elementName = GmlWriterUtil.getElementName(type);
// writer.writeStartElement(elementName.getNamespaceURI(), elementName.getLocalPart());
writeProperties(instance, type, true, false, report);
// writer.writeEndElement(); // type element name
}
/**
* Write the given feature's properties
*
* @param group the feature
* @param definition the feature type
* @param allowElements if element properties may be written
* @param parentIsNil if the parent property is nil
* @param report the reporter
* @throws XMLStreamException if writing the properties fails
*/
private void writeProperties(Group group, DefinitionGroup definition, boolean allowElements,
boolean parentIsNil, IOReporter report) throws XMLStreamException {
// eventually generate mandatory ID that is not set
GmlWriterUtil.writeRequiredID(writer, definition, group, true);
// writing the feature is controlled by the type definition
// so retrieving values from instance must happen based on actual
// structure! (e.g. including groups)
// write the attributes, as they must be handled first
writeProperties(group, DefinitionUtil.getAllChildren(definition), true, parentIsNil,
report);
if (allowElements) {
// write the elements
writeProperties(group, DefinitionUtil.getAllChildren(definition), false, parentIsNil,
report);
}
}
/**
* Write attribute or element properties.
*
* @param parent the parent group
* @param children the child definitions
* @param attributes <code>true</code> if attribute properties shall be
* written, <code>false</code> if element properties shall be
* written
* @param parentIsNil if the parent property is nil
* @param report the reporter
* @throws XMLStreamException if writing the attributes/elements fails
*/
private void writeProperties(Group parent, Collection<? extends ChildDefinition<?>> children,
boolean attributes, boolean parentIsNil, IOReporter report) throws XMLStreamException {
if (parent == null) {
return;
}
boolean parentIsChoice = parent.getDefinition() instanceof GroupPropertyDefinition
&& ((GroupPropertyDefinition) parent.getDefinition())
.getConstraint(ChoiceFlag.class).isEnabled();
for (ChildDefinition<?> child : children) {
Object[] values = parent.getProperty(child.getName());
if (child.asProperty() != null) {
PropertyDefinition propDef = child.asProperty();
boolean isAttribute = propDef.getConstraint(XmlAttributeFlag.class).isEnabled();
if (attributes && isAttribute) {
if (values != null && values.length > 0) {
boolean allowWrite = true;
// special case handling: omit nilReason
if (getParameter(PARAM_OMIT_NIL_REASON).as(Boolean.class, true)) {
Cardinality propCard = propDef.getConstraint(Cardinality.class);
if ("nilReason".equals(propDef.getName().getLocalPart())
&& propCard.getMinOccurs() < 1) {
allowWrite = parentIsNil;
}
}
// write attribute
if (allowWrite) {
// special case handling: replace incorrect
// nilReason "unpopulated"
if ("nilReason".equals(propDef.getName().getLocalPart())
&& "unpopulated".equals(values[0])) {
// TODO more strict check to ensure that this is
// a GML nilReason? (check property type and
// parent types)
writeAttribute("other:unpopulated", propDef);
}
else {
// default
writeAttribute(values[0], propDef);
}
}
if (values.length > 1) {
// TODO warning?!
}
}
}
else if (!attributes && !isAttribute) {
int numValues = 0;
if (values != null) {
// write element
for (Object value : values) {
writeElement(value, propDef, report);
}
numValues = values.length;
}
// write additional elements to satisfy minOccurrs
// only if parent is not a choice
if (!parentIsChoice) {
Cardinality cardinality = propDef.getConstraint(Cardinality.class);
if (cardinality.getMinOccurs() > numValues) {
if (propDef.getConstraint(NillableFlag.class).isEnabled()) {
// nillable element
for (int i = numValues; i < cardinality.getMinOccurs(); i++) {
// write nil element
writeElement(null, propDef, report);
}
}
else {
// no value for non-nillable element
for (int i = numValues; i < cardinality.getMinOccurs(); i++) {
// write empty element
GmlWriterUtil.writeEmptyElement(writer, propDef.getName());
}
// TODO add warning to report
}
}
}
}
}
else if (child.asGroup() != null) {
// handle to child groups
if (values != null) {
for (Object value : values) {
if (value instanceof Group) {
writeProperties((Group) value,
DefinitionUtil.getAllChildren(child.asGroup()), attributes,
parentIsNil, report);
}
else {
// TODO warning/error?
}
}
}
}
}
}
/**
* Write a property element.
*
* @param value the element value
* @param propDef the property definition
* @param report the reporter
* @throws XMLStreamException if writing the element fails
*/
private void writeElement(Object value, PropertyDefinition propDef, IOReporter report)
throws XMLStreamException {
Group group = null;
if (value instanceof Group) {
group = (Group) value;
if (value instanceof Instance) {
// extract value from instance
value = ((Instance) value).getValue();
}
}
if (group == null) {
// just a value
if (value == null) {
// null value
if (propDef.getConstraint(Cardinality.class).getMinOccurs() > 0) {
// write empty element
GmlWriterUtil.writeEmptyElement(writer, propDef.getName());
// mark as nil
writeElementValue(null, propDef);
}
// otherwise just skip it
}
else {
GmlWriterUtil.writeStartElement(writer, propDef.getName());
Pair<Geometry, CRSDefinition> pair = extractGeometry(value, true, report);
if (pair != null) {
String srsName = extractCode(pair.getSecond());
// write geometry
writeGeometry(pair.getFirst(), propDef, srsName, report);
}
else {
// simple element with value
// write value as content
writeElementValue(value, propDef);
}
writer.writeEndElement();
}
}
else {
// children and maybe a value
GmlWriterUtil.writeStartElement(writer, propDef.getName());
boolean hasValue = propDef.getPropertyType().getConstraint(HasValueFlag.class)
.isEnabled();
Pair<Geometry, CRSDefinition> pair = extractGeometry(value, true, report);
// handle about annotated geometries
if (!hasValue && pair != null) {
String srsName = extractCode(pair.getSecond());
// write geometry
writeGeometry(pair.getFirst(), propDef, srsName, report);
}
else {
boolean hasOnlyNilReason = hasOnlyNilReason(group);
// write no elements if there is a value or only a nil reason
boolean writeElements = !hasValue && !hasOnlyNilReason;
boolean isNil = !writeElements && (!hasValue || value == null);
// write all children
writeProperties(group, group.getDefinition(), writeElements, isNil, report);
// write value
if (hasValue) {
writeElementValue(value, propDef);
}
else if (hasOnlyNilReason) {
// complex element with a nil value -> write xsi:nil if
// possible
/*
* XXX open question: should xsi:nil be there also if there
* are other attributes than nilReason?
*/
writeElementValue(null, propDef);
}
}
writer.writeEndElement();
}
}
/**
* Determines if a group has as its only property the nilReason attribute.
*
* @param group the group to test
* @return <code>true</code> if the group has the nilReason attribute and no
* other children, or no children at all, <code>false</code>
* otherwise
*/
private boolean hasOnlyNilReason(Group group) {
int count = 0;
QName nilReasonName = null;
for (QName name : group.getPropertyNames()) {
if (count > 0)
// more than one property
return false;
if (!name.getLocalPart().equals("nilReason"))
// a property different from nilReason
return false;
nilReasonName = name;
count++;
}
if (nilReasonName != null) {
// make sure it is an attribute
DefinitionGroup parent = group.getDefinition();
ChildDefinition<?> child = parent.getChild(nilReasonName);
if (child.asProperty() == null) {
// is a group
return false;
}
if (!child.asProperty().getConstraint(XmlAttributeFlag.class).isEnabled()) {
// not an attribute
return false;
}
}
return true;
}
/**
* Write an element value, either as element content or as <code>nil</code>.
*
* @param value the element value
* @param propDef the property definition the value is associated to
* @throws XMLStreamException if an error occurs writing the value
*/
private void writeElementValue(Object value, PropertyDefinition propDef)
throws XMLStreamException {
if (value == null) {
// null value
if (!propDef.getConstraint(NillableFlag.class).isEnabled()) {
log.warn("Non-nillable element " + propDef.getName() + " is null"); //$NON-NLS-1$ //$NON-NLS-2$
}
else {
// nillable -> we may mark it as nil
writer.writeAttribute(SCHEMA_INSTANCE_NS, "nil", "true"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
else {
TypeDefinition propType = propDef.getPropertyType();
if (value instanceof Iterable
&& List.class
.isAssignableFrom(propType.getConstraint(Binding.class).getBinding())
&& propType.getConstraint(ElementType.class).getBinding() != null) {
// element is a list
// TODO more robust detection?
boolean first = true;
for (Object element : ((Iterable<?>) value)) {
if (first) {
first = false;
}
else {
// space delimits list elements
writer.writeCharacters(" ");
}
// write the element
writer.writeCharacters(SimpleTypeUtil.convertToXml(element,
propType.getConstraint(ElementType.class).getDefinition()));
}
}
else {
// write value as content
writer.writeCharacters(
SimpleTypeUtil.convertToXml(value, propDef.getPropertyType()));
}
}
}
/**
* Write a geometry
*
* @param geometry the geometry
* @param property the geometry property
* @param srsName the common SRS name, may be <code>null</code>
* @param report the reporter
* @throws XMLStreamException if an error occurs writing the geometry
*/
private void writeGeometry(Geometry geometry, PropertyDefinition property, String srsName,
IOReporter report) throws XMLStreamException {
// write geometries
getGeometryWriter().write(writer, geometry, property, srsName, report,
NumberFormatter.getFormatter(getGeometryWriteFormat()));
}
/**
* Get the geometry writer
*
* @return the geometry writer instance to use
*/
protected StreamGeometryWriter getGeometryWriter() {
if (geometryWriter == null) {
// default to true
boolean simplifyGeometry = getParameter(PARAM_SIMPLIFY_GEOMETRY).as(Boolean.class,
true);
geometryWriter = StreamGeometryWriter.getDefaultInstance(gmlNs, simplifyGeometry);
}
return geometryWriter;
}
/**
* Write a property attribute
*
* @param value the attribute value, may be <code>null</code>
* @param propDef the associated property definition
* @throws XMLStreamException if writing the attribute fails
*/
private void writeAttribute(Object value, PropertyDefinition propDef)
throws XMLStreamException {
GmlWriterUtil.writeAttribute(writer, value, propDef);
}
/**
* @see eu.esdihumboldt.hale.common.instance.io.impl.AbstractGeoInstanceWriter#getDefaultWindingOrder()
*/
@Override
protected EnumWindingOrderTypes getDefaultWindingOrder() {
return EnumWindingOrderTypes.counterClockwise;
}
}