Tuesday, February 14, 2012

Project 2 - REST WebService with Spring

Before I begin, I just had to mention, I was reading a review of web frameworks where the author rated JSF 2 as a zero for REST support.  The author is mistaken, there is nothing simpler then adding REST support to JSF.  First JSF is the view or client side, so there would be no need for REST as a serivce in JSF, and adding a REST client through JSF is trivial - as demonstrated.  In addition, although I put the REST support in an EJB in our first project and here we put it in a Component Bean for Spring, there is nothing stopping you from putting it directly into the backing bean (except, of course, proper coding style).

As mentioned we are going to implement a Jersey client with Spring in our container.

To do that, we have to modify our web.xml and add a servlet to handle the REST services:

    <servlet>
        <servlet-name>Jersey Spring</servlet-name>
        <servlet-class>com.sun.jersey.spi.spring.container.servlet.SpringServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Jersey Spring</servlet-name>
        <url-pattern>/resources/*</url-pattern>
    </servlet-mapping>

Next, we have our web service: com/sample/webservice/PartEntityFacadeRest

package com.sample.webservice;
import com.sample.entities.PartEntity;
import com.sample.services.PartService;
import com.sample.webservice.other.Part;
import javax.ejb.EJBTransactionRolledbackException;
import javax.inject.Inject;
import javax.persistence.EntityExistsException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
 * RESTful service for doing CRUD operations on the Parts table
 * This service allows vendors to create parts and subparts.
 * The vendor can remove parts, and the subparts. 
 * Or they can find a part.
 * There is no method for updating.
 * We provide two methods for creating, one via a POST, and one via a GET
 * 
 * @author Thomas Dias
 */
@Component
@Scope("request")
// This is the sub path from the root of rest resources path.  So the full URL would be http://[hostname]/JSFDemoSpringRich/resources/Part
@Path("/Part")
// @Path("com.sample.partentity") - you may want to designate the url similarly to your package definitions
public class PartEntityFacadeREST {
    /** Managed bean for interacting with the database */
    @Inject
    private PartService partService;
    /** Default constructor - explicitly coded to remind us they are required */
    public PartEntityFacadeREST() {
    }
    /**
     * create method, called via a post.
     * Since no path is stated, will use the root path.  http://[host]/JSFDemoSpringRich/resources/Part 
     * @param part to be created.  Note: the part may be sent in xml or json format.
     * @throws exception if part cannot be created, i.e. name not unique
     */
    @POST
    @Consumes({"application/xml", "application/json"})
    public void create(Part part) {
        PartEntity parent = partService.findByName(part.getParent());
        PartEntity entity = new PartEntity(part.getName(), parent);
        partService.create(entity);
    }
    /**
     * creates a part via the GET method.  URL is http://[host]/JSFDemoSpringRich/resources/Part/createByGet/{parentName}/{name}
     * @param parentName name of part to assign as the parent of this part
     * @param name of new part
     * @return part created in xml format.  Id of part will be 0l if there was an error creating the requested part.
     */
    @GET
    @Path("createByGet/{parent}/{name}")
    @Produces({"application/xml"})
    public Part createByGet(@PathParam("parent") String parentName, @PathParam("name") String name) {
        Part result = null;
        PartEntity parent = partService.findByName(parentName);
        PartEntity newPart = new PartEntity(name, parent);  
        // in case there is an error persisting the new part, set the newPart to null
        try {
           partService.create(newPart);
        } catch (EntityExistsException e) {
           newPart = null;         
        } catch (EJBTransactionRolledbackException e) {
           newPart = null;
        }
        // We are returning a Part, not a PartEntity - convert the PartEntity to a part.
        result = new Part(newPart);
        return result;
    }
    /**
     * remove the part id from the database via the DELETE method.  
     * URL: http://[host]/JSFDemoSpringRich/resources/Part
     * This will also remove all the children.
     * Although this is useful for example, what happens to the orders that reference this part?
     * What happens if we get an error?
     * @param id 
     * @throws exception if there is a database constraint violation
     */
    @DELETE
    @Path("{id}")
    public void remove(@PathParam("id") Long id) {
        partService.remove(id);
    }
    /**
     * retrieves a part via the GET method.  URL is http://[host]/JSFDemoSpringRich/resources/Part/findByName/{name}
     * @param name is the name of the part.
     * @return part, or an empty part if no part is found.
     */
    @GET
    @Path("findByName/{id}")
    @Produces({"application/xml"})
    public Part find(@PathParam("id") String name) {
        return new Part(partService.findByName(name));
    }
    public PartService getPartService() {
        return partService;
    }
    public void setPartService(PartService partService) {
        this.partService = partService;
    }
}

