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.qname.IEnhancedQName;
012import gov.nist.secauto.metaschema.core.util.CollectionUtil;
013import gov.nist.secauto.metaschema.core.util.ObjectUtils;
014import gov.nist.secauto.metaschema.databind.io.BindingException;
015import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
016import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelComplex;
017import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelFieldComplex;
018import gov.nist.secauto.metaschema.databind.model.IBoundFieldValue;
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.net.URI;
039import java.util.Collection;
040import java.util.HashSet;
041import java.util.LinkedHashMap;
042import java.util.LinkedList;
043import java.util.List;
044import java.util.Map;
045import java.util.Set;
046import java.util.function.Function;
047import java.util.stream.Collectors;
048
049import javax.xml.namespace.QName;
050import javax.xml.stream.Location;
051import javax.xml.stream.XMLStreamConstants;
052import javax.xml.stream.XMLStreamException;
053import javax.xml.stream.events.Attribute;
054import javax.xml.stream.events.StartElement;
055import javax.xml.stream.events.XMLEvent;
056
057import edu.umd.cs.findbugs.annotations.NonNull;
058import edu.umd.cs.findbugs.annotations.Nullable;
059
060/**
061 * Supports reading XML-based Metaschema module instances.
062 */
063@SuppressWarnings("PMD.CouplingBetweenObjects")
064public class MetaschemaXmlReader
065    implements IXmlParsingContext {
066  private static final Logger LOGGER = LogManager.getLogger(MetaschemaXmlReader.class);
067  @NonNull
068  private final XMLEventReader2 reader;
069  @NonNull
070  private final URI source;
071  @NonNull
072  private final IXmlProblemHandler problemHandler;
073
074  /**
075   * Construct a new Module-aware XML parser using the default problem handler.
076   *
077   * @param reader
078   *          the XML reader to parse with
079   * @param source
080   *          the resource being parsed
081   * @see DefaultXmlProblemHandler
082   */
083  public MetaschemaXmlReader(
084      @NonNull XMLEventReader2 reader,
085      @NonNull URI source) {
086    this(reader, source, new DefaultXmlProblemHandler());
087  }
088
089  /**
090   * Construct a new Module-aware parser.
091   *
092   * @param reader
093   *          the XML reader to parse with
094   * @param source
095   *          the resource being parsed
096   * @param problemHandler
097   *          the problem handler implementation to use
098   */
099  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(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) throws IOException {
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}