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.codehaus.stax2.XMLEventReader2;
34  
35  import java.io.IOException;
36  import java.util.Collection;
37  import java.util.HashSet;
38  import java.util.LinkedHashMap;
39  import java.util.LinkedList;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.Set;
43  import java.util.function.Function;
44  import java.util.stream.Collectors;
45  
46  import javax.xml.namespace.QName;
47  import javax.xml.stream.Location;
48  import javax.xml.stream.XMLStreamConstants;
49  import javax.xml.stream.XMLStreamException;
50  import javax.xml.stream.events.Attribute;
51  import javax.xml.stream.events.StartElement;
52  import javax.xml.stream.events.XMLEvent;
53  
54  import edu.umd.cs.findbugs.annotations.NonNull;
55  import edu.umd.cs.findbugs.annotations.Nullable;
56  
57  public class MetaschemaXmlReader
58      implements IXmlParsingContext {
59    @NonNull
60    private final XMLEventReader2 reader;
61    @NonNull
62    private final IXmlProblemHandler problemHandler;
63  
64    /**
65     * Construct a new Module-aware XML parser using the default problem handler.
66     *
67     * @param reader
68     *          the XML reader to parse with
69     * @see DefaultXmlProblemHandler
70     */
71    public MetaschemaXmlReader(
72        @NonNull XMLEventReader2 reader) {
73      this(reader, new DefaultXmlProblemHandler());
74    }
75  
76    public <ITEM> ITEM readItem(
77        @NonNull IBoundObject item,
78        @NonNull IBoundInstance<ITEM> instance,
79        @NonNull StartElement start) throws IOException {
80      return instance.readItem(item, new ItemReadHandler(start));
81    }
82  
83    /**
84     * Construct a new Module-aware parser.
85     *
86     * @param reader
87     *          the XML reader to parse with
88     * @param problemHandler
89     *          the problem handler implementation to use
90     */
91    public MetaschemaXmlReader(
92        @NonNull XMLEventReader2 reader,
93        @NonNull IXmlProblemHandler problemHandler) {
94      this.reader = reader;
95      this.problemHandler = problemHandler;
96    }
97  
98    @Override
99    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 }