001/* 002 * SPDX-FileCopyrightText: none 003 * SPDX-License-Identifier: CC0-1.0 004 */ 005 006package gov.nist.secauto.metaschema.databind.io.xml; 007 008import gov.nist.secauto.metaschema.core.model.IBoundObject; 009import gov.nist.secauto.metaschema.core.model.IMetaschemaData; 010import gov.nist.secauto.metaschema.core.model.util.XmlEventUtil; 011import gov.nist.secauto.metaschema.core.qname.IEnhancedQName; 012import gov.nist.secauto.metaschema.core.util.CollectionUtil; 013import gov.nist.secauto.metaschema.core.util.ObjectUtils; 014import gov.nist.secauto.metaschema.databind.io.BindingException; 015import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly; 016import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex; 017import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex; 018import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue; 019import gov.nist.secauto.metaschema.databind.model.IBoundInstanceFlag; 020import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModel; 021import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelAssembly; 022import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelChoiceGroup; 023import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldComplex; 024import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldScalar; 025import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedAssembly; 026import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedField; 027import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedNamed; 028import gov.nist.secauto.metaschema.databind.model.info.AbstractModelInstanceReadHandler; 029import gov.nist.secauto.metaschema.databind.model.info.IFeatureScalarItemValueHandler; 030import gov.nist.secauto.metaschema.databind.model.info.IItemReadHandler; 031import gov.nist.secauto.metaschema.databind.model.info.IModelInstanceCollectionInfo; 032 033import org.apache.logging.log4j.LogManager; 034import org.apache.logging.log4j.Logger; 035import org.codehaus.stax2.XMLEventReader2; 036 037import java.io.IOException; 038import java.net.URI; 039import java.util.Collection; 040import java.util.HashSet; 041import java.util.LinkedHashMap; 042import java.util.LinkedList; 043import java.util.List; 044import java.util.Map; 045import java.util.Set; 046import java.util.function.Function; 047import java.util.stream.Collectors; 048 049import javax.xml.namespace.QName; 050import javax.xml.stream.Location; 051import javax.xml.stream.XMLStreamConstants; 052import javax.xml.stream.XMLStreamException; 053import javax.xml.stream.events.Attribute; 054import javax.xml.stream.events.StartElement; 055import javax.xml.stream.events.XMLEvent; 056 057import edu.umd.cs.findbugs.annotations.NonNull; 058import edu.umd.cs.findbugs.annotations.Nullable; 059 060/** 061 * Supports reading XML-based Metaschema module instances. 062 */ 063@SuppressWarnings("PMD.CouplingBetweenObjects") 064public class MetaschemaXmlReader 065 implements IXmlParsingContext { 066 private static final Logger LOGGER = LogManager.getLogger(MetaschemaXmlReader.class); 067 @NonNull 068 private final XMLEventReader2 reader; 069 @NonNull 070 private final URI source; 071 @NonNull 072 private final IXmlProblemHandler problemHandler; 073 074 /** 075 * Construct a new Module-aware XML parser using the default problem handler. 076 * 077 * @param reader 078 * the XML reader to parse with 079 * @param source 080 * the resource being parsed 081 * @see DefaultXmlProblemHandler 082 */ 083 public MetaschemaXmlReader( 084 @NonNull XMLEventReader2 reader, 085 @NonNull URI source) { 086 this(reader, source, new DefaultXmlProblemHandler()); 087 } 088 089 /** 090 * Construct a new Module-aware parser. 091 * 092 * @param reader 093 * the XML reader to parse with 094 * @param source 095 * the resource being parsed 096 * @param problemHandler 097 * the problem handler implementation to use 098 */ 099 public MetaschemaXmlReader( 100 @NonNull XMLEventReader2 reader, 101 @NonNull URI source, 102 @NonNull IXmlProblemHandler problemHandler) { 103 this.reader = reader; 104 this.source = source; 105 this.problemHandler = problemHandler; 106 } 107 108 @Override 109 public XMLEventReader2 getReader() { 110 return reader; 111 } 112 113 @Override 114 public URI getSource() { 115 return source; 116 } 117 118 @Override 119 public IXmlProblemHandler getProblemHandler() { 120 return problemHandler; 121 } 122 123 /** 124 * Parses XML into a bound object based on the provided {@code definition}. 125 * <p> 126 * Parses the {@link XMLStreamConstants#START_DOCUMENT}, any processing 127 * instructions, and the element. 128 * 129 * @param <CLASS> 130 * the returned object type 131 * @param definition 132 * the definition describing the element data to read 133 * @return the parsed object 134 * @throws IOException 135 * if an error occurred while parsing the input 136 */ 137 @Override 138 @NonNull 139 public <CLASS> CLASS read(@NonNull IBoundDefinitionModelComplex definition) throws IOException { 140 URI resource = getSource(); 141 try { 142 // we may be at the START_DOCUMENT 143 if (reader.peek().isStartDocument()) { 144 XmlEventUtil.consumeAndAssert(reader, resource, XMLStreamConstants.START_DOCUMENT); 145 } 146 147 // advance past any other info to get to next start element 148 XmlEventUtil.skipEvents(reader, XMLStreamConstants.CHARACTERS, XMLStreamConstants.PROCESSING_INSTRUCTION, 149 XMLStreamConstants.DTD); 150 151 XMLEvent event = ObjectUtils.requireNonNull(reader.peek()); 152 if (!event.isStartElement()) { 153 throw new IOException( 154 String.format("The token '%s' is not an XML element%s.", 155 XmlEventUtil.toEventName(event), 156 XmlEventUtil.generateLocationMessage(event, resource))); 157 } 158 159 ItemReadHandler handler = new ItemReadHandler(ObjectUtils.notNull(event.asStartElement())); 160 Object value = definition.readItem(null, handler); 161 if (value == null) { 162 event = reader.peek(); 163 throw new IOException(String.format("Unable to read data.%s", 164 event == null ? "" : XmlEventUtil.generateLocationMessage(event, resource))); 165 } 166 167 return ObjectUtils.asType(value); 168 } catch (XMLStreamException ex) { 169 throw new IOException(ex); 170 } 171 } 172 173 /** 174 * Read the XML attribute data described by the {@code targetDefinition} and 175 * apply it to the provided {@code targetObject}. 176 * 177 * @param targetDefinition 178 * the Module definition that describes the syntax of the data to read 179 * @param targetObject 180 * the Java object that data parsed by this method will be stored in 181 * @param start 182 * the containing XML element that was previously parsed 183 * @throws IOException 184 * if an error occurred while parsing the input 185 * @throws XMLStreamException 186 * if an error occurred while parsing XML events 187 */ 188 protected void readFlagInstances( 189 @NonNull IBoundDefinitionModelComplex targetDefinition, 190 @NonNull IBoundObject targetObject, 191 @NonNull StartElement start) throws IOException, XMLStreamException { 192 URI resource = getSource(); 193 194 Map<IEnhancedQName, IBoundInstanceFlag> flagInstanceMap = targetDefinition.getFlagInstances().stream() 195 .collect(Collectors.toMap( 196 IBoundInstanceFlag::getQName, 197 Function.identity())); 198 199 for (Attribute attribute : CollectionUtil.toIterable(ObjectUtils.notNull(start.getAttributes()))) { 200 IEnhancedQName qname = IEnhancedQName.of(attribute.getName()); 201 IBoundInstanceFlag instance = flagInstanceMap.get(qname); 202 if (instance == null) { 203 // unrecognized flag 204 if (!getProblemHandler().handleUnknownAttribute(targetDefinition, targetObject, attribute, this)) { 205 throw new IOException( 206 String.format("Unrecognized attribute '%s'%s.", 207 qname, 208 XmlEventUtil.generateLocationMessage(attribute, resource))); 209 } 210 } else { 211 try { 212 // get the attribute value 213 Object value = instance.getDefinition().getJavaTypeAdapter().parse(ObjectUtils.notNull(attribute.getValue())); 214 // apply the value to the parentObject 215 instance.setValue(targetObject, value); 216 flagInstanceMap.remove(qname); 217 } catch (IllegalArgumentException ex) { 218 throw new IOException( 219 String.format("Malformed data '%s'%s. %s", 220 attribute.getValue(), 221 XmlEventUtil.generateLocationMessage(start, resource), 222 ex.getLocalizedMessage()), 223 ex); 224 } 225 } 226 } 227 228 if (!flagInstanceMap.isEmpty()) { 229 getProblemHandler().handleMissingFlagInstances( 230 targetDefinition, 231 targetObject, 232 ObjectUtils.notNull(flagInstanceMap.values())); 233 } 234 } 235 236 /** 237 * Read the XML element data described by the {@code targetDefinition} and apply 238 * it to the provided {@code targetObject}. 239 * 240 * @param targetDefinition 241 * the Module definition that describes the syntax of the data to read 242 * @param targetObject 243 * the Java object that data parsed by this method will be stored in 244 * @throws IOException 245 * if an error occurred while parsing the input 246 */ 247 protected void readModelInstances( 248 @NonNull IBoundDefinitionModelAssembly targetDefinition, 249 @NonNull IBoundObject targetObject) 250 throws IOException { 251 Collection<? extends IBoundInstanceModel<?>> instances = targetDefinition.getModelInstances(); 252 Set<IBoundInstanceModel<?>> unhandledProperties = new HashSet<>(); 253 for (IBoundInstanceModel<?> modelInstance : instances) { 254 assert modelInstance != null; 255 if (!readItems(modelInstance, targetObject, true)) { 256 unhandledProperties.add(modelInstance); 257 } 258 } 259 260 // process all properties that did not get a value 261 getProblemHandler().handleMissingModelInstances(targetDefinition, targetObject, unhandledProperties); 262 263 XMLEventReader2 reader = getReader(); 264 URI resource = getSource(); 265 266 // handle any 267 try { 268 if (!getReader().peek().isEndElement()) { 269 // handle any 270 XmlEventUtil.skipWhitespace(reader); 271 XmlEventUtil.skipElement(reader); 272 XmlEventUtil.skipWhitespace(reader); 273 } 274 275 XmlEventUtil.assertNext(reader, resource, XMLStreamConstants.END_ELEMENT); 276 } catch (XMLStreamException ex) { 277 throw new IOException(ex); 278 } 279 } 280 281 /** 282 * Determine if the next data to read corresponds to the next model instance. 283 * 284 * @param targetInstance 285 * the model instance that describes the syntax of the data to read 286 * @return {@code true} if the Module instance needs to be parsed, or 287 * {@code false} otherwise 288 * @throws XMLStreamException 289 * if an error occurred while parsing XML events 290 */ 291 @SuppressWarnings("PMD.OnlyOneReturn") 292 protected boolean isNextInstance( 293 @NonNull IBoundInstanceModel<?> targetInstance) 294 throws XMLStreamException { 295 296 XmlEventUtil.skipWhitespace(reader); 297 298 XMLEvent nextEvent = reader.peek(); 299 300 boolean retval = nextEvent.isStartElement(); 301 if (retval) { 302 IEnhancedQName qname = IEnhancedQName.of(ObjectUtils.notNull(nextEvent.asStartElement().getName())); 303 retval = qname.equals(targetInstance.getEffectiveXmlGroupAsQName()) // parse the grouping element 304 || targetInstance.canHandleXmlQName(qname); // parse the instance(s) 305 } 306 return retval; 307 } 308 309 /** 310 * Read the data associated with the {@code instance} and apply it to the 311 * provided {@code parentObject}. 312 * 313 * @param instance 314 * the instance to parse data for 315 * @param parentObject 316 * the Java object that data parsed by this method will be stored in 317 * @return {@code true} if the instance was parsed, or {@code false} if the data 318 * did not contain information for this instance 319 * @throws IOException 320 * if an error occurred while parsing the input 321 */ 322 @Override 323 public <T> boolean readItems( 324 @NonNull IBoundInstanceModel<T> instance, 325 @NonNull IBoundObject parentObject, 326 boolean parseGrouping) 327 throws IOException { 328 try { 329 boolean handled = isNextInstance(instance); 330 if (handled) { 331 XMLEventReader2 reader = getReader(); 332 URI resource = getSource(); 333 334 // XmlEventUtil.skipWhitespace(reader); 335 336 IEnhancedQName groupEQName = parseGrouping ? instance.getEffectiveXmlGroupAsQName() : null; 337 QName groupQName = groupEQName == null ? null : groupEQName.toQName(); 338 if (groupQName != null) { 339 // we need to parse the grouping element, if the next token matches 340 XmlEventUtil.requireStartElement(reader, resource, groupQName); 341 } 342 343 IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo(); 344 345 ModelInstanceReadHandler<T> handler = new ModelInstanceReadHandler<>(instance, parentObject); 346 347 // let the property info decide how to parse the value 348 Object value = collectionInfo.readItems(handler); 349 if (value != null) { 350 instance.setValue(parentObject, value); 351 } 352 353 // consume extra whitespace between elements 354 XmlEventUtil.skipWhitespace(reader); 355 356 if (groupQName != null) { 357 // consume the end of the group 358 XmlEventUtil.requireEndElement(reader, resource, groupQName); 359 } 360 } 361 return handled; 362 } catch (XMLStreamException ex) { 363 throw new IOException(ex); 364 } 365 } 366 367 private final class ModelInstanceReadHandler<ITEM> 368 extends AbstractModelInstanceReadHandler<ITEM> { 369 370 private ModelInstanceReadHandler( 371 @NonNull IBoundInstanceModel<ITEM> instance, 372 @NonNull IBoundObject parentObject) { 373 super(instance, parentObject); 374 } 375 376 @Override 377 public List<ITEM> readList() throws IOException { 378 return ObjectUtils.notNull(readCollection()); 379 } 380 381 @Override 382 public Map<String, ITEM> readMap() throws IOException { 383 IBoundInstanceModel<?> instance = getCollectionInfo().getInstance(); 384 385 return ObjectUtils.notNull(readCollection().stream() 386 .collect(Collectors.toMap( 387 item -> { 388 assert item != null; 389 390 IBoundInstanceFlag jsonKey = instance.getItemJsonKey(item); 391 assert jsonKey != null; 392 return ObjectUtils.requireNonNull(jsonKey.getValue(item)).toString(); 393 }, 394 Function.identity(), 395 (t, u) -> u, 396 LinkedHashMap::new))); 397 } 398 399 @NonNull 400 private List<ITEM> readCollection() throws IOException { 401 List<ITEM> retval = new LinkedList<>(); 402 XMLEventReader2 reader = getReader(); 403 try { 404 405 // consume extra whitespace between elements 406 XmlEventUtil.skipWhitespace(reader); 407 408 IBoundInstanceModel<?> instance = getCollectionInfo().getInstance(); 409 XMLEvent event; 410 while ((event = reader.peek()).isStartElement() 411 && instance.canHandleXmlQName( 412 IEnhancedQName.of(ObjectUtils.notNull(event.asStartElement().getName())))) { 413 414 // Consume the start element 415 ITEM value = readItem(); 416 retval.add(value); 417 418 // consume extra whitespace between elements 419 XmlEventUtil.skipWhitespace(reader); 420 } 421 } catch (XMLStreamException ex) { 422 throw new IOException(ex); 423 } 424 return retval; 425 } 426 427 @Override 428 public ITEM readItem() throws IOException { 429 try { 430 return getCollectionInfo().getInstance().readItem( 431 getParentObject(), 432 new ItemReadHandler(ObjectUtils.notNull(getReader().peek().asStartElement()))); 433 } catch (XMLStreamException ex) { 434 throw new IOException(ex); 435 } 436 } 437 } 438 439 private final class ItemReadHandler implements IItemReadHandler { 440 @NonNull 441 private final StartElement startElement; 442 443 private ItemReadHandler(@NonNull StartElement startElement) { 444 this.startElement = startElement; 445 } 446 447 /** 448 * Get the current start element. 449 * 450 * @return the startElement 451 */ 452 @NonNull 453 private StartElement getStartElement() { 454 return startElement; 455 } 456 457 @NonNull 458 private <DEF extends IBoundDefinitionModelComplex> IBoundObject readDefinitionElement( 459 @NonNull DEF definition, 460 @NonNull StartElement start, 461 @NonNull IEnhancedQName expectedEQName, 462 @Nullable IBoundObject parent, 463 @NonNull DefinitionBodyHandler<DEF, IBoundObject> bodyHandler) throws IOException { 464 XMLEventReader2 reader = getReader(); 465 URI resource = getSource(); 466 QName expectedQName = expectedEQName.toQName(); 467 468 try { 469 // consume the start element 470 XmlEventUtil.requireStartElement(reader, resource, expectedQName); 471 472 Location location = start.getLocation(); 473 474 // construct the item 475 IBoundObject item = definition.newInstance(location == null ? null : () -> new MetaschemaData(location)); 476 477 // call pre-parse initialization hook 478 definition.callBeforeDeserialize(item, parent); 479 480 // read the flags 481 readFlagInstances(definition, item, start); 482 483 // read the body 484 bodyHandler.accept(definition, item); 485 486 XmlEventUtil.skipWhitespace(reader); 487 488 // call post-parse initialization hook 489 definition.callAfterDeserialize(item, parent); 490 491 // consume the end element 492 XmlEventUtil.requireEndElement(reader, resource, expectedQName); 493 return ObjectUtils.asType(item); 494 } catch (BindingException | XMLStreamException ex) { 495 throw new IOException(ex); 496 } 497 } 498 499 @Override 500 public Object readItemFlag( 501 IBoundObject parent, 502 IBoundInstanceFlag flag) throws IOException { 503 // should never be called 504 throw new UnsupportedOperationException("should be handled by readFlagInstances()"); 505 } 506 507 private void handleFieldDefinitionBody( 508 @NonNull IBoundDefinitionModelFieldComplex definition, 509 @NonNull IBoundObject item) throws IOException { 510 IBoundFieldValue fieldValue = definition.getFieldValue(); 511 512 // parse the value 513 Object value = fieldValue.readItem(item, this); 514 if (value != null) { 515 fieldValue.setValue(item, value); 516 } 517 } 518 519 @Override 520 public Object readItemField( 521 IBoundObject parent, 522 IBoundInstanceModelFieldScalar instance) 523 throws IOException { 524 XMLEventReader2 reader = getReader(); 525 URI resource = getSource(); 526 try { 527 QName wrapper = null; 528 if (instance.isEffectiveValueWrappedInXml()) { 529 wrapper = instance.getQName().toQName(); 530 531 XmlEventUtil.skipWhitespace(reader); 532 XmlEventUtil.requireStartElement(reader, resource, wrapper); 533 } 534 535 Object retval = readScalarItem(instance); 536 537 if (wrapper != null) { 538 XmlEventUtil.skipWhitespace(reader); 539 540 XmlEventUtil.requireEndElement(reader, resource, wrapper); 541 } 542 return retval; 543 } catch (XMLStreamException ex) { 544 throw new IOException(ex); 545 } 546 } 547 548 @Override 549 public IBoundObject readItemField( 550 IBoundObject parent, 551 IBoundInstanceModelFieldComplex instance) 552 throws IOException { 553 return readDefinitionElement( 554 instance.getDefinition(), 555 getStartElement(), 556 instance.getQName(), 557 parent, 558 this::handleFieldDefinitionBody); 559 } 560 561 @Override 562 public IBoundObject readItemField(IBoundObject parent, IBoundInstanceModelGroupedField instance) 563 throws IOException { 564 return readDefinitionElement( 565 instance.getDefinition(), 566 getStartElement(), 567 instance.getQName(), 568 parent, 569 this::handleFieldDefinitionBody); 570 } 571 572 @Override 573 public IBoundObject readItemField( 574 IBoundObject parent, 575 IBoundDefinitionModelFieldComplex definition) throws IOException { 576 return readDefinitionElement( 577 definition, 578 getStartElement(), 579 definition.getQName(), 580 parent, 581 this::handleFieldDefinitionBody); 582 } 583 584 @Override 585 public Object readItemFieldValue( 586 IBoundObject parent, 587 IBoundFieldValue fieldValue) throws IOException { 588 return checkMissingFieldValue(readScalarItem(fieldValue)); 589 } 590 591 @Nullable 592 private Object checkMissingFieldValue(Object value) throws IOException { 593 if (value == null && LOGGER.isWarnEnabled()) { 594 StartElement start = getStartElement(); 595 LOGGER.atWarn().log("Missing property value{}", 596 XmlEventUtil.generateLocationMessage(start, getSource())); 597 } 598 return value; 599 } 600 601 private void handleAssemblyDefinitionBody( 602 @NonNull IBoundDefinitionModelAssembly definition, 603 @NonNull IBoundObject item) throws IOException { 604 readModelInstances(definition, item); 605 } 606 607 @Override 608 public IBoundObject readItemAssembly( 609 IBoundObject parent, 610 IBoundInstanceModelAssembly instance) throws IOException { 611 return readDefinitionElement( 612 instance.getDefinition(), 613 getStartElement(), 614 instance.getQName(), 615 parent, 616 this::handleAssemblyDefinitionBody); 617 } 618 619 @Override 620 public IBoundObject readItemAssembly(IBoundObject parent, IBoundInstanceModelGroupedAssembly instance) 621 throws IOException { 622 return readDefinitionElement( 623 instance.getDefinition(), 624 getStartElement(), 625 instance.getQName(), 626 parent, 627 this::handleAssemblyDefinitionBody); 628 } 629 630 @Override 631 public IBoundObject readItemAssembly( 632 IBoundObject parent, 633 IBoundDefinitionModelAssembly definition) throws IOException { 634 return readDefinitionElement( 635 definition, 636 getStartElement(), 637 ObjectUtils.requireNonNull(definition.getRootQName()), 638 parent, 639 this::handleAssemblyDefinitionBody); 640 } 641 642 @Nullable 643 private Object readScalarItem(@NonNull IFeatureScalarItemValueHandler handler) 644 throws IOException { 645 return handler.getJavaTypeAdapter().parse(getReader(), getSource()); 646 } 647 648 @Override 649 public IBoundObject readChoiceGroupItem(IBoundObject parent, IBoundInstanceModelChoiceGroup instance) 650 throws IOException { 651 try { 652 XMLEventReader2 eventReader = getReader(); 653 // consume extra whitespace between elements 654 XmlEventUtil.skipWhitespace(eventReader); 655 656 XMLEvent event = eventReader.peek(); 657 IEnhancedQName nextQName = IEnhancedQName.of(ObjectUtils.notNull(event.asStartElement().getName())); 658 IBoundInstanceModelGroupedNamed actualInstance = instance.getGroupedModelInstance(nextQName); 659 assert actualInstance != null; 660 return actualInstance.readItem(parent, this); 661 } catch (XMLStreamException ex) { 662 throw new IOException(ex); 663 } 664 } 665 } 666 667 private static class MetaschemaData implements IMetaschemaData { 668 private final int line; 669 private final int column; 670 private final long charOffset; 671 672 public MetaschemaData(@NonNull Location location) { 673 this.line = location.getLineNumber(); 674 this.column = location.getColumnNumber(); 675 this.charOffset = location.getCharacterOffset(); 676 } 677 678 @Override 679 public int getLine() { 680 return line; 681 } 682 683 @Override 684 public int getColumn() { 685 return column; 686 } 687 688 @Override 689 public long getCharOffset() { 690 return charOffset; 691 } 692 693 @Override 694 public long getByteOffset() { 695 return -1; 696 } 697 } 698 699 @FunctionalInterface 700 private interface DefinitionBodyHandler<DEF extends IBoundDefinitionModelComplex, ITEM> { 701 void accept( 702 @NonNull DEF definition, 703 @NonNull ITEM item) throws IOException; 704 } 705 706}