View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.metaschema.databind.io.xml;
28  
29  import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
30  import gov.nist.secauto.metaschema.core.model.util.XmlEventUtil;
31  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
32  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
33  import gov.nist.secauto.metaschema.databind.io.BindingException;
34  import gov.nist.secauto.metaschema.databind.model.IAssemblyClassBinding;
35  import gov.nist.secauto.metaschema.databind.model.IBoundAssemblyInstance;
36  import gov.nist.secauto.metaschema.databind.model.IBoundFieldInstance;
37  import gov.nist.secauto.metaschema.databind.model.IBoundFieldValueInstance;
38  import gov.nist.secauto.metaschema.databind.model.IBoundFlagInstance;
39  import gov.nist.secauto.metaschema.databind.model.IBoundNamedModelInstance;
40  import gov.nist.secauto.metaschema.databind.model.IClassBinding;
41  import gov.nist.secauto.metaschema.databind.model.IFieldClassBinding;
42  import gov.nist.secauto.metaschema.databind.model.info.IPropertyCollector;
43  
44  import org.codehaus.stax2.XMLEventReader2;
45  
46  import java.io.IOException;
47  import java.util.HashSet;
48  import java.util.Map;
49  import java.util.Set;
50  import java.util.function.Function;
51  import java.util.stream.Collectors;
52  
53  import javax.xml.namespace.QName;
54  import javax.xml.stream.XMLStreamConstants;
55  import javax.xml.stream.XMLStreamException;
56  import javax.xml.stream.events.Attribute;
57  import javax.xml.stream.events.StartElement;
58  import javax.xml.stream.events.XMLEvent;
59  
60  import edu.umd.cs.findbugs.annotations.NonNull;
61  import edu.umd.cs.findbugs.annotations.Nullable;
62  
63  public class MetaschemaXmlReader
64      implements IXmlParsingContext {
65    @NonNull
66    private final XMLEventReader2 reader;
67    @NonNull
68    private final IXmlProblemHandler problemHandler;
69  
70    /**
71     * Construct a new Module-aware XML parser using the default problem handler.
72     *
73     * @param reader
74     *          the XML reader to parse with
75     * @see DefaultXmlProblemHandler
76     */
77    public MetaschemaXmlReader(
78        @NonNull XMLEventReader2 reader) {
79      this(reader, new DefaultXmlProblemHandler());
80    }
81  
82    /**
83     * Construct a new Module-aware parser.
84     *
85     * @param reader
86     *          the XML reader to parse with
87     * @param problemHandler
88     *          the problem handler implementation to use
89     */
90    public MetaschemaXmlReader(
91        @NonNull XMLEventReader2 reader,
92        @NonNull IXmlProblemHandler problemHandler) {
93      this.reader = reader;
94      this.problemHandler = problemHandler;
95    }
96  
97    @Override
98    public XMLEventReader2 getReader() {
99      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 }