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.configuration.IMutableConfiguration;
011import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
012import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItemFactory;
013import gov.nist.secauto.metaschema.core.model.IBoundObject;
014import gov.nist.secauto.metaschema.core.util.AutoCloser;
015import gov.nist.secauto.metaschema.core.util.ObjectUtils;
016import gov.nist.secauto.metaschema.databind.io.AbstractDeserializer;
017import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
018import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
019
020import org.codehaus.stax2.XMLEventReader2;
021import org.codehaus.stax2.XMLInputFactory2;
022
023import java.io.IOException;
024import java.io.Reader;
025import java.net.URI;
026
027import javax.xml.stream.EventFilter;
028import javax.xml.stream.XMLEventReader;
029import javax.xml.stream.XMLInputFactory;
030import javax.xml.stream.XMLResolver;
031import javax.xml.stream.XMLStreamException;
032
033import edu.umd.cs.findbugs.annotations.NonNull;
034import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
035import nl.talsmasoftware.lazy4j.Lazy;
036
037/**
038 * Provides support for reading XML-based data based on a bound Metaschema
039 * module.
040 *
041 * @param <CLASS>
042 *          the Java type of the bound object representing the root node to read
043 */
044public class DefaultXmlDeserializer<CLASS extends IBoundObject>
045    extends AbstractDeserializer<CLASS> {
046  private Lazy<XMLInputFactory2> factory;
047
048  @NonNull
049  private final IBoundDefinitionModelAssembly rootDefinition;
050
051  /**
052   * Construct a new Module binding-based deserializer that reads XML-based Module
053   * content.
054   *
055   * @param definition
056   *          the assembly class binding describing the Java objects this
057   *          deserializer parses data into
058   */
059  @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields")
060  public DefaultXmlDeserializer(@NonNull IBoundDefinitionModelAssembly definition) {
061    super(definition);
062    this.rootDefinition = definition;
063    if (!definition.isRoot()) {
064      throw new UnsupportedOperationException(
065          String.format("The assembly '%s' is not a root assembly.", definition.getBoundClass().getName()));
066    }
067    resetFactory();
068  }
069
070  /**
071   * For use by subclasses to reset the underlying XML factory when an important
072   * change has occurred that will change how the factory produces an
073   * {@link XMLInputFactory2}.
074   */
075  protected final void resetFactory() {
076    this.factory = Lazy.lazy(this::newFactoryInstance);
077  }
078
079  @Override
080  protected void configurationChanged(IMutableConfiguration<DeserializationFeature<?>> config) {
081    super.configurationChanged(config);
082    resetFactory();
083  }
084
085  /**
086   * Get a JSON factory instance.
087   * <p>
088   * This method can be used by sub-classes to create a customized factory
089   * instance.
090   *
091   * @return the factory
092   */
093  @SuppressWarnings("resource")
094  @NonNull
095  protected XMLInputFactory2 newFactoryInstance() {
096    XMLInputFactory2 retval = (XMLInputFactory2) XMLInputFactory.newInstance();
097    assert retval instanceof WstxInputFactory;
098    retval.configureForXmlConformance();
099    retval.setProperty(XMLInputFactory.IS_COALESCING, false);
100    retval.setProperty(XMLInputFactory2.P_PRESERVE_LOCATION, true);
101    // xmlInputFactory.configureForSpeed();
102
103    if (isFeatureEnabled(DeserializationFeature.DESERIALIZE_XML_ALLOW_ENTITY_RESOLUTION)) {
104      retval.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true);
105      retval.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true);
106      retval.setProperty(XMLInputFactory.SUPPORT_DTD, true);
107      retval.setProperty(XMLInputFactory.RESOLVER,
108          (XMLResolver) (publicID, systemID, baseURI, namespace) -> {
109            URI base = URI.create(baseURI);
110            URI resource = base.resolve(systemID);
111            try {
112
113              return ObjectUtils.notNull(resource.toURL().openStream());
114            } catch (IOException ex) {
115              throw new XMLStreamException(ex);
116            }
117          });
118    }
119    return retval;
120  }
121
122  /**
123   * Get the XML input factory instance used to create XML parser instances.
124   * <p>
125   * Uses a built-in default if a user specified factory is not provided.
126   *
127   * @return the factory instance
128   * @see #setXMLInputFactory(XMLInputFactory2)
129   */
130  @NonNull
131  private XMLInputFactory2 getXMLInputFactory() {
132    return ObjectUtils.notNull(factory.get());
133  }
134
135  @NonNull
136  private XMLEventReader2 newXMLEventReader2(
137      @NonNull URI documentUri,
138      @NonNull Reader reader) throws XMLStreamException {
139    XMLEventReader2 eventReader
140        = (XMLEventReader2) getXMLInputFactory().createXMLEventReader(documentUri.toASCIIString(), reader);
141    EventFilter filter = new CommentFilter();
142    return ObjectUtils.notNull((XMLEventReader2) getXMLInputFactory().createFilteredReader(eventReader, filter));
143  }
144
145  @Override
146  protected final IDocumentNodeItem deserializeToNodeItemInternal(Reader reader, URI documentUri) throws IOException {
147    Object value = deserializeToValueInternal(reader, documentUri);
148    return INodeItemFactory.instance().newDocumentNodeItem(rootDefinition, documentUri, value);
149  }
150
151  @Override
152  public final CLASS deserializeToValueInternal(Reader reader, URI resource) throws IOException {
153    // doesn't auto close the underlying reader
154    try (AutoCloser<XMLEventReader2, XMLStreamException> closer = AutoCloser.autoClose(
155        newXMLEventReader2(resource, reader), XMLEventReader::close)) {
156      return parseXmlInternal(closer.getResource(), resource);
157    } catch (XMLStreamException ex) {
158      throw new IOException("Unable to create a new XMLEventReader2 instance.", ex);
159    }
160  }
161
162  @NonNull
163  private CLASS parseXmlInternal(@NonNull XMLEventReader2 reader, @NonNull URI resource)
164      throws IOException {
165
166    MetaschemaXmlReader parser = new MetaschemaXmlReader(reader, resource, new DefaultXmlProblemHandler());
167
168    try {
169      return parser.read(rootDefinition);
170    } catch (IOException | AssertionError ex) {
171      throw new IOException(
172          String.format("An unexpected error occurred during parsing: %s", ex.getMessage()),
173          ex);
174    }
175  }
176}