Ryan has posted 17 posts at DZone. You can read more from them at their website. View Full User Profile

XML bindings with JAXB and JAX-RS

02.01.2010
| 50997 views |
  • submit to reddit

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 GetOrdersResponse, and use it as a container for a collection of Order objects and a Customer object. You don't need to follow this convention.

package com.ryandelaplante.example;  

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;

@XmlRootElement
public class GetOrdersResponse {
private Customer customer;
private List<Order> orders = new ArrayList<Order>();

@XmlElement
public Customer getCustomer() {
return customer;
}

public void setCustomer(Customer customer) {
this.customer = customer;
}

@XmlElement
@XmlElementWrapper(name = "orders")
public List<Order> getOrders() {
return orders;
}

public void setOrders(List<Order> orders) {
this.orders = orders;
}
}


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 getOrdersResponse, customer, and orders. 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. I will show an example of this later.

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, which will be shown later.

package com.ryandelaplante.example;  

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

@XmlType(name = "Customer")
public class Customer {
private long customerNumber;
private String firstName;
private String lastName;

@XmlElement(name = "CustomerNumber")
public long getCustomerNumber() {
return customerNumber;
}

public void setCustomerNumber(long customerNumber) {
this.customerNumber = customerNumber;
}


@XmlElement(name = "FirstName")
public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

@XmlElement(name = "LastName")
public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}
}

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.

 

 package com.ryandelaplante.example;  

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;

@XmlType(propOrder = { "orderDate", "orderNumber", "lineItems" } )
@XmlAccessorType(XmlAccessType.FIELD)
public class Order {
@XmlElement
public Date orderDate;

@XmlElement
public long orderNumber;

@XmlElement
@XmlElementWrapper(name = "lineItems")
public LineItem[] lineItems;

public long getOrderNumber() {
return orderNumber;
}

public void setOrderNumber(long orderNumber) {
this.orderNumber = orderNumber;
}

public Date getOrderDate() {
return orderDate;
}

public void setOrderDate(Date orderDate) {
this.orderDate = orderDate;
}

public LineItem[] getLineItems() {
return lineItems;
}

public void setLineItems(LineItem[] lineItems) {
this.lineItems = lineItems;
}
}

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 order 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 LineItem[] array. It works with arrays and Lists.

  package com.ryandelaplante.example;  

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

@XmlType(propOrder = { "sku", "description", "quantity", "unitPrice",
"subTotal", "tax", "total" } )
public class LineItem {
private long sku;
private String description;
private short quantity;
private double unitPrice;

@XmlElement
public long getSku() {
return sku;
}

public void setSku(long sku) {
this.sku = sku;
}

@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.

