java use StAX to get children elements in a generic fashion

11,901

Solution 1

I don't really understand what you're trying to do, but if you want the local name of the tag causing a START_ELEMENT event, you can do it like this:

if (event.getEventType() == START_ELEMENT) {
    QName qname = event.asStartElement().getName()
    System.out.println("Start of element " + qname.getLocalPart());
}

Likewise, asEndElement, asCharacters, etc provide access to other types of nodes.

Personally, I usually find that the XMLStreamReader is handier for me in most situations, but I suppose that depends on the use case, as well as your own personal preferences. A pro tip is that the stricter the schema, the easier the data is to parse with StAX.

You may also want to look at JAX-B for automatic XML data binding.

Edit: Here's a naïve recursive descent StAX parser for the XML in the OP:

@Test
public void recursiveDescentStaxParser( ) throws XMLStreamException,
        FactoryConfigurationError
{
    String msg = "<ns1:Root xmlns:ns1=\"http://rootNameSpace.com/\"><ns1:A/><ns1:B><Book xmlns=\"http://www.myNameSpace.com\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><Data><Author>John</Author><Edition>1</Edition><PubHouse>Small Publishing House</PubHouse><Price>37.8</Price></Data></Book></ns1:B></ns1:Root>";
    XMLStreamReader reader = XMLInputFactory.newFactory( )
            .createXMLStreamReader( new StringReader( msg ) );

    reader.nextTag( );
    readRoot( reader );

}

private void readRoot( XMLStreamReader reader ) throws XMLStreamException
{
    while ( reader.nextTag( ) == XMLEvent.START_ELEMENT )
    {
        QName name = reader.getName( );
        if ( "B".equals( name.getLocalPart( ) ) )
            readBooks( reader );
        else
            reader.nextTag( ); // Empty <A>

    }
}

private void readBooks( XMLStreamReader reader ) throws XMLStreamException
{
    while ( reader.nextTag( ) == XMLEvent.START_ELEMENT )
    {
        QName name = reader.getName( );
        if ( !"Book".equals( name.getLocalPart( ) ) )
            throw new XMLStreamException( name.toString( ) );
        reader.nextTag( ); // Jump to <Data>
        readBook( reader );
        reader.nextTag( ); // Jump to </B>
    }
}

private void readBook( XMLStreamReader reader ) throws XMLStreamException
{
    reader.nextTag( ); // Skip to <Author>
    System.out.println( "Author: " + reader.getElementText( ) );
    reader.nextTag( ); // Skip to <Edition>
    System.out.println( "Edition: " + reader.getElementText( ) );
    reader.nextTag( ); // Skip to <PubHouse>
    System.out.println( "Publisher: " + reader.getElementText( ) );
    reader.nextTag( ); // Skip to <Price>
    System.out.println( "Price: " + reader.getElementText( ) );
    reader.nextTag( ); // Skip to </Book>

}

Writing stuff like this doesn't only make the code a lot easier to read and reason about, but also the stack traces when errors pop up.

Solution 2

It sounds like you may have chosen the wrong tool here: Stax is a great API to use for efficient handling of large content. But if convenience is more important than efficiency, yes, you probably should consider a tree model (not DOM necessarily, XOM is better for example) or data binding (JAXB or XStream). Specifically, Stax like SAX are stream-based so you only see whatever is the current event or token. There are no accessors for children or parents because there is no guaranteed way to get to them, as that is not necessarily possible considering current stream position.

But if performance or memory usage are a concern, you can still either consider JAXB (which is typically more efficient than tree models like DOM), or StaxMate. StaxMate is high-performance, low memory usage extension over Stax, and is bit more convenient to use. While you still need to iterate over elements in document order, its cursor approach maps more naturally with parent-then-children lookups. So it might work for your case.

Share:
11,901
Cratylus
Author by

Cratylus

Updated on August 21, 2022

Comments

  • Cratylus
    Cratylus over 1 year

    I am trying to use StAX (I already dislike it....)
    It seems that the only way to use it is by continuous if-else conditions.
    But most important it seems there is no way to associate an element with its children unless one knows beforehand the structure of the xml document being parsed.Is this correct?
    I have tried the following: I have this xml in a String

    <ns1:Root xmlns:ns1=\"http://rootNameSpace.com/\">
    <ns1:A/>
    <ns1:B>
            <Book xmlns=\"http://www.myNameSpace.com\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">
                <Data>
                    <Author>John</Author>
                    <Edition>1</Edition>
                    <PubHouse>Small Publishing House</PubHouse>
                    <Price>37.8</Price>
                </Data>
            </Book>
    </ns1:B>
    </ns1:Root>
    

    I would like to use StAX to get the Book element, but it seems I can only write code that has hardcoded all the structure.
    I.e. Use XMLEventReader and once you get Book, start looping for Data,Author etc.
    Is there a generic solution on this?
    I tried the following to get arround this: I tried to go from String to XMLEventReader and back to String but I can not get the exact String representation that I originally used (the namespaces are in brackets, extra colons etc).

    StringBuilder xml = new StringBuilder();
    XMLInputFactory inputFactory = XMLInputFactory.newInstance();
    String msg = "<ns1:Root xmlns:ns1=\"http://rootNameSpace.com/\"><ns1:A/><ns1:B><Book xmlns=\"http://www.myNameSpace.com\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><Data><Author>John</Author><Edition>1</Edition><PubHouse>Small Publishing House</PubHouse><Price>37.8</Price></Data></Book></ns1:B></ns1:Root>";
    InputStream input = new ByteArrayInputStream(msg.getBytes("UTF-8"));
    XMLEventReader xmlEventReader = inputFactory.createXMLEventReader(input);
    while (xmlEventReader.hasNext())
    {
    
        XMLEvent event = xmlEventReader.nextEvent();
        StringWriter sw = new StringWriter();
        event.writeAsEncodedUnicode(sw);
       xml.append(sw);
    
    }
    System.out.println(xml);
    

    I get the following:

    <?xml version="1.0" encoding='UTF-8' standalone='no'?><['http://rootNameSpace.com/']:ns1:Root xmlns:ns1='http://rootNameSpace.com/'><['http://rootNameSpace.com/']:ns1:A></ns1:A><['http://rootNameSpace.com/']:ns1:B><['http://www.myNameSpace.com']::Book xmlns:='http://www.myNameSpace.com' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'><['http://www.myNameSpace.com']::Data><['http://www.myNameSpace.com']::Author>John</Author><['http://www.myNameSpace.com']::Edition>1</Edition><['http://www.myNameSpace.com']::PubHouse>Small Publishing House</PubHouse><['http://www.myNameSpace.com']::Price>37.8</Price></Data></Book></ns1:B></ns1:Root>
    

    Can this case be addressed via StAX or DOM is the only solution?