001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package gov.nist.secauto.metaschema.databind.io.xml;
007
008import com.ctc.wstx.stax.WstxInputFactory;
009
010import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
011import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItemFactory;
012import gov.nist.secauto.metaschema.core.model.IBoundObject;
013import gov.nist.secauto.metaschema.core.util.AutoCloser;
014import gov.nist.secauto.metaschema.core.util.ObjectUtils;
015import gov.nist.secauto.metaschema.databind.io.AbstractDeserializer;
016import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
017import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
018
019import org.codehaus.stax2.XMLEventReader2;
020import org.codehaus.stax2.XMLInputFactory2;
021
022import java.io.IOException;
023import java.io.Reader;
024import java.net.URI;
025
026import javax.xml.stream.EventFilter;
027import javax.xml.stream.XMLEventReader;
028import javax.xml.stream.XMLInputFactory;
029import javax.xml.stream.XMLResolver;
030import javax.xml.stream.XMLStreamException;
031
032import edu.umd.cs.findbugs.annotations.NonNull;
033import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
034
035public class DefaultXmlDeserializer<CLASS extends IBoundObject>
036    extends AbstractDeserializer<CLASS> {
037  private XMLInputFactory2 xmlInputFactory;
038
039  @NonNull
040  private final IBoundDefinitionModelAssembly rootDefinition;
041
042  /**
043   * Construct a new Module binding-based deserializer that reads XML-based Module
044   * content.
045   *
046   * @param definition
047   *          the assembly class binding describing the Java objects this
048   *          deserializer parses data into
049   */
050  @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields")
051  public DefaultXmlDeserializer(@NonNull IBoundDefinitionModelAssembly definition) {
052    super(definition);
053    this.rootDefinition = definition;
054    if (!definition.isRoot()) {
055      throw new UnsupportedOperationException(
056          String.format("The assembly '%s' is not a root assembly.", definition.getBoundClass().getName()));
057    }
058  }
059
060  /**
061   * Get the XML input factory instance used to create XML parser instances.
062   * <p>
063   * Uses a built-in default if a user specified factory is not provided.
064   *
065   * @return the factory instance
066   * @see #setXMLInputFactory(XMLInputFactory2)
067   */
068  @NonNull
069  private XMLInputFactory2 getXMLInputFactory() {
070
071    synchronized (this) {
072      if (xmlInputFactory == null) {
073        xmlInputFactory = (XMLInputFactory2) XMLInputFactory.newInstance();
074        assert xmlInputFactory instanceof WstxInputFactory;
075        xmlInputFactory.configureForXmlConformance();
076        xmlInputFactory.setProperty(XMLInputFactory.IS_COALESCING, false);
077        xmlInputFactory.setProperty(XMLInputFactory2.P_PRESERVE_LOCATION, true);
078        // xmlInputFactory.configureForSpeed();
079
080        if (isFeatureEnabled(DeserializationFeature.DESERIALIZE_XML_ALLOW_ENTITY_RESOLUTION)) {
081          xmlInputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true);
082          xmlInputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true);
083          xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, true);
084          xmlInputFactory.setProperty(XMLInputFactory.RESOLVER,
085              (XMLResolver) (publicID, systemID, baseURI, namespace) -> {
086                URI base = URI.create(baseURI);
087                URI resource = base.resolve(systemID);
088                try {
089                  return resource.toURL().openStream();
090                } catch (IOException ex) {
091                  throw new XMLStreamException(ex);
092                }
093              });
094        }
095      }
096      return ObjectUtils.notNull(xmlInputFactory);
097    }
098  }
099
100  /**
101   * Provide a XML input factory instance that will be used to create XML parser
102   * instances.
103   *
104   * @param factory
105   *          the factory instance
106   */
107  protected void setXMLInputFactory(@NonNull XMLInputFactory2 factory) {
108    synchronized (this) {
109      this.xmlInputFactory = factory;
110    }
111  }
112
113  @NonNull
114  private XMLEventReader2 newXMLEventReader2(
115      @NonNull URI documentUri,
116      @NonNull Reader reader) throws XMLStreamException {
117    XMLEventReader2 eventReader
118        = (XMLEventReader2) getXMLInputFactory().createXMLEventReader(documentUri.toASCIIString(), reader);
119    EventFilter filter = new CommentFilter();
120    return ObjectUtils.notNull((XMLEventReader2) getXMLInputFactory().createFilteredReader(eventReader, filter));
121  }
122
123  @Override
124  protected final IDocumentNodeItem deserializeToNodeItemInternal(Reader reader, URI documentUri) throws IOException {
125    Object value = deserializeToValueInternal(reader, documentUri);
126    return INodeItemFactory.instance().newDocumentNodeItem(rootDefinition, documentUri, value);
127  }
128
129  @Override
130  public final CLASS deserializeToValueInternal(Reader reader, URI documentUri) throws IOException {
131    // doesn't auto close the underlying reader
132    try (AutoCloser<XMLEventReader2, XMLStreamException> closer = new AutoCloser<>(
133        newXMLEventReader2(documentUri, reader), XMLEventReader::close)) {
134      return parseXmlInternal(closer.getResource());
135    } catch (XMLStreamException ex) {
136      throw new IOException("Unable to create a new XMLEventReader2 instance.", ex);
137    }
138  }
139
140  @NonNull
141  private CLASS parseXmlInternal(@NonNull XMLEventReader2 reader)
142      throws IOException {
143
144    MetaschemaXmlReader parser = new MetaschemaXmlReader(reader, new DefaultXmlProblemHandler());
145
146    try {
147      return parser.read(rootDefinition);
148    } catch (IOException | AssertionError ex) {
149      throw new IOException(
150          String.format("An unexpected error occurred during parsing: %s", ex.getMessage()),
151          ex);
152    }
153  }
154}