StAX and namespaces

stax

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>

Problem

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 ```

Original source

Related problems