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.qname.IEnhancedQName;
12  import gov.nist.secauto.metaschema.core.util.CollectionUtil;
13  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
14  import gov.nist.secauto.metaschema.databind.io.BindingException;
15  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
16  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
17  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
18  import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue;
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.net.URI;
39  import java.util.Collection;
40  import java.util.HashSet;
41  import java.util.LinkedHashMap;
42  import java.util.LinkedList;
43  import java.util.List;
44  import java.util.Map;
45  import java.util.Set;
46  import java.util.function.Function;
47  import java.util.stream.Collectors;
48  
49  import javax.xml.namespace.QName;
50  import javax.xml.stream.Location;
51  import javax.xml.stream.XMLStreamConstants;
52  import javax.xml.stream.XMLStreamException;
53  import javax.xml.stream.events.Attribute;
54  import javax.xml.stream.events.StartElement;
55  import javax.xml.stream.events.XMLEvent;
56  
57  import edu.umd.cs.findbugs.annotations.NonNull;
58  import edu.umd.cs.findbugs.annotations.Nullable;
59  
60  /**
61   * Supports reading XML-based Metaschema module instances.
62   */
63  @SuppressWarnings("PMD.CouplingBetweenObjects")
64  public class MetaschemaXmlReader
65      implements IXmlParsingContext {
66    private static final Logger LOGGER = LogManager.getLogger(MetaschemaXmlReader.class);
67    @NonNull
68    private final XMLEventReader2 reader;
69    @NonNull
70    private final URI source;
71    @NonNull
72    private final IXmlProblemHandler problemHandler;
73  
74    /**
75     * Construct a new Module-aware XML parser using the default problem handler.
76     *
77     * @param reader
78     *          the XML reader to parse with
79     * @param source
80     *          the resource being parsed
81     * @see DefaultXmlProblemHandler
82     */
83    public MetaschemaXmlReader(
84        @NonNull XMLEventReader2 reader,
85        @NonNull URI source) {
86      this(reader, source, new DefaultXmlProblemHandler());
87    }
88  
89    /**
90     * Construct a new Module-aware parser.
91     *
92     * @param reader
93     *          the XML reader to parse with
94     * @param source
95     *          the resource being parsed
96     * @param problemHandler
97     *          the problem handler implementation to use
98     */
99    public MetaschemaXmlReader(
100       @NonNull XMLEventReader2 reader,
101       @NonNull URI source,
102       @NonNull IXmlProblemHandler problemHandler) {
103     this.reader = reader;
104     this.source = source;
105     this.problemHandler = problemHandler;
106   }
107 
108   @Override
109   public XMLEventReader2 getReader() {
110     return reader;
111   }
112 
113   @Override
114   public URI getSource() {
115     return source;
116   }
117 
118   @Override
119   public IXmlProblemHandler getProblemHandler() {
120     return problemHandler;
121   }
122 
123   /**
124    * Parses XML into a bound object based on the provided {@code definition}.
125    * <p>
126    * Parses the {@link XMLStreamConstants#START_DOCUMENT}, any processing
127    * instructions, and the element.
128    *
129    * @param <CLASS>
130    *          the returned object type
131    * @param definition
132    *          the definition describing the element data to read
133    * @return the parsed object
134    * @throws IOException
135    *           if an error occurred while parsing the input
136    */
137   @Override
138   @NonNull
139   public <CLASS> CLASS read(@NonNull IBoundDefinitionModelComplex definition) throws IOException {
140     URI resource = getSource();
141     try {
142       // we may be at the START_DOCUMENT
143       if (reader.peek().isStartDocument()) {
144         XmlEventUtil.consumeAndAssert(reader, resource, XMLStreamConstants.START_DOCUMENT);
145       }
146 
147       // advance past any other info to get to next start element
148       XmlEventUtil.skipEvents(reader, XMLStreamConstants.CHARACTERS, XMLStreamConstants.PROCESSING_INSTRUCTION,
149           XMLStreamConstants.DTD);
150 
151       XMLEvent event = ObjectUtils.requireNonNull(reader.peek());
152       if (!event.isStartElement()) {
153         throw new IOException(
154             String.format("The token '%s' is not an XML element%s.",
155                 XmlEventUtil.toEventName(event),
156                 XmlEventUtil.generateLocationMessage(event, resource)));
157       }
158 
159       ItemReadHandler handler = new ItemReadHandler(ObjectUtils.notNull(event.asStartElement()));
160       Object value = definition.readItem(null, handler);
161       if (value == null) {
162         event = reader.peek();
163         throw new IOException(String.format("Unable to read data.%s",
164             event == null ? "" : XmlEventUtil.generateLocationMessage(event, resource)));
165       }
166 
167       return ObjectUtils.asType(value);
168     } catch (XMLStreamException ex) {
169       throw new IOException(ex);
170     }
171   }
172 
173   /**
174    * Read the XML attribute data described by the {@code targetDefinition} and
175    * apply it to the provided {@code targetObject}.
176    *
177    * @param targetDefinition
178    *          the Module definition that describes the syntax of the data to read
179    * @param targetObject
180    *          the Java object that data parsed by this method will be stored in
181    * @param start
182    *          the containing XML element that was previously parsed
183    * @throws IOException
184    *           if an error occurred while parsing the input
185    * @throws XMLStreamException
186    *           if an error occurred while parsing XML events
187    */
188   protected void readFlagInstances(
189       @NonNull IBoundDefinitionModelComplex targetDefinition,
190       @NonNull IBoundObject targetObject,
191       @NonNull StartElement start) throws IOException, XMLStreamException {
192     URI resource = getSource();
193 
194     Map<IEnhancedQName, IBoundInstanceFlag> flagInstanceMap = targetDefinition.getFlagInstances().stream()
195         .collect(Collectors.toMap(
196             IBoundInstanceFlag::getQName,
197             Function.identity()));
198 
199     for (Attribute attribute : CollectionUtil.toIterable(ObjectUtils.notNull(start.getAttributes()))) {
200       IEnhancedQName qname = IEnhancedQName.of(ObjectUtils.requireNonNull(attribute.getName()));
201       IBoundInstanceFlag instance = flagInstanceMap.get(qname);
202       if (instance == null) {
203         // unrecognized flag
204         if (!getProblemHandler().handleUnknownAttribute(targetDefinition, targetObject, attribute, this)) {
205           throw new IOException(
206               String.format("Unrecognized attribute '%s'%s.",
207                   qname,
208                   XmlEventUtil.generateLocationMessage(attribute, resource)));
209         }
210       } else {
211         try {
212           // get the attribute value
213           Object value = instance.getDefinition().getJavaTypeAdapter().parse(ObjectUtils.notNull(attribute.getValue()));
214           // apply the value to the parentObject
215           instance.setValue(targetObject, value);
216           flagInstanceMap.remove(qname);
217         } catch (IllegalArgumentException ex) {
218           throw new IOException(
219               String.format("Malformed data '%s'%s. %s",
220                   attribute.getValue(),
221                   XmlEventUtil.generateLocationMessage(start, resource),
222                   ex.getLocalizedMessage()),
223               ex);
224         }
225       }
226     }
227 
228     if (!flagInstanceMap.isEmpty()) {
229       getProblemHandler().handleMissingFlagInstances(
230           targetDefinition,
231           targetObject,
232           ObjectUtils.notNull(flagInstanceMap.values()));
233     }
234   }
235 
236   /**
237    * Read the XML element data described by the {@code targetDefinition} and apply
238    * it to the provided {@code targetObject}.
239    *
240    * @param targetDefinition
241    *          the Module definition that describes the syntax of the data to read
242    * @param targetObject
243    *          the Java object that data parsed by this method will be stored in
244    * @throws IOException
245    *           if an error occurred while parsing the input
246    */
247   protected void readModelInstances(
248       @NonNull IBoundDefinitionModelAssembly targetDefinition,
249       @NonNull IBoundObject targetObject)
250       throws IOException {
251     Collection<? extends IBoundInstanceModel<?>> instances = targetDefinition.getModelInstances();
252     Set<IBoundInstanceModel<?>> unhandledProperties = new HashSet<>();
253     for (IBoundInstanceModel<?> modelInstance : instances) {
254       assert modelInstance != null;
255       if (!readItems(modelInstance, targetObject, true)) {
256         unhandledProperties.add(modelInstance);
257       }
258     }
259 
260     // process all properties that did not get a value
261     getProblemHandler().handleMissingModelInstances(targetDefinition, targetObject, unhandledProperties);
262 
263     XMLEventReader2 reader = getReader();
264     URI resource = getSource();
265 
266     // handle any
267     try {
268       if (!getReader().peek().isEndElement()) {
269         // handle any
270         XmlEventUtil.skipWhitespace(reader);
271         XmlEventUtil.skipElement(reader);
272         XmlEventUtil.skipWhitespace(reader);
273       }
274 
275       XmlEventUtil.assertNext(reader, resource, XMLStreamConstants.END_ELEMENT);
276     } catch (XMLStreamException ex) {
277       throw new IOException(ex);
278     }
279   }
280 
281   /**
282    * Determine if the next data to read corresponds to the next model instance.
283    *
284    * @param targetInstance
285    *          the model instance that describes the syntax of the data to read
286    * @return {@code true} if the Module instance needs to be parsed, or
287    *         {@code false} otherwise
288    * @throws XMLStreamException
289    *           if an error occurred while parsing XML events
290    */
291   @SuppressWarnings("PMD.OnlyOneReturn")
292   protected boolean isNextInstance(
293       @NonNull IBoundInstanceModel<?> targetInstance)
294       throws XMLStreamException {
295 
296     XmlEventUtil.skipWhitespace(reader);
297 
298     XMLEvent nextEvent = reader.peek();
299 
300     boolean retval = nextEvent.isStartElement();
301     if (retval) {
302       IEnhancedQName qname = IEnhancedQName.of(ObjectUtils.notNull(nextEvent.asStartElement().getName()));
303       retval = qname.equals(targetInstance.getEffectiveXmlGroupAsQName()) // parse the grouping element
304           || targetInstance.canHandleXmlQName(qname); // parse the instance(s)
305     }
306     return retval;
307   }
308 
309   /**
310    * Read the data associated with the {@code instance} and apply it to the
311    * provided {@code parentObject}.
312    *
313    * @param instance
314    *          the instance to parse data for
315    * @param parentObject
316    *          the Java object that data parsed by this method will be stored in
317    * @return {@code true} if the instance was parsed, or {@code false} if the data
318    *         did not contain information for this instance
319    * @throws IOException
320    *           if an error occurred while parsing the input
321    */
322   @Override
323   public <T> boolean readItems(
324       @NonNull IBoundInstanceModel<T> instance,
325       @NonNull IBoundObject parentObject,
326       boolean parseGrouping)
327       throws IOException {
328     try {
329       boolean handled = isNextInstance(instance);
330       if (handled) {
331         XMLEventReader2 reader = getReader();
332         URI resource = getSource();
333 
334         // XmlEventUtil.skipWhitespace(reader);
335 
336         IEnhancedQName groupEQName = parseGrouping ? instance.getEffectiveXmlGroupAsQName() : null;
337         QName groupQName = groupEQName == null ? null : groupEQName.toQName();
338         if (groupQName != null) {
339           // we need to parse the grouping element, if the next token matches
340           XmlEventUtil.requireStartElement(reader, resource, groupQName);
341         }
342 
343         IModelInstanceCollectionInfo<T> collectionInfo = instance.getCollectionInfo();
344 
345         ModelInstanceReadHandler<T> handler = new ModelInstanceReadHandler<>(instance, parentObject);
346 
347         // let the property info decide how to parse the value
348         Object value = collectionInfo.readItems(handler);
349         if (value != null) {
350           instance.setValue(parentObject, value);
351         }
352 
353         // consume extra whitespace between elements
354         XmlEventUtil.skipWhitespace(reader);
355 
356         if (groupQName != null) {
357           // consume the end of the group
358           XmlEventUtil.requireEndElement(reader, resource, groupQName);
359         }
360       }
361       return handled;
362     } catch (XMLStreamException ex) {
363       throw new IOException(ex);
364     }
365   }
366 
367   private final class ModelInstanceReadHandler<ITEM>
368       extends AbstractModelInstanceReadHandler<ITEM> {
369 
370     private ModelInstanceReadHandler(
371         @NonNull IBoundInstanceModel<ITEM> instance,
372         @NonNull IBoundObject parentObject) {
373       super(instance, parentObject);
374     }
375 
376     @Override
377     public List<ITEM> readList() throws IOException {
378       return ObjectUtils.notNull(readCollection());
379     }
380 
381     @Override
382     public Map<String, ITEM> readMap() throws IOException {
383       IBoundInstanceModel<?> instance = getCollectionInfo().getInstance();
384 
385       return ObjectUtils.notNull(readCollection().stream()
386           .collect(Collectors.toMap(
387               item -> {
388                 assert item != null;
389 
390                 IBoundInstanceFlag jsonKey = instance.getItemJsonKey(item);
391                 assert jsonKey != null;
392                 return ObjectUtils.requireNonNull(jsonKey.getValue(item)).toString();
393               },
394               Function.identity(),
395               (t, u) -> u,
396               LinkedHashMap::new)));
397     }
398 
399     @NonNull
400     private List<ITEM> readCollection() throws IOException {
401       List<ITEM> retval = new LinkedList<>();
402       XMLEventReader2 reader = getReader();
403       try {
404 
405         // consume extra whitespace between elements
406         XmlEventUtil.skipWhitespace(reader);
407 
408         IBoundInstanceModel<?> instance = getCollectionInfo().getInstance();
409         XMLEvent event;
410         while ((event = reader.peek()).isStartElement()
411             && instance.canHandleXmlQName(
412                 IEnhancedQName.of(ObjectUtils.notNull(event.asStartElement().getName())))) {
413 
414           // Consume the start element
415           ITEM value = readItem();
416           retval.add(value);
417 
418           // consume extra whitespace between elements
419           XmlEventUtil.skipWhitespace(reader);
420         }
421       } catch (XMLStreamException ex) {
422         throw new IOException(ex);
423       }
424       return retval;
425     }
426 
427     @Override
428     public ITEM readItem() throws IOException {
429       try {
430         return getCollectionInfo().getInstance().readItem(
431             getParentObject(),
432             new ItemReadHandler(ObjectUtils.notNull(getReader().peek().asStartElement())));
433       } catch (XMLStreamException ex) {
434         throw new IOException(ex);
435       }
436     }
437   }
438 
439   private final class ItemReadHandler implements IItemReadHandler {
440     @NonNull
441     private final StartElement startElement;
442 
443     private ItemReadHandler(@NonNull StartElement startElement) {
444       this.startElement = startElement;
445     }
446 
447     /**
448      * Get the current start element.
449      *
450      * @return the startElement
451      */
452     @NonNull
453     private StartElement getStartElement() {
454       return startElement;
455     }
456 
457     @NonNull
458     private <DEF extends IBoundDefinitionModelComplex> IBoundObject readDefinitionElement(
459         @NonNull DEF definition,
460         @NonNull StartElement start,
461         @NonNull IEnhancedQName expectedEQName,
462         @Nullable IBoundObject parent,
463         @NonNull DefinitionBodyHandler<DEF, IBoundObject> bodyHandler) throws IOException {
464       XMLEventReader2 reader = getReader();
465       URI resource = getSource();
466       QName expectedQName = expectedEQName.toQName();
467 
468       try {
469         // consume the start element
470         XmlEventUtil.requireStartElement(reader, resource, expectedQName);
471 
472         Location location = start.getLocation();
473 
474         // construct the item
475         IBoundObject item = definition.newInstance(location == null ? null : () -> new MetaschemaData(location));
476 
477         // call pre-parse initialization hook
478         definition.callBeforeDeserialize(item, parent);
479 
480         // read the flags
481         readFlagInstances(definition, item, start);
482 
483         // read the body
484         bodyHandler.accept(definition, item);
485 
486         XmlEventUtil.skipWhitespace(reader);
487 
488         // call post-parse initialization hook
489         definition.callAfterDeserialize(item, parent);
490 
491         // consume the end element
492         XmlEventUtil.requireEndElement(reader, resource, expectedQName);
493         return ObjectUtils.asType(item);
494       } catch (BindingException | XMLStreamException ex) {
495         throw new IOException(ex);
496       }
497     }
498 
499     @Override
500     public Object readItemFlag(
501         IBoundObject parent,
502         IBoundInstanceFlag flag) throws IOException {
503       // should never be called
504       throw new UnsupportedOperationException("should be handled by readFlagInstances()");
505     }
506 
507     private void handleFieldDefinitionBody(
508         @NonNull IBoundDefinitionModelFieldComplex definition,
509         @NonNull IBoundObject item) throws IOException {
510       IBoundFieldValue fieldValue = definition.getFieldValue();
511 
512       // parse the value
513       Object value = fieldValue.readItem(item, this);
514       if (value != null) {
515         fieldValue.setValue(item, value);
516       }
517     }
518 
519     @Override
520     public Object readItemField(
521         IBoundObject parent,
522         IBoundInstanceModelFieldScalar instance)
523         throws IOException {
524       XMLEventReader2 reader = getReader();
525       URI resource = getSource();
526       try {
527         QName wrapper = null;
528         if (instance.isEffectiveValueWrappedInXml()) {
529           wrapper = instance.getQName().toQName();
530 
531           XmlEventUtil.skipWhitespace(reader);
532           XmlEventUtil.requireStartElement(reader, resource, wrapper);
533         }
534 
535         Object retval = readScalarItem(instance);
536 
537         if (wrapper != null) {
538           XmlEventUtil.skipWhitespace(reader);
539 
540           XmlEventUtil.requireEndElement(reader, resource, wrapper);
541         }
542         return retval;
543       } catch (XMLStreamException ex) {
544         throw new IOException(ex);
545       }
546     }
547 
548     @Override
549     public IBoundObject readItemField(
550         IBoundObject parent,
551         IBoundInstanceModelFieldComplex instance)
552         throws IOException {
553       return readDefinitionElement(
554           instance.getDefinition(),
555           getStartElement(),
556           instance.getQName(),
557           parent,
558           this::handleFieldDefinitionBody);
559     }
560 
561     @Override
562     public IBoundObject readItemField(IBoundObject parent, IBoundInstanceModelGroupedField instance)
563         throws IOException {
564       return readDefinitionElement(
565           instance.getDefinition(),
566           getStartElement(),
567           instance.getQName(),
568           parent,
569           this::handleFieldDefinitionBody);
570     }
571 
572     @Override
573     public IBoundObject readItemField(
574         IBoundObject parent,
575         IBoundDefinitionModelFieldComplex definition) throws IOException {
576       return readDefinitionElement(
577           definition,
578           getStartElement(),
579           definition.getQName(),
580           parent,
581           this::handleFieldDefinitionBody);
582     }
583 
584     @Override
585     public Object readItemFieldValue(
586         IBoundObject parent,
587         IBoundFieldValue fieldValue) throws IOException {
588       return checkMissingFieldValue(readScalarItem(fieldValue));
589     }
590 
591     @Nullable
592     private Object checkMissingFieldValue(Object value) {
593       if (value == null && LOGGER.isWarnEnabled()) {
594         StartElement start = getStartElement();
595         LOGGER.atWarn().log("Missing property value{}",
596             XmlEventUtil.generateLocationMessage(start, getSource()));
597       }
598       return value;
599     }
600 
601     private void handleAssemblyDefinitionBody(
602         @NonNull IBoundDefinitionModelAssembly definition,
603         @NonNull IBoundObject item) throws IOException {
604       readModelInstances(definition, item);
605     }
606 
607     @Override
608     public IBoundObject readItemAssembly(
609         IBoundObject parent,
610         IBoundInstanceModelAssembly instance) throws IOException {
611       return readDefinitionElement(
612           instance.getDefinition(),
613           getStartElement(),
614           instance.getQName(),
615           parent,
616           this::handleAssemblyDefinitionBody);
617     }
618 
619     @Override
620     public IBoundObject readItemAssembly(IBoundObject parent, IBoundInstanceModelGroupedAssembly instance)
621         throws IOException {
622       return readDefinitionElement(
623           instance.getDefinition(),
624           getStartElement(),
625           instance.getQName(),
626           parent,
627           this::handleAssemblyDefinitionBody);
628     }
629 
630     @Override
631     public IBoundObject readItemAssembly(
632         IBoundObject parent,
633         IBoundDefinitionModelAssembly definition) throws IOException {
634       return readDefinitionElement(
635           definition,
636           getStartElement(),
637           ObjectUtils.requireNonNull(definition.getRootQName()),
638           parent,
639           this::handleAssemblyDefinitionBody);
640     }
641 
642     @Nullable
643     private Object readScalarItem(@NonNull IFeatureScalarItemValueHandler handler)
644         throws IOException {
645       return handler.getJavaTypeAdapter().parse(getReader(), getSource());
646     }
647 
648     @Override
649     public IBoundObject readChoiceGroupItem(IBoundObject parent, IBoundInstanceModelChoiceGroup instance)
650         throws IOException {
651       try {
652         XMLEventReader2 eventReader = getReader();
653         // consume extra whitespace between elements
654         XmlEventUtil.skipWhitespace(eventReader);
655 
656         XMLEvent event = eventReader.peek();
657         IEnhancedQName nextQName = IEnhancedQName.of(ObjectUtils.notNull(event.asStartElement().getName()));
658         IBoundInstanceModelGroupedNamed actualInstance = instance.getGroupedModelInstance(nextQName);
659         assert actualInstance != null;
660         return actualInstance.readItem(parent, this);
661       } catch (XMLStreamException ex) {
662         throw new IOException(ex);
663       }
664     }
665   }
666 
667   private static class MetaschemaData implements IMetaschemaData {
668     private final int line;
669     private final int column;
670     private final long charOffset;
671 
672     public MetaschemaData(@NonNull Location location) {
673       this.line = location.getLineNumber();
674       this.column = location.getColumnNumber();
675       this.charOffset = location.getCharacterOffset();
676     }
677 
678     @Override
679     public int getLine() {
680       return line;
681     }
682 
683     @Override
684     public int getColumn() {
685       return column;
686     }
687 
688     @Override
689     public long getCharOffset() {
690       return charOffset;
691     }
692 
693     @Override
694     public long getByteOffset() {
695       return -1;
696     }
697   }
698 
699   @FunctionalInterface
700   private interface DefinitionBodyHandler<DEF extends IBoundDefinitionModelComplex, ITEM> {
701     void accept(
702         @NonNull DEF definition,
703         @NonNull ITEM item) throws IOException;
704   }
705 
706 }