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}