Thursday, February 20, 2014

Simple JAX-RS Service

If you have a fundamental grasp of Java and want to learn how to build REST services "RESTful Java with JAX-RS 2.0" is an excellent book for you. 

After reading few chapters, I realized that REST is definitely not just new technology, it's whole new concept and thought about designing enterprise application. Thinking about integration, in REST way definitely going to make you feel exciting. 

I was reading Chapter 3, thought of sharing some knowledge about creating a simple REST service. I may not be able to provide more details about REST concepts in this blog, I would rather recommend/suggest to read some books or some detailed articles. 

Designing RESTful Services is a 4 step process:

1) Define Object Model
   Design Object/Class diagram and identify entities.

2) Model the URI
   First thing for creating distributed interface is to define and name your service endpoints.

3) Defining the data format
In this step you basically design your data structure, for example, XML structure for 'Create Order' service call.

4) Assigning HTTP method (think aboutt operation to be performed on Resources identified in Step 2 above

In this example, we're creating a 'Consumer Entry' service with 3 operations:
- Create Consumer
- Update Consumer
- Retrieve Consumer

1) My Object Model below, a simple POJO.

public class Customer {
   private int id;
   private String firstName;
   private String lastName;
   private String street;
   private String city;
   private String state;
   private String zip;
   private String country;

   public int getId() {
      return id;
   }

   public void setId(int id) {
      this.id = id;
   }
... //other accessors
}

2) Model the URI
In this step you assign URI to your resources; we have just one resource that simplify things. i'm going to create a URL '/customers' for this resource.

3) Defining the data format
In this step, you need to define your data structure to be exchanged over wire, for our example, it's:

<customer id="2">
   <first-name>Andy</first-name>
   <last-name>Reeves</last-name>
   <street>3232 Delaware Street</street>
   <city>Atlanta</city>
   <state>GA</state>
   <zip>39116</zip>
   <country>USA</country>
</customer>

4) Assigning HTTP method (think about operation to be performed on Resources identified in Step 2 above)

We're going to design a service with 3 operation, and we need to think about what HTTP method would apply to these operations. 

- Retrieve Consumer: For this operation we could assign a 'GET' HTTP method on our resource
- Create Consumer: For this operation we could assign a 'POST' or 'PUT' HTTP method. Ideally we use 'POST' for create operations and 'PUT' for update. Read more about this here
- Update Consumer: As I mentioned above, we can assign 'PUT' to our resource.

Theory over.. code magic begins.

Below is my Service class. This is straight from the book, did a little modification by adding '@Singleton' on class because I wanted to store the data after every request, and I didn't want container to create a new instance of my service class for every request. There are bettrer ways of achieving this in REST.Read more about Application.java here.

Author decided to keep things very basic, because of which we ended up marshalling and unmarshalling XML through DOM and creating a static representation of response data.  You can see 3 methods for each of my operation.

