001/* 002 * Portions of this software was developed by employees of the National Institute 003 * of Standards and Technology (NIST), an agency of the Federal Government and is 004 * being made available as a public service. Pursuant to title 17 United States 005 * Code Section 105, works of NIST employees are not subject to copyright 006 * protection in the United States. This software may be subject to foreign 007 * copyright. Permission in the United States and in foreign countries, to the 008 * extent that NIST may hold copyright, to use, copy, modify, create derivative 009 * works, and distribute this software and its documentation without fee is hereby 010 * granted on a non-exclusive basis, provided that this notice and disclaimer 011 * of warranty appears in all copies. 012 * 013 * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER 014 * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY 015 * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF 016 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM 017 * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE 018 * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT 019 * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT, 020 * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM, 021 * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY, 022 * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR 023 * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT 024 * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER. 025 */ 026 027package gov.nist.secauto.metaschema.databind.io.xml; 028 029import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter; 030import gov.nist.secauto.metaschema.core.model.util.XmlEventUtil; 031import gov.nist.secauto.metaschema.core.util.CollectionUtil; 032import gov.nist.secauto.metaschema.core.util.ObjectUtils; 033import gov.nist.secauto.metaschema.databind.io.BindingException; 034import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding; 035import gov.nist.secauto.metaschema.databind.model.IBoundAssemblyInstance; 036import gov.nist.secauto.metaschema.databind.model.IBoundFieldInstance; 037import gov.nist.secauto.metaschema.databind.model.IBoundFieldValueInstance; 038import gov.nist.secauto.metaschema.databind.model.IBoundFlagInstance; 039import gov.nist.secauto.metaschema.databind.model.IBoundNamedModelInstance; 040import gov.nist.secauto.metaschema.databind.model.IClassBinding; 041import gov.nist.secauto.metaschema.databind.model.IFieldClassBinding; 042import gov.nist.secauto.metaschema.databind.model.info.IPropertyCollector; 043 044import org.codehaus.stax2.XMLEventReader2; 045 046import java.io.IOException; 047import java.util.HashSet; 048import java.util.Map; 049import java.util.Set; 050import java.util.function.Function; 051import java.util.stream.Collectors; 052 053import javax.xml.namespace.QName; 054import javax.xml.stream.XMLStreamConstants; 055import javax.xml.stream.XMLStreamException; 056import javax.xml.stream.events.Attribute; 057import javax.xml.stream.events.StartElement; 058import javax.xml.stream.events.XMLEvent; 059 060import edu.umd.cs.findbugs.annotations.NonNull; 061import edu.umd.cs.findbugs.annotations.Nullable; 062 063public class MetaschemaXmlReader 064 implements IXmlParsingContext { 065 @NonNull 066 private final XMLEventReader2 reader; 067 @NonNull 068 private final IXmlProblemHandler problemHandler; 069 070 /** 071 * Construct a new Module-aware XML parser using the default problem handler. 072 * 073 * @param reader 074 * the XML reader to parse with 075 * @see DefaultXmlProblemHandler 076 */ 077 public MetaschemaXmlReader( 078 @NonNull XMLEventReader2 reader) { 079 this(reader, new DefaultXmlProblemHandler()); 080 } 081 082 /** 083 * Construct a new Module-aware parser. 084 * 085 * @param reader 086 * the XML reader to parse with 087 * @param problemHandler 088 * the problem handler implementation to use 089 */ 090 public MetaschemaXmlReader( 091 @NonNull XMLEventReader2 reader, 092 @NonNull IXmlProblemHandler problemHandler) { 093 this.reader = reader; 094 this.problemHandler = problemHandler; 095 } 096 097 @Override 098 public XMLEventReader2 getReader() { 099 return reader; 100 } 101 102 @Override 103 public IXmlProblemHandler getProblemHandler() { 104 return problemHandler; 105 } 106 107 /** 108 * Parses XML into a bound object based on the provided {@code definition}. 109 * <p> 110 * Parses the {@link XMLStreamConstants#START_DOCUMENT}, the root element, and 111 * the {@link XMLStreamConstants#END_DOCUMENT}. 112 * 113 * @param <CLASS> 114 * the returned object type 115 * @param targetDefinition 116 * the definition describing the root element data to read 117 * @return the parsed object 118 * @throws XMLStreamException 119 * if an error occurred while parsing XML events 120 * @throws IOException 121 * if an error occurred while parsing the input 122 */ 123 @NonNull 124 public <CLASS> CLASS read(@NonNull IAssemblyClassBinding targetDefinition) throws IOException, XMLStreamException { 125 126 // we may be at the START_DOCUMENT 127 if (reader.peek().isStartDocument()) { 128 XmlEventUtil.consumeAndAssert(reader, XMLStreamConstants.START_DOCUMENT); 129 } 130 131 XmlEventUtil.skipEvents(reader, XMLStreamConstants.CHARACTERS, XMLStreamConstants.PROCESSING_INSTRUCTION); 132 133 QName rootQName = targetDefinition.getRootXmlQName(); 134 XMLEvent event = XmlEventUtil.consumeAndAssert(reader, XMLStreamConstants.START_ELEMENT, rootQName); 135 136 StartElement start = ObjectUtils.notNull(event.asStartElement()); 137 138 CLASS retval = readDefinitionValue(targetDefinition, null, start); 139 140 XmlEventUtil.consumeAndAssert(reader, XMLStreamConstants.END_ELEMENT, rootQName); 141 142 // if (reader.hasNext() && LOGGER.isDebugEnabled()) { 143 // LOGGER.debug("After Parse: {}", XmlEventUtil.toString(reader.peek())); 144 // } 145 146 return retval; 147 } 148 149 @SuppressWarnings("PMD.CyclomaticComplexity") 150 @Override 151 public <T> T readDefinitionValue( 152 IClassBinding targetDefinition, 153 Object parentObject, 154 StartElement start) throws IOException, XMLStreamException { 155 156 Object targetObject; 157 try { 158 targetObject = targetDefinition.newInstance(); 159 targetDefinition.callBeforeDeserialize(targetObject, parentObject); 160 } catch (BindingException ex) { 161 throw new IOException(ex); 162 } 163 164 readFlagInstances(targetDefinition, targetObject, start); 165 166 if (targetDefinition instanceof IAssemblyClassBinding) { 167 readModelInstances((IAssemblyClassBinding) targetDefinition, targetObject, start); 168 } else if (targetDefinition instanceof IFieldClassBinding) { 169 readFieldValue((IFieldClassBinding) targetDefinition, targetObject); 170 } else { 171 throw new UnsupportedOperationException( 172 String.format("Unsupported class binding type: %s", targetDefinition.getClass().getName())); 173 } 174 175 XmlEventUtil.skipWhitespace(reader); 176 177 XMLEvent nextEvent = ObjectUtils.notNull(reader.peek()); 178 if (!XmlEventUtil.isEventEndElement(nextEvent, ObjectUtils.notNull(start.getName()))) { 179 throw new IOException( 180 String.format("Unrecognized element '%s'%s.", 181 XmlEventUtil.toEventName(nextEvent), 182 XmlEventUtil.generateLocationMessage(nextEvent))); 183 } 184 185 try { 186 targetDefinition.callAfterDeserialize(targetObject, parentObject); 187 } catch (BindingException ex) { 188 throw new IOException(ex); 189 } 190 return ObjectUtils.asType(targetObject); 191 } 192 193 /** 194 * Read the XML attribute data described by the {@code targetDefinition} and 195 * apply it to the provided {@code targetObject}. 196 * 197 * @param targetDefinition 198 * the Module definition that describes the syntax of the data to read 199 * @param targetObject 200 * the Java object that data parsed by this method will be stored in 201 * @param start 202 * the containing XML element that was previously parsed 203 * @throws IOException 204 * if an error occurred while parsing the input 205 * @throws XMLStreamException 206 * if an error occurred while parsing XML events 207 */ 208 protected void readFlagInstances( 209 @NonNull IClassBinding targetDefinition, 210 @NonNull Object targetObject, 211 @NonNull StartElement start) throws IOException, XMLStreamException { 212 213 Map<QName, IBoundFlagInstance> flagInstanceMap = targetDefinition.getFlagInstances().stream() 214 .collect(Collectors.toMap(IBoundFlagInstance::getXmlQName, Function.identity())); 215 216 for (Attribute attribute : CollectionUtil.toIterable(ObjectUtils.notNull(start.getAttributes()))) { 217 QName qname = attribute.getName(); 218 IBoundFlagInstance instance = flagInstanceMap.get(qname); 219 if (instance == null) { 220 // unrecognized flag 221 if (!getProblemHandler().handleUnknownAttribute(targetDefinition, targetObject, attribute, this)) { 222 throw new IOException( 223 String.format("Unrecognized attribute '%s'%s.", 224 qname, 225 XmlEventUtil.generateLocationMessage(attribute))); 226 } 227 } else { 228 // get the attribute value 229 Object value = instance.getDefinition().getJavaTypeAdapter().parse(ObjectUtils.notNull(attribute.getValue())); 230 // apply the value to the parentObject 231 instance.setValue(targetObject, value); 232 flagInstanceMap.remove(qname); 233 } 234 } 235 236 if (!flagInstanceMap.isEmpty()) { 237 getProblemHandler().handleMissingFlagInstances( 238 targetDefinition, 239 targetObject, 240 ObjectUtils.notNull(flagInstanceMap.values())); 241 } 242 } 243 244 /** 245 * Read the XML element data described by the {@code targetDefinition} and apply 246 * it to the provided {@code targetObject}. 247 * 248 * @param targetDefinition 249 * the Module definition that describes the syntax of the data to read 250 * @param targetObject 251 * the Java object that data parsed by this method will be stored in 252 * @param start 253 * the XML element start and attribute data previously parsed 254 * @throws IOException 255 * if an error occurred while parsing the input 256 * @throws XMLStreamException 257 * if an error occurred while parsing XML events 258 */ 259 protected void readModelInstances( 260 @NonNull IAssemblyClassBinding targetDefinition, 261 @NonNull Object targetObject, 262 @NonNull StartElement start) 263 throws IOException, XMLStreamException { 264 Set<IBoundNamedModelInstance> unhandledProperties = new HashSet<>(); 265 for (IBoundNamedModelInstance modelProperty : targetDefinition.getModelInstances()) { 266 assert modelProperty != null; 267 if (!readModelInstanceValues(modelProperty, targetObject, start)) { 268 unhandledProperties.add(modelProperty); 269 } 270 } 271 272 // process all properties that did not get a value 273 getProblemHandler().handleMissingModelInstances(targetDefinition, targetObject, unhandledProperties); 274 } 275 276 /** 277 * Read the XML element and text data described by the {@code targetDefinition} 278 * and apply it to the provided {@code targetObject}. 279 * 280 * @param targetDefinition 281 * the Module definition that describes the syntax of the data to read 282 * @param targetObject 283 * the Java object that data parsed by this method will be stored in 284 * @throws IOException 285 * if an error occurred while parsing the input 286 */ 287 protected void readFieldValue( 288 @NonNull IFieldClassBinding targetDefinition, 289 @NonNull Object targetObject) 290 throws IOException { 291 IBoundFieldValueInstance fieldValue = targetDefinition.getFieldValueInstance(); 292 293 // parse the value 294 Object value = fieldValue.getJavaTypeAdapter().parse(reader); 295 fieldValue.setValue(targetObject, value); 296 } 297 298 /** 299 * Determine if the next data to read corresponds to the next model instance. 300 * 301 * @param targetInstance 302 * the model instance that describes the syntax of the data to read 303 * @return {@code true} if the Module instance needs to be parsed, or 304 * {@code false} otherwise 305 * @throws XMLStreamException 306 * if an error occurred while parsing XML events 307 */ 308 @SuppressWarnings("PMD.OnlyOneReturn") 309 protected boolean isNextInstance( 310 @NonNull IBoundNamedModelInstance targetInstance) 311 throws XMLStreamException { 312 313 XmlEventUtil.skipWhitespace(reader); 314 315 XMLEvent nextEvent = reader.peek(); 316 if (!nextEvent.isStartElement()) { 317 return false; 318 } 319 320 QName nextQName = ObjectUtils.notNull(nextEvent.asStartElement().getName()); 321 322 if (nextQName.equals(targetInstance.getXmlGroupAsQName())) { 323 // we are to parse the grouping element 324 return true; 325 } 326 327 if (nextQName.equals(targetInstance.getXmlQName())) { 328 // we are to parse the element 329 return true; 330 } 331 332 if (targetInstance instanceof IBoundFieldInstance) { 333 IBoundFieldInstance fieldInstance = (IBoundFieldInstance) targetInstance; 334 IDataTypeAdapter<?> adapter = fieldInstance.getDefinition().getJavaTypeAdapter(); 335 // we are to parse the data type 336 return !fieldInstance.isInXmlWrapped() 337 && adapter.isUnrappedValueAllowedInXml() 338 && adapter.canHandleQName(nextQName); 339 } 340 return false; 341 } 342 343 /** 344 * Read the data associated with the {@code instance} and apply it to the 345 * provided {@code parentObject}. 346 * 347 * @param instance 348 * the instance to parse data for 349 * @param parentObject 350 * the Java object that data parsed by this method will be stored in 351 * @param start 352 * the XML element start and attribute data previously parsed 353 * @return {@code true} if the instance was parsed, or {@code false} if the data 354 * did not contain information for this instance 355 * @throws IOException 356 * if an error occurred while parsing the input 357 * @throws XMLStreamException 358 * if an error occurred while parsing XML events 359 */ 360 protected boolean readModelInstanceValues( 361 @NonNull IBoundNamedModelInstance instance, 362 @NonNull Object parentObject, 363 @NonNull StartElement start) 364 throws IOException, XMLStreamException { 365 boolean handled = isNextInstance(instance); 366 if (handled) { 367 XmlEventUtil.skipWhitespace(reader); 368 369 StartElement currentStart = start; 370 371 QName groupQName = instance.getXmlGroupAsQName(); 372 if (groupQName != null) { 373 // we are to parse the grouping element, if the next token matches 374 XMLEvent groupEvent = XmlEventUtil.consumeAndAssert(reader, XMLStreamConstants.START_ELEMENT, groupQName); 375 currentStart = ObjectUtils.notNull(groupEvent.asStartElement()); 376 } 377 378 IPropertyCollector collector = instance.getPropertyInfo().newPropertyCollector(); 379 // There are zero or more named values based on cardinality 380 instance.getPropertyInfo().readValues(collector, parentObject, currentStart, this); 381 382 Object value = collector.getValue(); 383 384 // consume extra whitespace between elements 385 XmlEventUtil.skipWhitespace(reader); 386 387 if (groupQName != null) { 388 // consume the end of the group 389 XmlEventUtil.consumeAndAssert(reader, XMLStreamConstants.END_ELEMENT, groupQName); 390 } 391 392 instance.setValue(parentObject, value); 393 } 394 return handled; 395 } 396 397 @Override 398 public <T> T readModelInstanceValue(IBoundNamedModelInstance instance, Object parentObject, StartElement start) 399 throws XMLStreamException, IOException { 400 Object retval; 401 if (instance instanceof IBoundAssemblyInstance) { 402 retval = readModelInstanceValue((IBoundAssemblyInstance) instance, parentObject, start); 403 } else if (instance instanceof IBoundFieldInstance) { 404 retval = readModelInstanceValue((IBoundFieldInstance) instance, parentObject, start); 405 } else { 406 throw new UnsupportedOperationException( 407 String.format("Unsupported instance type: %s", instance.getClass().getName())); 408 } 409 return ObjectUtils.asNullableType(retval); 410 } 411 412 /** 413 * Read the XML data associated with the {@code instance} and apply it to the 414 * provided {@code parentObject}. 415 * 416 * @param instance 417 * the instance to parse data for 418 * @param parentObject 419 * the Java object that data parsed by this method will be stored in 420 * @param start 421 * the XML element start and attribute data previously parsed 422 * @return the Java object read, or {@code null} if no data was read 423 * @throws IOException 424 * if an error occurred while parsing the input 425 * @throws XMLStreamException 426 * if an error occurred while parsing XML events 427 */ 428 @Nullable 429 protected Object readModelInstanceValue( 430 @NonNull IBoundAssemblyInstance instance, 431 @NonNull Object parentObject, 432 @NonNull StartElement start) throws XMLStreamException, IOException { 433 // consume extra whitespace between elements 434 XmlEventUtil.skipWhitespace(reader); 435 436 Object retval = null; 437 XMLEvent event = reader.peek(); 438 if (event.isStartElement()) { 439 StartElement nextStart = event.asStartElement(); 440 QName nextQName = nextStart.getName(); 441 if (instance.getXmlQName().equals(nextQName)) { 442 // Consume the start element 443 reader.nextEvent(); 444 445 // consume the value 446 retval = instance.getDataTypeHandler().readItem(parentObject, nextStart, this); 447 448 // consume the end element 449 XmlEventUtil.consumeAndAssert(reader, XMLStreamConstants.END_ELEMENT, nextQName); 450 } 451 } 452 return retval; 453 } 454 455 /** 456 * Revise 457 * <p> 458 * Reads an individual XML item from the XML stream. 459 * 460 * @param instance 461 * the instance to parse data for 462 * @param parentObject 463 * the Java object that data parsed by this method will be stored in 464 * @param start 465 * the XML element start and attribute data previously parsed 466 * @return the Java object read, or {@code null} if no data was read 467 * @throws IOException 468 * if an error occurred while parsing the input 469 * @throws XMLStreamException 470 * if an error occurred while parsing XML events 471 */ 472 @NonNull 473 protected Object readModelInstanceValue( 474 @NonNull IBoundFieldInstance instance, 475 @NonNull Object parentObject, 476 @NonNull StartElement start) throws XMLStreamException, IOException { 477 // figure out if we need to parse the wrapper or not 478 IDataTypeAdapter<?> adapter = instance.getDefinition().getJavaTypeAdapter(); 479 boolean parseWrapper = true; 480 if (!instance.isInXmlWrapped() && adapter.isUnrappedValueAllowedInXml()) { 481 parseWrapper = false; 482 } 483 484 StartElement currentStart = start; 485 if (parseWrapper) { 486 // TODO: not sure this is needed, since there is a peek just before this 487 // parse any whitespace before the element 488 XmlEventUtil.skipWhitespace(reader); 489 490 QName xmlQName = instance.getXmlQName(); 491 XMLEvent event = reader.peek(); 492 if (event.isStartElement() && xmlQName.equals(event.asStartElement().getName())) { 493 // Consume the start element 494 currentStart = ObjectUtils.notNull(reader.nextEvent().asStartElement()); 495 } else { 496 throw new IOException(String.format("Found '%s' instead of expected element '%s'%s.", 497 event.asStartElement().getName(), 498 xmlQName, 499 XmlEventUtil.generateLocationMessage(event))); 500 } 501 } 502 503 // consume the value 504 Object retval = instance.getDataTypeHandler().readItem(parentObject, currentStart, this); 505 506 if (parseWrapper) { 507 // consume the end element 508 XmlEventUtil.consumeAndAssert(reader, XMLStreamConstants.END_ELEMENT, currentStart.getName()); 509 } 510 511 return retval; 512 } 513}