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}