|
Domain model
As we've figured out, domain model should not depend on technologies used for
serving application. That's why we can start implementing business logic
without keeping in mind any information about JAXB.
Here is UML class diagram of librarian program:
A book represented by interface given below has unique ISBN
(International Standard Book Number), title, a list of authors
and single category associated with it.
package com.intelrate.jaxb.domain;
import java.util.List;
public interface Book
{
String getISBN();
void setISBN(final String ISBN);
String getTitle();
void setTitle(final String title);
List<String> getAuthors();
void setAuthors(final List<String> authors);
Category getCategory();
void setCategory(final Category category);
}
Categories combine into hierarchical classification and simplify such
activities as search, storing, building renewal requests, etc. Thus,
each category has unique name within a system and a list of subcategories.
package com.intelrate.jaxb.domain;
import java.util.List;
public interface Category
{
String getName();
void setName(final String name);
List<Category> getSubcategories();
void addSubcategory(final Category category);
}
Implementations of both interfaces are simple and reflect only
domain logic. It makes code readable what is the most important
property of software today. Put these files into appropriate folders
inside of src/main/java:
package com.intelrate.jaxb.domain.impl;
import java.util.Collections;
import java.util.List;
import com.intelrate.jaxb.domain.Book;
import com.intelrate.jaxb.domain.Category;
public class BookImpl implements Book
{
private String ISBN;
private String title;
private List<String> authors;
private Category category;
public String getISBN()
{
return ISBN;
}
public void setISBN(final String ISBN)
{
this.ISBN = ISBN;
}
public String getTitle()
{
return title;
}
public void setTitle(final String title)
{
this.title = title;
}
public List<String> getAuthors()
{
if (authors == null) {
authors = Collections.emptyList();
}
return authors;
}
public void setAuthors(final List<String> authors)
{
this.authors = authors;
}
public Category getCategory()
{
return category;
}
public void setCategory(final Category category)
{
this.category = category;
}
}
Digressing from the main idea, I want to pay attention to getAuthors() method.
It never returns null. This general Java approach seems very reasonable to me
as opposed to checking if a reference is not equal to null each time in client
code in order to prevent NullPointerException. Collection may also be modifiable
if it is more convenient.
package com.intelrate.jaxb.domain.impl;
import java.util.ArrayList;
import java.util.List;
import com.intelrate.jaxb.domain.Category;
public class CategoryImpl implements Category
{
private String name;
private List<Category> subcategories;
public String getName()
{
return name;
}
public void setName(final String name)
{
this.name = name;
}
public List<Category> getSubcategories()
{
if (subcategories == null) {
subcategories = new ArrayList<Category>();
}
return subcategories;
}
public void addSubcategory(final Category category)
{
getSubcategories().add(category);
}
}
That's all about domain model. Of course, it's useless itself
but very meaningful and brief at one time so that everyone
can become familiar with the system extremely fast.
Moving toward to DTO (Data Transfer Object), I'd like to remind that domain
classes shouldn't depend on anything but themselves. That's why
DTO reflecting a structure of desired XML doesn't match domain classes.
To indicate this brighter I've purposely combined DTO objects
in a different way rather then domain objects.
On the diagram below CategoryDTO aggregates a list of BookDTO objects:
It's time to say a few words about Java Architecture for XML Binding (JAXB)
before we continue with the code.
Over a long period there were two dominant kinds of XML parsers based on
SAX (Simple API for XML) and DOM (Document Object Model).
The main aim of SAX is serial access to XML. DOM supplies standard API for manipulating
XML documents. In DOM whole XML is loaded at once so that we have full control
and access to all nodes of the document. SAX requires less memory because it loads
XML step by step notifying us when something is happened, e.g. new tag is opened.
Usually SAX based programs work faster. Therefore, SAX is recommended when a huge amount
of data is processed but DOM is much more comfortable.
JAXB is neither SAX nor DOM. We'll use it in such a way as to combine
advantages of full control and high performance. Evolution of DDD
exerted big influence on approaches to XML handling. As soon as
domain object is a cornerstone of DDD based system, XML should be
binded to it. Let's review BookDTO class to understand what the
JAXB is:
package com.intelrate.jaxb.dto;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "book")
public class BookDTO
{
@XmlAttribute(name = "ISBN", required = true)
private String isbn;
@XmlElement(name = "title", required = true)
private String title;
@XmlElementWrapper(name = "authors")
@XmlElement(name = "author", required = true)
private List<AuthorDTO> authors;
public String getIsbn()
{
return isbn;
}
public void setIsbn(final String isbn)
{
this.isbn = isbn;
}
public String getTitle()
{
return title;
}
public void setTitle(final String title)
{
this.title = title;
}
public List<AuthorDTO> getAuthors()
{
return authors;
}
public void setAuthors(final List<AuthorDTO> skuAuthors)
{
this.authors = skuAuthors;
}
}
Actually, BookDTO encapsulates XML so instead of
working with DOM items client code operates with instance
of POJO (Plain Old Java Object) class. Mapping between
fields and XML nodes is declared by means of JAXB annotations.
A process of transformation from Java object into XML is named
Marshalling. Reverse process is Unmarshalling.
Compare Jaxb annotations with XML nodes given in the example below:
<book ISBN="978-0201310054">
<title>Effective Java</title>
<authors>
<author>...</author>
<author>...</author>
</authors>
</book>
Root element "book" encloses piece of XML described by BookDTO.
ISBN is XML attribute. Title is a simple XML field nested in root node.
The list of authors aggregates several AuthorDTO elements. Note that
a name of each single element "author" is declared in BookDTO too.
AuthorDTO class is very simple:
package com.intelrate.jaxb.dto;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlValue;
@XmlAccessorType(XmlAccessType.NONE)
public class AuthorDTO
{
@XmlValue
private String name;
public String getName()
{
return name;
}
public void setName(final String name)
{
this.name = name;
}
}
It introduces new type of Jaxb annotation - @XmlValue.
Field marked with @XmlValue isn't simple XML node. It's just
a string value, so resulting XML for BookDTO may look like
<book ISBN="978-0201310054">
<title>Effective Java</title>
<authors>
<author>Joshua Bloch</author>
</authors>
</book>
If we had @XmlElement(name = "author", required = true) annotation
for author field, XML would look in the next way:
<book ISBN="978-0201310054">
<title>Effective Java</title>
<authors>
<author>
<author>Joshua Bloch</author>
</author>
</authors>
</book>
CategoryDTO forms hierarchical structure of categories which often occurs
in real systems. It also holds a list of books expressed in terms of BookDTO:
package com.intelrate.jaxb.dto;
import java.util.List;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElementRefs;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "category")
public class CategoryDTO
{
@XmlElement(name = "name", required = true)
private String name;
@XmlElementWrapper(name = "books")
@XmlElement(name = "book", required = true)
private List<BookDTO> books;
@XmlElementRefs({
@XmlElementRef(type = CategoryDTO.class)
})
private List<CategoryDTO> categories;
public String getName()
{
return name;
}
public void setName(final String name)
{
this.name = name;
}
public List<BookDTO> getBooks()
{
return books;
}
public void setBooks(final List<BookDTO> books)
{
this.books = books;
}
public List<CategoryDTO> getSubcategories()
{
return categories;
}
public void setCategories(final List<CategoryDTO> categories)
{
this.categories = categories;
}
}
List marked with @XmlElementRefs annotation may contain objects of
different types inherited from one common class. In our case we
keep only CategoryDTO itself: @XmlElementRef(type = CategoryDTO.class)
Here is sample resulting XML for single category without subcategories:
<category>
<name>Poetry</name>
<books>
<book ISBN="978-0691019055">
<title>Eugene Onegin</title>
<authors>
<author>Aleksandr Pushkin</author>
</authors>
</book>
</books>
</category>
There are no other DTO classes in our system. Though, combining annotations
investigated above, we can design all possible XML structures.
They are:
-
Simple XML tag: @XmlElement(name = "name", required = true)
-
Untagged XML value: @XmlValue
-
XML attribute: @XmlAttribute(name = "attribute", required = true)
-
List of nodes: @XmlElementWrapper(name = "nodes")
@XmlElement(name = "node", required = true)
-
Tree: @XmlElementRefs({ @XmlElementRef(type = CategoryDTO.class) })
-
DTO nested in another DTO: private List<BookDTO> books;
But how can we use these DTO classes to export XML if our system
operates with domain classes? Proposed approach is to use Adaptors:
Desired domain object "poetry" is supplied by application Service Layer.
Then special adaptor is used to transform domain object into JAXB DTO object
"poetryDto". Process of data copying is named population.
After that Exporter or another part of application can use DTO to marshall
it into XML. Marshaller is responsible for writing DTO object directly
into output stream which can be saved as XML file after that.
Everything what we need from BookAdapter is to copy all fields from
book object to book DTO object:
package com.intelrate.jaxb.adapter;
import java.util.ArrayList;
import java.util.List;
import com.intelrate.jaxb.domain.Book;
import com.intelrate.jaxb.dto.AuthorDTO;
import com.intelrate.jaxb.dto.BookDTO;
public class BookAdapter
{
public void populateDTO(final BookDTO bookDto, final Book book)
{
bookDto.setIsbn(book.getISBN());
bookDto.setTitle(book.getTitle());
populateAuthors(bookDto, book);
}
private void populateAuthors(final BookDTO bookDto, final Book book)
{
final List<AuthorDTO> authors = new ArrayList<AuthorDTO>();
for (String authorName : book.getAuthors())
{
final AuthorDTO author = new AuthorDTO();
author.setName(authorName);
authors.add(author);
}
bookDto.setAuthors(authors);
}
}
CategoryAdapter is a little bit more complex because it
calls itself recursively to populate all subcategories:
package com.intelrate.jaxb.adapter;
import java.util.ArrayList;
import java.util.List;
import com.intelrate.jaxb.domain.Book;
import com.intelrate.jaxb.domain.Category;
import com.intelrate.jaxb.dto.BookDTO;
import com.intelrate.jaxb.dto.CategoryDTO;
import com.intelrate.jaxb.service.CategoryService;
import com.intelrate.jaxb.service.impl.CategoryServiceImpl;
public class CategoryAdapter
{
private final BookAdapter bookAdapter = new BookAdapter();
private final CategoryService categoryService = new CategoryServiceImpl();
public void populateDTO(final CategoryDTO categoryDto, final Category category)
{
categoryDto.setName(category.getName());
populateBookList(categoryDto, category);
populateSubcategories(categoryDto, category);
}
private void populateBookList(final CategoryDTO categoryDto, final Category category)
{
List<BookDTO> bookDtoList = new ArrayList<BookDTO>();
for (Book book : categoryService.listBooksInCategory(category.getName()))
{
BookDTO bookDto = new BookDTO();
bookAdapter.populateDTO(bookDto, book);
bookDtoList.add(bookDto);
}
categoryDto.setBooks(bookDtoList);
}
private void populateSubcategories(final CategoryDTO categoryDto, final Category category)
{
List<CategoryDTO> categoryDtoList = new ArrayList<CategoryDTO>();
for (Category subCategory : category.getSubcategories())
{
CategoryDTO subCategoryDto = new CategoryDTO();
populateDTO(subCategoryDto, subCategory);
categoryDtoList.add(subCategoryDto);
}
categoryDto.setCategories(categoryDtoList);
}
}
Adapters are kind of glue between application domain
and JAXB as technology. Now the conception is narrated.
To complete our application and see how it works we
have to write environmental classes: Service, Exporter and
Application entry point.
CategoryService allows to find categories and
books inside of specified category:
package com.intelrate.jaxb.service;
import java.util.List;
import com.intelrate.jaxb.domain.Book;
import com.intelrate.jaxb.domain.Category;
public interface CategoryService
{
List<String> listCategoryNames();
Category findByName(final String name);
List<Book> listBooksInCategory(final String name);
}
We'll use fake implementation of CategoryService just to
make our example work. In real system there will be Persistence Layer instead:
package com.intelrate.jaxb.service.impl;
import java.util.Arrays;
import java.util.List;
import com.intelrate.jaxb.domain.Book;
import com.intelrate.jaxb.domain.Category;
import com.intelrate.jaxb.domain.impl.BookImpl;
import com.intelrate.jaxb.domain.impl.CategoryImpl;
import com.intelrate.jaxb.service.CategoryService;
public class CategoryServiceImpl implements CategoryService
{
private static final String CATEGORY_1 = "Computer Science";
private static final String SUBCATEGORY_1 = "Java";
private static final String CATEGORY_2 = "Poetry";
public List<String> listCategoryNames()
{
return Arrays.asList(CATEGORY_1, CATEGORY_2);
}
public Category findByName(final String name)
{
final Category category = new CategoryImpl();
category.setName(name);
if (CATEGORY_1.equals(name))
{
final Category subCategory = new CategoryImpl();
subCategory.setName(SUBCATEGORY_1);
category.addSubcategory(subCategory);
}
return category;
}
public List<Book> listBooksInCategory(final String name)
{
final Book book = new BookImpl();
book.setCategory(findByName(name));
if (CATEGORY_1.equals(name))
{
book.setTitle("Design Patterns");
book.setAuthors(Arrays.asList("Erich Gamma", "Richard Helm",
"Ralph Johnson", "John M. Vlissides"));
book.setISBN("978-0201633610");
final Book book2 = new BookImpl();
book2.setTitle("Refactoring");
book2.setAuthors(Arrays.asList("Martin Fowler"));
book2.setISBN("978-0201485677");
return Arrays.asList(book, book2);
}
else if (CATEGORY_2.equals(name))
{
book.setTitle("Eugene Onegin");
book.setAuthors(Arrays.asList("Aleksandr Pushkin"));
book.setISBN("978-0691019055");
}
else if (SUBCATEGORY_1.equals(name))
{
book.setTitle("Effective Java");
book.setAuthors(Arrays.asList("Joshua Bloch"));
book.setISBN("978-0201310054");
}
return Arrays.asList(book);
}
}
As it was stated above, our system should be efficient
because it works with huge arrays of data. That's why
we cannot put whole library in one DTO object and marshall it.
Remember that DTO object is held in memory after population.
Exporter listed below retrieves category by category using
CategoryService then populates and exports each single category
separately:
package com.intelrate.jaxb.importexport;
import java.io.FileOutputStream;
import java.io.PrintStream;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import com.intelrate.jaxb.adapter.CategoryAdapter;
import com.intelrate.jaxb.domain.Category;
import com.intelrate.jaxb.dto.CategoryDTO;
import com.intelrate.jaxb.service.CategoryService;
import com.intelrate.jaxb.service.impl.CategoryServiceImpl;
public class Exporter
{
private final CategoryService categoryService = new CategoryServiceImpl();
private final CategoryAdapter categoryAdapter = new CategoryAdapter();
public void exportLibrary(final String fileName) throws Exception
{
final FileOutputStream file = new FileOutputStream(fileName);
final PrintStream printer = new PrintStream(file);
Marshaller marshaller = prepareMarshaller(fileName);
printer.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
printer.print("<library>");
for (String categoryName : categoryService.listCategoryNames())
{
CategoryDTO categoryDTO = new CategoryDTO();
Category category = categoryService.findByName(categoryName);
categoryAdapter.populateDTO(categoryDTO, category);
marshaller.marshal(categoryDTO, file);
}
printer.println("</library>");
}
private Marshaller prepareMarshaller(final String fileName) throws Exception
{
final JAXBContext jaxbContext = JAXBContext.newInstance(CategoryDTO.class);
final Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
return marshaller;
}
}
Marshaller and JAXBContext classes are provided by JAXB library.
JAXBContext is an entry point to JAXB API. It is initialized by Types of DTO objects
passed as parameters (CategoryDTO.class) and DTO objects statically
reachable from these classes (BookDTO.class, AuthorDTO.class).
Marshaller obtained in that way can receive DTO objects known by context
and output stream to marshall XML into.
The last piece of code is the main function asking Exporter to
export library into the given file:
package com.intelrate.jaxb;
import com.intelrate.jaxb.importexport.Exporter;
public class App
{
public static void main(final String[] args) throws Exception
{
if (args.length != 2) {
System.out.println( "Usage: App -e fileName.xml" );
return;
}
final String fileName = args[1];
final Exporter exporter = new Exporter();
exporter.exportLibrary(fileName);
}
}
Congratulations! The system is finished and should be successfully compiled now.
Build the project:
mvn package
Execute resulting jar file to export the library:
java -cp target/jaxb-1.0-SNAPSHOT.jar com.intelrate.jaxb.App -e library.xml
It should meditate for a while and then produce xml file library.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<library>
<category>
<name>Computer Science</name>
<books>
<book ISBN="978-0201633610">
<title>Design Patterns</title>
<authors>
<author>Erich Gamma</author>
<author>Richard Helm</author>
<author>Ralph Johnson</author>
<author>John M. Vlissides</author>
</authors>
</book>
<book ISBN="978-0201485677">
<title>Refactoring</title>
<authors>
<author>Martin Fowler</author>
</authors>
</book>
</books>
<category>
<name>Java</name>
<books>
<book ISBN="978-0201310054">
<title>Effective Java</title>
<authors>
<author>Joshua Bloch</author>
</authors>
</book>
</books>
</category>
</category>
<category>
<name>Poetry</name>
<books>
<book ISBN="978-0691019055">
<title>Eugene Onegin</title>
<authors>
<author>Aleksandr Pushkin</author>
</authors>
</book>
</books>
</category>
</library>
That's it! Now you are familiar with the best instrument
used for XML processing in modern Enterprise Java applications
based on Domain Driven Design.
Next time I'll talk about XML import and validation.
Thank you for your time and hard work!
|