JAXB unmarshalling multiple XML elements into single class

50,046

Solution 1

@XmlElementWrapper will do the job:

@XmlElementWrapper(name="Wrapper")
@XmlElement(name="Channel")
private List<Channel> channels;

For more advanced cases you can use the @XmlPath extension in EclipseLink JAXB (MOXy):


Here is what I have so far. I'm still trying to eliminate the need for the helper objects. This example requires EclipseLink JAXB (MOXy).

Model Objects

Your model objects are:

package example;

import java.util.ArrayList;
import java.util.List;

public class Wrapper {

    private List<Channel> channels = new ArrayList<Channel>();

    public List<Channel> getChannels() {
        return channels;
    }

    public void setChannels(List<Channel> channels) {
        this.channels = channels;
    }

}

and:

package example;

import javax.xml.bind.annotation.XmlID;

public class Channel {

    private String id;
    private String type;
    private String name;

    @XmlID
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

Helper Objects

My current solution involves some helper objects:

package example.adapted;

import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;

import example.Channel;
import example.Wrapper;

@XmlRootElement(name="Output")
@XmlType(propOrder={"channels", "channelNames"})
public class AdaptedWrapper {

    private Wrapper wrapper = new Wrapper();
    private List<ChannelName> channelNames;

    @XmlTransient
    public Wrapper getWrapper() {
        for(ChannelName channelName : channelNames) {
            channelName.getChannel().setName(channelName.getName());
        }
        return wrapper;
    }

    @XmlElementWrapper(name="Wrapper")
    @XmlElement(name="Channel")
    public List<Channel> getChannels() {
        return wrapper.getChannels();
    }

    public void setChannels(List<Channel> channels) {
        wrapper.setChannels(channels);
    }

    @XmlElementWrapper(name="Wrapper")
    @XmlElement(name="ChannelName")
    public List<ChannelName> getChannelNames() {
        return channelNames;
    }

    public void setChannelNames(List<ChannelName> channelNames) {
        this.channelNames = channelNames;
    }

}

and:

package example.adapted;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlIDREF;

import example.Channel;

public class ChannelName {

    private String name;
    private Channel channel;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @XmlIDREF
    @XmlElement(name="id")
    public Channel getChannel() {
        return channel;
    }

    public void setChannel(Channel channel) {
        this.channel = channel;
    }

}

Demo Code

package example;

import java.io.File;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;

import example.adapted.AdaptedWrapper;

public class Demo {

    public static void main(String[] args) throws Exception {
        JAXBContext jc = JAXBContext.newInstance(AdaptedWrapper.class);

        File xml = new File("input.xml");
        Unmarshaller unmarshaller = jc.createUnmarshaller();
        AdaptedWrapper adaptedWrapper = (AdaptedWrapper) unmarshaller.unmarshal(xml);
        Wrapper wrapper = adaptedWrapper.getWrapper();

        for(Channel channel : wrapper.getChannels()) {
            System.out.println(channel.getName());
        }
    }

}

Solution 2

You can save your coding time by automating this process in JAXB:

Create a XML schema for your XML using the link below as save it as output.xsd file: http://www.xmlforasp.net/CodeBank/System_Xml_Schema/BuildSchema/BuildXMLSchema.aspx

Run the batch script file (name it as output.bat) below from the project root folder (.) using JDK as only JDK has xjc.exe tool (fill in the necessary details):

"C:\Program Files\Java\jdk1.6.0_24\bin\xjc.exe" -p %1 %2 -d %3

where...

syntax: output.bat %1 %2 %3
%1 = target package name
%2 = full file path name of the generated XML schema .xsd 
%3 = root source folder to store generated JAXB java files

Example:

let say project folder is organized as follows:

.
\_src

Run following at command prompt from (.):

output.bat com.project.xml .\output.xsd .\src

It will create a few files:

.
\_src
  \_com
    \_project
      \_xml
        |_ObjectFactory.java
        |_Output.java

Then, you can create a few useful methods below to manipulate Output objects:

private JAXBContext jaxbContext = null;
private Unmarshaller unmarshaller = null;
private Marshaller marshaller = null;

public OutputManager(String packageName) {
    try {
        jaxbContext = JAXBContext.newInstance(packageName);
        unmarshaller = jaxbContext.createUnmarshaller();
        marshaller = jaxbContext.createMarshaller();
    } catch (JAXBException e) {
    }
}

public Output loadXML(InputStream istrm) {

    Output load = null;

    try {
        Object o = unmarshaller.unmarshal(istrm); 

        if (o != null) {

            load = (Output) o;

        }

    } catch (JAXBException e) {

        JOptionPane.showMessageDialog(null, e.getLocalizedMessage(), e.getClass().getSimpleName(), JOptionPane.ERROR_MESSAGE);

    }
    return load;
}

public void saveXML(Object o, java.io.File file) {

    Output save = null;

    try {
        save = (Output) o;

        if (save != null) {
            marshaller.marshal(save, file);
        }

    } catch (JAXBException e) {

        JOptionPane.showMessageDialog(null, e.getLocalizedMessage(), e.getClass().getSimpleName(), JOptionPane.ERROR_MESSAGE);

    }
}

public void saveXML(Object o, FileOutputStream ostrm) {

    Output save = null;

    try {

        save = (Output) o;

        if (save != null) {
            marshaller.marshal(save, ostrm);
        }

    } catch (JAXBException e) {

        JOptionPane.showMessageDialog(null, e.getLocalizedMessage(), e.getClass().getSimpleName(), JOptionPane.ERROR_MESSAGE);

    }
}
Share:
50,046
andyb
Author by

andyb

Coding since 1993. Always try to remember that the first implementation is almost never the most elegant solution. I like writing clever but not overly complex code. "Simplicity is prerequisite for reliability" - Edsger W. Dijkstra Most of the principles I have adopted are listed in The Principles of Good Programming.

Updated on March 17, 2020

Comments

