Search code examples
stax

StAX and namespaces


I am trying to convert some code from using DOM (via jDOM) to use StAX instead. At the same time I am migrating from DTD-based validation to XSD_based validation. Oh, and just for good measure I am introducing JAXB into the equation :)

Anyway, as an interim migration step I would like to allow users to still provide legacy documents (aka, using DTD and therefore no namespace). I will still validate the document using XSD, so the DTD is ignored. This works except that StAX (nor JAXB) seems to not like the non-namespaced document. I tried disabling namespace support (using javax.xml.stream.isNamespaceAware), but that did not have any effect. Explicitly adding xmlns to the document root fixed the problem, so I am fairly confident it is a namespacing issue.

Is there a way using StAX XMLEventReader to "introduce" a default namespace? Something along the lines of this approach (which is SAX specific), but for StAX...

Or any other ideas on how to achieve that?

An example document looks like:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
    "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="org.hibernate.test.abstractembeddedcomponents.cid">
    ...
</hibernate-mapping>

The code I am currently using to read these documents is:

public JaxbRoot unmarshal(InputStream stream, Origin origin) {
    try {
        XMLEventReader staxReader = staxFactory().createXMLEventReader( stream );
        try {
            return unmarshal( staxReader, origin );
        }
        finally {
            try {
                staxReader.close();
            }
            catch ( Exception ignore ) {
            }
        }
    }
    catch ( XMLStreamException e ) {
        throw new MappingException( "Unable to create stax reader", e, origin );
    }
}

private XMLInputFactory staxFactory;

private XMLInputFactory staxFactory() {
    if ( staxFactory == null ) {
        staxFactory = buildStaxFactory();
    }
    return staxFactory;
}

@SuppressWarnings( { "UnnecessaryLocalVariable" })
private XMLInputFactory buildStaxFactory() {
    XMLInputFactory staxFactory = XMLInputFactory.newInstance();
    // tried with and without, no effect
    //staxFactory.setProperty( "javax.xml.stream.isNamespaceAware", false );
    return staxFactory;
}

@SuppressWarnings( { "unchecked" })
private JaxbRoot unmarshal(XMLEventReader staxEventReader, final Origin origin) {
    XMLEvent event;
    try {
        event = staxEventReader.peek();
        while ( event != null && !event.isStartElement() ) {
            staxEventReader.nextEvent();
            event = staxEventReader.peek();
        }
    }
    catch ( Exception e ) {
        throw new MappingException( "Error accessing stax stream", e, origin );
    }

    if ( event == null ) {
        throw new MappingException( "Could not locate root element", origin );
    }

    final Schema validationSchema;
    final Class jaxbTarget;

    final String elementName = event.asStartElement().getName().getLocalPart();

    if ( "entity-mappings".equals( elementName ) ) {
        final Attribute attribute = event.asStartElement().getAttributeByName( ORM_VERSION_ATTRIBUTE_QNAME );
        final String explicitVersion = attribute == null ? null : attribute.getValue();
        validationSchema = validateXml ? resolveSupportedOrmXsd( explicitVersion ) : null;
        jaxbTarget = JaxbEntityMappings.class;
    }
    else {
        validationSchema = validateXml ? hbmSchema() : null;
        jaxbTarget = JaxbHibernateMapping.class;
    }

    final Object target;
    final ContextProvidingValidationEventHandler handler = new ContextProvidingValidationEventHandler();
    try {
        JAXBContext jaxbContext = JAXBContext.newInstance( jaxbTarget );
        Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
        unmarshaller.setSchema( validationSchema );
        unmarshaller.setEventHandler( handler );
        target = unmarshaller.unmarshal( staxEventReader );
    }
    catch ( JAXBException e ) {
        throw new MappingException( ... );
    }

    return new JaxbRoot( target, origin );
}

In my testing the DTD being there or not has no effect. And like I said before, simply changing

<hibernate-mapping package="org.hibernate.test.abstractembeddedcomponents.cid">

to

<hibernate-mapping xmlns="http://www.hibernate.org/xsd/hibernate-mapping" package="org.hibernate.test.abstractembeddedcomponents.cid">

fixes the failures I see, which are:

[org.xml.sax.SAXParseException: cvc-elt.1: Cannot find the declaration of element 'hibernate-mapping'.]
    at ...