Although I used the same name as our last REST service, there are a couple changes that were made.  Since we did not implement a facade in this project, we changed the facade to a service.  And the annotations at the top went to Spring annotations instead of CDI.  Also, we do not need an ApplicationConfig.java file since we did that with the servlets in the web.xml.  Otherwise, the implementation is the same.  Please, see the first discussion at: http://jsf-tying-it-all-together.blogspot.com/2011/12/rest-service.html

And lastly our client: com/sample/webservice/client/

package com.sample.webservice.client;
import com.sample.webservice.other.Part;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource;
/**
 * RESTClient for accessing the RESTful web service at http://localhost:8080/JSFDemoApp/resources/Part
 *  USAGE:<pre>
 *        PartEntityRESTClient client = new PartEntityRESTClient();
 *        Object response = client.XXX(...);
 *        // do whatever with response
 *        client.close();
 *  </pre>
 * @author Thomas Dias
 */
public class PartEntityRESTClient {
    private WebResource webResource;
    private Client client;
    private static final String BASE_URI = "http://localhost:8080/JSFDemoSpringRich/resources";
    /**
     * Default constructor - initializes client to reference appropriate web service.
     */
    public PartEntityRESTClient() {
        com.sun.jersey.api.client.config.ClientConfig config = new com.sun.jersey.api.client.config.DefaultClientConfig();
        client = Client.create(config);
        webResource = client.resource(BASE_URI).path("Part");
    }
    /**
     * CRUD operation to delete a record from the database
     * @param id of part to be removed
     * @throws UniformInterfaceException if web service throws an exception. i.e. remove failed.
     */
    public void remove(String id) throws UniformInterfaceException {
        webResource.path(java.text.MessageFormat.format("{0}", new Object[]{id})).delete();
    }
    /**
     * Operation to retrieve a record by supplying its name.
     * @param name of part to find.
     * @return the part found.  Part will have an id of 0l if no part was found.
     * @throws UniformInterfaceException if web service throws an exception
     */
    public Part findByName(String name) throws UniformInterfaceException {
        WebResource resource = webResource;
        resource = resource.path(java.text.MessageFormat.format("findByName/{0}", new Object[]{name}));
        return resource.accept(javax.ws.rs.core.MediaType.APPLICATION_XML).get(Part.class);
    }
    /**
     * creates a part and persists it in the customers database via an http POST method.
     * creates an xml file from the part to be supplied with the POST method
     * @param part to be created
     * @throws UniformInterfaceException if web service throws an exception. i.e. part is not unique.
     */
    public void create_XML(Part part) throws UniformInterfaceException {
        webResource.type(javax.ws.rs.core.MediaType.APPLICATION_XML).post(part);
    }
    /**
     * creates a part and persists it in the customers database via an http GET method.
     * supplies the pertinent parts of the part to be created in the URL
     * @param parent name of the part that is the parent of this part.  Supply an empty string if their is no parent.
     * @param name of the part to be created.
     * @return the part created by the web service.
     * @throws UniformInterfaceException if web service throws and exception.  
     */
    public Part createByGet(String parent, String name) throws UniformInterfaceException {
        WebResource resource = webResource;
        resource = resource.path(java.text.MessageFormat.format("createByGet/{0}/{1}", new Object[]{parent, name}));
        return resource.accept(javax.ws.rs.core.MediaType.APPLICATION_XML).get(Part.class);
    }
    /**
     * release resources associated with this client.
     */
    public void close() {
        client.destroy();
    }
}

Here the only change was the path so it went to the new application instead of the services referenced in the first.  See: http://jsf-tying-it-all-together.blogspot.com/2011/12/rest-client.html

To see the discussion regarding testing, see: http://jsf-tying-it-all-together.blogspot.com/2011/12/testing-rest-service.html

And the backing bean only had its annotations changed to Spring.  So, besides modifying the code to handle the Spring annotations instead of CDI (which we could have done through xml if we weren't using annotations), all we did was add Jersey Spring servlet to the web-xml and it all worked.

1 comment: