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/