August 20, 2010

Creating a RESTful Web Service - Part 4/5

Java API for RESTful Web Services (JAX-RS) is the Java EE standard for creating RESTful Web Services.  In this post we will create a RESTful service from an EJB session bean using JAX-RS.



Customer Service

We will use the @PersistenceContent annotation to inject an EntityManager onto our EJB session bean.  We will use this EntityManager to perform all of our persistence operations.  Back in part 2 we configured our persistence unit to use the Java Transaction API  (JTA), so we will not need to do any of our own transaction handling.

You can see below how little JPA code is required to implement our RESTful service.  Of course to make this production quality service we will need to include response objects and exception handling.  In future posts I'll cover these topics in detail.  For the purpose of this example the following is all the code you need to run your service.

package org.example;

import java.util.List;

import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.PersistenceContextType;
import javax.persistence.Query;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
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;

@Stateless
@LocalBean
@Path("/customers")
public class CustomerService {

    @PersistenceContext(unitName="CustomerService", 
                        type=PersistenceContextType.TRANSACTION)
    EntityManager entityManager;

    @POST
    @Consumes(MediaType.APPLICATION_XML)
    public void create(Customer customer) {
        entityManager.persist(customer);
    }

    @GET
    @Produces(MediaType.APPLICATION_XML)
    @Path("{id}")
    public Customer read(@PathParam("id") long id) {
        return entityManager.find(Customer.class, id);
    }

    @PUT
    @Consumes(MediaType.APPLICATION_XML)
    public void update(Customer customer) {
        entityManager.merge(customer);
    }

    @DELETE
    @Path("{id}")
    public void delete(@PathParam("id") long id) {
        Customer customer = read(id);
        if(null != customer) {
            entityManager.remove(customer);
        }
    }

    @GET
    @Produces(MediaType.APPLICATION_XML)
    @Path("findCustomersByCity/{city}")
    public List<Customer> findCustomersByCity(@PathParam("city") String city) {
        Query query = entityManager.createNamedQuery("findCustomersByCity");
        query.setParameter("city", city);
        return query.getResultList();
    }

}

WEB-INF/web.xml

In the WEB-INF/web.xml file we need to configure three things:

  1. Start the Jersey service for our application.  We will use the Jersey implmenation of JAX-RS, it is included in the version of GlassFish we are using for this example.
  2. You interact with JAX-RS through URIs.  We need to tell Jersey which URLs (a URL is a type of URI) relate our service.  In our web context URLs starting with "rest" correspond to our RESTful service.
  3. We need to make our JPA persistence context we created in part 3 available for use by our EJB session bean.
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <servlet>
        <servlet-name>Jersey Web Application</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 Web Application</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>
    <persistence-context-ref>
        <persistence-context-ref-name>persistence/em</persistence-context-ref-name>
        <persistence-unit-name>CustomerService</persistence-unit-name>
    </persistence-context-ref>
</web-app>

Packaging/Deployment

We will package up everying in a WAR file.  The JAR file containing the contents we created in part 2 and part 3 should be inluded in the war in the directory WEB-INF/lib.

CustomerService.war
  • WEB-INF
    • lib
      • CustomerService.jar
        • org
          • example
            • Customer.class
            • Address.class
            • PhoneNumber.class
            • jaxb.properties
        • META-INF
          • MANIFEST.MF
          • persistence.xml
    • classes
      • org
        • example
          • CustomerService.class
    • web.xml
  • META-INF
    • MANIFEST.MF

Next Steps

In the next post we'll look at creating a client for our RESTful service.


Further Reading

If you enjoyed this post you may also be interested in:

