Using JAX-RS With JAXB

Java Architecture for XML Binding (JAXB) is an XML-to-Java binding technology that simplifies the development of web services by enabling transformations between schema and Java objects and between XML instance documents and Java object instances. An XML schema defines the data elements and structure of an XML document. You can use JAXB APIs and tools to establish mappings between Java classes and XML schema. JAXB technology provides the tools that enable you to convert your XML documents to and from Java objects.

There are many tutorials and examples of using JAX-RS to create RESTful web services, but most fall short of explaining how to produce and consume complex object graphs using XML and JAXB. This article will show how easy it can be, several approaches to where you place the annotations, and how you can configure them.

Lets start with the root XML element. I chose to call mine GetBooksResponse, and use it as a container for a collection of Book objects and a Student object. You don't need to follow this convention.
GetBooksResponse.java
package com.dineshonjava.ws.rest;

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;

import com.dineshonjava.ws.xml.Book;
import com.dineshonjava.ws.xml.Student;

/**
 * @author Dinesh Rajput
 *
 */
@XmlRootElement
public class GetBooksResponse {
 private Student student;  
    private List<Book> books = new ArrayList<Book>();
 
    @XmlElement
    public Student getStudent() {
  return student;
 }
 public void setStudent(Student student) {
  this.student = student;
 }
 
 @XmlElement
 @XmlElementWrapper(name = "books") 
 public List<Book> getBooks() {
  return books;
 }
 public void setBooks(List<Book> books) {
  this.books = books;
 }  
    
}

JAXB's default naming convention is to use your class or bean getter name as-is, but starting with a lower case letter. In this example, once marshaled to XML the element names will be getBooksResponse, student, and books. I will show an example of how to override the default naming later in this article.

Also notice that I placed the @XmlElement annotations on the getter methods instead of on the private fields. When placed on the private fields, JAXB will give you an error unless you add @XmlAccessorType(XmlAccessType.FIELD) at the class level.

Finally, notice the @XmlElementWrapper annotation on the List collection. This makes JAXB wrap all of the order XML elements inside of an orders XML element. This annotation can be used with an array instead of a List too.
Student.java
package com.dineshonjava.ws.xml;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

/**
 * @author Dinesh Rajput
 *
 */
@XmlType(name="students")
public class Student {
 private long rollNumber;  
    private String name;  
    private String course;
    
    @XmlElement(name="rollNumber")
 public long getRollNumber() {
  return rollNumber;
 }
 public void setRollNumber(long rollNumber) {
  this.rollNumber = rollNumber;
 }
 @XmlElement(name="name")
 public String getName() {
  return name;
 }
 public void setName(String name) {
  this.name = name;
 }
 @XmlElement(name="course")
 public String getCourse() {
  return course;
 }
 public void setCourse(String course) {
  this.course = course;
 }  
}

In the example above, we use @XmlType at the class level instead of @XmlRootElement because it is not the root element. Also notice the name parameter in each of the annotations. This is how you override JAXB's default element naming.

Book.java
package com.dineshonjava.ws.xml;

import java.util.Date;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlType;

/**
 * @author Dinesh Rajput
 *
 */
@XmlType(propOrder = { "publishDate", "publishNumber", "publishedBooks", "bookName" } )  
@XmlAccessorType(XmlAccessType.FIELD)
public class Book {
 @XmlElement  
 public Date publishDate;  
    
 @XmlElement  
 public long publishNumber;  
 
 @XmlElement 
 public String bookName;
 
 @XmlElement  
 @XmlElementWrapper(name = "publishedBooks")  
 public PublishedBooks[] publishedBooks;

 public Date getPublishDate() {
  return publishDate;
 }

 public void setPublishDate(Date publishDate) {
  this.publishDate = publishDate;
 }

 public long getPublishNumber() {
  return publishNumber;
 }

 public void setPublishNumber(long publishNumber) {
  this.publishNumber = publishNumber;
 }

 public String getBookName() {
  return bookName;
 }

 public PublishedBooks[] getPublishedBooks() {
  return publishedBooks;
 }

 public void setPublishedBooks(PublishedBooks[] publishedBooks) {
  this.publishedBooks = publishedBooks;
 }

 public void setBookName(String bookName) {
  this.bookName = bookName;
 }

}

