001/*
002 * Portions of this software was developed by employees of the National Institute
003 * of Standards and Technology (NIST), an agency of the Federal Government and is
004 * being made available as a public service. Pursuant to title 17 United States
005 * Code Section 105, works of NIST employees are not subject to copyright
006 * protection in the United States. This software may be subject to foreign
007 * copyright. Permission in the United States and in foreign countries, to the
008 * extent that NIST may hold copyright, to use, copy, modify, create derivative
009 * works, and distribute this software and its documentation without fee is hereby
010 * granted on a non-exclusive basis, provided that this notice and disclaimer
011 * of warranty appears in all copies.
012 *
013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
025 */
026
027package gov.nist.secauto.metaschema.core.model.util;
028
029import org.codehaus.stax2.XMLEventReader2;
030import org.codehaus.stax2.XMLStreamReader2;
031
032import java.util.HashMap;
033import java.util.Map;
034import java.util.Set;
035import java.util.regex.Pattern;
036import java.util.stream.Collectors;
037import java.util.stream.IntStream;
038
039import javax.xml.namespace.QName;
040import javax.xml.stream.Location;
041import javax.xml.stream.XMLStreamConstants;
042import javax.xml.stream.XMLStreamException;
043import javax.xml.stream.events.Characters;
044import javax.xml.stream.events.EndElement;
045import javax.xml.stream.events.StartElement;
046import javax.xml.stream.events.XMLEvent;
047
048import edu.umd.cs.findbugs.annotations.NonNull;
049import edu.umd.cs.findbugs.annotations.Nullable;
050
051public final class XmlEventUtil { // NOPMD this is a set of utility methods
052  // private static final Logger LOGGER =
053  // LogManager.getLogger(XmlEventUtil.class);
054
055  private static final Pattern WHITESPACE_ONLY = Pattern.compile("^\\s+$");
056
057  private static final Map<Integer, String> EVENT_NAME_MAP = new HashMap<>(); // NOPMD - this value is immutable
058
059  static {
060    EVENT_NAME_MAP.put(XMLStreamConstants.START_ELEMENT, "START_ELEMENT");
061    EVENT_NAME_MAP.put(XMLStreamConstants.END_ELEMENT, "END_ELEMENT");
062    EVENT_NAME_MAP.put(XMLStreamConstants.PROCESSING_INSTRUCTION, "PROCESSING_INSTRUCTION");
063    EVENT_NAME_MAP.put(XMLStreamConstants.CHARACTERS, "CHARACTERS");
064    EVENT_NAME_MAP.put(XMLStreamConstants.COMMENT, "COMMENT");
065    EVENT_NAME_MAP.put(XMLStreamConstants.SPACE, "SPACE");
066    EVENT_NAME_MAP.put(XMLStreamConstants.START_DOCUMENT, "START_DOCUMENT");
067    EVENT_NAME_MAP.put(XMLStreamConstants.END_DOCUMENT, "END_DOCUMENT");
068    EVENT_NAME_MAP.put(XMLStreamConstants.ENTITY_REFERENCE, "ENTITY_REFERENCE");
069    EVENT_NAME_MAP.put(XMLStreamConstants.ATTRIBUTE, "ATTRIBUTE");
070    EVENT_NAME_MAP.put(XMLStreamConstants.DTD, "DTD");
071    EVENT_NAME_MAP.put(XMLStreamConstants.CDATA, "CDATA");
072    EVENT_NAME_MAP.put(XMLStreamConstants.NAMESPACE, "NAMESPACE");
073    EVENT_NAME_MAP.put(XMLStreamConstants.NOTATION_DECLARATION, "NOTATION_DECLARATION");
074    EVENT_NAME_MAP.put(XMLStreamConstants.ENTITY_DECLARATION, "ENTITY_DECLARATION");
075  }
076
077  private XmlEventUtil() {
078    // disable construction
079  }
080
081  @SuppressWarnings("null")
082  @NonNull
083  private static Object escape(@NonNull String data) {
084    return data.chars().mapToObj(c -> (char) c).map(c -> escape(c)).collect(Collectors.joining());
085  }
086
087  @SuppressWarnings("null")
088  @NonNull
089  private static String escape(char ch) {
090    String retval;
091    switch (ch) {
092    case '\n':
093      retval = "\\n";
094      break;
095    case '\r':
096      retval = "\\r";
097      break;
098    default:
099      retval = String.valueOf(ch);
100      break;
101    }
102    return retval;
103  }
104
105  /**
106   * Generate a message suitable for logging that describes the provided
107   * {@link XMLEvent}.
108   *
109   * @param xmlEvent
110   *          the event to generate the message for
111   * @return the message
112   */
113  @NonNull
114  public static CharSequence toString(XMLEvent xmlEvent) {
115    CharSequence retval;
116    if (xmlEvent == null) {
117      retval = "EOF";
118    } else {
119      @SuppressWarnings("null")
120      @NonNull StringBuilder builder = new StringBuilder()
121          .append(toEventName(xmlEvent));
122      QName name = toQName(xmlEvent);
123      if (name != null) {
124        builder.append(": ").append(name.toString());
125      }
126      if (xmlEvent.isCharacters()) {
127        String text = xmlEvent.asCharacters().getData();
128        if (text != null) {
129          builder.append(" '").append(escape(text)).append('\'');
130        }
131      }
132      Location location = toLocation(xmlEvent);
133      if (location != null) {
134        builder.append(" at ").append(toString(location));
135      }
136      retval = builder;
137    }
138    return retval;
139  }
140
141  /**
142   * Generates a message for the provided {@link Location}.
143   *
144   * @param location
145   *          the location to generate the message for
146   * @return the message
147   */
148  @SuppressWarnings("null")
149  @NonNull
150  public static CharSequence toString(@Nullable Location location) {
151    return location == null ? "unknown"
152        : new StringBuilder()
153            .append(location.getLineNumber())
154            .append(':')
155            .append(location.getColumnNumber());
156  }
157
158  /**
159   * Generates a string containing the current event and location of the stream
160   * reader.
161   *
162   * @param reader
163   *          the stream reader
164   * @return the generated string
165   */
166  @NonNull
167  public static CharSequence toString(@NonNull XMLStreamReader2 reader) { // NO_UCD (unused code)
168    int type = reader.getEventType();
169
170    @SuppressWarnings("null")
171    @NonNull StringBuilder builder = new StringBuilder().append(toEventName(type));
172    QName name = reader.getName();
173    if (name != null) {
174      builder.append(": ").append(name.toString());
175    }
176    if (XMLStreamConstants.CHARACTERS == type) {
177      String text = reader.getText();
178      if (text != null) {
179        builder.append(" '").append(escape(text)).append('\'');
180      }
181    }
182    Location location = reader.getLocation();
183    if (location != null) {
184      builder.append(" at ").append(toString(location));
185    }
186    return builder;
187  }
188
189  /**
190   * Retrieve the resource location of {@code event}.
191   *
192   * @param event
193   *          the event to identify the location for
194   * @return the location or {@code null} if the location is unknown
195   */
196  @Nullable
197  public static Location toLocation(@NonNull XMLEvent event) {
198    Location retval = null;
199    if (event.isStartElement()) {
200      StartElement start = event.asStartElement();
201      retval = start.getLocation();
202    } else if (event.isEndElement()) {
203      EndElement end = event.asEndElement();
204      retval = end.getLocation();
205    } else if (event.isCharacters()) {
206      Characters characters = event.asCharacters();
207      retval = characters.getLocation();
208    }
209    return retval;
210  }
211
212  /**
213   * Retrieve the name of the node associated with {@code event}.
214   *
215   * @param event
216   *          the event to get the {@link QName} for
217   * @return the name of the node or {@code null} if the event is not a start or
218   *         end element
219   */
220  @Nullable
221  public static QName toQName(@NonNull XMLEvent event) {
222    QName retval = null;
223    if (event.isStartElement()) {
224      StartElement start = event.asStartElement();
225      retval = start.getName();
226    } else if (event.isEndElement()) {
227      EndElement end = event.asEndElement();
228      retval = end.getName();
229    }
230    return retval;
231  }
232
233  /**
234   * Get the event name of the {@code event}.
235   *
236   * @param event
237   *          the event to get the event name for
238   * @return the event name
239   */
240  @NonNull
241  public static String toEventName(@NonNull XMLEvent event) {
242    return toEventName(event.getEventType());
243  }
244
245  /**
246   * Get the event name of the {@code eventType}, which is one of the types
247   * defined by {@link XMLStreamConstants}.
248   *
249   * @param eventType
250   *          the event constant to get the event name for as defined by
251   *          {@link XMLStreamConstants}
252   * @return the event name
253   */
254  @NonNull
255  public static String toEventName(int eventType) {
256    String retval = EVENT_NAME_MAP.get(eventType);
257    if (retval == null) {
258      retval = "unknown event '" + Integer.toString(eventType) + "'";
259    }
260    return retval;
261  }
262
263  /**
264   * Advance through XMLEvents until the event type identified by
265   * {@code eventType} is reached or the end of stream is found.
266   *
267   * @param reader
268   *          the event reader to advance
269   * @param eventType
270   *          the event type to stop on as defined by {@link XMLStreamConstants}
271   * @return the next event of the specified type or {@code null} if the end of
272   *         stream is reached
273   * @throws XMLStreamException
274   *           if an error occurred while advancing the stream
275   */
276  @Nullable
277  public static XMLEvent advanceTo(@NonNull XMLEventReader2 reader, int eventType)
278      throws XMLStreamException { // NO_UCD (unused code)
279    XMLEvent xmlEvent;
280    do {
281      xmlEvent = reader.nextEvent();
282      // if (LOGGER.isWarnEnabled()) {
283      // LOGGER.warn("skipping over: {}", XmlEventUtil.toString(xmlEvent));
284      // }
285      if (xmlEvent.isStartElement()) {
286        advanceTo(reader, XMLStreamConstants.END_ELEMENT);
287        // skip this end element
288        xmlEvent = reader.nextEvent();
289        // if (LOGGER.isDebugEnabled()) {
290        // LOGGER.debug("skipping over: {}", XmlEventUtil.toString(xmlEvent));
291        // }
292      }
293    } while (reader.hasNext() && (xmlEvent = reader.peek()).getEventType() != eventType);
294    return xmlEvent;
295  }
296
297  /**
298   * Skip over any processing instructions.
299   *
300   * @param reader
301   *          the event reader to advance
302   * @return the last processing instruction event or the reader's next event if
303   *         no processing instruction was found
304   * @throws XMLStreamException
305   *           if an error occurred while advancing the stream
306   */
307  @NonNull
308  public static XMLEvent skipProcessingInstructions(@NonNull XMLEventReader2 reader) throws XMLStreamException {
309    XMLEvent nextEvent;
310    while ((nextEvent = reader.peek()).isProcessingInstruction()) {
311      nextEvent = reader.nextEvent();
312    }
313    return nextEvent;
314  }
315
316  /**
317   * Skip over any whitespace.
318   *
319   * @param reader
320   *          the event reader to advance
321   * @return the last character event containing whitespace or the reader's next
322   *         event if no character event was found
323   * @throws XMLStreamException
324   *           if an error occurred while advancing the stream
325   */
326  @SuppressWarnings("null")
327  @NonNull
328  public static XMLEvent skipWhitespace(@NonNull XMLEventReader2 reader) throws XMLStreamException {
329    @NonNull XMLEvent nextEvent;
330    while ((nextEvent = reader.peek()).isCharacters()) {
331      Characters characters = nextEvent.asCharacters();
332      String data = characters.getData();
333      if (WHITESPACE_ONLY.matcher(data).matches()) {
334        nextEvent = reader.nextEvent();
335      } else {
336        break;
337      }
338    }
339    return nextEvent;
340  }
341
342  /**
343   * Determine if the {@code event} is an end element whose name matches the
344   * provided {@code expectedQName}.
345   *
346   * @param event
347   *          the event
348   * @param expectedQName
349   *          the expected element name
350   * @return {@code true} if the next event matches the {@code expectedQName}
351   */
352  public static boolean isEventEndElement(XMLEvent event, @NonNull QName expectedQName) {
353    return event != null
354        && event.isEndElement()
355        && expectedQName.equals(event.asEndElement().getName());
356  }
357
358  /**
359   * Determine if the {@code event} is an end of document event.
360   *
361   * @param event
362   *          the event
363   * @return {@code true} if the next event is an end of document event
364   */
365  public static boolean isEventEndDocument(XMLEvent event) {
366    return event != null
367        && event.isEndElement();
368  }
369
370  /**
371   * Determine if the {@code event} is a start element whose name matches the
372   * provided {@code expectedQName}.
373   *
374   * @param event
375   *          the event
376   * @param expectedQName
377   *          the expected element name
378   * @return {@code true} if the next event is a start element that matches the
379   *         {@code expectedQName}
380   * @throws XMLStreamException
381   *           if an error occurred while looking at the next event
382   */
383  public static boolean isEventStartElement(XMLEvent event, @NonNull QName expectedQName) throws XMLStreamException {
384    return event != null
385        && event.isStartElement()
386        && expectedQName.equals(event.asStartElement().getName());
387  }
388
389  /**
390   * Consume the next event from {@code reader} and assert that this event is of
391   * the type identified by {@code presumedEventType}.
392   *
393   * @param reader
394   *          the event reader
395   * @param presumedEventType
396   *          the expected event type as defined by {@link XMLStreamConstants}
397   * @return the next event
398   * @throws XMLStreamException
399   *           if an error occurred while looking at the next event
400   */
401  public static XMLEvent consumeAndAssert(XMLEventReader2 reader, int presumedEventType)
402      throws XMLStreamException {
403    return consumeAndAssert(reader, presumedEventType, null);
404  }
405
406  /**
407   * Consume the next event from {@code reader} and assert that this event is of
408   * the type identified by {@code presumedEventType} and has the name identified
409   * by {@code presumedName}.
410   *
411   * @param reader
412   *          the event reader
413   * @param presumedEventType
414   *          the expected event type as defined by {@link XMLStreamConstants}
415   * @param presumedName
416   *          the expected name of the node associated with the event
417   * @return the next event
418   * @throws XMLStreamException
419   *           if an error occurred while looking at the next event
420   */
421  public static XMLEvent consumeAndAssert(XMLEventReader2 reader, int presumedEventType, QName presumedName)
422      throws XMLStreamException {
423    XMLEvent retval = reader.nextEvent();
424
425    int eventType = retval.getEventType();
426    QName name = toQName(retval);
427    assert eventType == presumedEventType
428        && (presumedName == null
429            || presumedName.equals(name)) : generateExpectedMessage(
430                retval,
431                presumedEventType,
432                presumedName);
433    return retval;
434  }
435
436  /**
437   * Assert that the next event from {@code reader} is of the type identified by
438   * {@code presumedEventType}.
439   *
440   * @param reader
441   *          the event reader
442   * @param presumedEventType
443   *          the expected event type as defined by {@link XMLStreamConstants}
444   * @return the next event
445   * @throws XMLStreamException
446   *           if an error occurred while looking at the next event
447   * @throws AssertionError
448   *           if the next event does not match the presumed event
449   */
450  public static XMLEvent assertNext(
451      @NonNull XMLEventReader2 reader,
452      int presumedEventType)
453      throws XMLStreamException {
454    return assertNext(reader, presumedEventType, null);
455  }
456
457  /**
458   * Assert that the next event from {@code reader} is of the type identified by
459   * {@code presumedEventType} and has the name identified by
460   * {@code presumedName}.
461   *
462   * @param reader
463   *          the event reader
464   * @param presumedEventType
465   *          the expected event type as defined by {@link XMLStreamConstants}
466   * @param presumedName
467   *          the expected name of the node associated with the event
468   * @return the next event
469   * @throws XMLStreamException
470   *           if an error occurred while looking at the next event
471   * @throws AssertionError
472   *           if the next event does not match the presumed event
473   */
474  public static XMLEvent assertNext(
475      @NonNull XMLEventReader2 reader,
476      int presumedEventType,
477      @Nullable QName presumedName)
478      throws XMLStreamException {
479    XMLEvent nextEvent = reader.peek();
480
481    int eventType = nextEvent.getEventType();
482    assert eventType == presumedEventType
483        && (presumedName == null
484            || presumedName.equals(toQName(nextEvent))) : generateExpectedMessage(
485                nextEvent,
486                presumedEventType,
487                presumedName);
488    return nextEvent;
489  }
490
491  public static CharSequence generateLocationMessage(@NonNull XMLEvent event) {
492    Location location = XmlEventUtil.toLocation(event);
493    return location == null ? "" : generateLocationMessage(location);
494  }
495
496  public static CharSequence generateLocationMessage(@NonNull Location location) {
497    return new StringBuilder(12)
498        .append(" at ")
499        .append(XmlEventUtil.toString(location));
500  }
501
502  public static CharSequence generateExpectedMessage(
503      @Nullable XMLEvent event,
504      int presumedEventType,
505      @Nullable QName presumedName) {
506    StringBuilder builder = new StringBuilder(64);
507    builder
508        .append("Expected XML ")
509        .append(toEventName(presumedEventType));
510
511    if (presumedName != null) {
512      builder.append(" for QName '")
513          .append(presumedName.toString());
514    }
515
516    if (event == null) {
517      builder.append("', instead found null event");
518    } else {
519      builder.append("', instead found ")
520          .append(toString(event))
521          .append(generateLocationMessage(event));
522    }
523    return builder;
524  }
525
526  /**
527   * Skips events specified by {@code events}.
528   *
529   * @param reader
530   *          the event reader
531   * @param events
532   *          the events to skip
533   * @return the next non-mataching event returned by
534   *         {@link XMLEventReader2#peek()}, or {@code null} if there was no next
535   *         event
536   * @throws XMLStreamException
537   *           if an error occurred while reading
538   */
539  public static XMLEvent skipEvents(XMLEventReader2 reader, int... events) throws XMLStreamException {
540    Set<Integer> skipEvents = IntStream.of(events).boxed().collect(Collectors.toSet());
541
542    XMLEvent nextEvent = null;
543    while (reader.hasNext()) {
544      nextEvent = reader.peek();
545      if (!skipEvents.contains(nextEvent.getEventType())) {
546        break;
547      }
548      reader.nextEvent();
549    }
550    return nextEvent;
551  }
552}