Search code examples
javaxmljaxbmarshalling

JAXB marshalling: Filter values of leaf elements


I've got a fairly complex JAXB tree object. For every leaf node, I need to filter its actual value

E.g.

<Book>
    <Title>Yogasana Vijnana: the Science of Yoga</Title>
    <Author>Dhirendra Brahmachari</Author>
    <Date>1966</Date>
</Book>

The leaf nodes here would be Title,author and Date.
Imagine that I need a marshalled document for this JAXB model with the first character removed for every leaf node:

<Book>
    <Title>ogasana Vijnana: the Science of Yoga</Title>
    <Author>hirendra Brahmachari</Author>
    <Date>966</Date>
</Book>


What is the best approach?
I see two starting points, however, I'm currently stuck.

1. Do the change in the JAXB model
Is there some traversal mechnism which I can use to get the leaf elements of any JAXB object (some kind of Visitor pattern or something)?

2. Hook into the marshalling
Maybe we can hook into marshalling, e.g. using a XMLStreamWriter..

Is there an elegant solution for this kind of problem?


Solution

  • Another approach based on a decorator of type XMLStreamWriter that will simply skip the first character of a text content however you won't have the ability to limit it to leaf nodes only, it will apply the same logic to all the text contents not only of leaf nodes which won't be an issue if your marshalling don't generate mixed contents like in your example. Indeed if you don't have mixed content (text content and nodes mixed together), only leaf nodes can have text content.

    Your decorator could be something like this:

    public class RemoveFirstCharacter implements XMLStreamWriter {
    
        private final XMLStreamWriter delegate;
    
        public RemoveFirstCharacter(final XMLStreamWriter delegate) {
            this.delegate = delegate;
        }
    
        @Override
        public void writeStartElement(final String localName) throws XMLStreamException {
            delegate.writeStartElement(localName);
        }
    
        @Override
        public void writeStartElement(final String namespaceURI, final String localName) 
            throws XMLStreamException {
            delegate.writeStartElement(namespaceURI, localName);
        }
    
        ...
    
        @Override
        public void writeCharacters(final String text) throws XMLStreamException {
            // Skip the first character
            delegate.writeCharacters(text.substring(1));
        }
    
        @Override
        public void writeCharacters(final char[] text, final int start, final int len)
            throws XMLStreamException {
            if (start == 0) {
                // Skip the first character
                delegate.writeCharacters(text, 1, len - 1);
            } else {
                delegate.writeCharacters(text, start, len);
            }
        }
    }
    

    Then your code would be:

    // Create the marshaller for the class Book
    JAXBContext jaxbContext = JAXBContext.newInstance(Book.class);
    Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
    
    // Create the main XMLStreamWriter
    XMLOutputFactory output = XMLOutputFactory.newInstance();
    XMLStreamWriter writer = output.createXMLStreamWriter(System.out);
    
    // Apply the custom XMLStreamWriter that will remove the first character
    // of each text content
    jaxbMarshaller.marshal(book, new RemoveFirstCharacter(writer));