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}