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.apache.logging.log4j.LogManager;
034import org.apache.logging.log4j.Logger;
035import org.codehaus.stax2.XMLEventReader2;
036
037import java.io.IOException;
038import java.util.Collection;
039import java.util.HashSet;
040import java.util.LinkedHashMap;
041import java.util.LinkedList;
042import java.util.List;
043import java.util.Map;
044import java.util.Set;
045import java.util.function.Function;
046import java.util.stream.Collectors;
047
048import javax.xml.namespace.QName;
049import javax.xml.stream.Location;
050import javax.xml.stream.XMLStreamConstants;
051import javax.xml.stream.XMLStreamException;
052import javax.xml.stream.events.Attribute;
053import javax.xml.stream.events.StartElement;
054import javax.xml.stream.events.XMLEvent;
055
056import edu.umd.cs.findbugs.annotations.NonNull;
057import edu.umd.cs.findbugs.annotations.Nullable;
058
059public class MetaschemaXmlReader
060    implements IXmlParsingContext {
061  private static final Logger LOGGER = LogManager.getLogger(MetaschemaXmlReader.class);
062  @NonNull
063  private final XMLEventReader2 reader;
064  @NonNull
065  private final IXmlProblemHandler problemHandler;
066
067  /**
068   * Construct a new Module-aware XML parser using the default problem handler.
069   *
070   * @param reader
071   *          the XML reader to parse with
072   * @see DefaultXmlProblemHandler
073   */
074  public MetaschemaXmlReader(
075      @NonNull XMLEventReader2 reader) {
076    this(reader, new DefaultXmlProblemHandler());
077  }
078
079  public <ITEM> ITEM readItem(
080      @NonNull IBoundObject item,
081      @NonNull IBoundInstance<ITEM> instance,
082      @NonNull StartElement start) throws IOException {
083    return instance.readItem(item, new ItemReadHandler(start));
084  }
085
086  /**
087   * Construct a new Module-aware parser.
088   *
089   * @param reader
090   *          the XML reader to parse with
091   * @param problemHandler
092   *          the problem handler implementation to use
093   */
094  public MetaschemaXmlReader(
095      @NonNull XMLEventReader2 reader,
096      @NonNull IXmlProblemHandler problemHandler) {
097    this.reader = reader;
098    this.problemHandler = problemHandler;
099  }
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}