16 comments:

  1. Hi Blaise,

    Nice set of blog posts!

    I was trying out to build something based on your example. But, I could not go far. I use the same set of configuration as described in your posts (Oracle XE, GF 3.0.1, SQL scripts). When I try to add a customer using Jersey Client, I get this exception:

    Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.0.1.v20100213-r6600): org.eclipse.persistence.exceptions.DatabaseException
    Internal Exception: java.sql.SQLException: ORA-02291: integrity constraint (CUSTOMER.ADDRESS_FK) violated - parent key not found

    Error Code: 2291
    Call: INSERT INTO ADDRESS (ID, STREET, CITY) VALUES (?, ?, ?)
    bind => [0, 1660 Blue Blvd, City]
    Query: InsertObjectQuery(org.example.Address@2a82b5c3)
    at org.eclipse.persistence.exceptions.DatabaseException.sqlException(DatabaseException.java:324)

    Any clues on what I may doing wrong?

    Cheers,
    Arul

    ReplyDelete
  2. Hi Arul,

    Good catch. At the last minute I made the relationship between Customer and Address unidirectional and only verified the code by running a GET which worked. I have put the bidirectional relationship back. If you grab the updated source from part 3, then everything should work now.

    Thanks,
    Blaise

    ReplyDelete
  3. Hi Blaise,

    Thanks for fixing that. I am now not getting a different exception now.

    Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.0.1.v20100213-r6600): org.eclipse.persistence.exceptions.DatabaseException
    Internal Exception: java.sql.SQLException: ORA-00001: unique constraint (CUSTOMER.PHONE_NUMBER_PK) violated

    Error Code: 1
    Call: INSERT INTO PHONE_NUMBER (ID, NUM, TYPE, ID_CUSTOMER) VALUES (?, ?, ?, ?)
    bind => [0, 123-456-7891, cell, 0]
    Query: InsertObjectQuery(org.example.PhoneNumber@79adf9f1)

    I am not sure why I would get a "ORA-00001: unique constraint". Here is my client code, if that is helpful.

    Client client = Client.create();
    WebResource r = client.resource("http://localhost:8080/customerservice/rest/customers");
    Customer c = new Customer();
    c.setFirstName("John");
    c.setLastName("Doe");
    Address a = new Address();
    a.setCity("SFO");
    a.setStreet("Country Manor Blvd");
    PhoneNumber home = new PhoneNumber();
    home.setNum("123-456-7890");
    home.setType("home");
    PhoneNumber cell = new PhoneNumber();
    cell.setNum("123-456-7891");
    cell.setType("cell");
    c.setAddress(a);
    Set phoneNumbers = new HashSet();
    phoneNumbers.add(home);
    phoneNumbers.add(cell);
    c.setPhoneNumbers(phoneNumbers);
    r.post(c);

    Also, when I copied the latest EclipseLink jars to glassfish\modules dir, it still references the old version (2.0.1) that ships with GF 3.0.1. I had no luck with updating EL using the pkg tool.

    -Arul

    ReplyDelete
  4. Hi Arul,

    In this particular example none of the IDs are automatically generated, so you will need to set them on all the objects. You are getting that error because both instances of PhoneNumber have id = 0.

    -Blaise

    ReplyDelete
  5. Ok cool. I should have checked that. I believe adding the JPA annotation to id should fix my code, right? @GeneratedValue(strategy=GenerationType.SEQUENCE)

    Thanks,
    Arul

    ReplyDelete
  6. That's correct, check out the following for more information about EclipseLink JPA and @GeneratedValue: http://wiki.eclipse.org/Introduction_to_EclipseLink_JPA_%28ELUG%29#.40GeneratedValue

    ReplyDelete
  7. Hi Dheenu,

    The purpose of @XmlInverseReference is to populate the bidirectional relationship:

    - JPA Entities to XML - Bidirectional Relationships

    I'm not sure I understand your use case. If you wish to provide more details I can be reached through the "Contact Me" page:

    - "Contact Me" page

    -Blaise

    ReplyDelete
  8. Hi Blaise,

    Thanks for this excellent article.

    This example requires EJB container to create your CustomerService REST resource. Could you please tell me how I can create a REST resource to run in the servlet container that uses the entities you created.

    Thanks,
    Amit.

    ReplyDelete
  9. Hi Amit,

    The service implementation will remain mostly the same. It will differ in how you get the entity manager since you can't use injection, instead you will need to do something like the following:

       EntityManagerFactory emf = Persistence.createEntityManagerFactory(persistentUnitName);
       this.entityManager = emf.createEntityManager();

    Also you will need to manage your own transactions, since you won't be able to leverage a session beans transactions:

       entityManager.getTransaction().begin();
       entityManager.persist(entity);
       entityManager.getTransaction().commit();

    I hope this helps.

    -Blaise

    ReplyDelete
  10. Hi Blaise,

    love your articles. Can you help me and answer what I have to do if my input type is MediaType.APPLICATION_JSON instead of MediaType.APPLICATION_XML.
    Would this work or do I have to add something else:

    @PUT
    @Consumes(MediaType.APPLICATION_JSON)
    public void update(Customer customer) {
    entityManager.merge(customer);
    }

    Thanks,
    tomy

    ReplyDelete
  11. Hi Tomy,

    I'm very happy to hear you are enjoying the blog. The sample code you have provided will definitely work. If you want to have your service support both JSON and XML messages then you could do the following:

    @PUT
    @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
    public void update(Customer customer) {
    entityManager.merge(customer);
    }

    -Blaise

    ReplyDelete
  12. Very informative blog!

    I'm currently doing something similar to this except I'm working with JBoss/Jersey. However, with this configuration I cannot get the EntityManager injection to work. Is this something that will only work properly with GF/Jersey?

    ReplyDelete
  13. Hi Sean,

    This is something that should work with JBoss/Jersey as well.

    -Blaise

    ReplyDelete
  14. Great set of posts! Something I noticed was that in JBoss AS 7.1.1, in addition to commenting out the Jersey stuff in web.xml (since that's provided by RESTEasy), even though it may deploy, to activate the endpoint to accept requests, it's helpful to have the following in web.xml (mentioned in: https://issues.jboss.org/browse/AS7-1674):


    javax.ws.rs.core.Application
    /*


    Thanks again!

    ReplyDelete
  15. Great Tutorial.. Thanks for sharing.

    ReplyDelete
  16. Very detailed and helpful tutorial. I had been looking for this for while. Glad I finally found it.:) Thanks a lot.:)

    ReplyDelete