Saturday, December 17, 2011

REST Service

Again, we will start with an example:
PartEntityFacadeREST.java

package com.sample.service;

import com.sample.PartEntity;
import com.sample.PartEntityFacade;
import com.sample.service.other.Part;
import javax.ejb.EJB;
import javax.ejb.EJBTransactionRolledbackException;
import javax.ejb.Stateless;
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;

/**
 * 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
 */
@Stateless
// This is the sub path from the root of rest resources path.  So the full URL would be http://[hostname]/JSFDemo/resources/Part
@Path("/Part")
// @Path("com.sample.partentity") - you may want to designate the url similarly to your package definitions
public class PartEntityFacadeREST {
    /** EJB for interacting with the database */
    @Inject
    private PartEntityFacade partEntityFacade;

    /** 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]/JSFDemo/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 = partEntityFacade.findByName(part.getParent());
        PartEntity entity = new PartEntity(part.getName(), parent);
        partEntityFacade.create(entity);
    }

    /**
     * creates a part via the GET method.  URL is http://[host]/JSFDemoApp/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 = partEntityFacade.findByName(parentName);
        PartEntity newPart = new PartEntity(name, parent);
      
        // in case there is an error persisting the new part, set the newPart to null
        try {
           partEntityFacade.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]/JSFDemoApp/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) {
        partEntityFacade.remove(id);
    }

    /**
     * retrieves a part via the GET method.  URL is http://[host]/JSFDemoApp/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(partEntityFacade.findByName(name));
    }
    public PartEntityFacade getPartEntityFacade() {
        return partEntityFacade;
    }
    public void setPartEntityFacade(PartEntityFacade partEntityFacade) {
        this.partEntityFacade = partEntityFacade;
    }
}


Part.java

package com.sample.service.other;

import com.sample.PartEntity;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
public class Part implements Serializable {
    Long id = 0l;
    String name = "";
    String parent = "";
    List<String> subParts = new ArrayList<String>();

    public Part() {
    }
    public Part(PartEntity part) {
        if (part != null) {
            id = part.getId();
            name = part.getName();
            if (part.getParent() != null) {
                parent = part.getParent().getName();
            }
            for (PartEntity subPart : part.getSubParts()) {
                subParts.add(subPart.getName());
            }
        }
    }
    public Part(String name, String parent) {
        this.name = name;
        this.parent = parent;
    }
    @Override
    public String toString() {
        StringBuilder s = new StringBuilder("Found: ");
        s.append(id);
        s.append("\n");
        s.append(name);
        s.append("\n");
        s.append(parent);
        s.append("\n");
        for (String subPart : subParts) {
            s.append(subPart);
            s.append("\n");
        }
        return s.toString();
    }
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public List<String> getSubParts() {
        return subParts;
    }
    public void setSubParts(List<String> subParts) {
        this.subParts = subParts;
    }
    public String getParent() {
        return parent;
    }
    public void setParent(String parent) {
        this.parent = parent;
    }
}


Discussion:

First we start with a stateless bean.  To have a RESTful service, there are certain constraints we adhere to.  One constraint is that it be stateless.  A quote from Wikipedia on RESTful services  :
Stateless
The client–server communication is further constrained by no client context being stored on the server between requests. Each request from any client contains all of the information necessary to service the request, and any session state is held in the client. The server can be stateful; this constraint merely requires that server-side state be addressable by URL as a resource. This not only makes servers more visible for monitoring, but also makes them more reliable in the face of partial network failures as well as further enhancing their scalability.
Next we register the path for this service as "/Part" with the annotation: @Path("/Part").  This service is defined by PartEntityFacadeREST.class.  So, all the methods in this class are potential methods of this service and are accessed by .../Part/... .  The service is running in the JEE container.  The JEE container is managing the EJBs.  So, we inject an EJB which has the entity manager, transactional support, etc. and will handle all the access to the Parts table.

Continuing down the code we find:
    @POST
    @Consumes({"application/xml", "application/json"})
    public void create(Part part) {

This defined the method create, as a service method.  It says it is accessed by a POST, and it has no additional path.  So, POST http://[host]/JSFDemoApp/resources/Part will access this method.  (We haven't yet discussed where the "resources" part came from, see later in the post regarding configuration.)

This code also specifies that the post will contain either XML or JSON to define a part.  The part parameter will be created from this information and passed to the Java method.

The next method's header looks slightly different:
    @GET
    @Path("createByGet/{parent}/{name}")
    @Produces({"application/xml"})
    public Part createByGet(@PathParam("parent") String parentName, @PathParam("name") String name) {

This code defines the method to be a service method that is accessed by a GET method.  It has an additional path to the base URL, so the fully qualified url would be: GET http://[host]/JSFDemoApp/resources/Part/createByGet/{parentname}/{newpartname} where {parentname} and {newpartname} are parameters that are passed in.  Although we have to note that using the GET method to do transactional changes is not actually RESTful.  See: http://www.ibm.com/developerworks/webservices/library/ws-restful/ And although this method has an advantage that it can return something, it can't handle a lot of optional parameters.  Even embedding parameter names has limited functionality.

The @Produces designates that this method will return xml.  The Java method defines the xml that will be returned will be a Part.  The method parameters are annotated to define what parts of the URL are interpreted to be passed in for each parameter.

Now notice the try/catch in the createByGet method.  This ensures that an xml document is returned instead of an error cause by an exception if the uniqueness constraint is violated on the name.

Going through the rest of the file we notice it is simply a matter of annotating the methods and playing nice by controlling what we return.

When we move to Part.java we note a couple things.
First: the @XmlRootElement annotation defines the class for converting to/from xml.  You can also convert to / from JSON.  This annotation goes hand in hand with the annotations discussed above for Consuming, or Producing the xml file.

Second: that we even have a Part class.  We have a PartEntity class, but that is a db entity.  I would not consider it appropriate to be transferring your internal entities to external sources.  I'm sure you could find a case where it is appropriate, but you would need to carefully consider the security ramifications.


Now, we have one more file to discuss.  This file was automatically created by NetBeans to configure the service.



Configuration File
ApplicationConfig.java

package org.netbeans.rest.application.config;
/**
 * This class is generated by the Netbeans IDE,
 * and registers all REST root resources created in the project.
 * Please, DO NOT EDIT this class unless you really need and
 * understand the results of changes.
 * It is safe to change REST resources path value of
 * ApplicationPath annotation. It can be done also via UI action
 * called on RESTful Web Services node ( REST Resources Configuration dialog ).
 *
 */
@javax.ws.rs.ApplicationPath("resources")
public class ApplicationConfig extends javax.ws.rs.core.Application {
}


The @javax.ws.rs.ApplicationPath("resources") defines the base URI for the RESTful services.  It appends "resources" to the application URI of http://localhost:8080/JSFDemoApp.  When we annotate a path for a method, it is appended to this base URI such that @Path("/find") means that that service's URI is: http://localhost:8080/JSFDemoApp/resources/find.  Note: the ending / is automatically appended if not supplied.

No comments:

Post a Comment