Sunday, October 27, 2013

XML bindings with JAXB and JAX-RS

02.01.2010
 | 40123 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.
01.package com.ryandelaplante.example; 
02. 
03.import java.util.ArrayList; 
04.import java.util.List; 
05.import javax.xml.bind.annotation.XmlElement; 
06.import javax.xml.bind.annotation.XmlElementWrapper; 
07.import javax.xml.bind.annotation.XmlRootElement; 
08. 
09.@XmlRootElement 
10.public class GetOrdersResponse { 
11.private Customer customer; 
12.private List orders = new ArrayList(); 
13. 
14.@XmlElement 
15.public Customer getCustomer() { 
16.return customer; 
17.
18. 
19.public void setCustomer(Customer customer) { 
20.this.customer = customer; 
21.
22. 
23.@XmlElement 
24.@XmlElementWrapper(name = "orders"
25.public List getOrders() { 
26.return orders; 
27.
28. 
29.public void setOrders(List orders) { 
30.this.orders = orders; 
31.
32.

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.
01.package com.ryandelaplante.example; 
02. 
03.import javax.xml.bind.annotation.XmlElement; 
04.import javax.xml.bind.annotation.XmlType; 
05. 
06.@XmlType(name = "Customer"
07.public class Customer { 
08.private long customerNumber; 
09.private String firstName; 
10.private String lastName; 
11. 
12.@XmlElement(name = "CustomerNumber"
13.public long getCustomerNumber() { 
14.return customerNumber; 
15.
16. 
17.public void setCustomerNumber(long customerNumber) { 
18.this.customerNumber = customerNumber; 
19.
20. 
21. 
22.@XmlElement(name = "FirstName"
23.public String getFirstName() { 
24.return firstName; 
25.
26. 
27.public void setFirstName(String firstName) { 
28.this.firstName = firstName; 
29.
30. 
31.@XmlElement(name = "LastName"
32.public String getLastName() { 
33.return lastName; 
34.
35. 
36.public void setLastName(String lastName) { 
37.this.lastName = lastName; 
38.
39.
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.

01.package com.ryandelaplante.example; 
02. 
03.import java.util.Date; 
04.import javax.xml.bind.annotation.XmlAccessType; 
05.import javax.xml.bind.annotation.XmlAccessorType; 
06.import javax.xml.bind.annotation.XmlElement; 
07.import javax.xml.bind.annotation.XmlElementWrapper; 
08.import javax.xml.bind.annotation.XmlType; 
09. 
10.@XmlType(propOrder = { "orderDate""orderNumber""lineItems" } ) 
11.@XmlAccessorType(XmlAccessType.FIELD) 
12.public class Order { 
13.@XmlElement 
14.public Date orderDate; 
15. 
16.@XmlElement 
17.public long orderNumber; 
18. 
19.@XmlElement 
20.@XmlElementWrapper(name = "lineItems"
21.public LineItem[] lineItems; 
22. 
23.public long getOrderNumber() { 
24.return orderNumber; 
25.
26. 
27.public void setOrderNumber(long orderNumber) { 
28.this.orderNumber = orderNumber; 
29.
30. 
31.public Date getOrderDate() { 
32.return orderDate; 
33.
34. 
35.public void setOrderDate(Date orderDate) { 
36.this.orderDate = orderDate; 
37.
38. 
39.public LineItem[] getLineItems() { 
40.return lineItems; 
41.
42. 
43.public void setLineItems(LineItem[] lineItems) { 
44.this.lineItems = lineItems; 
45.
46.
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.
01.package com.ryandelaplante.example; 
02. 
03.import javax.xml.bind.annotation.XmlElement; 
04.import javax.xml.bind.annotation.XmlType; 
05. 
06.@XmlType(propOrder = { "sku""description""quantity""unitPrice"
07."subTotal""tax""total" } ) 
08.public class LineItem { 
09.private long sku; 
10.private String description; 
11.private short quantity; 
12.private double unitPrice; 
13. 
14.@XmlElement 
15.public long getSku() { 
16.return sku; 
17.
18. 
19.public void setSku(long sku) { 
20.this.sku = sku; 
21.
22. 
23.@XmlElement 
24.public String getDescription() { 
25.return description; 
26.
27. 
28.public void setDescription(String description) { 
29.this.description = description; 
30.
31. 
32.@XmlElement 
33.public short getQuantity() { 
34.return quantity; 
35.
36. 
37.public void setQuantity(short quantity) { 
38.this.quantity = quantity; 
39.
40. 
41.@XmlElement 
42.public double getUnitPrice() { 
43.return unitPrice; 
44.
45. 
46.public void setUnitPrice(double unitPrice) { 
47.this.unitPrice = unitPrice; 
48.
49. 
50.@XmlElement 
51.public double getSubTotal() { 
52.return unitPrice * quantity; 
53.
54. 
55.@XmlElement 
56.public double getTax() { 
57.return getSubTotal() * 0.15F; 
58.
59. 
60.@XmlElement 
61.public double getTotal() { 
62.return getSubTotal() + getTax(); 
63.
64.
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.
01.package com.ryandelaplante.example; 
02. 
03.import java.util.Date; 
04.import javax.ws.rs.GET; 
05.import javax.ws.rs.PUT; 
06.import javax.ws.rs.Path; 
07.import javax.ws.rs.PathParam; 
08.import javax.ws.rs.Produces; 
09.import javax.ws.rs.core.MediaType; 
10.import javax.ws.rs.core.Response; 
11.import javax.ws.rs.core.Response.ResponseBuilder; 
12. 
13.@Path("/api/orders"
14.public class OrderResource { 
15.@GET 
16.@Produces("text/xml"
17.public GetOrdersResponse getOrders() { 
18. 
19.GetOrdersResponse response = new GetOrdersResponse(); 
20.Customer customer = new Customer(); 
21.LineItem lineItem1; 
22.LineItem lineItem2; 
23. 
24.// customer 
25.customer.setCustomerNumber(12345); 
26.customer.setFirstName("Ryan"); 
27.customer.setLastName("de Laplante"); 
28.response.setCustomer(customer); 
29. 
30.// first order 
31.Order order1 = new Order(); 
32.order1.setOrderNumber(54321); 
33.order1.setOrderDate(new Date()); 
34. 
35.lineItem1 = new LineItem(); 
36.lineItem1.setSku(77777); 
37.lineItem1.setDescription("winning lottery ticket"); 
38.lineItem1.setQuantity((short10); 
39.lineItem1.setUnitPrice(5.00F); 
40. 
41.lineItem2 = new LineItem(); 
42.lineItem2.setSku(12121212); 
43.lineItem2.setDescription("Real World Java EE Patterns Rethinking " 
44."Best Practices"); 
45.lineItem2.setQuantity((short1); 
46.lineItem2.setUnitPrice(40.40F); 
47. 
48.order1.setLineItems(new LineItem[] { lineItem1, lineItem2 } ); 
49.response.getOrders().add(order1); 
50. 
51.// second order 
52.Order order2 = new Order(); 
53.order2.setOrderNumber(12345); 
54.order2.setOrderDate(new Date()); 
55. 
56.lineItem1 = new LineItem(); 
57.lineItem1.setSku(787878); 
58.lineItem1.setDescription("JavaServer Faces 2.0, The Complete " 
59."Reference"); 
60.lineItem1.setQuantity((short10); 
61.lineItem1.setUnitPrice(31.49F); 
62. 
63.lineItem2 = new LineItem(); 
64.lineItem2.setSku(1111111); 
65.lineItem2.setDescription("Beginning Java EE 6 with GlassFish 3, " 
66."Second Edition"); 
67.lineItem2.setQuantity((short1); 
68.lineItem2.setUnitPrice(41.73F); 
69. 
70.order2.setLineItems(new LineItem[] { lineItem1, lineItem2 } ); 
71.response.getOrders().add(order2); 
72. 
73.return response; 
74.
75.
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:
01.package com.ryandelaplante.example; 
02. 
03.import javax.xml.bind.annotation.XmlElement; 
04.import javax.xml.bind.annotation.XmlRootElement; 
05. 
06.@XmlRootElement 
07.public class UpdateOrderRequest { 
08.private Order order; 
09. 
10.@XmlElement 
11.public Order getOrder() { 
12.return order; 
13.
14. 
15.public void setOrder(Order order) { 
16.this.order = order; 
17.
18.
Now lets use this object in a PUT request:
01.@PUT 
02.@Path("{orderNumber}.xml"
03.@Produces(MediaType.TEXT_PLAIN) 
04.public Response updateOrder(@PathParam("orderNumber") String orderNumber, 
05.UpdateOrderRequest request) throws OrderNotFoundException { 
06. 
07.ResponseBuilder response; 
08. 
09.if ("12345".equals(orderNumber)) { 
10.response = Response.status(Response.Status.ACCEPTED).entity( 
11."Saved changes to order '" 
12.request.getOrder().getOrderNumber() + "'."); 
13.else 
14.throw new OrderNotFoundException("Order number '" + orderNumber + 
15."' does not exist."); 
16.
17.return response.build(); 
18.}
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:
01.package com.ryandelaplante.example; 
02. 
03.import javax.ws.rs.WebApplicationException; 
04.import javax.ws.rs.core.MediaType; 
05.import javax.ws.rs.core.Response; 
06.import javax.ws.rs.core.Response.Status; 
07. 
08.public class OrderNotFoundException extends WebApplicationException { 
09.public OrderNotFoundException(String message) { 
10.super(Response.status(Status.NOT_FOUND).entity(message).type( 
11.MediaType.TEXT_PLAIN).build()); 
12.
13.
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.
Published at DZone with permission of Ryan De Laplante, author and DZone MVB.

No comments: