/*
* 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;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.Set;
import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stax.StAXResult;
import org.geotools.gml2.SrsSyntax;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.vividsolutions.jts.geom.Geometry;
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.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.instance.model.Instance;
import eu.esdihumboldt.hale.common.schema.geometry.CRSDefinition;
import eu.esdihumboldt.hale.common.schema.groovy.DefinitionAccessor;
import eu.esdihumboldt.hale.common.schema.model.Definition;
import eu.esdihumboldt.hale.common.schema.model.TypeDefinition;
import eu.esdihumboldt.hale.io.gml.InspireUtil;
import eu.esdihumboldt.hale.io.gml.writer.internal.AbstractXMLStreamWriterDecorator;
import eu.esdihumboldt.hale.io.gml.writer.internal.StreamGmlWriter;
import eu.esdihumboldt.hale.io.xsd.model.XmlElement;
import eu.esdihumboldt.hale.io.xsd.model.XmlIndex;
import eu.esdihumboldt.util.Pair;
import eu.esdihumboldt.util.groovy.paths.Path;
/**
* Instance writer for Inspire schemas, using SpatialDataSet as container.
*
* @author Kai Schwierczek
* @author Simon Templer
*/
public class InspireInstanceWriter extends GmlInstanceWriter {
/**
* Identifier of the writer.
*/
public static final String ID = "eu.esdihumboldt.hale.io.inspiregml.writer";
/**
* The parameter name for the identifier.localId attribute, defaults to an
* empty string.
*/
public static final String PARAM_SPATIAL_DATA_SET_LOCALID = "inspire.sds.localId";
/**
* The parameter name for the identifier.namespace attribute, defaults to an
* empty string.
*/
public static final String PARAM_SPATIAL_DATA_SET_NAMESPACE = "inspire.sds.namespace";
/**
* The parameter name for the XML file to load to fill the metadata with.
*/
public static final String PARAM_SPATIAL_DATA_SET_METADATA_FILE = "inspire.sds.metadata";
/**
* The parameter name metadata provided as DOM.
*/
public static final String PARAM_SPATIAL_DATA_SET_METADATA_DOM = "inspire.sds.metadata.inline";
/**
* The parameter specifying whether a dataset feed should be created,
* defaults to <code>false</code>.
*/
public static final String PARAM_SPATIAL_DATA_SET_CREATE_FEED = "inspire.sds.create_feed";
private final Set<TypeDefinition> types = new HashSet<>();
private final Multiset<CRSDefinition> crss = HashMultiset.create();
/**
* Default constructor.
*/
public InspireInstanceWriter() {
addSupportedParameter(PARAM_SPATIAL_DATA_SET_LOCALID);
addSupportedParameter(PARAM_SPATIAL_DATA_SET_NAMESPACE);
addSupportedParameter(PARAM_SPATIAL_DATA_SET_METADATA_FILE);
addSupportedParameter(PARAM_SPATIAL_DATA_SET_METADATA_DOM);
addSupportedParameter(PARAM_SPATIAL_DATA_SET_CREATE_FEED);
for (String param : InspireDatasetFeedWriter.getAdditionalParams()) {
addSupportedParameter(param);
}
// set alternating default values
// EPSG prefix:
setCustomEPSGPrefix(SrsSyntax.OGC_HTTP_URI.getPrefix());
}
@Override
protected IOReport execute(ProgressIndicator progress, IOReporter reporter)
throws IOProviderConfigurationException, IOException {
// first write gml file (also collects types-set)
super.execute(progress, reporter);
// run feed writer if applicable
if (getParameter(PARAM_SPATIAL_DATA_SET_CREATE_FEED).as(Boolean.class, false)) {
InspireDatasetFeedWriter feedWriter = new InspireDatasetFeedWriter();
for (String copyParam : InspireDatasetFeedWriter.getAdditionalParams()) {
feedWriter.setParameter(copyParam, getParameter(copyParam));
}
feedWriter.setOccurringTypes(types);
feedWriter.setOccurringCRSs(crss);
feedWriter.setParameter(PARAM_TARGET,
Value.of(getDatasetFeedTarget(getTarget().getLocation()).toString()));
reporter.importMessages(feedWriter.execute(progress));
// Ignore success of feedWriter-report, since main success criteria
// is successfully writing the data.
}
return reporter;
}
/**
* Transforms the given GML file target to a target URI for the dataset feed
* creation.
*
* @param gmlTarget the original GML target
* @return the dataset feed target
*/
public static URI getDatasetFeedTarget(URI gmlTarget) {
String location = gmlTarget.toString();
String locationPath;
String locationName;
int nameIndex = location.lastIndexOf('/');
if (nameIndex != -1) {
locationName = location.substring(nameIndex + 1);
locationPath = location.substring(0, nameIndex + 1);
}
else {
locationName = location;
locationPath = "";
}
// XXX split filename at which '.'? (for now first)
int suffixIndex = locationName.indexOf('.');
if (suffixIndex != -1) {
locationName = locationName.substring(0, suffixIndex);
}
String feedTaregt = locationPath + locationName + "-feed.atom";
return URI.create(feedTaregt);
}
/**
* @see StreamGmlWriter#findDefaultContainter(XmlIndex, IOReporter)
*/
@Override
protected XmlElement findDefaultContainter(XmlIndex targetIndex, IOReporter reporter) {
XmlElement result = InspireUtil.findSpatialDataSet(targetIndex);
if (result != null)
return result;
throw new IllegalStateException(MessageFormat.format(
"Element {0} not found in the schema.", "SpatialDataSet"));
}
@Override
protected void writeMember(Instance instance, TypeDefinition type, IOReporter report)
throws XMLStreamException {
// collect written types in case a dataset feed will be written
types.add(type);
super.writeMember(instance, type, report);
}
@Override
protected Pair<Geometry, CRSDefinition> extractGeometry(Object value, boolean allowConvert,
IOReporter report) {
// collect used CRS definitions
Pair<Geometry, CRSDefinition> result = super.extractGeometry(value, allowConvert, report);
if (result != null) {
crss.add(result.getSecond());
}
return result;
}
/**
* @see StreamGmlWriter#writeAdditionalElements(XMLStreamWriter,
* TypeDefinition, IOReporter)
*/
@Override
protected void writeAdditionalElements(XMLStreamWriter writer,
TypeDefinition containerDefinition, IOReporter reporter) throws XMLStreamException {
super.writeAdditionalElements(writer, containerDefinition, reporter);
// determine INSPIRE identifier and metadata names
Path<Definition<?>> localIdPath = new DefinitionAccessor(containerDefinition)
.findChildren("identifier").findChildren("Identifier").findChildren("localId")
.eval(false);
QName identifierName = localIdPath.getElements().get(1).getName();
Definition<?> internalIdentifierDef = localIdPath.getElements().get(2);
QName internalIdentifierName = internalIdentifierDef.getName();
QName localIdName = localIdPath.getElements().get(3).getName();
Path<Definition<?>> namespacePath = new DefinitionAccessor(internalIdentifierDef)
.findChildren("namespace").eval(false);
QName namespaceName = namespacePath.getElements().get(1).getName();
Path<Definition<?>> metadataPath = new DefinitionAccessor(containerDefinition)
.findChildren("metadata").eval(false);
QName metadataName = metadataPath.getElements().get(1).getName();
// write INSPIRE identifier
writer.writeStartElement(identifierName.getNamespaceURI(), identifierName.getLocalPart());
writer.writeStartElement(internalIdentifierName.getNamespaceURI(),
internalIdentifierName.getLocalPart());
writer.writeStartElement(localIdName.getNamespaceURI(), localIdName.getLocalPart());
writer.writeCharacters(getParameter(PARAM_SPATIAL_DATA_SET_LOCALID).as(String.class, ""));
writer.writeEndElement();
writer.writeStartElement(namespaceName.getNamespaceURI(), namespaceName.getLocalPart());
writer.writeCharacters(getParameter(PARAM_SPATIAL_DATA_SET_NAMESPACE).as(String.class, ""));
writer.writeEndElement();
writer.writeEndElement();
writer.writeEndElement();
// write metadata
writer.writeStartElement(metadataName.getNamespaceURI(), metadataName.getLocalPart());
// retrieve metadata element (if any)
Element metadataElement = getParameter(PARAM_SPATIAL_DATA_SET_METADATA_DOM).as(
Element.class);
// metadata from file (if any)
if (metadataElement == null) {
String metadataFile = getParameter(PARAM_SPATIAL_DATA_SET_METADATA_FILE).as(
String.class);
if (metadataFile != null && !metadataFile.isEmpty()) {
try (InputStream input = new BufferedInputStream(new FileInputStream(new File(
metadataFile)))) {
metadataElement = findMetadata(input, reporter);
} catch (IOException e) {
reporter.warn(new IOMessageImpl("Could not load specified metadata file.", e));
}
}
}
if (metadataElement != null) {
try {
writeElement(metadataElement, writer);
} catch (TransformerException e) {
reporter.warn(new IOMessageImpl("Couldn't include specified metadata file.", e));
}
}
else {
writer.writeAttribute(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "nil", "true");
}
writer.writeEndElement();
}
/**
* Loads the given file and tries to find a MD_Metadata element.
*
* @param input the metadata source
* @param reporter the reporter
* @return the metadata element or <code>null</code> if it couldn't be found
*/
private Element findMetadata(InputStream input, IOReporter reporter) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
Document doc;
try {
doc = dbf.newDocumentBuilder().parse(input);
} catch (SAXException | IOException | ParserConfigurationException e) {
reporter.warn(new IOMessageImpl("Couldn't parse specified metadata file.", e));
return null;
}
NodeList nl = doc.getElementsByTagNameNS("http://www.isotc211.org/2005/gmd", "MD_Metadata");
Element result = null;
if (nl.getLength() == 1)
result = (Element) nl.item(0);
else if (nl.getLength() == 0)
reporter.warn(new IOMessageImpl(
"Couldn't include specified metadata file, no MD_Metadata element found.", null));
else {
// XXX Maybe ask the user somehow? Or better not include it?
reporter.warn(new IOMessageImpl(
"Found multiple MD_Metadata elements. Using first one.", null));
result = (Element) nl.item(0);
}
return result;
}
// XXX If needed writeElement could go to some Util-Class.
/**
* Writes a DOM element to a stream writer without starting a new document.
*
* @param element the element to write
* @param writer the writer to write to
* @throws TransformerException if an unrecoverable error occurs during the
* course of the transformation
*/
private void writeElement(Element element, XMLStreamWriter writer) throws TransformerException {
TransformerFactory tf = TransformerFactory.newInstance();
Transformer t = tf.newTransformer();
t.transform(new DOMSource(element), new StAXResult(new InternalXMLStreamWriter(writer)));
}
/**
* Stream writer for elements. Ignores startDocument and endDocument calls.
*
* @author Kai Schwierczek
*/
private class InternalXMLStreamWriter extends AbstractXMLStreamWriterDecorator {
/**
* @param writer the writer to forward to
*/
protected InternalXMLStreamWriter(XMLStreamWriter writer) {
super(writer);
}
@Override
public void writeDTD(String dtd) throws XMLStreamException {
// ignore DTD
}
@Override
public void writeEndDocument() throws XMLStreamException {
// ignore endDocument
}
@Override
public void writeStartDocument() throws XMLStreamException {
// ignore startDocument
}
@Override
public void writeStartDocument(String encoding, String version) throws XMLStreamException {
// ignore startDocument
}
@Override
public void writeStartDocument(String version) throws XMLStreamException {
// ignore startDocument
}
@Override
public void close() throws XMLStreamException {
writer.flush();
}
}
}