View Javadoc
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  
27  package gov.nist.secauto.metaschema.core.model.util;
28  
29  import org.codehaus.stax2.XMLEventReader2;
30  import org.codehaus.stax2.XMLStreamReader2;
31  
32  import java.util.HashMap;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.regex.Pattern;
36  import java.util.stream.Collectors;
37  import java.util.stream.IntStream;
38  
39  import javax.xml.namespace.QName;
40  import javax.xml.stream.Location;
41  import javax.xml.stream.XMLStreamConstants;
42  import javax.xml.stream.XMLStreamException;
43  import javax.xml.stream.events.Characters;
44  import javax.xml.stream.events.EndElement;
45  import javax.xml.stream.events.StartElement;
46  import javax.xml.stream.events.XMLEvent;
47  
48  import edu.umd.cs.findbugs.annotations.NonNull;
49  import edu.umd.cs.findbugs.annotations.Nullable;
50  
51  public final class XmlEventUtil { // NOPMD this is a set of utility methods
52    // private static final Logger LOGGER =
53    // LogManager.getLogger(XmlEventUtil.class);
54  
55    private static final Pattern WHITESPACE_ONLY = Pattern.compile("^\\s+$");
56  
57    private static final Map<Integer, String> EVENT_NAME_MAP = new HashMap<>(); // NOPMD - this value is immutable
58  
59    static {
60      EVENT_NAME_MAP.put(XMLStreamConstants.START_ELEMENT, "START_ELEMENT");
61      EVENT_NAME_MAP.put(XMLStreamConstants.END_ELEMENT, "END_ELEMENT");
62      EVENT_NAME_MAP.put(XMLStreamConstants.PROCESSING_INSTRUCTION, "PROCESSING_INSTRUCTION");
63      EVENT_NAME_MAP.put(XMLStreamConstants.CHARACTERS, "CHARACTERS");
64      EVENT_NAME_MAP.put(XMLStreamConstants.COMMENT, "COMMENT");
65      EVENT_NAME_MAP.put(XMLStreamConstants.SPACE, "SPACE");
66      EVENT_NAME_MAP.put(XMLStreamConstants.START_DOCUMENT, "START_DOCUMENT");
67      EVENT_NAME_MAP.put(XMLStreamConstants.END_DOCUMENT, "END_DOCUMENT");
68      EVENT_NAME_MAP.put(XMLStreamConstants.ENTITY_REFERENCE, "ENTITY_REFERENCE");
69      EVENT_NAME_MAP.put(XMLStreamConstants.ATTRIBUTE, "ATTRIBUTE");
70      EVENT_NAME_MAP.put(XMLStreamConstants.DTD, "DTD");
71      EVENT_NAME_MAP.put(XMLStreamConstants.CDATA, "CDATA");
72      EVENT_NAME_MAP.put(XMLStreamConstants.NAMESPACE, "NAMESPACE");
73      EVENT_NAME_MAP.put(XMLStreamConstants.NOTATION_DECLARATION, "NOTATION_DECLARATION");
74      EVENT_NAME_MAP.put(XMLStreamConstants.ENTITY_DECLARATION, "ENTITY_DECLARATION");
75    }
76  
77    private XmlEventUtil() {
78      // disable construction
79    }
80  
81    @SuppressWarnings("null")
82    @NonNull
83    private static Object escape(@NonNull String data) {
84      return data.chars().mapToObj(c -> (char) c).map(c -> escape(c)).collect(Collectors.joining());
85    }
86  
87    @SuppressWarnings("null")
88    @NonNull
89    private static String escape(char ch) {
90      String retval;
91      switch (ch) {
92      case '\n':
93        retval = "\\n";
94        break;
95      case '\r':
96        retval = "\\r";
97        break;
98      default:
99        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 }