XmlEventUtil.java

  1. /*
  2.  * Portions of this software was developed by employees of the National Institute
  3.  * of Standards and Technology (NIST), an agency of the Federal Government and is
  4.  * being made available as a public service. Pursuant to title 17 United States
  5.  * Code Section 105, works of NIST employees are not subject to copyright
  6.  * protection in the United States. This software may be subject to foreign
  7.  * copyright. Permission in the United States and in foreign countries, to the
  8.  * extent that NIST may hold copyright, to use, copy, modify, create derivative
  9.  * works, and distribute this software and its documentation without fee is hereby
  10.  * granted on a non-exclusive basis, provided that this notice and disclaimer
  11.  * of warranty appears in all copies.
  12.  *
  13.  * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
  14.  * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
  15.  * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
  16.  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
  17.  * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
  18.  * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
  19.  * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
  20.  * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
  21.  * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
  22.  * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
  23.  * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
  24.  * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
  25.  */

  26. package gov.nist.secauto.metaschema.core.model.util;

  27. import org.codehaus.stax2.XMLEventReader2;
  28. import org.codehaus.stax2.XMLStreamReader2;

  29. import java.util.HashMap;
  30. import java.util.Map;
  31. import java.util.Set;
  32. import java.util.regex.Pattern;
  33. import java.util.stream.Collectors;
  34. import java.util.stream.IntStream;

  35. import javax.xml.namespace.QName;
  36. import javax.xml.stream.Location;
  37. import javax.xml.stream.XMLStreamConstants;
  38. import javax.xml.stream.XMLStreamException;
  39. import javax.xml.stream.events.Characters;
  40. import javax.xml.stream.events.EndElement;
  41. import javax.xml.stream.events.StartElement;
  42. import javax.xml.stream.events.XMLEvent;

  43. import edu.umd.cs.findbugs.annotations.NonNull;
  44. import edu.umd.cs.findbugs.annotations.Nullable;

  45. public final class XmlEventUtil { // NOPMD this is a set of utility methods
  46.   // private static final Logger LOGGER =
  47.   // LogManager.getLogger(XmlEventUtil.class);

  48.   private static final Pattern WHITESPACE_ONLY = Pattern.compile("^\\s+$");

  49.   private static final Map<Integer, String> EVENT_NAME_MAP = new HashMap<>(); // NOPMD - this value is immutable

  50.   static {
  51.     EVENT_NAME_MAP.put(XMLStreamConstants.START_ELEMENT, "START_ELEMENT");
  52.     EVENT_NAME_MAP.put(XMLStreamConstants.END_ELEMENT, "END_ELEMENT");
  53.     EVENT_NAME_MAP.put(XMLStreamConstants.PROCESSING_INSTRUCTION, "PROCESSING_INSTRUCTION");
  54.     EVENT_NAME_MAP.put(XMLStreamConstants.CHARACTERS, "CHARACTERS");
  55.     EVENT_NAME_MAP.put(XMLStreamConstants.COMMENT, "COMMENT");
  56.     EVENT_NAME_MAP.put(XMLStreamConstants.SPACE, "SPACE");
  57.     EVENT_NAME_MAP.put(XMLStreamConstants.START_DOCUMENT, "START_DOCUMENT");
  58.     EVENT_NAME_MAP.put(XMLStreamConstants.END_DOCUMENT, "END_DOCUMENT");
  59.     EVENT_NAME_MAP.put(XMLStreamConstants.ENTITY_REFERENCE, "ENTITY_REFERENCE");
  60.     EVENT_NAME_MAP.put(XMLStreamConstants.ATTRIBUTE, "ATTRIBUTE");
  61.     EVENT_NAME_MAP.put(XMLStreamConstants.DTD, "DTD");
  62.     EVENT_NAME_MAP.put(XMLStreamConstants.CDATA, "CDATA");
  63.     EVENT_NAME_MAP.put(XMLStreamConstants.NAMESPACE, "NAMESPACE");
  64.     EVENT_NAME_MAP.put(XMLStreamConstants.NOTATION_DECLARATION, "NOTATION_DECLARATION");
  65.     EVENT_NAME_MAP.put(XMLStreamConstants.ENTITY_DECLARATION, "ENTITY_DECLARATION");
  66.   }

  67.   private XmlEventUtil() {
  68.     // disable construction
  69.   }

  70.   @SuppressWarnings("null")
  71.   @NonNull
  72.   private static Object escape(@NonNull String data) {
  73.     return data.chars().mapToObj(c -> (char) c).map(c -> escape(c)).collect(Collectors.joining());
  74.   }

  75.   @SuppressWarnings("null")
  76.   @NonNull
  77.   private static String escape(char ch) {
  78.     String retval;
  79.     switch (ch) {
  80.     case '\n':
  81.       retval = "\\n";
  82.       break;
  83.     case '\r':
  84.       retval = "\\r";
  85.       break;
  86.     default:
  87.       retval = String.valueOf(ch);
  88.       break;
  89.     }
  90.     return retval;
  91.   }

  92.   /**
  93.    * Generate a message suitable for logging that describes the provided
  94.    * {@link XMLEvent}.
  95.    *
  96.    * @param xmlEvent
  97.    *          the event to generate the message for
  98.    * @return the message
  99.    */
  100.   @NonNull
  101.   public static CharSequence toString(XMLEvent xmlEvent) {
  102.     CharSequence retval;
  103.     if (xmlEvent == null) {
  104.       retval = "EOF";
  105.     } else {
  106.       @SuppressWarnings("null")
  107.       @NonNull StringBuilder builder = new StringBuilder()
  108.           .append(toEventName(xmlEvent));
  109.       QName name = toQName(xmlEvent);
  110.       if (name != null) {
  111.         builder.append(": ").append(name.toString());
  112.       }
  113.       if (xmlEvent.isCharacters()) {
  114.         String text = xmlEvent.asCharacters().getData();
  115.         if (text != null) {
  116.           builder.append(" '").append(escape(text)).append('\'');
  117.         }
  118.       }
  119.       Location location = toLocation(xmlEvent);
  120.       if (location != null) {
  121.         builder.append(" at ").append(toString(location));
  122.       }
  123.       retval = builder;
  124.     }
  125.     return retval;
  126.   }

  127.   /**
  128.    * Generates a message for the provided {@link Location}.
  129.    *
  130.    * @param location
  131.    *          the location to generate the message for
  132.    * @return the message
  133.    */
  134.   @SuppressWarnings("null")
  135.   @NonNull
  136.   public static CharSequence toString(@Nullable Location location) {
  137.     return location == null ? "unknown"
  138.         : new StringBuilder()
  139.             .append(location.getLineNumber())
  140.             .append(':')
  141.             .append(location.getColumnNumber());
  142.   }

  143.   /**
  144.    * Generates a string containing the current event and location of the stream
  145.    * reader.
  146.    *
  147.    * @param reader
  148.    *          the stream reader
  149.    * @return the generated string
  150.    */
  151.   @NonNull
  152.   public static CharSequence toString(@NonNull XMLStreamReader2 reader) { // NO_UCD (unused code)
  153.     int type = reader.getEventType();

  154.     @SuppressWarnings("null")
  155.     @NonNull StringBuilder builder = new StringBuilder().append(toEventName(type));
  156.     QName name = reader.getName();
  157.     if (name != null) {
  158.       builder.append(": ").append(name.toString());
  159.     }
  160.     if (XMLStreamConstants.CHARACTERS == type) {
  161.       String text = reader.getText();
  162.       if (text != null) {
  163.         builder.append(" '").append(escape(text)).append('\'');
  164.       }
  165.     }
  166.     Location location = reader.getLocation();
  167.     if (location != null) {
  168.       builder.append(" at ").append(toString(location));
  169.     }
  170.     return builder;
  171.   }

  172.   /**
  173.    * Retrieve the resource location of {@code event}.
  174.    *
  175.    * @param event
  176.    *          the event to identify the location for
  177.    * @return the location or {@code null} if the location is unknown
  178.    */
  179.   @Nullable
  180.   public static Location toLocation(@NonNull XMLEvent event) {
  181.     Location retval = null;
  182.     if (event.isStartElement()) {
  183.       StartElement start = event.asStartElement();
  184.       retval = start.getLocation();
  185.     } else if (event.isEndElement()) {
  186.       EndElement end = event.asEndElement();
  187.       retval = end.getLocation();
  188.     } else if (event.isCharacters()) {
  189.       Characters characters = event.asCharacters();
  190.       retval = characters.getLocation();
  191.     }
  192.     return retval;
  193.   }

  194.   /**
  195.    * Retrieve the name of the node associated with {@code event}.
  196.    *
  197.    * @param event
  198.    *          the event to get the {@link QName} for
  199.    * @return the name of the node or {@code null} if the event is not a start or
  200.    *         end element
  201.    */
  202.   @Nullable
  203.   public static QName toQName(@NonNull XMLEvent event) {
  204.     QName retval = null;
  205.     if (event.isStartElement()) {
  206.       StartElement start = event.asStartElement();
  207.       retval = start.getName();
  208.     } else if (event.isEndElement()) {
  209.       EndElement end = event.asEndElement();
  210.       retval = end.getName();
  211.     }
  212.     return retval;
  213.   }

  214.   /**
  215.    * Get the event name of the {@code event}.
  216.    *
  217.    * @param event
  218.    *          the event to get the event name for
  219.    * @return the event name
  220.    */
  221.   @NonNull
  222.   public static String toEventName(@NonNull XMLEvent event) {
  223.     return toEventName(event.getEventType());
  224.   }

  225.   /**
  226.    * Get the event name of the {@code eventType}, which is one of the types
  227.    * defined by {@link XMLStreamConstants}.
  228.    *
  229.    * @param eventType
  230.    *          the event constant to get the event name for as defined by
  231.    *          {@link XMLStreamConstants}
  232.    * @return the event name
  233.    */
  234.   @NonNull
  235.   public static String toEventName(int eventType) {
  236.     String retval = EVENT_NAME_MAP.get(eventType);
  237.     if (retval == null) {
  238.       retval = "unknown event '" + Integer.toString(eventType) + "'";
  239.     }
  240.     return retval;
  241.   }

  242.   /**
  243.    * Advance through XMLEvents until the event type identified by
  244.    * {@code eventType} is reached or the end of stream is found.
  245.    *
  246.    * @param reader
  247.    *          the event reader to advance
  248.    * @param eventType
  249.    *          the event type to stop on as defined by {@link XMLStreamConstants}
  250.    * @return the next event of the specified type or {@code null} if the end of
  251.    *         stream is reached
  252.    * @throws XMLStreamException
  253.    *           if an error occurred while advancing the stream
  254.    */
  255.   @Nullable
  256.   public static XMLEvent advanceTo(@NonNull XMLEventReader2 reader, int eventType)
  257.       throws XMLStreamException { // NO_UCD (unused code)
  258.     XMLEvent xmlEvent;
  259.     do {
  260.       xmlEvent = reader.nextEvent();
  261.       // if (LOGGER.isWarnEnabled()) {
  262.       // LOGGER.warn("skipping over: {}", XmlEventUtil.toString(xmlEvent));
  263.       // }
  264.       if (xmlEvent.isStartElement()) {
  265.         advanceTo(reader, XMLStreamConstants.END_ELEMENT);
  266.         // skip this end element
  267.         xmlEvent = reader.nextEvent();
  268.         // if (LOGGER.isDebugEnabled()) {
  269.         // LOGGER.debug("skipping over: {}", XmlEventUtil.toString(xmlEvent));
  270.         // }
  271.       }
  272.     } while (reader.hasNext() && (xmlEvent = reader.peek()).getEventType() != eventType);
  273.     return xmlEvent;
  274.   }

  275.   /**
  276.    * Skip over any processing instructions.
  277.    *
  278.    * @param reader
  279.    *          the event reader to advance
  280.    * @return the last processing instruction event or the reader's next event if
  281.    *         no processing instruction was found
  282.    * @throws XMLStreamException
  283.    *           if an error occurred while advancing the stream
  284.    */
  285.   @NonNull
  286.   public static XMLEvent skipProcessingInstructions(@NonNull XMLEventReader2 reader) throws XMLStreamException {
  287.     XMLEvent nextEvent;
  288.     while ((nextEvent = reader.peek()).isProcessingInstruction()) {
  289.       nextEvent = reader.nextEvent();
  290.     }
  291.     return nextEvent;
  292.   }

  293.   /**
  294.    * Skip over any whitespace.
  295.    *
  296.    * @param reader
  297.    *          the event reader to advance
  298.    * @return the last character event containing whitespace or the reader's next
  299.    *         event if no character event was found
  300.    * @throws XMLStreamException
  301.    *           if an error occurred while advancing the stream
  302.    */
  303.   @SuppressWarnings("null")
  304.   @NonNull
  305.   public static XMLEvent skipWhitespace(@NonNull XMLEventReader2 reader) throws XMLStreamException {
  306.     @NonNull XMLEvent nextEvent;
  307.     while ((nextEvent = reader.peek()).isCharacters()) {
  308.       Characters characters = nextEvent.asCharacters();
  309.       String data = characters.getData();
  310.       if (WHITESPACE_ONLY.matcher(data).matches()) {
  311.         nextEvent = reader.nextEvent();
  312.       } else {
  313.         break;
  314.       }
  315.     }
  316.     return nextEvent;
  317.   }

  318.   /**
  319.    * Determine if the {@code event} is an end element whose name matches the
  320.    * provided {@code expectedQName}.
  321.    *
  322.    * @param event
  323.    *          the event
  324.    * @param expectedQName
  325.    *          the expected element name
  326.    * @return {@code true} if the next event matches the {@code expectedQName}
  327.    */
  328.   public static boolean isEventEndElement(XMLEvent event, @NonNull QName expectedQName) {
  329.     return event != null
  330.         && event.isEndElement()
  331.         && expectedQName.equals(event.asEndElement().getName());
  332.   }

  333.   /**
  334.    * Determine if the {@code event} is an end of document event.
  335.    *
  336.    * @param event
  337.    *          the event
  338.    * @return {@code true} if the next event is an end of document event
  339.    */
  340.   public static boolean isEventEndDocument(XMLEvent event) {
  341.     return event != null
  342.         && event.isEndElement();
  343.   }

  344.   /**
  345.    * Determine if the {@code event} is a start element whose name matches the
  346.    * provided {@code expectedQName}.
  347.    *
  348.    * @param event
  349.    *          the event
  350.    * @param expectedQName
  351.    *          the expected element name
  352.    * @return {@code true} if the next event is a start element that matches the
  353.    *         {@code expectedQName}
  354.    * @throws XMLStreamException
  355.    *           if an error occurred while looking at the next event
  356.    */
  357.   public static boolean isEventStartElement(XMLEvent event, @NonNull QName expectedQName) throws XMLStreamException {
  358.     return event != null
  359.         && event.isStartElement()
  360.         && expectedQName.equals(event.asStartElement().getName());
  361.   }

  362.   /**
  363.    * Consume the next event from {@code reader} and assert that this event is of
  364.    * the type identified by {@code presumedEventType}.
  365.    *
  366.    * @param reader
  367.    *          the event reader
  368.    * @param presumedEventType
  369.    *          the expected event type as defined by {@link XMLStreamConstants}
  370.    * @return the next event
  371.    * @throws XMLStreamException
  372.    *           if an error occurred while looking at the next event
  373.    */
  374.   public static XMLEvent consumeAndAssert(XMLEventReader2 reader, int presumedEventType)
  375.       throws XMLStreamException {
  376.     return consumeAndAssert(reader, presumedEventType, null);
  377.   }

  378.   /**
  379.    * Consume the next event from {@code reader} and assert that this event is of
  380.    * the type identified by {@code presumedEventType} and has the name identified
  381.    * by {@code presumedName}.
  382.    *
  383.    * @param reader
  384.    *          the event reader
  385.    * @param presumedEventType
  386.    *          the expected event type as defined by {@link XMLStreamConstants}
  387.    * @param presumedName
  388.    *          the expected name of the node associated with the event
  389.    * @return the next event
  390.    * @throws XMLStreamException
  391.    *           if an error occurred while looking at the next event
  392.    */
  393.   public static XMLEvent consumeAndAssert(XMLEventReader2 reader, int presumedEventType, QName presumedName)
  394.       throws XMLStreamException {
  395.     XMLEvent retval = reader.nextEvent();

  396.     int eventType = retval.getEventType();
  397.     QName name = toQName(retval);
  398.     assert eventType == presumedEventType
  399.         && (presumedName == null
  400.             || presumedName.equals(name)) : generateExpectedMessage(
  401.                 retval,
  402.                 presumedEventType,
  403.                 presumedName);
  404.     return retval;
  405.   }

  406.   /**
  407.    * Assert that the next event from {@code reader} is of the type identified by
  408.    * {@code presumedEventType}.
  409.    *
  410.    * @param reader
  411.    *          the event reader
  412.    * @param presumedEventType
  413.    *          the expected event type as defined by {@link XMLStreamConstants}
  414.    * @return the next event
  415.    * @throws XMLStreamException
  416.    *           if an error occurred while looking at the next event
  417.    * @throws AssertionError
  418.    *           if the next event does not match the presumed event
  419.    */
  420.   public static XMLEvent assertNext(
  421.       @NonNull XMLEventReader2 reader,
  422.       int presumedEventType)
  423.       throws XMLStreamException {
  424.     return assertNext(reader, presumedEventType, null);
  425.   }

  426.   /**
  427.    * Assert that the next event from {@code reader} is of the type identified by
  428.    * {@code presumedEventType} and has the name identified by
  429.    * {@code presumedName}.
  430.    *
  431.    * @param reader
  432.    *          the event reader
  433.    * @param presumedEventType
  434.    *          the expected event type as defined by {@link XMLStreamConstants}
  435.    * @param presumedName
  436.    *          the expected name of the node associated with the event
  437.    * @return the next event
  438.    * @throws XMLStreamException
  439.    *           if an error occurred while looking at the next event
  440.    * @throws AssertionError
  441.    *           if the next event does not match the presumed event
  442.    */
  443.   public static XMLEvent assertNext(
  444.       @NonNull XMLEventReader2 reader,
  445.       int presumedEventType,
  446.       @Nullable QName presumedName)
  447.       throws XMLStreamException {
  448.     XMLEvent nextEvent = reader.peek();

  449.     int eventType = nextEvent.getEventType();
  450.     assert eventType == presumedEventType
  451.         && (presumedName == null
  452.             || presumedName.equals(toQName(nextEvent))) : generateExpectedMessage(
  453.                 nextEvent,
  454.                 presumedEventType,
  455.                 presumedName);
  456.     return nextEvent;
  457.   }

  458.   public static CharSequence generateLocationMessage(@NonNull XMLEvent event) {
  459.     Location location = XmlEventUtil.toLocation(event);
  460.     return location == null ? "" : generateLocationMessage(location);
  461.   }

  462.   public static CharSequence generateLocationMessage(@NonNull Location location) {
  463.     return new StringBuilder(12)
  464.         .append(" at ")
  465.         .append(XmlEventUtil.toString(location));
  466.   }

  467.   public static CharSequence generateExpectedMessage(
  468.       @Nullable XMLEvent event,
  469.       int presumedEventType,
  470.       @Nullable QName presumedName) {
  471.     StringBuilder builder = new StringBuilder(64);
  472.     builder
  473.         .append("Expected XML ")
  474.         .append(toEventName(presumedEventType));

  475.     if (presumedName != null) {
  476.       builder.append(" for QName '")
  477.           .append(presumedName.toString());
  478.     }

  479.     if (event == null) {
  480.       builder.append("', instead found null event");
  481.     } else {
  482.       builder.append("', instead found ")
  483.           .append(toString(event))
  484.           .append(generateLocationMessage(event));
  485.     }
  486.     return builder;
  487.   }

  488.   /**
  489.    * Skips events specified by {@code events}.
  490.    *
  491.    * @param reader
  492.    *          the event reader
  493.    * @param events
  494.    *          the events to skip
  495.    * @return the next non-mataching event returned by
  496.    *         {@link XMLEventReader2#peek()}, or {@code null} if there was no next
  497.    *         event
  498.    * @throws XMLStreamException
  499.    *           if an error occurred while reading
  500.    */
  501.   public static XMLEvent skipEvents(XMLEventReader2 reader, int... events) throws XMLStreamException {
  502.     Set<Integer> skipEvents = IntStream.of(events).boxed().collect(Collectors.toSet());

  503.     XMLEvent nextEvent = null;
  504.     while (reader.hasNext()) {
  505.       nextEvent = reader.peek();
  506.       if (!skipEvents.contains(nextEvent.getEventType())) {
  507.         break;
  508.       }
  509.       reader.nextEvent();
  510.     }
  511.     return nextEvent;
  512.   }
  513. }