001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.databind.io.xml;
007
008import org.apache.logging.log4j.LogManager;
009import org.apache.logging.log4j.Logger;
010import org.codehaus.stax2.XMLEventReader2;
011import org.w3c.dom.Element;
012
013import java.io.IOException;
014import java.net.URI;
015import java.util.ArrayList;
016import java.util.Collection;
017import java.util.HashSet;
018import java.util.LinkedHashMap;
019import java.util.LinkedList;
020import java.util.List;
021import java.util.Map;
022import java.util.Set;
023import java.util.function.Function;
024import java.util.stream.Collectors;
025
026import javax.xml.namespace.QName;
027import javax.xml.stream.Location;
028import javax.xml.stream.XMLStreamConstants;
029import javax.xml.stream.XMLStreamException;
030import javax.xml.stream.events.Attribute;
031import javax.xml.stream.events.StartElement;
032import javax.xml.stream.events.XMLEvent;
033
034import dev.metaschema.core.model.IAnyInstance;
035import dev.metaschema.core.model.IBoundObject;
036import dev.metaschema.core.model.IResourceLocation;
037import dev.metaschema.core.model.SimpleResourceLocation;
038import dev.metaschema.core.model.util.XmlEventUtil;
039import dev.metaschema.core.qname.IEnhancedQName;
040import dev.metaschema.core.util.CollectionUtil;
041import dev.metaschema.core.util.ObjectUtils;
042import dev.metaschema.databind.io.BindingException;
043import dev.metaschema.databind.io.Format;
044import dev.metaschema.databind.io.PathTracker;
045import dev.metaschema.databind.io.ValidationContext;
046import dev.metaschema.databind.model.IBoundDefinitionModelAssembly;
047import dev.metaschema.databind.model.IBoundDefinitionModelComplex;
048import dev.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
049import dev.metaschema.databind.model.IBoundFieldValue;
050import dev.metaschema.databind.model.IBoundInstanceFlag;
051import dev.metaschema.databind.model.IBoundInstanceModel;
052import dev.metaschema.databind.model.IBoundInstanceModelAny;
053import dev.metaschema.databind.model.IBoundInstanceModelAssembly;
054import dev.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
055import dev.metaschema.databind.model.IBoundInstanceModelFieldComplex;
056import dev.metaschema.databind.model.IBoundInstanceModelFieldScalar;
057import dev.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
058import dev.metaschema.databind.model.IBoundInstanceModelGroupedField;
059import dev.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
060import dev.metaschema.databind.model.info.AbstractModelInstanceReadHandler;
061import dev.metaschema.databind.model.info.IFeatureScalarItemValueHandler;
062import dev.metaschema.databind.model.info.IItemReadHandler;
063import dev.metaschema.databind.model.info.IModelInstanceCollectionInfo;
064import edu.umd.cs.findbugs.annotations.NonNull;
065import edu.umd.cs.findbugs.annotations.Nullable;
066
067/**
068 * Supports reading XML-based Metaschema module instances.
069 */
070@SuppressWarnings("PMD.CouplingBetweenObjects")
071public class MetaschemaXmlReader
072    implements IXmlParsingContext {
073  private static final Logger LOGGER = LogManager.getLogger(MetaschemaXmlReader.class);
074  @NonNull
075  private final XMLEventReader2 reader;
076  @NonNull
077  private final URI source;
078  @NonNull
079  private final IXmlProblemHandler problemHandler;
080  /**
081   * Tracks the current parsing path for context-aware error reporting.
082   */
083  @NonNull
084  private final PathTracker pathTracker = new PathTracker();
085
086  /**
087   * Construct a new Module-aware XML parser using the default problem handler.
088   *
089   * @param reader
090   *          the XML reader to parse with
091   * @param source
092   *          the resource being parsed
093   * @see DefaultXmlProblemHandler
094   */
095  public MetaschemaXmlReader(
096      @NonNull XMLEventReader2 reader,
097      @NonNull URI source) {
098    this(reader, source, new DefaultXmlProblemHandler());
099  }
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}