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
037public class DefaultXmlDeserializer<CLASS extends IBoundObject>
038    extends AbstractDeserializer<CLASS> {
039  private Lazy<XMLInputFactory2> factory;
040
041  @NonNull
042  private final IBoundDefinitionModelAssembly rootDefinition;
043
044  /**
045   * Construct a new Module binding-based deserializer that reads XML-based Module
046   * content.
047   *
048   * @param definition
049   *          the assembly class binding describing the Java objects this
050   *          deserializer parses data into
051   */
052  @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields")
053  public DefaultXmlDeserializer(@NonNull IBoundDefinitionModelAssembly definition) {
054    super(definition);
055    this.rootDefinition = definition;
056    if (!definition.isRoot()) {
057      throw new UnsupportedOperationException(
058          String.format("The assembly '%s' is not a root assembly.", definition.getBoundClass().getName()));
059    }
060    resetFactory();
061  }
062
063  protected final void resetFactory() {
064    this.factory = Lazy.lazy(this::newFactoryInstance);
065  }
066
067  @Override
068  protected void configurationChanged(IMutableConfiguration<DeserializationFeature<?>> config) {
069    super.configurationChanged(config);
070    resetFactory();
071  }
072
073  /**
074   * Get a JSON factory instance.
075   * <p>
076   * This method can be used by sub-classes to create a customized factory
077   * instance.
078   *
079   * @return the factory
080   */
081  @SuppressWarnings("resource")
082  @NonNull
083  protected XMLInputFactory2 newFactoryInstance() {
084    XMLInputFactory2 retval = (XMLInputFactory2) XMLInputFactory.newInstance();
085    assert retval instanceof WstxInputFactory;
086    retval.configureForXmlConformance();
087    retval.setProperty(XMLInputFactory.IS_COALESCING, false);
088    retval.setProperty(XMLInputFactory2.P_PRESERVE_LOCATION, true);
089    // xmlInputFactory.configureForSpeed();
090
091    if (isFeatureEnabled(DeserializationFeature.DESERIALIZE_XML_ALLOW_ENTITY_RESOLUTION)) {
092      retval.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true);
093      retval.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true);
094      retval.setProperty(XMLInputFactory.SUPPORT_DTD, true);
095      retval.setProperty(XMLInputFactory.RESOLVER,
096          (XMLResolver) (publicID, systemID, baseURI, namespace) -> {
097            URI base = URI.create(baseURI);
098            URI resource = base.resolve(systemID);
099            try {
100
101              return ObjectUtils.notNull(resource.toURL().openStream());
102            } catch (IOException ex) {
103              throw new XMLStreamException(ex);
104            }
105          });
106    }
107    return retval;
108  }
109
110  /**
111   * Get the XML input factory instance used to create XML parser instances.
112   * <p>
113   * Uses a built-in default if a user specified factory is not provided.
114   *
115   * @return the factory instance
116   * @see #setXMLInputFactory(XMLInputFactory2)
117   */
118  @NonNull
119  private XMLInputFactory2 getXMLInputFactory() {
120    return ObjectUtils.notNull(factory.get());
121  }
122
123  @NonNull
124  private XMLEventReader2 newXMLEventReader2(
125      @NonNull URI documentUri,
126      @NonNull Reader reader) throws XMLStreamException {
127    XMLEventReader2 eventReader
128        = (XMLEventReader2) getXMLInputFactory().createXMLEventReader(documentUri.toASCIIString(), reader);
129    EventFilter filter = new CommentFilter();
130    return ObjectUtils.notNull((XMLEventReader2) getXMLInputFactory().createFilteredReader(eventReader, filter));
131  }
132
133  @Override
134  protected final IDocumentNodeItem deserializeToNodeItemInternal(Reader reader, URI documentUri) throws IOException {
135    Object value = deserializeToValueInternal(reader, documentUri);
136    return INodeItemFactory.instance().newDocumentNodeItem(rootDefinition, documentUri, value);
137  }
138
139  @Override
140  public final CLASS deserializeToValueInternal(Reader reader, URI documentUri) throws IOException {
141    // doesn't auto close the underlying reader
142    try (AutoCloser<XMLEventReader2, XMLStreamException> closer = AutoCloser.autoClose(
143        newXMLEventReader2(documentUri, reader), XMLEventReader::close)) {
144      return parseXmlInternal(closer.getResource());
145    } catch (XMLStreamException ex) {
146      throw new IOException("Unable to create a new XMLEventReader2 instance.", ex);
147    }
148  }
149
150  @NonNull
151  private CLASS parseXmlInternal(@NonNull XMLEventReader2 reader)
152      throws IOException {
153
154    MetaschemaXmlReader parser = new MetaschemaXmlReader(reader, new DefaultXmlProblemHandler());
155
156    try {
157      return parser.read(rootDefinition);
158    } catch (IOException | AssertionError ex) {
159      throw new IOException(
160          String.format("An unexpected error occurred during parsing: %s", ex.getMessage()),
161          ex);
162    }
163  }
164}