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  public class DefaultXmlDeserializer<CLASS extends IBoundObject>
38      extends AbstractDeserializer<CLASS> {
39    private Lazy<XMLInputFactory2> factory;
40  
41    @NonNull
42    private final IBoundDefinitionModelAssembly rootDefinition;
43  
44    /**
45     * Construct a new Module binding-based deserializer that reads XML-based Module
46     * content.
47     *
48     * @param definition
49     *          the assembly class binding describing the Java objects this
50     *          deserializer parses data into
51     */
52    @SuppressFBWarnings(value = "CT_CONSTRUCTOR_THROW", justification = "Use of final fields")
53    public DefaultXmlDeserializer(@NonNull IBoundDefinitionModelAssembly definition) {
54      super(definition);
55      this.rootDefinition = definition;
56      if (!definition.isRoot()) {
57        throw new UnsupportedOperationException(
58            String.format("The assembly '%s' is not a root assembly.", definition.getBoundClass().getName()));
59      }
60      resetFactory();
61    }
62  
63    protected final void resetFactory() {
64      this.factory = Lazy.lazy(this::newFactoryInstance);
65    }
66  
67    @Override
68    protected void configurationChanged(IMutableConfiguration<DeserializationFeature<?>> config) {
69      super.configurationChanged(config);
70      resetFactory();
71    }
72  
73    /**
74     * Get a JSON factory instance.
75     * <p>
76     * This method can be used by sub-classes to create a customized factory
77     * instance.
78     *
79     * @return the factory
80     */
81    @SuppressWarnings("resource")
82    @NonNull
83    protected XMLInputFactory2 newFactoryInstance() {
84      XMLInputFactory2 retval = (XMLInputFactory2) XMLInputFactory.newInstance();
85      assert retval instanceof WstxInputFactory;
86      retval.configureForXmlConformance();
87      retval.setProperty(XMLInputFactory.IS_COALESCING, false);
88      retval.setProperty(XMLInputFactory2.P_PRESERVE_LOCATION, true);
89      // xmlInputFactory.configureForSpeed();
90  
91      if (isFeatureEnabled(DeserializationFeature.DESERIALIZE_XML_ALLOW_ENTITY_RESOLUTION)) {
92        retval.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true);
93        retval.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true);
94        retval.setProperty(XMLInputFactory.SUPPORT_DTD, true);
95        retval.setProperty(XMLInputFactory.RESOLVER,
96            (XMLResolver) (publicID, systemID, baseURI, namespace) -> {
97              URI base = URI.create(baseURI);
98              URI resource = base.resolve(systemID);
99              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 }