 package com.ryandelaplante.example;  

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;

@Path("/api/orders")
public class OrderResource {
@GET
@Produces("text/xml")
public GetOrdersResponse getOrders() {

GetOrdersResponse response = new GetOrdersResponse();
Customer customer = new Customer();
LineItem lineItem1;
LineItem lineItem2;

// customer
customer.setCustomerNumber(12345);
customer.setFirstName("Ryan");
customer.setLastName("de Laplante");
response.setCustomer(customer);

// first order
Order order1 = new Order();
order1.setOrderNumber(54321);
order1.setOrderDate(new Date());

lineItem1 = new LineItem();
lineItem1.setSku(77777);
lineItem1.setDescription("winning lottery ticket");
lineItem1.setQuantity((short) 10);
lineItem1.setUnitPrice(5.00F);

lineItem2 = new LineItem();
lineItem2.setSku(12121212);
lineItem2.setDescription("Real World Java EE Patterns Rethinking " +
"Best Practices");
lineItem2.setQuantity((short) 1);
lineItem2.setUnitPrice(40.40F);

order1.setLineItems(new LineItem[] { lineItem1, lineItem2 } );
response.getOrders().add(order1);

// second order
Order order2 = new Order();
order2.setOrderNumber(12345);
order2.setOrderDate(new Date());

lineItem1 = new LineItem();
lineItem1.setSku(787878);
lineItem1.setDescription("JavaServer Faces 2.0, The Complete " +
"Reference");
lineItem1.setQuantity((short) 10);
lineItem1.setUnitPrice(31.49F);

lineItem2 = new LineItem();
lineItem2.setSku(1111111);
lineItem2.setDescription("Beginning Java EE 6 with GlassFish 3, " +
"Second Edition");
lineItem2.setQuantity((short) 1);
lineItem2.setUnitPrice(41.73F);

order2.setLineItems(new LineItem[] { lineItem1, lineItem2 } );
response.getOrders().add(order2);

return response;
}
}

Notice the @Produces("text/xml") annotation, and that the method returns a GetOrdersResponse object. Since the GetOrdersResponse 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:

package com.ryandelaplante.example;  

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

@XmlRootElement
public class UpdateOrderRequest {
private Order order;

@XmlElement
public Order getOrder() {
return order;
}

public void setOrder(Order order) {
this.order = order;
}
}

Now lets use this object in a PUT request:

@PUT  
@Path("{orderNumber}.xml")
@Produces(MediaType.TEXT_PLAIN)
public Response updateOrder(@PathParam("orderNumber") String orderNumber,
UpdateOrderRequest request) throws OrderNotFoundException {

ResponseBuilder response;

if ("12345".equals(orderNumber)) {
response = Response.status(Response.Status.ACCEPTED).entity(
"Saved changes to order '" +
request.getOrder().getOrderNumber() + "'.");
} else {
throw new OrderNotFoundException("Order number '" + orderNumber +
"' does not exist.");
}
return response.build();
}

Since the UpdateOrderRequest 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 OrderNotFoundException:

package com.ryandelaplante.example;  

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

public class OrderNotFoundException extends WebApplicationException {
public OrderNotFoundException(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.

Another interesting issue is API versioning. When I created my first RESTful web service I included an API version number in the URL. For example, /rest/v1/orders. When it came to implementation, a number of thoughts came to mind:

  1. Some companies might deploy/install multiple copies of their service into production, each with a different context root. For example, https://api.company.com/v1/ and https://api.company.com/v2/ could be two separate copies of the same service/.war file, just different versions.
  2. Another approach might be to detect the version number in the URL, and process the request/response accordingly. There may be challenges such as returning an Order object in v1 has fewer fields than v2. Since the same object implementation is probably being used for both versions, is it important that v1 does not include the new v2 fields?
  3. Is there any benefit to distinguishing the API version number in the URL if you ensure backward compatibility in all releases?

I decided that the last option was best for me, so I do not include version numbers in the URI.

From http://www.ryandelaplante.com/

Published at DZone with permission of its author, Ryan Developer.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags:

Comments

Nick Wiedenbrueck replied on Mon, 2010/02/01 - 11:27am

Thanks for the nice article. I was recently wondering how to wrap collection types into a separate XML Element.

And also, I thought about versioning of the API. Instead of a version in the URL I went with versions in the content type, so for example

 application/vnd.vendorname.app-v1+xml

Thanks.

George Farmer replied on Mon, 2010/02/01 - 1:43pm

Actually to make this work I had to:

1. In LineItem's @XmlType(propOrder ...: change "subtotal" to "subTotal"

2. In OrderResource: modify getOrders() to return String. And also add:

 

ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            JAXBContext jc = JAXBContext.newInstance(com.ryandelaplante.example.GetOrdersResponse.class);
           
            Marshaller m = jc.createMarshaller();
            m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
            m.marshal(response, out);
        } catch (Exception e) {
            throw new Error("oops ...", e);
        }

        return new String(out.toByteArray()); 

 

If you think it should automatically convert the object to String I would appreciate some clarification.

 

George Farmer replied on Tue, 2010/02/02 - 1:24pm

Yes, you can make it work. The MIME type for OrderResource.getOrders() must be "application/xml". Too many mistakes to my taste. it is better to copy a source code from a working example or just attach/link a working one. Otherwise, it looks like a self-promotion exercise.

Ryan Developer replied on Wed, 2010/02/03 - 8:32am

Most entries I write in my personal blog are intended to be notes for future reference. DZone sometimes re-publishes blog posts they like using my user account on javalobby.  I didn't post it here.  

Thanks for pointing out the "subtotal" typo.  As for getOrders returning the Order object, what I wrote in this post is what I do in a real application (but code cannot be shared) and it works without having to manually marshall.   I do not use "application/xml" MIME type, I use "text/xml". 

Mohammady Mahdy replied on Wed, 2012/01/04 - 8:45am

Great post, one note about the mime type you use. It is preferred to use application/xml than using text/xml for the reasons mentioned in this post.

Shahar Muky replied on Sun, 2012/02/05 - 2:00pm

Great article. just one question. How do you pass a collection/list? Let say you have several types of SpecificOrder implement Order?

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.