In the example above I used the @XmlAccessorType(XmlAccessType.FIELD) to allow me to place the @XmlElement annotations on the private fields instead of on the getter methods.

Also notice the propOrder parameter of the @XmlType annotation. By default JAXB will book the elements alphabetically. Use the propOrder parameter to specify the order when marshaling to XML. The values are the bean names, not the overridden names in the @XmlElement(name = "overridenName") annotation.

Finally, notice the @XmlElementWrapper used on a PublishedBooks[] array. It works with arrays and Lists.

PublishedBooks.java
package com.dineshonjava.ws.xml;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

/**
 * @author Dinesh Rajput
 *
 */
@XmlType(propOrder = { "bookId", "description", "quantity", "unitPrice",  
        "subTotal", "tax", "total" } )
public class PublishedBooks {
 
 private long bookId;  
 private String description;  
 private short quantity;  
 private double unitPrice;
 
 @XmlElement
 public long getBookId() {
  return bookId;
 }
 public void setBookId(long bookId) {
  this.bookId = bookId;
 }
 @XmlElement
 public String getDescription() {
  return description;
 }
 public void setDescription(String description) {
  this.description = description;
 }
 @XmlElement
 public short getQuantity() {
  return quantity;
 }
 public void setQuantity(short quantity) {
  this.quantity = quantity;
 }
 @XmlElement
 public double getUnitPrice() {
  return unitPrice;
 }
 public void setUnitPrice(double unitPrice) {
  this.unitPrice = unitPrice;
 }
   
 @XmlElement  
 public double getSubTotal() {  
  return unitPrice * quantity;  
 }  
   
 @XmlElement  
 public double getTax() {  
  return getSubTotal() * 0.15F;  
 }  
   
 @XmlElement  
 public double getTotal() {  
  return getSubTotal() + getTax();  
  }  
}

In the example above there are no setter methods that correspond to getSubTotal, getTax and getTotal. Now lets create a JAX-RS RESTful web service that can return this object graph in the response.
BookResource.java
package com.dineshonjava.ws.order;

import java.util.Date;

import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.ResponseBuilder;

import com.dineshonjava.ws.error.BookNotFoundException;
import com.dineshonjava.ws.rest.GetBooksResponse;
import com.dineshonjava.ws.xml.Book;
import com.dineshonjava.ws.xml.PublishedBooks;
import com.dineshonjava.ws.xml.Student;

/**
 * @author Dinesh Rajput
 *
 */
@Path("/order/books")
public class BookResource {
 @GET  
 @Produces("text/xml")  
 public GetBooksResponse getBooks() {  
       
  GetBooksResponse response = new GetBooksResponse();  
  Student student = new Student();  
  PublishedBooks publishedBooks1;  
  PublishedBooks publishedBooks2;  
   
  // student  
  student.setRollNumber(12345);  
  student.setName("Dinesh Rajput");  
  student.setCourse("Computer Science");  
  response.setStudent(student);  
       
  // first book  
  Book book1 = new Book();  
  book1.setPublishNumber(54321);  
  book1.setPublishDate(new Date()); 
  book1.setBookName("Java");
   
  publishedBooks1 = new PublishedBooks();  
  publishedBooks1.setBookId(77777);  
  publishedBooks1.setDescription("winning lottery ticket");  
  publishedBooks1.setQuantity((short) 10);  
  publishedBooks1.setUnitPrice(5.00F);  
   
  publishedBooks2 = new PublishedBooks();  
  publishedBooks2.setBookId(12121212);  
  publishedBooks2.setDescription("Real World Java EE Patterns Rethinking Best Practices");  
  publishedBooks2.setQuantity((short) 1);  
  publishedBooks2.setUnitPrice(40.40F);  
   
  book1.setPublishedBooks(new PublishedBooks[] { publishedBooks1, publishedBooks2 } );  
  response.getBooks().add(book1);  
   
  // second book  
  Book book2 = new Book();  
  book2.setPublishNumber(12345);  
  book2.setPublishDate(new Date());  
   
  publishedBooks1 = new PublishedBooks();  
  publishedBooks1.setBookId(787878);  
  publishedBooks1.setDescription("JavaServer Faces 2.0, The Complete Reference");  
  publishedBooks1.setQuantity((short) 10);  
  publishedBooks1.setUnitPrice(31.49F);  
   
  publishedBooks2 = new PublishedBooks();  
  publishedBooks2.setBookId(1111111);  
  publishedBooks2.setDescription("Beginning Java EE 6 with GlassFish 3, Second Edition");  
  publishedBooks2.setQuantity((short) 1);  
  publishedBooks2.setUnitPrice(41.73F);  
   
  book2.setPublishedBooks(new PublishedBooks[] { publishedBooks1, publishedBooks1 } );  
  response.getBooks().add(book2);  
       
  return response;  
   }
 