  • andyb
    andyb about 4 years

    I have the following XML structure, which is modelling a single concept across multiple XML elements. This format is not in my control.

    <Output>
      <Wrapper>
        <Channel>
          <id>1</id>
          <type>x</type>
        </Channel>
        <Channel>
          <id>2</id>
          <type>y</type>
        </Channel>
        <ChannelName>
          <id>1</id>
          <name>Channel name</name>
        </ChannelName>
        <ChannelName>
          <id>2</id>
          <name>Another channel name</name>
        </ChannelName>
      </Wrapper>
    </Output>
    

    I want to model this in a database that I do have control over and can have a more simple Channel table with id, type and name fields. Therefore I would like to unmarshal into a single List<Channel> on the Wrapper class.

    Can this be done with @Xml... annotations automatically? I am currently using JAXB to unmarshal into separate @XmlElement(name="Channel") and @XmlElement(name="ChannelName") class lists and then post-processing the transient ChannelName/name on the Channel but I am thinking there must be an easier automated way to map these elements. Or is it a job for XSLT?

    It might help to know that the XML is coming in as an HTTP file POST file and I'm using Spring 3, Java and Hibernate. I'm hoping something in EclipseLink JAXB (MOXy) might help :)

  • andyb
    andyb about 13 years
    Was hoping you'd be on @Blaise :) I had a look at XmlPath but couldn't work out how to look up a name for a given id. So when unmarshalling the first Channel, the XPath I want to pick up the name is ChannelName[id="1"]/name but couldn't see how to plug the id in. I'll have a look at your blog right now.
  • bdoughan
    bdoughan about 13 years
    @andyb - We're currently working on a related feature (see bugs.eclipse.org/339596). The initial phase is aimed at conditions on the attribute (i.e. ChannelName[@id="1"]/Name). Do you need to marshal to or just unmarshal?
  • andyb
    andyb about 13 years
    @Blaise - I only need to unmarshal and it looks like the predicate feature might be the solution since I need to grab data from elements elsewhere in the document? Also, I've looked at your (excellent) blog and have almost got MOXy working in Spring. Took a while for me to understand about the jaxb.properties and jaxb.index files. I am currently getting a class org.springframework.oxm.jaxb.Jaxb2Marshaller$ByteArrayDataSo‌​urce requires a zero argument constructor exception though. I'm using Eclipse Persistence Services 2.3.0.v20110312-r9123
  • andyb
    andyb about 13 years
    OK, so I have solved the zero argument constructor error. It was due to my misunderstanding of what should go in the jaxb.index file and I had listed my XMLHelper class as well! See MOXy Spring JAXB Annotations guide if anyone wants to know the example I was following.
  • andyb
    andyb about 13 years
    Many thanks for this @Blaise. I am away from my code at the moment but will try this out tomorrow. I might be almost there with the predicate solution you suggested. I am planning to run the XML through a simple XSLT first to add the attributes necessary to link the elements together.
  • bdoughan
    bdoughan about 13 years
    XSLT is a good approach that integrates cleanly with JAXB: stackoverflow.com/questions/5112404/…
  • andyb
    andyb about 13 years
    This is useful but I do not think it will solve my problem as I really want to write as little code as possible but also manipulate the XML structure at bind time. Writing as little code as possible is not because I am lazy, I just truly think it is unnecessary and also adds complexity and requires more extensive testing. Simple, elegant code makes me happy. However, +1 for the answer as I was not aware of the link or tool you mentioned.
  • andyb
    andyb about 13 years
    Thanks for the XSLT link and the answer update. I suspected that it might require some helper objects. I have decided to use XSLT to bend the XML into a nicer structure before binding, mainly as I know XSLT/XPath well but it also doesn't involve extra objects and is less complex. I don't think that the MOXy predicate mapping feature is suitable since I need to combine XML elements based on a variable attribute value match on the predicate. i.e. For Channel@id=1 I would need to find ChannelName[@id="1"]/Name and then a different element for @id=2
  • andyb
    andyb about 13 years
    That said, I'm still going to use MOXy as I like the @XmlPath annotation. I'll also follow it's development. I just think I am attempting to bind and re-organise a very flat XML structure at the same time is too complex without either using helper objects or transformation. I thought I could get away with some very clever annotations. Anyway, thanks again and answer accepted :-)
  • eee
    eee about 13 years
    @andyb: Maybe you can automate first and later edit the generated files.
  • bdoughan
    bdoughan about 13 years
    @andyb - The predicate feature is now available, for more information see: bdoughan.blogspot.com/2011/03/…