March 11, 2013

JAXB Unmarshalling To a Sorted Map

Introduction

While using JAXB for my last project, I came across the need to access a list of data in two ways.  First, the data was to be accessed as a sorted list with the sort order being determined by the /Book[@SortOrder] attribute. Second, the data was to be access as a map, supplying a key value from /Book[@Id] to get a specific book by key. With these two requirements, a LinkedHashMap object is clearly the way to go. Book objects are added to the LinkedHashMap in /Book[@SortOrder] order and /Book[@Id] is the map key. My initial thought was to let JAXB unmarshal the XML into a regular List then as a post processing step I would reorder the list and create a map.  But the more I thought about this the more I didn't like this solution.  Since JAXB is doing the work of unmarshalling the XML, I figured JAXB should be able to put it into a LinkedHashMap like I wanted. The purpose of this article is to demonstrate how to get JAXB to create a LinkedHashMap for you so you can access a list of data in both by key and as a correctly sorted list.

The XML

 Let's first consider a simple XML document to demonstrate what's needed.  Below is sample XML document representing search results for books. The list of books has a [@SortOrder] and an [@Id].  The application will need to be able to display the books in the correct sort order but also be able to lookup books quickly by id.
<SearchResults>
    <Books>
        <Book Id="794N-98" SortOrder="5">
            <Title>Open to the Sky</Title>
        </Book> 
        <Book Id="998X-78" SortOrder="2">
            <Title>Mars, Friend or Foe?</Title>
        </Book>
        <Book Id="445M-09" SortOrder="3">
            <Title>Jumping Beans Jump</Title>
        </Book>  
        <Book Id="002I-11" SortOrder="1">
            <Title>Little Worms Go To School</Title>
        </Book> 
        <Book Id="951G-42" SortOrder="4">
            <Title>America, The Early Years</Title>
        </Book> 
    </Books>
</SearchResults>


The Classes

In order to get JAXB to create a LinkedHashMap for you automatically, you will need need the following classes, with the BooksAdapter class one really doing all of the work for you:

SearchResults.java

Unmarshalled object for the <SearchResults> tag
@XmlRootElement(name="SearchResults")
public class SearchResults 
{
	@XmlElement(name="Books")
	@XmlJavaTypeAdapter(BooksAdapter.class)
	private Map<String, Book> books;
	public Map<String, Book> getBooksMap() {
		return books;
	}
	
	public List<Book> getBooksList() {
		return new ArrayList<Book>(books.values());
	}
}

Books.java

Unmarshalled object for the <Books> tag. This is initially needed by JAXB to convert all of the <Book>  elements into a list.  This class is then later used by BooksAdapter to convert it into a LinkedHashMap.
@XmlRootElement(name="Books")
public class Books 
{
	private List<Book> books;
	
	@XmlElement(name="Book")
	public List<Book> getBooks() {
		return books;
	}
	
	public void setBooks(List<Book> books) {
		this.books = books;
	}
}

Book.java

Unmarshalled object for the <Book> tag
@XmlRootElement(name="Book")
public class Book 
{
	@XmlAttribute(name="SortOrder")
	private Integer sortOrder;
	public Integer getSortOrder() {
		return sortOrder;
	}

	
	@XmlAttribute(name="Id")
	private String id;
	public String getId() {
		return id;
	}
	
	@XmlElement(name="Title")
	private String title;
	public String getTitle() {
		return title;
	}
}

BooksAdapter.java

An XmlAdapter implementation which converts the Books object into a LinkedHashMap object where the Book objects are put into the map by key /Book[@Id] and in the order specified by /Book[@SortOrder]. 
public class BooksAdapter extends XmlAdapter<Books, Map<String, Book>> {
	
	@Override
	public Map<String, Book> unmarshal(Books books) throws Exception {
		Collections.sort(books.getBooks(), new BookComparatorBySortOrder());
		Map<String, Book> map = new LinkedHashMap<String,Book>();
        for (Book book : books.getBooks()) {
            map.put(book.getId(), book);
        }
        return map;
	}

	@Override
	public Books marshal(Map<String, Book> map) throws Exception {
		Books books = new Books();
		books.setBooks(new LinkedList<Book>(map.values()));
		return books;
	}
}

class BookComparatorBySortOrder implements Comparator<Book> {
	@Override
	public int compare(Book o1, Book o2) {
		return o1.getSortOrder().compareTo(o2.getSortOrder());
	}
	
}

 

Testing

Testing this is pretty easy, just supply the XML document and have JAXB do it's thing. Here is a unit test:

SearchResultsTest.java 

public class SearchResultsTest 
{
	private SearchResults searchResults; 
	
	@Before
	public void before() throws Exception 
	{
		// Read file from classpath
		String filename = "/SearchResults.xml";
    	InputStream istream = getClass().getResourceAsStream(filename);
    	assertNotNull(String.format("Cannot find classpath resource \"%s\"", filename), istream);
    	
    	// Prepare JAXB objects
		JAXBContext jc = JAXBContext.newInstance(SearchResults.class);
		Unmarshaller u = jc.createUnmarshaller();

		// Prepare the input
		InputSource isource = new InputSource(istream);

		// Do unmarshalling
		searchResults = (SearchResults)u.unmarshal(isource);
		
        assertNotNull(String.format("Unmarshal returned null for SearchResults object"), searchResults);
        
        istream.close();
        istream = null;
	}
	
	@Test
	public void testBookFindById() 
	{
		
		Map<String, Book> books = searchResults.getBooksMap();
		assertNotNull(books);
		assertEquals(5, books.size());
		
		assertEquals("Little Worms Go To School",books.get("002I-11").getTitle());
		assertEquals("Mars, Friend or Foe?", 	 books.get("998X-78").getTitle());
		assertEquals("Jumpping Beans Jump", 	 books.get("445M-09").getTitle());
		assertEquals("America, The Early Years", books.get("951G-42").getTitle());
		assertEquals("Open to the Sky", 		 books.get("794N-98").getTitle());	
	}
	
	@Test
	public void testBookSorted() 
	{
		
		List<Book> books = searchResults.getBooksList();
		assertNotNull(books);
		assertEquals(5, books.size());
		
		assertEquals(new Integer(1), books.get(0).getSortOrder());
		assertEquals(new Integer(2), books.get(1).getSortOrder());
		assertEquals(new Integer(3), books.get(2).getSortOrder());
		assertEquals(new Integer(4), books.get(3).getSortOrder());
		assertEquals(new Integer(5), books.get(4).getSortOrder());		
	}

}

Download

You can download a Maven project of this from sourceforge:

http://ferris.cvs.sourceforge.net/viewvc/ferris/ferris-jaxb-sortedmap/