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