 @GET  
 @Path("{publishNumber}")  
 @Produces(MediaType.TEXT_PLAIN)  
 public Response updateOrder(@PathParam("publishNumber") String publishNumber) throws BookNotFoundException {  
    
  ResponseBuilder response;  
     
     if ("12345".equals(publishNumber)) {  
       response = Response.status(Response.Status.ACCEPTED).entity( "Saved changes to book '" +publishNumber+ "'.");  
     } else {  
       throw new BookNotFoundException("Publisher number '" + publishNumber +  
           "' does not exist.");  
     }  
     return response.build();  
   }

}

Notice the @Produces("text/xml") annotation, and that the method returns a GetBooksResponse object. Since the GetBooksResponse is annotated with JAXB annotations, JAX-RS will automatically marshal the response to XML.

Next lets add a method that takes part of the object graph as a request parameter. We'll start by creating an object to represent the XML root element:
UpdateBookRequest.java
package com.dineshonjava.ws.order;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import com.dineshonjava.ws.xml.Book;

/**
 * @author Dinesh Rajput
 *
 */
@XmlRootElement
public class UpdateBookRequest {
 private Book  book;
 
 @XmlElement
 public Book getBook() {
  return book;
 }

 public void setBook(Book book) {
  this.book = book;
 }
 
}

Now lets use this object in a PUT request:
@PUT  
 @Path("{publishNumber}.xml")  
 @Produces(MediaType.TEXT_PLAIN)  
 public Response updateOrder(@PathParam("publishNumber") String publishNumber) throws BookNotFoundException {  
    
  ResponseBuilder response;  
     
     if ("12345".equals(publishNumber)) {  
       response = Response.status(Response.Status.ACCEPTED).entity( "Saved changes to book '" +publishNumber+ "'.");  
     } else {  
       throw new BookNotFoundException("Publisher number '" + publishNumber +  
           "' does not exist.");  
     }  
     return response.build();  
   }
Since the UpdateBookRequest is annotated with JAXB annotations, JAX-RS will automatically unmarshal it from XML.

This example returns a javax.ws.rs.core.Response built using javax.ws.rs.core.ResponseBuilder. You can use ResponseBuilder to set response headers, the status code, and many other things. The response body is called the entity, and you can place anything in it. For example, a String, or a JAXB annotated object graph.

This example also throws an BookNotFoundException:
BookNotFoundException.java
package com.dineshonjava.ws.error;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

/**
 * @author Dinesh Rajput
 *
 */
public class BookNotFoundException extends WebApplicationException {  
 /**
  * version
  */
 private static final long serialVersionUID = 1L;

 public BookNotFoundException(String message) {  
  super(Response.status(Status.NOT_FOUND).entity(message).type(  
    MediaType.TEXT_PLAIN).build());  
     } 
}

The custom exception extends the WebApplicationException in JAX-RS. There are many constructors in WebApplicationException. I chose to use the one that lets me provide the complete response data.
web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>JaxRSJaxBApps</display-name>
  <servlet>
  <servlet-name>jersey-serlvet</servlet-name>
  <servlet-class>
         com.sun.jersey.spi.container.servlet.ServletContainer
        </servlet-class>
  <load-on-startup>1</load-on-startup>
 </servlet>
 
 <servlet-mapping>
  <servlet-name>jersey-serlvet</servlet-name>
  <url-pattern>/*</url-pattern>
 </servlet-mapping>
</web-app>

Directory structure for this web application.


Now deploy this application to the tomcat server and hit the following urls.

http://localhost:8181/doj/order/books

http://localhost:8181/doj/order/books/12345


Download SourceCode and Libs
JaxRSJaxBApps.zip



References
1. JAVA REST Web Services
2. Wikipedia for REST Web Service

 



<<Previous <<   || Index ||   >>Next >>

No comments:

Post a Comment