1
2
3
4
5
6 package dev.metaschema.databind.io.xml;
7
8 import org.apache.logging.log4j.LogManager;
9 import org.apache.logging.log4j.Logger;
10 import org.codehaus.stax2.XMLEventReader2;
11 import org.w3c.dom.Element;
12
13 import java.io.IOException;
14 import java.net.URI;
15 import java.util.ArrayList;
16 import java.util.Collection;
17 import java.util.HashSet;
18 import java.util.LinkedHashMap;
19 import java.util.LinkedList;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.function.Function;
24 import java.util.stream.Collectors;
25
26 import javax.xml.namespace.QName;
27 import javax.xml.stream.Location;
28 import javax.xml.stream.XMLStreamConstants;
29 import javax.xml.stream.XMLStreamException;
30 import javax.xml.stream.events.Attribute;
31 import javax.xml.stream.events.StartElement;
32 import javax.xml.stream.events.XMLEvent;
33
34 import dev.metaschema.core.model.IAnyInstance;
35 import dev.metaschema.core.model.IBoundObject;
36 import dev.metaschema.core.model.IResourceLocation;
37 import dev.metaschema.core.model.SimpleResourceLocation;
38 import dev.metaschema.core.model.util.XmlEventUtil;
39 import dev.metaschema.core.qname.IEnhancedQName;
40 import dev.metaschema.core.util.CollectionUtil;
41 import dev.metaschema.core.util.ObjectUtils;
42 import dev.metaschema.databind.io.BindingException;
43 import dev.metaschema.databind.io.Format;
44 import dev.metaschema.databind.io.PathTracker;
45 import dev.metaschema.databind.io.ValidationContext;
46 import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
47 import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
48 import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
49 import dev.metaschema.databind.model.IBoundFieldValue;
50 import dev.metaschema.databind.model.IBoundInstanceFlag;
51 import dev.metaschema.databind.model.IBoundInstanceModel;
52 import dev.metaschema.databind.model.IBoundInstanceModelAny;
53 import dev.metaschema.databind.model.IBoundInstanceModelAssembly;
54 import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
55 import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex;
56 import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar;
57 import dev.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
58 import dev.metaschema.databind.model.IBoundInstanceModelGroupedField;
59 import dev.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
60 import dev.metaschema.databind.model.info.AbstractModelInstanceReadHandler;
61 import dev.metaschema.databind.model.info.IFeatureScalarItemValueHandler;
62 import dev.metaschema.databind.model.info.IItemReadHandler;
63 import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
64 import edu.umd.cs.findbugs.annotations.NonNull;
65 import edu.umd.cs.findbugs.annotations.Nullable;
66
67
68
69
70 @SuppressWarnings("PMD.CouplingBetweenObjects")
71 public class MetaschemaXmlReader
72 implements IXmlParsingContext {
73 private static final Logger LOGGER = LogManager.getLogger(MetaschemaXmlReader.class);
74 @NonNull
75 private final XMLEventReader2 reader;
76 @NonNull
77 private final URI source;
78 @NonNull
79 private final IXmlProblemHandler problemHandler;
80
81
82
83 @NonNull
84 private final PathTracker pathTracker = new PathTracker();
85
86
87
88
89
90
91
92
93
94
95 public MetaschemaXmlReader(
96 @NonNull XMLEventReader2 reader,
97 @NonNull URI source) {
98 this(reader, source, new DefaultXmlProblemHandler());
99 }
100
101
102
103
104
105
106
107
108
109
110
111 public MetaschemaXmlReader(
112 @NonNull XMLEventReader2 reader,
113 @NonNull URI source,
114 @NonNull IXmlProblemHandler problemHandler) {
115 this.reader = reader;
116 this.source = source;
117 this.problemHandler = problemHandler;
118 }
119
120 @Override
121 public XMLEventReader2 getReader() {
122 return reader;
123 }
124
125 @Override
126 public URI getSource() {
127 return source;
128 }
129
130 @Override
131 public IXmlProblemHandler getProblemHandler() {
132 return problemHandler;
133 }
134
135
136
137
138
139
140
141
142 @NonNull
143 private ValidationContext buildValidationContext(@Nullable Location location) {
144 IResourceLocation resourceLocation = location == null
145 ? SimpleResourceLocation.UNKNOWN
146 : SimpleResourceLocation.fromXmlLocation(location);
147 return ValidationContext.of(source, resourceLocation, pathTracker.getCurrentPath(), Format.XML);
148 }
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164 @Override
165 @NonNull
166 public <CLASS> CLASS read(@NonNull IBoundDefinitionModelComplex definition) throws IOException {
167 URI resource = getSource();
168 try {
169
170 if (reader.peek().isStartDocument()) {
171 XmlEventUtil.consumeAndAssert(reader, resource, XMLStreamConstants.START_DOCUMENT);
172 }
173
174
175 XmlEventUtil.skipEvents(reader, XMLStreamConstants.CHARACTERS, XMLStreamConstants.PROCESSING_INSTRUCTION,
176 XMLStreamConstants.DTD);
177
178 XMLEvent event = ObjectUtils.requireNonNull(reader.peek());
179 if (!event.isStartElement()) {
180 throw new IOException(
181 String.format("The token '%s' is not an XML element%s.",
182 XmlEventUtil.toEventName(event),
183 XmlEventUtil.generateLocationMessage(event, resource)));
184 }
185
186 ItemReadHandler handler = new ItemReadHandler(ObjectUtils.notNull(event.asStartElement()));
187 Object value = definition.readItem(null, handler);
188 if (value == null) {
189 event = reader.peek();
190 throw new IOException(String.format("Unable to read data.%s",
191 event == null ? "" : XmlEventUtil.generateLocationMessage(event, resource)));
192 }
193
194 return ObjectUtils.asType(value);
195 } catch (XMLStreamException ex) {
196 throw new IOException(ex);
197 }
198 }
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215 protected void readFlagInstances(
216 @NonNull IBoundDefinitionModelComplex targetDefinition,
217 @NonNull IBoundObject targetObject,
218 @NonNull StartElement start) throws IOException, XMLStreamException {
219 URI resource = getSource();
220
221 Map<IEnhancedQName, IBoundInstanceFlag> flagInstanceMap = targetDefinition.getFlagInstances().stream()
222 .collect(Collectors.toMap(
223 IBoundInstanceFlag::getQName,
224 Function.identity()));
225
226 for (Attribute attribute : CollectionUtil.toIterable(ObjectUtils.notNull(start.getAttributes()))) {
227 IEnhancedQName qname = IEnhancedQName.of(ObjectUtils.requireNonNull(attribute.getName()));
228 IBoundInstanceFlag instance = flagInstanceMap.get(qname);
229 if (instance == null) {
230
231 if (!getProblemHandler().handleUnknownAttribute(targetDefinition, targetObject, attribute, this)) {
232 throw new IOException(
233 String.format("Unrecognized attribute '%s'%s.",
234 qname,
235 XmlEventUtil.generateLocationMessage(attribute, resource)));
236 }
237 } else {
238 try {
239
240 Object value = instance.getDefinition().getJavaTypeAdapter()
241 .parse(ObjectUtils.notNull(attribute.getValue()));
242
243 instance.setValue(targetObject, value);
244 flagInstanceMap.remove(qname);
245 } catch (IllegalArgumentException ex) {
246 throw new IOException(
247 String.format("Malformed data '%s'%s. %s",
248 attribute.getValue(),
249 XmlEventUtil.generateLocationMessage(start, resource),
250 ex.getLocalizedMessage()),
251 ex);
252 }
253 }
254 }
255
256 if (!flagInstanceMap.isEmpty()) {
257
258 ValidationContext context = buildValidationContext(start.getLocation());
259 getProblemHandler().handleMissingFlagInstances(
260 targetDefinition,
261 targetObject,
262 ObjectUtils.notNull(flagInstanceMap.values()),
263 context);
264 }
265 }
266
267
268
269
270
271
272
273
274
275
276
277
278 protected void readModelInstances(
279 @NonNull IBoundDefinitionModelAssembly targetDefinition,
280 @NonNull IBoundObject targetObject)
281 throws IOException {
282 Collection<? extends IBoundInstanceModel<?>> instances = targetDefinition.getModelInstances();
283 Set<IBoundInstanceModel<?>> unhandledProperties = new HashSet<>();
284 for (IBoundInstanceModel<?> modelInstance : instances) {
285 assert modelInstance != null;
286 if (!readItems(modelInstance, targetObject, true)) {
287 unhandledProperties.add(modelInstance);
288 }
289 }
290
291
292 try {
293 XMLEvent event = getReader().peek();
294 Location location = event != null ? event.getLocation() : null;
295 ValidationContext context = buildValidationContext(location);
296 getProblemHandler().handleMissingModelInstances(targetDefinition, targetObject, unhandledProperties, context);
297 } catch (XMLStreamException ex) {
298 throw new IOException(ex);
299 }
300
301 XMLEventReader2 reader = getReader();
302 URI resource = getSource();
303
304
305 try {
306 XmlEventUtil.skipWhitespace(reader);
307
308 IAnyInstance anyInstance = targetDefinition.getModelContainer().getAnyInstance();
309
310 if (anyInstance instanceof IBoundInstanceModelAny && !reader.peek().isEndElement()) {
311 IBoundInstanceModelAny boundAny = (IBoundInstanceModelAny) anyInstance;
312
313 List<Element> capturedElements = new ArrayList<>();
314 while (reader.peek().isStartElement()) {
315 capturedElements.add(XmlDomUtil.staxToElement(reader));
316 XmlEventUtil.skipWhitespace(reader);
317 }
318 if (!capturedElements.isEmpty()) {
319 boundAny.setAnyContent(targetObject, new XmlAnyContent(capturedElements));
320 }
321 } else if (!reader.peek().isEndElement()) {
322
323 XmlEventUtil.skipElement(reader);
324 XmlEventUtil.skipWhitespace(reader);
325 }
326
327 XmlEventUtil.assertNext(reader, resource, XMLStreamConstants.END_ELEMENT);
328 } catch (XMLStreamException ex) {
329 throw new IOException(ex);
330 }
331 }
332
333
334
335
336
337
338
339
340
341
342
343 protected boolean isNextInstance(
344 @NonNull IBoundInstanceModel<?> targetInstance)
345 throws XMLStreamException {
346
347 XmlEventUtil.skipWhitespace(reader);
348
349 XMLEvent nextEvent = reader.peek();
350
351 boolean retval = nextEvent.isStartElement();
352 if (retval) {
353 IEnhancedQName qname = IEnhancedQName.of(ObjectUtils.notNull(nextEvent.asStartElement().getName()));
354 retval = qname.equals(targetInstance.getEffectiveXmlGroupAsQName())
355 || targetInstance.canHandleXmlQName(qname);
356 }
357 return retval;
358 }
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373 @Override
374 public <T> boolean readItems(
375 @NonNull IBoundInstanceModel<T> instance,
376 @NonNull IBoundObject parentObject,
377 boolean parseGrouping)
378 throws IOException {
379 try {
380 boolean handled = isNextInstance(instance);
381 if (handled) {
382 XMLEventReader2 reader = getReader();
383 URI resource = getSource();
384
385
386
387 IEnhancedQName groupEQName = parseGrouping ? instance.getEffectiveXmlGroupAsQName() : null;
388 QName groupQName = groupEQName == null ? null : groupEQName.toQName();
389 if (groupQName != null) {
390
391 XmlEventUtil.requireStartElement(reader, resource, groupQName);
392 }
393
394 IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo();
395
396 ModelInstanceReadHandler<T> handler = new ModelInstanceReadHandler<>(instance, parentObject);
397
398
399 Object value = collectionInfo.readItems(handler);
400 if (value != null) {
401 instance.setValue(parentObject, value);
402 }
403
404
405 XmlEventUtil.skipWhitespace(reader);
406
407 if (groupQName != null) {
408
409 XmlEventUtil.requireEndElement(reader, resource, groupQName);
410 }
411 }
412 return handled;
413 } catch (XMLStreamException ex) {
414 throw new IOException(ex);
415 }
416 }
417
418 private final class ModelInstanceReadHandler<ITEM>
419 extends AbstractModelInstanceReadHandler<ITEM> {
420
421 private ModelInstanceReadHandler(
422 @NonNull IBoundInstanceModel<ITEM> instance,
423 @NonNull IBoundObject parentObject) {
424 super(instance, parentObject);
425 }
426
427 @Override
428 public List<ITEM> readList() throws IOException {
429 return ObjectUtils.notNull(readCollection());
430 }
431
432 @Override
433 public Map<String, ITEM> readMap() throws IOException {
434 IBoundInstanceModel<?> instance = getCollectionInfo().getInstance();
435
436 return ObjectUtils.notNull(readCollection().stream()
437 .collect(Collectors.toMap(
438 item -> {
439 assert item != null;
440
441 IBoundInstanceFlag jsonKey = instance.getItemJsonKey(item);
442 assert jsonKey != null;
443 return ObjectUtils.requireNonNull(jsonKey.getValue(item)).toString();
444 },
445 Function.identity(),
446 (t, u) -> u,
447 LinkedHashMap::new)));
448 }
449
450 @NonNull
451 private List<ITEM> readCollection() throws IOException {
452 List<ITEM> retval = new LinkedList<>();
453 XMLEventReader2 reader = getReader();
454 try {
455
456
457 XmlEventUtil.skipWhitespace(reader);
458
459 IBoundInstanceModel<?> instance = getCollectionInfo().getInstance();
460 XMLEvent event;
461 while ((event = reader.peek()).isStartElement()
462 && instance.canHandleXmlQName(
463 IEnhancedQName.of(ObjectUtils.notNull(event.asStartElement().getName())))) {
464
465
466 ITEM value = readItem();
467 retval.add(value);
468
469
470 XmlEventUtil.skipWhitespace(reader);
471 }
472 } catch (XMLStreamException ex) {
473 throw new IOException(ex);
474 }
475 return retval;
476 }
477
478 @Override
479 public ITEM readItem() throws IOException {
480 try {
481 return getCollectionInfo().getInstance().readItem(
482 getParentObject(),
483 new ItemReadHandler(ObjectUtils.notNull(getReader().peek().asStartElement())));
484 } catch (XMLStreamException ex) {
485 throw new IOException(ex);
486 }
487 }
488 }
489
490 private final class ItemReadHandler implements IItemReadHandler {
491 @NonNull
492 private final StartElement startElement;
493
494 private ItemReadHandler(@NonNull StartElement startElement) {
495 this.startElement = startElement;
496 }
497
498
499
500
501
502
503 @NonNull
504 private StartElement getStartElement() {
505 return startElement;
506 }
507
508 @NonNull
509 private <DEF extends IBoundDefinitionModelComplex> IBoundObject readDefinitionElement(
510 @NonNull DEF definition,
511 @NonNull StartElement start,
512 @NonNull IEnhancedQName expectedEQName,
513 @Nullable IBoundObject parent,
514 @NonNull DefinitionBodyHandler<DEF, IBoundObject> bodyHandler) throws IOException {
515 XMLEventReader2 reader = getReader();
516 URI resource = getSource();
517 QName expectedQName = expectedEQName.toQName();
518
519
520 pathTracker.push(definition.getEffectiveName());
521
522 try {
523
524 XmlEventUtil.requireStartElement(reader, resource, expectedQName);
525
526 Location location = start.getLocation();
527
528
529 IBoundObject item = definition.newInstance(
530 location == null ? null : () -> SimpleResourceLocation.fromXmlLocation(location));
531
532
533 definition.callBeforeDeserialize(item, parent);
534
535
536 readFlagInstances(definition, item, start);
537
538
539 bodyHandler.accept(definition, item);
540
541 XmlEventUtil.skipWhitespace(reader);
542
543
544 definition.callAfterDeserialize(item, parent);
545
546
547 XmlEventUtil.requireEndElement(reader, resource, expectedQName);
548 return ObjectUtils.asType(item);
549 } catch (BindingException | XMLStreamException ex) {
550 throw new IOException(ex);
551 } finally {
552 pathTracker.pop();
553 }
554 }
555
556 @Override
557 public Object readItemFlag(
558 IBoundObject parent,
559 IBoundInstanceFlag flag) throws IOException {
560
561 throw new UnsupportedOperationException("should be handled by readFlagInstances()");
562 }
563
564 private void handleFieldDefinitionBody(
565 @NonNull IBoundDefinitionModelFieldComplex definition,
566 @NonNull IBoundObject item) throws IOException {
567 IBoundFieldValue fieldValue = definition.getFieldValue();
568
569
570 Object value = fieldValue.readItem(item, this);
571 if (value != null) {
572 fieldValue.setValue(item, value);
573 }
574 }
575
576 @Override
577 public Object readItemField(
578 IBoundObject parent,
579 IBoundInstanceModelFieldScalar instance)
580 throws IOException {
581 XMLEventReader2 reader = getReader();
582 URI resource = getSource();
583 try {
584 QName wrapper = null;
585 if (instance.isEffectiveValueWrappedInXml()) {
586 wrapper = instance.getQName().toQName();
587
588 XmlEventUtil.skipWhitespace(reader);
589 XmlEventUtil.requireStartElement(reader, resource, wrapper);
590 }
591
592 Object retval = readScalarItem(instance);
593
594 if (wrapper != null) {
595 XmlEventUtil.skipWhitespace(reader);
596
597 XmlEventUtil.requireEndElement(reader, resource, wrapper);
598 }
599 return retval;
600 } catch (XMLStreamException ex) {
601 throw new IOException(ex);
602 }
603 }
604
605 @Override
606 public IBoundObject readItemField(
607 IBoundObject parent,
608 IBoundInstanceModelFieldComplex instance)
609 throws IOException {
610 return readDefinitionElement(
611 instance.getDefinition(),
612 getStartElement(),
613 instance.getQName(),
614 parent,
615 this::handleFieldDefinitionBody);
616 }
617
618 @Override
619 public IBoundObject readItemField(IBoundObject parent, IBoundInstanceModelGroupedField instance)
620 throws IOException {
621 return readDefinitionElement(
622 instance.getDefinition(),
623 getStartElement(),
624 instance.getQName(),
625 parent,
626 this::handleFieldDefinitionBody);
627 }
628
629 @Override
630 public IBoundObject readItemField(
631 IBoundObject parent,
632 IBoundDefinitionModelFieldComplex definition) throws IOException {
633 return readDefinitionElement(
634 definition,
635 getStartElement(),
636 definition.getQName(),
637 parent,
638 this::handleFieldDefinitionBody);
639 }
640
641 @Override
642 public Object readItemFieldValue(
643 IBoundObject parent,
644 IBoundFieldValue fieldValue) throws IOException {
645 return checkMissingFieldValue(readScalarItem(fieldValue));
646 }
647
648 @Nullable
649 private Object checkMissingFieldValue(Object value) {
650 if (value == null && LOGGER.isWarnEnabled()) {
651 StartElement start = getStartElement();
652 LOGGER.atWarn().log("Missing property value{}",
653 XmlEventUtil.generateLocationMessage(start, getSource()));
654 }
655 return value;
656 }
657
658 private void handleAssemblyDefinitionBody(
659 @NonNull IBoundDefinitionModelAssembly definition,
660 @NonNull IBoundObject item) throws IOException {
661 readModelInstances(definition, item);
662 }
663
664 @Override
665 public IBoundObject readItemAssembly(
666 IBoundObject parent,
667 IBoundInstanceModelAssembly instance) throws IOException {
668 return readDefinitionElement(
669 instance.getDefinition(),
670 getStartElement(),
671 instance.getQName(),
672 parent,
673 this::handleAssemblyDefinitionBody);
674 }
675
676 @Override
677 public IBoundObject readItemAssembly(IBoundObject parent, IBoundInstanceModelGroupedAssembly instance)
678 throws IOException {
679 return readDefinitionElement(
680 instance.getDefinition(),
681 getStartElement(),
682 instance.getQName(),
683 parent,
684 this::handleAssemblyDefinitionBody);
685 }
686
687 @Override
688 public IBoundObject readItemAssembly(
689 IBoundObject parent,
690 IBoundDefinitionModelAssembly definition) throws IOException {
691 return readDefinitionElement(
692 definition,
693 getStartElement(),
694 ObjectUtils.requireNonNull(definition.getRootQName()),
695 parent,
696 this::handleAssemblyDefinitionBody);
697 }
698
699 @Nullable
700 private Object readScalarItem(@NonNull IFeatureScalarItemValueHandler handler)
701 throws IOException {
702 return handler.getJavaTypeAdapter().parse(getReader(), getSource());
703 }
704
705 @Override
706 public IBoundObject readChoiceGroupItem(IBoundObject parent, IBoundInstanceModelChoiceGroup instance)
707 throws IOException {
708 try {
709 XMLEventReader2 eventReader = getReader();
710
711 XmlEventUtil.skipWhitespace(eventReader);
712
713 XMLEvent event = eventReader.peek();
714 IEnhancedQName nextQName = IEnhancedQName.of(ObjectUtils.notNull(event.asStartElement().getName()));
715 IBoundInstanceModelGroupedNamed actualInstance = instance.getGroupedModelInstance(nextQName);
716 assert actualInstance != null;
717 return actualInstance.readItem(parent, this);
718 } catch (XMLStreamException ex) {
719 throw new IOException(ex);
720 }
721 }
722 }
723
724 @FunctionalInterface
725 private interface DefinitionBodyHandler<DEF extends IBoundDefinitionModelComplex, ITEM> {
726 void accept(
727 @NonNull DEF definition,
728 @NonNull ITEM item) throws IOException;
729 }
730
731 }