1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
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   * Supports reading XML-based Metaschema module instances.
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     * Tracks the current parsing path for context-aware error reporting.
82     */
83    @NonNull
84    private final PathTracker pathTracker = new PathTracker();
85  
86    /**
87     * Construct a new Module-aware XML parser using the default problem handler.
88     *
89     * @param reader
90     *          the XML reader to parse with
91     * @param source
92     *          the resource being parsed
93     * @see DefaultXmlProblemHandler
94     */
95    public MetaschemaXmlReader(
96        @NonNull XMLEventReader2 reader,
97        @NonNull URI source) {
98      this(reader, source, new DefaultXmlProblemHandler());
99    }
100 
101   /**
102    * Construct a new Module-aware parser.
103    *
104    * @param reader
105    *          the XML reader to parse with
106    * @param source
107    *          the resource being parsed
108    * @param problemHandler
109    *          the problem handler implementation to use
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    * Build a validation context from the current XML parser state.
137    *
138    * @param location
139    *          the XML location, may be null
140    * @return a new validation context with current location and path
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    * Parses XML into a bound object based on the provided {@code definition}.
152    * <p>
153    * Parses the {@link XMLStreamConstants#START_DOCUMENT}, any processing
154    * instructions, and the element.
155    *
156    * @param <CLASS>
157    *          the returned object type
158    * @param definition
159    *          the definition describing the element data to read
160    * @return the parsed object
161    * @throws IOException
162    *           if an error occurred while parsing the input
163    */
164   @Override
165   @NonNull
166   public <CLASS> CLASS read(@NonNull IBoundDefinitionModelComplex definition) throws IOException {
167     URI resource = getSource();
168     try {
169       // we may be at the START_DOCUMENT
170       if (reader.peek().isStartDocument()) {
171         XmlEventUtil.consumeAndAssert(reader, resource, XMLStreamConstants.START_DOCUMENT);
172       }
173 
174       // advance past any other info to get to next start element
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    * Read the XML attribute data described by the {@code targetDefinition} and
202    * apply it to the provided {@code targetObject}.
203    *
204    * @param targetDefinition
205    *          the Module definition that describes the syntax of the data to read
206    * @param targetObject
207    *          the Java object that data parsed by this method will be stored in
208    * @param start
209    *          the containing XML element that was previously parsed
210    * @throws IOException
211    *           if an error occurred while parsing the input
212    * @throws XMLStreamException
213    *           if an error occurred while parsing XML events
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         // unrecognized flag
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           // get the attribute value
240           Object value = instance.getDefinition().getJavaTypeAdapter()
241               .parse(ObjectUtils.notNull(attribute.getValue()));
242           // apply the value to the parentObject
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       // Build validation context with current location and path
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    * Read the XML element data described by the {@code targetDefinition} and apply
269    * it to the provided {@code targetObject}.
270    *
271    * @param targetDefinition
272    *          the Module definition that describes the syntax of the data to read
273    * @param targetObject
274    *          the Java object that data parsed by this method will be stored in
275    * @throws IOException
276    *           if an error occurred while parsing the input
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     // process all properties that did not get a value
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     // handle any content
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         // Capture remaining child elements as DOM elements
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         // No any instance defined; fall through to existing skip behavior
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    * Determine if the next data to read corresponds to the next model instance.
335    *
336    * @param targetInstance
337    *          the model instance that describes the syntax of the data to read
338    * @return {@code true} if the Module instance needs to be parsed, or
339    *         {@code false} otherwise
340    * @throws XMLStreamException
341    *           if an error occurred while parsing XML events
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()) // parse the grouping element
355           || targetInstance.canHandleXmlQName(qname); // parse the instance(s)
356     }
357     return retval;
358   }
359 
360   /**
361    * Read the data associated with the {@code instance} and apply it to the
362    * provided {@code parentObject}.
363    *
364    * @param instance
365    *          the instance to parse data for
366    * @param parentObject
367    *          the Java object that data parsed by this method will be stored in
368    * @return {@code true} if the instance was parsed, or {@code false} if the data
369    *         did not contain information for this instance
370    * @throws IOException
371    *           if an error occurred while parsing the input
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         // XmlEventUtil.skipWhitespace(reader);
386 
387         IEnhancedQName groupEQName = parseGrouping ? instance.getEffectiveXmlGroupAsQName() : null;
388         QName groupQName = groupEQName == null ? null : groupEQName.toQName();
389         if (groupQName != null) {
390           // we need to parse the grouping element, if the next token matches
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         // let the property info decide how to parse the value
399         Object value = collectionInfo.readItems(handler);
400         if (value != null) {
401           instance.setValue(parentObject, value);
402         }
403 
404         // consume extra whitespace between elements
405         XmlEventUtil.skipWhitespace(reader);
406 
407         if (groupQName != null) {
408           // consume the end of the group
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         // consume extra whitespace between elements
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           // Consume the start element
466           ITEM value = readItem();
467           retval.add(value);
468 
469           // consume extra whitespace between elements
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      * Get the current start element.
500      *
501      * @return the startElement
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       // Track path for error messages
520       pathTracker.push(definition.getEffectiveName());
521 
522       try {
523         // consume the start element
524         XmlEventUtil.requireStartElement(reader, resource, expectedQName);
525 
526         Location location = start.getLocation();
527 
528         // construct the item
529         IBoundObject item = definition.newInstance(
530             location == null ? null : () -> SimpleResourceLocation.fromXmlLocation(location));
531 
532         // call pre-parse initialization hook
533         definition.callBeforeDeserialize(item, parent);
534 
535         // read the flags
536         readFlagInstances(definition, item, start);
537 
538         // read the body
539         bodyHandler.accept(definition, item);
540 
541         XmlEventUtil.skipWhitespace(reader);
542 
543         // call post-parse initialization hook
544         definition.callAfterDeserialize(item, parent);
545 
546         // consume the end element
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       // should never be called
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       // parse the value
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         // consume extra whitespace between elements
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 }