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/