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}