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