1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 {
52
53
54
55 private static final Pattern WHITESPACE_ONLY = Pattern.compile("^\\s+$");
56
57 private static final Map<Integer, String> EVENT_NAME_MAP = new HashMap<>();
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
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
107
108
109
110
111
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
143
144
145
146
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
160
161
162
163
164
165
166 @NonNull
167 public static CharSequence toString(@NonNull XMLStreamReader2 reader) {
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
191
192
193
194
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
214
215
216
217
218
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
235
236
237
238
239
240 @NonNull
241 public static String toEventName(@NonNull XMLEvent event) {
242 return toEventName(event.getEventType());
243 }
244
245
246
247
248
249
250
251
252
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
265
266
267
268
269
270
271
272
273
274
275
276 @Nullable
277 public static XMLEvent advanceTo(@NonNull XMLEventReader2 reader, int eventType)
278 throws XMLStreamException {
279 XMLEvent xmlEvent;
280 do {
281 xmlEvent = reader.nextEvent();
282
283
284
285 if (xmlEvent.isStartElement()) {
286 advanceTo(reader, XMLStreamConstants.END_ELEMENT);
287
288 xmlEvent = reader.nextEvent();
289
290
291
292 }
293 } while (reader.hasNext() && (xmlEvent = reader.peek()).getEventType() != eventType);
294 return xmlEvent;
295 }
296
297
298
299
300
301
302
303
304
305
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
318
319
320
321
322
323
324
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
344
345
346
347
348
349
350
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
360
361
362
363
364
365 public static boolean isEventEndDocument(XMLEvent event) {
366 return event != null
367 && event.isEndElement();
368 }
369
370
371
372
373
374
375
376
377
378
379
380
381
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
391
392
393
394
395
396
397
398
399
400
401 public static XMLEvent consumeAndAssert(XMLEventReader2 reader, int presumedEventType)
402 throws XMLStreamException {
403 return consumeAndAssert(reader, presumedEventType, null);
404 }
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
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
438
439
440
441
442
443
444
445
446
447
448
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
459
460
461
462
463
464
465
466
467
468
469
470
471
472
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
528
529
530
531
532
533
534
535
536
537
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 }