XmlEventUtil.java
/*
* Portions of this software was developed by employees of the National Institute
* of Standards and Technology (NIST), an agency of the Federal Government and is
* being made available as a public service. Pursuant to title 17 United States
* Code Section 105, works of NIST employees are not subject to copyright
* protection in the United States. This software may be subject to foreign
* copyright. Permission in the United States and in foreign countries, to the
* extent that NIST may hold copyright, to use, copy, modify, create derivative
* works, and distribute this software and its documentation without fee is hereby
* granted on a non-exclusive basis, provided that this notice and disclaimer
* of warranty appears in all copies.
*
* THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
* EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
* THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
* INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
* SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT
* SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
* INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
* OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
* CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
* PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
* OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
*/
package gov.nist.secauto.metaschema.core.model.util;
import org.codehaus.stax2.XMLEventReader2;
import org.codehaus.stax2.XMLStreamReader2;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.xml.namespace.QName;
import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
public final class XmlEventUtil { // NOPMD this is a set of utility methods
// private static final Logger LOGGER =
// LogManager.getLogger(XmlEventUtil.class);
private static final Pattern WHITESPACE_ONLY = Pattern.compile("^\\s+$");
private static final Map<Integer, String> EVENT_NAME_MAP = new HashMap<>(); // NOPMD - this value is immutable
static {
EVENT_NAME_MAP.put(XMLStreamConstants.START_ELEMENT, "START_ELEMENT");
EVENT_NAME_MAP.put(XMLStreamConstants.END_ELEMENT, "END_ELEMENT");
EVENT_NAME_MAP.put(XMLStreamConstants.PROCESSING_INSTRUCTION, "PROCESSING_INSTRUCTION");
EVENT_NAME_MAP.put(XMLStreamConstants.CHARACTERS, "CHARACTERS");
EVENT_NAME_MAP.put(XMLStreamConstants.COMMENT, "COMMENT");
EVENT_NAME_MAP.put(XMLStreamConstants.SPACE, "SPACE");
EVENT_NAME_MAP.put(XMLStreamConstants.START_DOCUMENT, "START_DOCUMENT");
EVENT_NAME_MAP.put(XMLStreamConstants.END_DOCUMENT, "END_DOCUMENT");
EVENT_NAME_MAP.put(XMLStreamConstants.ENTITY_REFERENCE, "ENTITY_REFERENCE");
EVENT_NAME_MAP.put(XMLStreamConstants.ATTRIBUTE, "ATTRIBUTE");
EVENT_NAME_MAP.put(XMLStreamConstants.DTD, "DTD");
EVENT_NAME_MAP.put(XMLStreamConstants.CDATA, "CDATA");
EVENT_NAME_MAP.put(XMLStreamConstants.NAMESPACE, "NAMESPACE");
EVENT_NAME_MAP.put(XMLStreamConstants.NOTATION_DECLARATION, "NOTATION_DECLARATION");
EVENT_NAME_MAP.put(XMLStreamConstants.ENTITY_DECLARATION, "ENTITY_DECLARATION");
}
private XmlEventUtil() {
// disable construction
}
@SuppressWarnings("null")
@NonNull
private static Object escape(@NonNull String data) {
return data.chars().mapToObj(c -> (char) c).map(c -> escape(c)).collect(Collectors.joining());
}
@SuppressWarnings("null")
@NonNull
private static String escape(char ch) {
String retval;
switch (ch) {
case '\n':
retval = "\\n";
break;
case '\r':
retval = "\\r";
break;
default:
retval = String.valueOf(ch);
break;
}
return retval;
}
/**
* Generate a message suitable for logging that describes the provided
* {@link XMLEvent}.
*
* @param xmlEvent
* the event to generate the message for
* @return the message
*/
@NonNull
public static CharSequence toString(XMLEvent xmlEvent) {
CharSequence retval;
if (xmlEvent == null) {
retval = "EOF";
} else {
@SuppressWarnings("null")
@NonNull StringBuilder builder = new StringBuilder()
.append(toEventName(xmlEvent));
QName name = toQName(xmlEvent);
if (name != null) {
builder.append(": ").append(name.toString());
}
if (xmlEvent.isCharacters()) {
String text = xmlEvent.asCharacters().getData();
if (text != null) {
builder.append(" '").append(escape(text)).append('\'');
}
}
Location location = toLocation(xmlEvent);
if (location != null) {
builder.append(" at ").append(toString(location));
}
retval = builder;
}
return retval;
}
/**
* Generates a message for the provided {@link Location}.
*
* @param location
* the location to generate the message for
* @return the message
*/
@SuppressWarnings("null")
@NonNull
public static CharSequence toString(@Nullable Location location) {
return location == null ? "unknown"
: new StringBuilder()
.append(location.getLineNumber())
.append(':')
.append(location.getColumnNumber());
}
/**
* Generates a string containing the current event and location of the stream
* reader.
*
* @param reader
* the stream reader
* @return the generated string
*/
@NonNull
public static CharSequence toString(@NonNull XMLStreamReader2 reader) { // NO_UCD (unused code)
int type = reader.getEventType();
@SuppressWarnings("null")
@NonNull StringBuilder builder = new StringBuilder().append(toEventName(type));
QName name = reader.getName();
if (name != null) {
builder.append(": ").append(name.toString());
}
if (XMLStreamConstants.CHARACTERS == type) {
String text = reader.getText();
if (text != null) {
builder.append(" '").append(escape(text)).append('\'');
}
}
Location location = reader.getLocation();
if (location != null) {
builder.append(" at ").append(toString(location));
}
return builder;
}
/**
* Retrieve the resource location of {@code event}.
*
* @param event
* the event to identify the location for
* @return the location or {@code null} if the location is unknown
*/
@Nullable
public static Location toLocation(@NonNull XMLEvent event) {
Location retval = null;
if (event.isStartElement()) {
StartElement start = event.asStartElement();
retval = start.getLocation();
} else if (event.isEndElement()) {
EndElement end = event.asEndElement();
retval = end.getLocation();
} else if (event.isCharacters()) {
Characters characters = event.asCharacters();
retval = characters.getLocation();
}
return retval;
}
/**
* Retrieve the name of the node associated with {@code event}.
*
* @param event
* the event to get the {@link QName} for
* @return the name of the node or {@code null} if the event is not a start or
* end element
*/
@Nullable
public static QName toQName(@NonNull XMLEvent event) {
QName retval = null;
if (event.isStartElement()) {
StartElement start = event.asStartElement();
retval = start.getName();
} else if (event.isEndElement()) {
EndElement end = event.asEndElement();
retval = end.getName();
}
return retval;
}
/**
* Get the event name of the {@code event}.
*
* @param event
* the event to get the event name for
* @return the event name
*/
@NonNull
public static String toEventName(@NonNull XMLEvent event) {
return toEventName(event.getEventType());
}
/**
* Get the event name of the {@code eventType}, which is one of the types
* defined by {@link XMLStreamConstants}.
*
* @param eventType
* the event constant to get the event name for as defined by
* {@link XMLStreamConstants}
* @return the event name
*/
@NonNull
public static String toEventName(int eventType) {
String retval = EVENT_NAME_MAP.get(eventType);
if (retval == null) {
retval = "unknown event '" + Integer.toString(eventType) + "'";
}
return retval;
}
/**
* Advance through XMLEvents until the event type identified by
* {@code eventType} is reached or the end of stream is found.
*
* @param reader
* the event reader to advance
* @param eventType
* the event type to stop on as defined by {@link XMLStreamConstants}
* @return the next event of the specified type or {@code null} if the end of
* stream is reached
* @throws XMLStreamException
* if an error occurred while advancing the stream
*/
@Nullable
public static XMLEvent advanceTo(@NonNull XMLEventReader2 reader, int eventType)
throws XMLStreamException { // NO_UCD (unused code)
XMLEvent xmlEvent;
do {
xmlEvent = reader.nextEvent();
// if (LOGGER.isWarnEnabled()) {
// LOGGER.warn("skipping over: {}", XmlEventUtil.toString(xmlEvent));
// }
if (xmlEvent.isStartElement()) {
advanceTo(reader, XMLStreamConstants.END_ELEMENT);
// skip this end element
xmlEvent = reader.nextEvent();
// if (LOGGER.isDebugEnabled()) {
// LOGGER.debug("skipping over: {}", XmlEventUtil.toString(xmlEvent));
// }
}
} while (reader.hasNext() && (xmlEvent = reader.peek()).getEventType() != eventType);
return xmlEvent;
}
/**
* Skip over any processing instructions.
*
* @param reader
* the event reader to advance
* @return the last processing instruction event or the reader's next event if
* no processing instruction was found
* @throws XMLStreamException
* if an error occurred while advancing the stream
*/
@NonNull
public static XMLEvent skipProcessingInstructions(@NonNull XMLEventReader2 reader) throws XMLStreamException {
XMLEvent nextEvent;
while ((nextEvent = reader.peek()).isProcessingInstruction()) {
nextEvent = reader.nextEvent();
}
return nextEvent;
}
/**
* Skip over any whitespace.
*
* @param reader
* the event reader to advance
* @return the last character event containing whitespace or the reader's next
* event if no character event was found
* @throws XMLStreamException
* if an error occurred while advancing the stream
*/
@SuppressWarnings("null")
@NonNull
public static XMLEvent skipWhitespace(@NonNull XMLEventReader2 reader) throws XMLStreamException {
@NonNull XMLEvent nextEvent;
while ((nextEvent = reader.peek()).isCharacters()) {
Characters characters = nextEvent.asCharacters();
String data = characters.getData();
if (WHITESPACE_ONLY.matcher(data).matches()) {
nextEvent = reader.nextEvent();
} else {
break;
}
}
return nextEvent;
}
/**
* Determine if the {@code event} is an end element whose name matches the
* provided {@code expectedQName}.
*
* @param event
* the event
* @param expectedQName
* the expected element name
* @return {@code true} if the next event matches the {@code expectedQName}
*/
public static boolean isEventEndElement(XMLEvent event, @NonNull QName expectedQName) {
return event != null
&& event.isEndElement()
&& expectedQName.equals(event.asEndElement().getName());
}
/**
* Determine if the {@code event} is an end of document event.
*
* @param event
* the event
* @return {@code true} if the next event is an end of document event
*/
public static boolean isEventEndDocument(XMLEvent event) {
return event != null
&& event.isEndElement();
}
/**
* Determine if the {@code event} is a start element whose name matches the
* provided {@code expectedQName}.
*
* @param event
* the event
* @param expectedQName
* the expected element name
* @return {@code true} if the next event is a start element that matches the
* {@code expectedQName}
* @throws XMLStreamException
* if an error occurred while looking at the next event
*/
public static boolean isEventStartElement(XMLEvent event, @NonNull QName expectedQName) throws XMLStreamException {
return event != null
&& event.isStartElement()
&& expectedQName.equals(event.asStartElement().getName());
}
/**
* Consume the next event from {@code reader} and assert that this event is of
* the type identified by {@code presumedEventType}.
*
* @param reader
* the event reader
* @param presumedEventType
* the expected event type as defined by {@link XMLStreamConstants}
* @return the next event
* @throws XMLStreamException
* if an error occurred while looking at the next event
*/
public static XMLEvent consumeAndAssert(XMLEventReader2 reader, int presumedEventType)
throws XMLStreamException {
return consumeAndAssert(reader, presumedEventType, null);
}
/**
* Consume the next event from {@code reader} and assert that this event is of
* the type identified by {@code presumedEventType} and has the name identified
* by {@code presumedName}.
*
* @param reader
* the event reader
* @param presumedEventType
* the expected event type as defined by {@link XMLStreamConstants}
* @param presumedName
* the expected name of the node associated with the event
* @return the next event
* @throws XMLStreamException
* if an error occurred while looking at the next event
*/
public static XMLEvent consumeAndAssert(XMLEventReader2 reader, int presumedEventType, QName presumedName)
throws XMLStreamException {
XMLEvent retval = reader.nextEvent();
int eventType = retval.getEventType();
QName name = toQName(retval);
assert eventType == presumedEventType
&& (presumedName == null
|| presumedName.equals(name)) : generateExpectedMessage(
retval,
presumedEventType,
presumedName);
return retval;
}
/**
* Assert that the next event from {@code reader} is of the type identified by
* {@code presumedEventType}.
*
* @param reader
* the event reader
* @param presumedEventType
* the expected event type as defined by {@link XMLStreamConstants}
* @return the next event
* @throws XMLStreamException
* if an error occurred while looking at the next event
* @throws AssertionError
* if the next event does not match the presumed event
*/
public static XMLEvent assertNext(
@NonNull XMLEventReader2 reader,
int presumedEventType)
throws XMLStreamException {
return assertNext(reader, presumedEventType, null);
}
/**
* Assert that the next event from {@code reader} is of the type identified by
* {@code presumedEventType} and has the name identified by
* {@code presumedName}.
*
* @param reader
* the event reader
* @param presumedEventType
* the expected event type as defined by {@link XMLStreamConstants}
* @param presumedName
* the expected name of the node associated with the event
* @return the next event
* @throws XMLStreamException
* if an error occurred while looking at the next event
* @throws AssertionError
* if the next event does not match the presumed event
*/
public static XMLEvent assertNext(
@NonNull XMLEventReader2 reader,
int presumedEventType,
@Nullable QName presumedName)
throws XMLStreamException {
XMLEvent nextEvent = reader.peek();
int eventType = nextEvent.getEventType();
assert eventType == presumedEventType
&& (presumedName == null
|| presumedName.equals(toQName(nextEvent))) : generateExpectedMessage(
nextEvent,
presumedEventType,
presumedName);
return nextEvent;
}
public static CharSequence generateLocationMessage(@NonNull XMLEvent event) {
Location location = XmlEventUtil.toLocation(event);
return location == null ? "" : generateLocationMessage(location);
}
public static CharSequence generateLocationMessage(@NonNull Location location) {
return new StringBuilder(12)
.append(" at ")
.append(XmlEventUtil.toString(location));
}
public static CharSequence generateExpectedMessage(
@Nullable XMLEvent event,
int presumedEventType,
@Nullable QName presumedName) {
StringBuilder builder = new StringBuilder(64);
builder
.append("Expected XML ")
.append(toEventName(presumedEventType));
if (presumedName != null) {
builder.append(" for QName '")
.append(presumedName.toString());
}
if (event == null) {
builder.append("', instead found null event");
} else {
builder.append("', instead found ")
.append(toString(event))
.append(generateLocationMessage(event));
}
return builder;
}
/**
* Skips events specified by {@code events}.
*
* @param reader
* the event reader
* @param events
* the events to skip
* @return the next non-mataching event returned by
* {@link XMLEventReader2#peek()}, or {@code null} if there was no next
* event
* @throws XMLStreamException
* if an error occurred while reading
*/
public static XMLEvent skipEvents(XMLEventReader2 reader, int... events) throws XMLStreamException {
Set<Integer> skipEvents = IntStream.of(events).boxed().collect(Collectors.toSet());
XMLEvent nextEvent = null;
while (reader.hasNext()) {
nextEvent = reader.peek();
if (!skipEvents.contains(nextEvent.getEventType())) {
break;
}
reader.nextEvent();
}
return nextEvent;
}
}