001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.databind.io.xml;
007
008import gov.nist.secauto.metaschema.core.model.IBoundObject;
009import gov.nist.secauto.metaschema.core.model.IMetaschemaData;
010import gov.nist.secauto.metaschema.core.model.util.XmlEventUtil;
011import gov.nist.secauto.metaschema.core.util.CollectionUtil;
012import gov.nist.secauto.metaschema.core.util.ObjectUtils;
013import gov.nist.secauto.metaschema.databind.io.BindingException;
014import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
015import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
016import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
017import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue;
018import gov.nist.secauto.metaschema.databind.model.IBoundInstance;
019import gov.nist.secauto.metaschema.databind.model.IBoundInstanceFlag;
020import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModel;
021import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelAssembly;
022import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelChoiceGroup;
023import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldComplex;
024import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelFieldScalar;
025import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedAssembly;
026import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedField;
027import gov.nist.secauto.metaschema.databind.model.IBoundInstanceModelGroupedNamed;
028import gov.nist.secauto.metaschema.databind.model.info.AbstractModelInstanceReadHandler;
029import gov.nist.secauto.metaschema.databind.model.info.IFeatureScalarItemValueHandler;
030import gov.nist.secauto.metaschema.databind.model.info.IItemReadHandler;
031import gov.nist.secauto.metaschema.databind.model.info.IModelInstanceCollectionInfo;
032
033import org.codehaus.stax2.XMLEventReader2;
034
035import java.io.IOException;
036import java.util.Collection;
037import java.util.HashSet;
038import java.util.LinkedHashMap;
039import java.util.LinkedList;
040import java.util.List;
041import java.util.Map;
042import java.util.Set;
043import java.util.function.Function;
044import java.util.stream.Collectors;
045
046import javax.xml.namespace.QName;
047import javax.xml.stream.Location;
048import javax.xml.stream.XMLStreamConstants;
049import javax.xml.stream.XMLStreamException;
050import javax.xml.stream.events.Attribute;
051import javax.xml.stream.events.StartElement;
052import javax.xml.stream.events.XMLEvent;
053
054import edu.umd.cs.findbugs.annotations.NonNull;
055import edu.umd.cs.findbugs.annotations.Nullable;
056
057public class MetaschemaXmlReader
058    implements IXmlParsingContext {
059  @NonNull
060  private final XMLEventReader2 reader;
061  @NonNull
062  private final IXmlProblemHandler problemHandler;
063
064  /**
065   * Construct a new Module-aware XML parser using the default problem handler.
066   *
067   * @param reader
068   *          the XML reader to parse with
069   * @see DefaultXmlProblemHandler
070   */
071  public MetaschemaXmlReader(
072      @NonNull XMLEventReader2 reader) {
073    this(reader, new DefaultXmlProblemHandler());
074  }
075
076  public <ITEM> ITEM readItem(
077      @NonNull IBoundObject item,
078      @NonNull IBoundInstance<ITEM> instance,
079      @NonNull StartElement start) throws IOException {
080    return instance.readItem(item, new ItemReadHandler(start));
081  }
082
083  /**
084   * Construct a new Module-aware parser.
085   *
086   * @param reader
087   *          the XML reader to parse with
088   * @param problemHandler
089   *          the problem handler implementation to use
090   */
091  public MetaschemaXmlReader(
092      @NonNull XMLEventReader2 reader,
093      @NonNull IXmlProblemHandler problemHandler) {
094    this.reader = reader;
095    this.problemHandler = problemHandler;
096  }
097
098  @Override
099  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}