org.n52.svalbard.util.XmlHelper Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2015-2022 52°North Spatial Information Research GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.n52.svalbard.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import javax.xml.namespace.QName;
import org.apache.xmlbeans.SchemaType;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlError;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import org.apache.xmlbeans.XmlValidationError;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.n52.janmayen.exception.CompositeException;
import org.n52.shetland.ogc.gml.GmlConstants;
import org.n52.shetland.ogc.ows.OWSConstants.RequestParams;
import org.n52.shetland.ogc.ows.exception.NoApplicableCodeException;
import org.n52.shetland.ogc.ows.exception.OwsExceptionReport;
import org.n52.shetland.ogc.sos.Sos2Constants;
import org.n52.shetland.ogc.swes.SwesConstants;
import org.n52.shetland.util.CollectionHelper;
import org.n52.shetland.w3c.W3CConstants;
import org.n52.svalbard.decode.exception.DecodingException;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
/**
* XML utility class TODO add javadoc to public methods.
*
* @since 1.0.0
*
*/
public final class XmlHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(XmlHelper.class);
private static final Set GML_NAMESPACES = Sets.newHashSet(GmlConstants.NS_GML, GmlConstants.NS_GML_32);
private XmlHelper() {
}
// /**
// * Parse XML document from HTTP-Post request.
// *
// * @param request
// * HTTP-Post request
// *
// * @return XML document
// *
// * @throws DecodingException
// * If an error occurs
// */
// public static XmlObject parseXmlRequest(final HttpServletRequest request)
// throws DecodingException {
// try {
// if (request.getParameterMap().isEmpty()) {
// String requestContent =
// StringHelper.convertStreamToString(HttpUtils.getInputStream(request),
// request.getCharacterEncoding());
// return parseXmlString(requestContent);
// } else {
// return
// XmlObject.Factory.parse(parseHttpPostBodyWithParameter(request.getParameterMap()));
// }
// } catch (XmlException e) {
// throw new DecodingException("An xml error occured when parsing the
// request!", e);
// } catch (IOException e) {
// throw new DecodingException("Error while reading request!", e);
// }
// }
/**
* Parses the HTTP-Post body with a parameter
*
* @param parameterMap
* Parameter map
* @return Value of the parameter
*
* @throws DecodingException
* * If the parameter is not supported by this SOS.
*/
public static String parseHttpPostBodyWithParameter(Map parameterMap) throws DecodingException {
for (Entry e : parameterMap.entrySet()) {
String paramName = e.getKey();
if (RequestParams.request.name().equalsIgnoreCase(paramName)) {
String[] paramValues = parameterMap.get(paramName);
if (paramValues.length == 1) {
return paramValues[0];
} else {
throw new DecodingException(
"The parameter '%s' has more than one value " +
"or is empty for HTTP-Post requests by this SOS!",
paramName);
}
} else {
throw new DecodingException("The parameter '%s' is not supported for HTTP-Post requests by this SOS!",
paramName);
}
}
// FIXME: valid exception
throw new DecodingException("No request parameter forund for HTTP-Post!");
}
public static XmlObject parseXmlString(String xmlString) throws DecodingException {
try {
return XmlObject.Factory.parse(xmlString);
} catch (final XmlException xmle) {
throw new DecodingException("An xml error occured when parsing the request!", xmle);
}
}
/**
* Get element Node from NodeList.
*
* @param nodeList
* NodeList.
* @return Element Node
*/
public static Node getNodeFromNodeList(NodeList nodeList) {
if (nodeList != null && nodeList.getLength() > 0) {
for (int i = 0; i < nodeList.getLength(); i++) {
if (nodeList.item(i).getNodeType() == Node.ELEMENT_NODE) {
return nodeList.item(i);
}
}
}
return null;
}
/**
* checks whether the XMLDocument is valid
*
* @param doc
* the document which should be checked
*
* @throws T
* * if the Document is not valid
*/
/*
* TODO Replace this version with a method that uses LaxValidationCases and
* provides means to access the errors after validating the document
*/
public static X validateDocument(X doc, Function supplier)
throws T {
// Create an XmlOptions instance and set the error listener.
LinkedList validationErrors = new LinkedList<>();
XmlOptions validationOptions = new XmlOptions().setErrorListener(validationErrors)
.setLoadLineNumbers(XmlOptions.LOAD_LINE_NUMBERS_END_ELEMENT);
// Create Exception with error message if the xml document is invalid
if (!doc.validate(validationOptions)) {
String message;
// getValidation error and throw service exception for the first
// error
Iterator iter = validationErrors.iterator();
List errors = new LinkedList<>();
while (iter.hasNext()) {
XmlError error = iter.next();
boolean shouldPass = false;
if (error instanceof XmlValidationError) {
for (LaxValidationCase lvc : LaxValidationCase.values()) {
if (lvc.shouldPass((XmlValidationError) error)) {
shouldPass = true;
LOGGER.debug("Lax validation case found for XML validation error: {}", error);
break;
}
}
}
if (!shouldPass) {
errors.add(error);
}
}
CompositeException exceptions = new CompositeException();
for (XmlError error : errors) {
// get name of the missing or invalid parameter
message = error.getMessage();
if (message != null) {
exceptions.add(new DecodingException(message, "[XmlBeans validation error:] %s", message));
}
}
if (!errors.isEmpty()) {
throw supplier.apply(exceptions);
}
}
return doc;
}
/**
* @param doc the XML document to validate.
* @return true
if the given xml document is valid, else an exception is thrown.
* @throws DecodingException thrown if the given xml document is invalid.
*/
public static boolean validateDocument(XmlObject doc) throws DecodingException {
validateDocument(doc, DecodingException::new);
return true;
}
/**
* Loads a XML document from File.
*
* @param file
* File
* @return XML document
*
* @throws OwsExceptionReport
* If an error occurs
*/
public static XmlObject loadXmlDocumentFromFile(File file) throws OwsExceptionReport {
try (InputStream is = new FileInputStream(file)) {
return XmlObject.Factory.parse(is);
} catch (XmlException | IOException xmle) {
throw new NoApplicableCodeException().causedBy(xmle).withMessage("Error while parsing file %s!",
file.getName());
}
}
/**
* Recurse through a node and its children and make all gml:ids unique
*
* @param node
* The root node
*/
public static void makeGmlIdsUnique(final Node node) {
makeGmlIdsUnique(node, new HashMap<>());
}
/**
* Recurse through a node and its children and make all gml:ids unique
*
* @param node The node to examine
* @param foundIds the already found ids
*/
public static void makeGmlIdsUnique(Node node, Map foundIds) {
// check this node's attributes
NamedNodeMap attributes = node.getAttributes();
String nodeNamespace = node.getNamespaceURI();
if (attributes != null) {
for (int i = 0, len = attributes.getLength(); i < len; i++) {
Attr attr = (Attr) attributes.item(i);
if (attr.getLocalName().equals(GmlConstants.AN_ID)) {
if (checkAttributeForGmlId(attr, nodeNamespace)) {
final String gmlId = attr.getValue();
if (foundIds.containsKey(gmlId)) {
/*
* id has already been found, suffix this one with
* the found count for this id
*/
attr.setValue(gmlId + foundIds.get(gmlId));
// increment the found count for this id
foundIds.put(gmlId, foundIds.get(gmlId) + 1);
} else {
// id is new, add it to the foundIds map
foundIds.put(gmlId, 1);
}
}
}
}
}
// recurse this node's children
final NodeList children = node.getChildNodes();
if (children != null) {
for (int i = 0, len = children.getLength(); i < len; i++) {
makeGmlIdsUnique(children.item(i), foundIds);
}
}
}
//CHECKSTYLE:OFF
public static void updateGmlIDs(Node node, String gmlID, String oldGmlID) {
// check this node's attributes
if (node != null) {
final String nodeNamespace = node.getNamespaceURI();
final NamedNodeMap attributes = node.getAttributes();
if (attributes != null) {
for (int i = 0, len = attributes.getLength(); i < len; i++) {
final Attr attr = (Attr) attributes.item(i);
if (attr.getLocalName().equals(GmlConstants.AN_ID)) {
if (checkAttributeForGmlId(attr, nodeNamespace)) {
if (oldGmlID == null) {
oldGmlID = attr.getValue();
attr.setValue(gmlID);
} else {
String helperString = attr.getValue();
helperString = helperString.replace(oldGmlID, gmlID);
attr.setValue(helperString);
}
}
}
}
// recurse this node's children
final NodeList children = node.getChildNodes();
if (children != null) {
for (int i = 0, len = children.getLength(); i < len; i++) {
updateGmlIDs(children.item(i), gmlID, oldGmlID);
}
}
}
}
}
//CHECKSTYLE:ON
/**
* Check if attribute or node namespace is a GML id.
*
* @param attr
* Attribure to check
* @param nodeNamespace
* Node namespace
* @return true
, if attribute or node is a GML id
*/
private static boolean checkAttributeForGmlId(Attr attr, String nodeNamespace) {
final String attrNamespace = attr.getNamespaceURI();
if (GmlConstants.GML_ID_WITH_PREFIX.equals(attr.getName())) {
return true;
} else {
if (!Strings.isNullOrEmpty(attrNamespace)) {
return isNotNullAndEqualsNSs(attrNamespace, getGmlNSs());
} else {
return isNotNullAndEqualsNSs(nodeNamespace, getGmlNSs());
}
}
}
/**
* Check if namespace is not null and equals GML 3.1.1 or GML 3.2.1
* namespace.
*
* @param namespaceToCheck
* Namespace to check
* @param namespaces
* GML namespaces
* @return true
, if namespaceToCheck is a GML namespace
*/
private static boolean isNotNullAndEqualsNSs(String namespaceToCheck, Collection namespaces) {
return !Strings.isNullOrEmpty(namespaceToCheck) && namespaces.contains(namespaceToCheck);
}
/**
* Get set with GML 3.1.1 and GML 3.2.1 namespaces.
*
* @return GML namespace set
*/
private static Collection getGmlNSs() {
return Collections.unmodifiableCollection(GML_NAMESPACES);
}
public static String getNamespace(final XmlObject doc) {
Node domNode = doc.getDomNode();
String namespaceURI = domNode.getNamespaceURI();
if (namespaceURI == null && domNode.getFirstChild() != null) {
namespaceURI = domNode.getFirstChild().getNamespaceURI();
}
/*
* if document starts with a comment, get next sibling (and ignore
* initial comment)
*/
if (namespaceURI == null && domNode.getFirstChild() != null
&& domNode.getFirstChild().getNextSibling() != null) {
namespaceURI = domNode.getFirstChild().getNextSibling().getNamespaceURI();
}
// check with schemaType namespace, necessary for anyType elements
final String schemaTypeNamespace = getSchemaTypeNamespace(doc);
if (schemaTypeNamespace == null) {
return namespaceURI;
} else {
if (schemaTypeNamespace.equals(namespaceURI)) {
return namespaceURI;
} else {
return schemaTypeNamespace;
}
}
}
private static String getSchemaTypeNamespace(XmlObject doc) {
QName name;
if (doc.schemaType().isAttributeType()) {
name = doc.schemaType().getAttributeTypeAttributeName();
} else {
// TODO check else/if for ...schemaType().isDocumentType ?
name = doc.schemaType().getName();
}
if (name != null) {
return name.getNamespaceURI();
}
return null;
}
public static XmlObject substituteElement(XmlObject elementToSubstitute, XmlObject substitutionElement) {
final Node domNode = substitutionElement.getDomNode();
QName name;
if (domNode.getNamespaceURI() != null && domNode.getLocalName() != null) {
final String prefix = getPrefixForNamespace(elementToSubstitute, domNode.getNamespaceURI());
if (prefix != null && !prefix.isEmpty()) {
name = new QName(domNode.getNamespaceURI(), domNode.getLocalName(), prefix);
} else {
name = new QName(domNode.getNamespaceURI(), domNode.getLocalName());
}
} else {
final QName nameOfElement = substitutionElement.schemaType().getName();
final String localPart = nameOfElement.getLocalPart().replace(GmlConstants.EN_PART_TYPE, "");
name = new QName(nameOfElement.getNamespaceURI(), localPart,
getPrefixForNamespace(elementToSubstitute, nameOfElement.getNamespaceURI()));
}
return substituteElement(elementToSubstitute, substitutionElement.schemaType(), name);
}
public static XmlObject substituteElement(XmlObject elementToSubstitute, SchemaType schemaType, QName name) {
return elementToSubstitute.substitute(name, schemaType);
}
public static String getPrefixForNamespace(XmlObject element, String namespace) {
final XmlCursor cursor = element.newCursor();
final String prefix = cursor.prefixForNamespace(namespace);
cursor.dispose();
return prefix;
}
public static String getLocalName(XmlObject element) {
return element == null ? null : element.getDomNode().getLocalName();
}
/**
* Interface for providing exceptional cases in XML validation (e.g.
* substitution groups).
*
* FIXME Review code and use new procedure from OX-F to validate offending
* content!
*/
public enum LaxValidationCase {
// FIXME make private again
ABSTRACT_OFFERING {
@SuppressWarnings("unchecked")
@Override
public boolean shouldPass(final XmlValidationError xve) {
return checkQNameIsExpected(xve.getFieldQName(), SwesConstants.QN_OFFERING)
&& checkExpectedQNamesContainsQNames(xve.getExpectedQNames(),
Lists.newArrayList(SwesConstants.QN_ABSTRACT_OFFERING))
&& checkMessageOrOffendingQName(xve, Sos2Constants.QN_OBSERVATION_OFFERING);
}
},
/**
* Allow substitutions of gml:AbstractFeature. This lax validation lets
* pass every child, hence it checks not _if_ this is a valid
* substitution.
*/
ABSTRACT_FEATURE_GML {
@SuppressWarnings("unchecked")
@Override
public boolean shouldPass(final XmlValidationError xve) {
return checkExpectedQNamesContainsQNames(xve.getExpectedQNames(), Lists
.newArrayList(GmlConstants.QN_ABSTRACT_FEATURE_GML, GmlConstants.QN_ABSTRACT_FEATURE_GML_32));
}
},
ABSTRACT_TIME_GML_3_2_1 {
@SuppressWarnings("unchecked")
@Override
public boolean shouldPass(final XmlValidationError xve) {
return checkExpectedQNamesContainsQNames(xve.getExpectedQNames(),
Lists.newArrayList(GmlConstants.QN_ABSTRACT_TIME_32));
}
},
SOS_INSERTION_META_DATA {
@SuppressWarnings("unchecked")
@Override
public boolean shouldPass(final XmlValidationError xve) {
return checkQNameIsExpected(xve.getFieldQName(), SwesConstants.QN_METADATA)
&& checkExpectedQNamesContainsQNames(xve.getExpectedQNames(),
Lists.newArrayList(SwesConstants.QN_INSERTION_METADATA))
&& checkMessageOrOffendingQName(xve, Sos2Constants.QN_SOS_INSERTION_METADATA);
}
},
SOS_INSERTION_META_DATA_2 {
@Override
public boolean shouldPass(final XmlValidationError xve) {
return checkQNameIsExpected(xve.getOffendingQName(), SwesConstants.QN_INSERTION_METADATA)
&& xve.getCursorLocation().getAttributeText(W3CConstants.QN_XSI_TYPE)
.contains(SOS_INSERTION_METADATA_TYPE);
}
},
SOS_GET_DATA_AVAILABILITY_RESPONSE {
@Override
public boolean shouldPass(final XmlValidationError xve) {
if (xve.getObjectLocation() != null && xve.getObjectLocation().getDomNode() != null
&& xve.getObjectLocation().getDomNode().getFirstChild() != null) {
String nodeName = xve.getObjectLocation().getDomNode().getFirstChild().getNodeName();
return !Strings.isNullOrEmpty(nodeName) && nodeName.contains(GET_DATA_AVAILABILITY);
}
return false;
}
};
private static final String BEFORE_END_CONTENT_ELEMENT = "before the end of the content in element";
private static final String SOS_INSERTION_METADATA_TYPE = "SosInsertionMetadataType";
private static final String GET_DATA_AVAILABILITY = "GetDataAvailability";
public abstract boolean shouldPass(XmlValidationError xve);
/**
* Check if the QName equals expected QName
*
* @param qName
* QName to check
* @param expected
* Expected QName
* @return true
, if QName equals expected QName
*/
private static boolean checkQNameIsExpected(QName qName, QName expected) {
return qName != null && qName.equals(expected);
}
/**
* Check if expected QNames contains one QName
*
* @param expected
* Expected QNames
* @param shouldContain
* Contains expected QNames this
* @return true
, if expected QNames contains one QName
*/
private static boolean checkExpectedQNamesContainsQNames(List expected, List shouldContain) {
if (CollectionHelper.isNotEmpty(expected)) {
if (shouldContain.stream().anyMatch(expected::contains)) {
return true;
}
}
return false;
}
/**
* Check if message contains defined pattern or offending QName equals
* expected
*
* @param xve
* Xml validation error
* @param expectedOffendingQname
* Expected offending QName
* @return true
, if message contains defined pattern or
* offending QName equals expected
*/
private static boolean checkMessageOrOffendingQName(XmlValidationError xve, QName expectedOffendingQname) {
return xve.getMessage().contains(BEFORE_END_CONTENT_ELEMENT)
|| checkQNameIsExpected(xve.getOffendingQName(), expectedOffendingQname);
}
}
/**
* Utility method to append the contents of the child docment to the end of
* the parent XmlObject. This is useful when dealing with elements without
* generated methods (like elements with xs:any)
*
* @param parent
* Parent to append contents to
* @param childDoc
* Xml document containing contents to be appended
*/
public static void append(final XmlObject parent, final XmlObject childDoc) {
final XmlCursor parentCursor = parent.newCursor();
parentCursor.toEndToken();
final XmlCursor childCursor = childDoc.newCursor();
childCursor.toFirstChild();
childCursor.moveXml(parentCursor);
parentCursor.dispose();
childCursor.dispose();
}
/**
* Remove namespace declarations from an xml fragment (useful for moving all
* declarations to a document root
*
* @param x
* The fragment to localize
*/
public static void removeNamespaces(final XmlObject x) {
final XmlCursor c = x.newCursor();
while (c.hasNextToken()) {
if (c.isNamespace()) {
c.removeXml();
} else {
c.toNextToken();
}
}
c.dispose();
}
/**
* Remove the element from XML document
*
* @param element
* Element to remove
* @return true
, if element is removed
*/
public static boolean removeElement(XmlObject element) {
XmlCursor cursor = element.newCursor();
boolean removed = cursor.removeXml();
cursor.dispose();
return removed;
}
public static void fixNamespaceForXsiType(final XmlObject object, final QName value) {
final XmlCursor cursor = object.newCursor();
while (cursor.hasNextToken()) {
if (cursor.toNextToken().isStart()) {
final String xsiType = cursor.getAttributeText(W3CConstants.QN_XSI_TYPE);
if (xsiType != null) {
final String[] toks = xsiType.split(":");
String localName;
if (toks.length > 1) {
localName = toks[1];
} else {
localName = toks[0];
}
if (localName.equals(value.getLocalPart())) {
cursor.setAttributeText(W3CConstants.QN_XSI_TYPE,
Joiner.on(":").join(XmlHelper.getPrefixForNamespace(object, value.getNamespaceURI()),
value.getLocalPart()));
}
}
}
}
cursor.dispose();
}
public static void fixNamespaceForXsiType(XmlObject content, Map, ?> namespaces) {
final XmlCursor cursor = content.newCursor();
while (cursor.hasNextToken()) {
if (cursor.toNextToken().isStart()) {
final String xsiType = cursor.getAttributeText(W3CConstants.QN_XSI_TYPE);
if (xsiType != null) {
final String[] toks = xsiType.split(":");
if (toks.length > 1) {
String prefix = toks[0];
String localName = toks[1];
if (namespaces.containsKey(prefix)) {
cursor.setAttributeText(
W3CConstants.QN_XSI_TYPE,
Joiner.on(":").join(
XmlHelper.getPrefixForNamespace(content, (String) namespaces.get(prefix)),
localName));
}
}
}
}
}
cursor.dispose();
}
public static Map, ?> getNamespaces(XmlObject xmlObject) {
XmlCursor cursor = xmlObject.newCursor();
Map, ?> nsMap = Maps.newHashMap();
cursor.getAllNamespaces(nsMap);
cursor.dispose();
return nsMap;
}
/**
* @param prefix The prefix
* @param namespace The namespace
* @return The path
*/
public static String getXPathPrefix(String prefix, String namespace) {
return String.format("declare namespace %s='%s';", prefix, namespace);
}
}