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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | < 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | @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].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | public class SearchResultsTest
{
private SearchResults searchResults;
@Before
public void before() throws Exception
{
String filename = "/SearchResults.xml" ;
InputStream istream = getClass().getResourceAsStream(filename);
assertNotNull(String.format( "Cannot find classpath resource \"%s\"" , filename), istream);
JAXBContext jc = JAXBContext.newInstance(SearchResults. class );
Unmarshaller u = jc.createUnmarshaller();
InputSource isource = new InputSource(istream);
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/