JAXB, Custom XML to Java Map using XMLAdapter

In some cases XML structure that is specific to a Java container is very different, e.g. Map. And we want to bind our Java classes with a customized and more readable XML, JAXB XmlJavaTypeAdapter annotation comes very handy in such situations. Such mappings require an adapter class, written as an extension of XmlAdapter, and XmlJavaTypeAdapter annotation suggests JAXB to use the adapter at specified location:

For illustration purpose, we’ll have a custom XML:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<profile>
	<messages>
		<message id="1">
			<subject>hi</subject>
			<body>wat's up mike.. r u gonna catch us tonight?</body>
		</message>
		<message id="2">
			<subject>re:hi</subject>
			<body>My apologies, forgot to tell ya, I'm out of town!!!</body>
		</message>
	</messages>
</profile>

And this is our JAXB annotated message class(minimal):


public class Message {
	@XmlAttribute
	private String id;
	@XmlElement
	private String subject;
	@XmlElement
	private String body;
}

And the profile class with required annotations, note that we have a HashMap of Message where message id is the key and Message object itself as a value. Also note the use of @XmlJavaTypeAdapter which provides mapping between custom XML and HashMap during the process of marshaling/ unmarshaling. Going forward we’ll see how we are going to achieve that.

@XmlRootElement(name="profile")
public class Profile {
    @XmlElement
    @XmlJavaTypeAdapter(MessageAdapter.class)
    private HashMap<String, Message> messages;
    
    public Profile(){}
    public Profile(HashMap<String, Message> b ){
    	messages = b;
    }
}

To fill the gap we need to provide something that JAXB knows how to handle, we can use a wrapper class which contains Array of objects of type Messagse.

public class Messages {
    @XmlElement(name="message")
    public Message[] messages;
}

Now you must have got a fair idea that how JAXB is going to read XML messages to Message array back and forth. Let’s write an adapter to map our Array to a HasMap.

public class MessageAdapter extends XmlAdapter<Messages,Map<String, Message>> {
    @Override
    public Map<String, Message> unmarshal( Messages value ){
    	Map<String, Message> map = new HashMap<String, Message>();
        for( Message msg : value.messages )
            map.put( msg.getId(), msg );
        return map;
    }  

    @Override
    public Messages marshal( Map<String, Message> map ){
        Messages msgCont = new Messages();
        Collection<Message> msgs = map.values();
        msgCont.messages = msgs.toArray(new Message[msgs.size()]);
        return msgCont;
    }
}

And here is the test class to assert our assumptions:

public class XmlAdapterTest extends TestCase{
	
	public void testAdapter() throws Exception {
		InputStream is = this.getClass().getClassLoader().getResourceAsStream("profile.xml");
		if (is != null) {
			JAXBContext jc;
			try {
				//test unmarshaling
				jc = JAXBContext.newInstance(Profile.class.getPackage().getName());
				Unmarshaller u = jc.createUnmarshaller();
				Profile profile = (Profile) u.unmarshal(is);
				assertNotNull(profile.getMessages());
				assertEquals( 2, profile.getMessages().size());
				
				//test marshaling
				Marshaller marshaller=jc.createMarshaller();
				File xmlDocument = new File("output.xml");
				marshaller.marshal(profile, new FileOutputStream(xmlDocument));
				assertTrue(xmlDocument.length() > 0);
				xmlDocument.delete();
			} catch (JAXBException e) {
				e.printStackTrace();
				fail();
			}
		}
	}
}

Still need the source code, find here: jaxb-typeadapter source, Sushant