CustomerResource.java  (Service Class)
@Singleton
@Path("/customers")
public class CustomerResource {
private Map<Integer, Customer> customerDB = new ConcurrentHashMap<Integer, Customer>();
private AtomicInteger idCounter = new AtomicInteger();

public CustomerResource() {
}

@POST
@Consumes("application/xml")
public Response createCustomer(InputStream is) {
System.out.println("Inside POST createCustomer");
Customer customer = readCustomer(is);
customer.setId(idCounter.incrementAndGet());
System.out.println("Setting id to " + customer.getId());
customerDB.put(customer.getId(), customer);
System.out.println("Created customer " + customer.getId());
Response res = Response.created(
URI.create("customers/" + customer.getId())).build();
return res;
        }

@GET
@Path("{id}")
@Produces("application/xml")
public StreamingOutput getCustomer(@PathParam("id") int id) {
final Customer customer = customerDB.get(id);
if (customer == null) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
return new StreamingOutput() {
public void write(OutputStream outputStream) throws IOException,
WebApplicationException {
System.out.println(outputStream.getClass().getName());
outputCustomer(outputStream, customer);
}
};
}

@PUT
@Path("{id}")
@Consumes("application/xml")
public void updateCustomer(@PathParam("id") int id, InputStream is) {
Customer update = readCustomer(is);
Customer current = customerDB.get(id);
if (current == null)
throw new WebApplicationException(Response.Status.NOT_FOUND);

current.setFirstName(update.getFirstName());
current.setLastName(update.getLastName());
current.setStreet(update.getStreet());
current.setState(update.getState());
current.setZip(update.getZip());
current.setCountry(update.getCountry());
}

protected void outputCustomer(OutputStream os, Customer cust)
throws IOException {
PrintStream writer = new PrintStream(os);
writer.println("<customer id=\"" + cust.getId() + "\">");
writer.println("   <first-name>" + cust.getFirstName()
+ "</first-name>");
writer.println("   <last-name>" + cust.getLastName() + "</last-name>");
writer.println("   <street>" + cust.getStreet() + "</street>");
writer.println("   <city>" + cust.getCity() + "</city>");
writer.println("   <state>" + cust.getState() + "</state>");
writer.println("   <zip>" + cust.getZip() + "</zip>");
writer.println("   <country>" + cust.getCountry() + "</country>");
writer.println("</customer>");
}

protected Customer readCustomer(InputStream is) {
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance()
.newDocumentBuilder();
Document doc = builder.parse(is);
Element root = doc.getDocumentElement();
Customer cust = new Customer();
if (root.getAttribute("id") != null
&& !root.getAttribute("id").trim().equals(""))
cust.setId(Integer.valueOf(root.getAttribute("id")));
NodeList nodes = root.getChildNodes();
for (int i = 0; i < nodes.getLength(); i++) {
Element element = (Element) nodes.item(i);
if (element.getTagName().equals("first-name")) {
cust.setFirstName(element.getTextContent());
} else if (element.getTagName().equals("last-name")) {
cust.setLastName(element.getTextContent());
} else if (element.getTagName().equals("street")) {
cust.setStreet(element.getTextContent());
} else if (element.getTagName().equals("city")) {
cust.setCity(element.getTextContent());
} else if (element.getTagName().equals("state")) {
cust.setState(element.getTextContent());
} else if (element.getTagName().equals("zip")) {
cust.setZip(element.getTextContent());
} else if (element.getTagName().equals("country")) {
cust.setCountry(element.getTextContent());
}
}
return cust;
} catch (Exception e) {
throw new WebApplicationException(e, Response.Status.BAD_REQUEST);
}
}

}


Server
After using JAX-WS, I always thought of deploying my REST web services through command line client. I'm using grizzly as containe. Calling 'start()' in Main.java will start an instance of grizzly.

public class Main {
    // Base URI the Grizzly HTTP server will listen on
    public static final String BASE_URI = "http://localhost:8080/services";

    /**
     * Starts Grizzly HTTP server exposing JAX-RS resources defined in this application.
     * @return Grizzly HTTP server.
     */
    public static HttpServer startServer() {
        // create a resource config that scans for JAX-RS resources and providers
        // in com.example package
        final ResourceConfig rc = new ResourceConfig().packages("com.restfully.shop.services");

        // create and start a new instance of grizzly http server
        // exposing the Jersey application at BASE_URI
        return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc);
    }

    /**
     * Main method.
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        final HttpServer server = startServer();
        System.out.println(String.format("Jersey app started with WADL available at "
                + "%sapplication.wadl\nHit enter to stop it...", BASE_URI));
        System.in.read();
        server.stop();
    }

}

Test Client
I'm simply creating a Customer and retrieving it from my service.

public class ServiceTester {
public static void main(String args[]){
        Client c = ClientBuilder.newClient();
        WebTarget target = c.target("http://localhost:8080/services");
        target = target.path("/customers");
        Invocation.Builder requestBuilder = target.request();
        
        String xml = "<customer>"
                + "<first-name>Bill</first-name>"
                + "<last-name>Burke</last-name>"
                + "<street>256 Clarendon Street</street>"
                + "<city>Boston</city>"
                + "<state>MA</state>"
                + "<zip>02115</zip>"
                + "<country>USA</country>"
                + "</customer>";

        Response response = requestBuilder.post(Entity.xml(xml));
        if (response.getStatus() != 201) throw new RuntimeException("Failed to create");
        String location = response.getLocation().toString();
        System.out.println("Location: " + location);
        response.close();

        String responseStr = c.target(location).request().get(String.class);// java.lang.String is response entity class
        System.out.println("Response is : "+ responseStr);

}

pom.xml
Grizzly has lot of depenmdencies, you can use this pom file to download them.

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>simple-service</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>simple-service</name>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.glassfish.jersey</groupId>
                <artifactId>jersey-bom</artifactId>
                <version>${jersey.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-grizzly2-http</artifactId>
        </dependency>
        <!-- uncomment this to get JSON support:
         <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-moxy</artifactId>
        </dependency>
        -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.9</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.5.1</version>
                <inherited>true</inherited>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.2.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>com.example.Main</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
    <properties>
        <jersey.version>2.4.1</jersey.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>

Overall it was simple to design except the marshalling/unmarshalling piece, and integrating Grizzly with book examples. Anyways, happy reading, drop me a comment, if you've any question.


No comments:

Post a Comment