Caused by: org.xml.sax.SAXParseException: cvc-elt.1: Cannot find the declaration of element 'hibernate-mapping'.
    at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.createSAXParseException(ErrorHandlerWrapper.java:195)
    at com.sun.org.apache.xerces.internal.util.ErrorHandlerWrapper.error(ErrorHandlerWrapper.java:131)
    at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:384)
    at com.sun.org.apache.xerces.internal.impl.XMLErrorReporter.reportError(XMLErrorReporter.java:318)
    at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.handleStartElement(XMLSchemaValidator.java:1916)
    at com.sun.org.apache.xerces.internal.impl.xs.XMLSchemaValidator.startElement(XMLSchemaValidator.java:705)
    at com.sun.org.apache.xerces.internal.jaxp.validation.ValidatorHandlerImpl.startElement(ValidatorHandlerImpl.java:550)
    at com.sun.xml.internal.bind.v2.runtime.unmarshaller.ValidatingUnmarshaller.startElement(ValidatingUnmarshaller.java:78)
    at com.sun.xml.internal.bind.v2.runtime.unmarshaller.InterningXmlVisitor.startElement(InterningXmlVisitor.java:60)
    at com.sun.xml.internal.bind.v2.runtime.unmarshaller.StAXEventConnector.handleStartElement(StAXEventConnector.java:247)
    at com.sun.xml.internal.bind.v2.runtime.unmarshaller.StAXEventConnector.bridge(StAXEventConnector.java:116)
    at com.sun.xml.internal.bind.v2.runtime.unmarshaller.UnmarshallerImpl.unmarshal0(UnmarshallerImpl.java:394)
    ... 27 more

Solution

  • This can be done by implementing a filter which adds a default namespace declaration to the first (i.e. root) StartELement event. StAX already provides the EventReaderDelegate utility class, where the peek() and nextEvent() methods need to be overridden.

    Here's the code:

    import java.util.ArrayList;
    import java.util.Iterator;
    import java.util.List;
    
    import javax.xml.namespace.QName;
    import javax.xml.stream.XMLEventFactory;
    import javax.xml.stream.XMLEventReader;
    import javax.xml.stream.XMLStreamException;
    import javax.xml.stream.events.StartElement;
    import javax.xml.stream.events.XMLEvent;
    import javax.xml.stream.util.EventReaderDelegate;
    
    /**
     * Filter adding default namespace declaration to root element.
     */
    public class NamespaceAddingEventReader extends EventReaderDelegate {
        private final XMLEventFactory factory = XMLEventFactory.newInstance();
        private final String namespaceURI;
    
        private int startElementCount = 0;
    
        public NamespaceAddingEventReader(XMLEventReader reader, String namespaceURI) {
            super(reader);
            this.namespaceURI = namespaceURI;
        }
    
        /**
         * Duplicate event with additional namespace declaration.
         * @param startElement
         * @return event with namespace
         */
        private StartElement withNamespace(StartElement startElement) {
            List<Object> namespaces = new ArrayList<Object>();
            namespaces.add(factory.createNamespace(namespaceURI));
            Iterator<?> originalNamespaces = startElement.getNamespaces();
            while (originalNamespaces.hasNext()) {
                namespaces.add(originalNamespaces.next());
            }
            return factory.createStartElement(
                    new QName(namespaceURI, startElement.getName().getLocalPart()),
                    startElement.getAttributes(),
                    namespaces.iterator());
        }
    
        @Override
        public XMLEvent nextEvent() throws XMLStreamException {
            XMLEvent event = super.nextEvent();
            if (event.isStartElement()) {
                if (++startElementCount == 1) {
                    return withNamespace(event.asStartElement());
                }
            }
            return event;
        }
    
        @Override
        public XMLEvent peek() throws XMLStreamException {
            XMLEvent event = super.peek();
            if (startElementCount == 0 && event.isStartElement()) {
                return withNamespace(event.asStartElement());
            } else {
                return event;
            }
        }
    }
    

    To see how this is used, let's copy some XML without namespace declaration to System.out using the event API:

    StringReader xml = new StringReader("<?xml version='1.0'?><alice>bob</alice>");
    XMLEventReader reader = XMLInputFactory.newInstance().createXMLEventReader(xml);
    reader = new NamespaceAddingEventReader(reader, "http://foo");
    XMLEventWriter writer = XMLOutputFactory.newInstance().createXMLEventWriter(System.out);
    writer.add(reader);
    writer.flush();
    

    Running the code will print

    <?xml version='1.0' encoding='UTF-8'?><alice xmlns="http://foo">bob</alice>