XmlObjectParser.java
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/
package gov.nist.secauto.metaschema.core.model.xml.impl;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlCursor.XmlBookmark;
import org.apache.xmlbeans.XmlLineNumber;
import org.apache.xmlbeans.XmlObject;
import org.apache.xmlbeans.XmlOptions;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import javax.xml.namespace.QName;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
/**
* Supports parsing Metaschema assembly and field XMLBeans objects that contain
* other Metaschema objects.
*
* @param <T>
* the Java type of the state that is passed to the element parsing
* handlers
*/
public class XmlObjectParser<T> {
private static final XmlOptions XML_OPTIONS = new XmlOptions().setXPathUseSaxon(false).setXPathUseXmlBeans(true);
private final Map<QName, Handler<T>> elementNameToHandlerMap;
private final String xpath;
private static String generatePath(@NonNull Collection<QName> nodes) {
// build a mapping of namespace prefix to namespace
AtomicInteger count = new AtomicInteger();
Map<String, String> namespaceToPrefixMap = nodes.stream()
.map(QName::getNamespaceURI)
.distinct()
.map(ns -> Pair.of(ns, "m" + count.getAndIncrement()))
.collect(Collectors.toMap(
Pair::getKey,
Pair::getValue,
(k1, k2) -> k1,
LinkedHashMap::new));
// generate namespace declarations using prefix and namespace
StringBuilder builder = new StringBuilder(24);
namespaceToPrefixMap.entrySet().forEach((entry) -> {
builder.append("declare namespace ")
.append(entry.getValue())
.append("='")
.append(entry.getKey())
.append("';");
});
// generate child path
builder.append(nodes.stream()
.map(qname -> {
return new StringBuilder()
.append("$this/")
.append(namespaceToPrefixMap.get(qname.getNamespaceURI()))
.append(':')
.append(qname.getLocalPart())
.toString();
}).collect(Collectors.joining("|")));
return builder.toString();
}
/**
* Construct a new XmlObject parser.
*
* @param elementNameToHandlerMap
* the mapping of element names to associated handlers
*/
public XmlObjectParser(@NonNull Map<QName, Handler<T>> elementNameToHandlerMap) {
this.elementNameToHandlerMap = elementNameToHandlerMap;
this.xpath = generatePath(ObjectUtils.notNull(elementNameToHandlerMap.keySet()));
}
private Map<QName, Handler<T>> getElementNameToHandlerMap() {
return elementNameToHandlerMap;
}
private String getXpath() {
return xpath;
}
/**
* Get the resource location of the provided object.
*
* @param obj
* the XMLBeans object to get the location for
* @return the resource location or {@code null} if the location is not known
*/
@SuppressWarnings({ "resource", "null" })
@Nullable
public static String toLocation(@NonNull XmlObject obj) {
return toLocation(obj.newCursor());
}
/**
* Get the resource location of the provided cursor.
*
* @param cursor
* the XMLBeans cursor to get the location for
* @return the resource location or {@code null} if the location is not known
*/
@Nullable
public static String toLocation(@NonNull XmlCursor cursor) {
String retval = null;
XmlBookmark bookmark = cursor.getBookmark(XmlLineNumber.class);
if (bookmark != null) {
StringBuilder locationBuilder = new StringBuilder();
XmlLineNumber lineNumber = (XmlLineNumber) bookmark;
String source = cursor.documentProperties().getSourceName();
if (source != null) {
locationBuilder.append(source)
.append(':');
}
locationBuilder.append(lineNumber.getLine())
.append(':')
.append(lineNumber.getColumn());
retval = locationBuilder.toString();
}
return retval;
}
/**
* Used to determine which parser {@link Handler} implementation to use to parse
* the object.
* <p>
* Subclasses can override this method to implement a more efficient or advanced
* detection method.
*
* @param cursor
* the current XmlCursor location
* @param obj
* the strongly typed XmlObject at the current location
* @return the identified handler
* @throws IllegalStateException
* if a suitable handler cannot be identified
*/
@NonNull
protected Handler<T> identifyHandler(@NonNull XmlCursor cursor, @NonNull XmlObject obj) {
QName qname = cursor.getName();
Handler<T> retval = getElementNameToHandlerMap().get(qname);
if (retval == null) {
String location = toLocation(cursor);
if (location == null) {
location = "";
} else {
location = new StringBuilder()
.append(" at location '")
.append(location)
.append('\'')
.toString();
}
throw new IllegalStateException(String.format("Unhandled node '%s'%s.", qname, location));
}
return retval;
}
/**
* Parse an XmlObject element tree using the configured child element handlers.
*
* @param xmlObject
* the XmlObject container to parse
* @param state
* parsing state to pass to the handlers
* @return the state
*/
public T parse(@NonNull XmlObject xmlObject, T state) {
try (XmlCursor cursor = xmlObject.newCursor()) {
assert cursor != null;
cursor.selectPath(getXpath(), XML_OPTIONS);
while (cursor.toNextSelection()) {
XmlObject obj = cursor.getObject();
assert obj != null;
Handler<T> handler = identifyHandler(cursor, obj);
handler.handle(obj, state);
}
}
return state;
}
/**
* Provides a common interface for element parsing handlers.
*
* @param <T>
* the Java type of the state that is passed to the element parsing
* handlers
*/
@FunctionalInterface
public interface Handler<T> {
/**
* Parse the provided {@code obj} using the provided {@code state}.
*
* @param obj
* the object to parse
* @param state
* the state to use for parsing
*/
void handle(@NonNull XmlObject obj, T state);
}
}