1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.databind.io.xml;
7   
8   import gov.nist.secauto.metaschema.core.model.IBoundObject;
9   import gov.nist.secauto.metaschema.core.model.IMetaschemaData;
10  import gov.nist.secauto.metaschema.core.model.util.XmlEventUtil;
11  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
12  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
13  import gov.nist.secauto.metaschema.databind.io.BindingException;
14  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
15  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
16  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
17  import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue;
18  import gov.nist.secauto.metaschema.databind.model.IBoundInstance;
19  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceFlag;
20  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModel;
21  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelAssembly;
22  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
23  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldComplex;
24  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldScalar;
25  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
26  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedField;
27  import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
28  import gov.nist.secauto.metaschema.databind.model.info.AbstractModelInstanceReadHandler;
29  import gov.nist.secauto.metaschema.databind.model.info.IFeatureScalarItemValueHandler;
30  import gov.nist.secauto.metaschema.databind.model.info.IItemReadHandler;
31  import gov.nist.secauto.metaschema.databind.model.info.IModelInstanceCollectionInfo;
32  
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.Logger;
35  import org.codehaus.stax2.XMLEventReader2;
36  
37  import java.io.IOException;
38  import java.util.Collection;
39  import java.util.HashSet;
40  import java.util.LinkedHashMap;
41  import java.util.LinkedList;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.Set;
45  import java.util.function.Function;
46  import java.util.stream.Collectors;
47  
48  import javax.xml.namespace.QName;
49  import javax.xml.stream.Location;
50  import javax.xml.stream.XMLStreamConstants;
51  import javax.xml.stream.XMLStreamException;
52  import javax.xml.stream.events.Attribute;
53  import javax.xml.stream.events.StartElement;
54  import javax.xml.stream.events.XMLEvent;
55  
56  import edu.umd.cs.findbugs.annotations.NonNull;
57  import edu.umd.cs.findbugs.annotations.Nullable;
58  
59  public class MetaschemaXmlReader
60      implements IXmlParsingContext {
61    private static final Logger LOGGER = LogManager.getLogger(MetaschemaXmlReader.class);
62    @NonNull
63    private final XMLEventReader2 reader;
64    @NonNull
65    private final IXmlProblemHandler problemHandler;
66  
67    /**
68     * Construct a new Module-aware XML parser using the default problem handler.
69     *
70     * @param reader
71     *          the XML reader to parse with
72     * @see DefaultXmlProblemHandler
73     */
74    public MetaschemaXmlReader(
75        @NonNull XMLEventReader2 reader) {
76      this(reader, new DefaultXmlProblemHandler());
77    }
78  
79    public <ITEM> ITEM readItem(
80        @NonNull IBoundObject item,
81        @NonNull IBoundInstance<ITEM> instance,
82        @NonNull StartElement start) throws IOException {
83      return instance.readItem(item, new ItemReadHandler(start));
84    }
85  
86    /**
87     * Construct a new Module-aware parser.
88     *
89     * @param reader
90     *          the XML reader to parse with
91     * @param problemHandler
92     *          the problem handler implementation to use
93     */
94    public MetaschemaXmlReader(
95        @NonNull XMLEventReader2 reader,
96        @NonNull IXmlProblemHandler problemHandler) {
97      this.reader = reader;
98      this.problemHandler = problemHandler;
99    }
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 }