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