1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.databind.io.xml;
7   
8   import com.ctc.wstx.stax.WstxInputFactory;
9   
10  import gov.nist.secauto.metaschema.core.configuration.IMutableConfiguration;
11  import gov.nist.secauto.metaschema.core.metapath.item.node.IDocumentNodeItem;
12  import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItemFactory;
13  import gov.nist.secauto.metaschema.core.model.IBoundObject;
14  import gov.nist.secauto.metaschema.core.util.AutoCloser;
15  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
16  import gov.nist.secauto.metaschema.databind.io.AbstractDeserializer;
17  import gov.nist.secauto.metaschema.databind.io.DeserializationFeature;
18  import gov.nist.secauto.metaschema.databind.model.IBoundDefinitionModelAssembly;
19  
20  import org.codehaus.stax2.XMLEventReader2;
21  import org.codehaus.stax2.XMLInputFactory2;
22  
23  import java.io.IOException;
24  import java.io.Reader;
25  import java.net.URI;
26  
27  import javax.xml.stream.EventFilter;
28  import javax.xml.stream.XMLEventReader;
29  import javax.xml.stream.XMLInputFactory;
30  import javax.xml.stream.XMLResolver;
31  import javax.xml.stream.XMLStreamException;
32  
33  import edu.umd.cs.findbugs.annotations.NonNull;
34  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
35  import nl.talsmasoftware.lazy4j.Lazy;
36  
37  /**
38   * Provides support for reading XML-based data based on a bound Metaschema
39   * module.
40   *
41   * @param <CLASS>
42   *          the Java type of the bound object representing the root node to read
43   */
44  public class DefaultXmlDeserializer<CLASS extends IBoundObject>
45      extends AbstractDeserializer<CLASS> {
46    private Lazy<XMLInputFactory2> factory;
47  
48    @NonNull
49    private final IBoundDefinitionModelAssembly rootDefinition;
50  
51    /**
52     * Construct a new Module binding-based deserializer that reads XML-based Module
53     * content.
54     *
55     * @param definition
56     *          the assembly class binding describing the Java objects this
57     *          deserializer parses data into
58     */
59    @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields")
60    public DefaultXmlDeserializer(@NonNull IBoundDefinitionModelAssembly definition) {
61      super(definition);
62      this.rootDefinition = definition;
63      if (!definition.isRoot()) {
64        throw new UnsupportedOperationException(
65            String.format("The assembly '%s' is not a root assembly.", definition.getBoundClass().getName()));
66      }
67      resetFactory();
68    }
69  
70    /**
71     * For use by subclasses to reset the underlying XML factory when an important
72     * change has occurred that will change how the factory produces an
73     * {@link XMLInputFactory2}.
74     */
75    protected final void resetFactory() {
76      this.factory = Lazy.lazy(this::newFactoryInstance);
77    }
78  
79    @Override
80    protected void configurationChanged(IMutableConfiguration<DeserializationFeature<?>> config) {
81      super.configurationChanged(config);
82      resetFactory();
83    }
84  
85    /**
86     * Get a JSON factory instance.
87     * <p>
88     * This method can be used by sub-classes to create a customized factory
89     * instance.
90     *
91     * @return the factory
92     */
93    @SuppressWarnings("resource")
94    @NonNull
95    protected XMLInputFactory2 newFactoryInstance() {
96      XMLInputFactory2 retval = (XMLInputFactory2) XMLInputFactory.newInstance();
97      assert retval instanceof WstxInputFactory;
98      retval.configureForXmlConformance();
99      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 }