Multi-project Web Service Application
Summary
This page details an application that is comprised of multiple maven projects. It will detail the overall maven project structure, the way the code is structured and webservice ports I have exposed
A web services can transmit any type of text structure but JSON is the most common. The jackson api can be used to marshal data to and from the wire. Rather then manipulating strings to and from json format directly, I have created Java classes that have the attributes on them that I want to transmit across the wire. For example all responses from the server should be in json form so I have created the following class
package com.celestial.util.objects; /** * * @author Selvyn */ public class SimpleErrorMessage { private String message; public SimpleErrorMessage( String msg ) { message = msg; } public String getMessage() { return message; } public void setMessage( String msg ) { message = msg; } }
When transmitted the result will be marshalled as {"message":"some value"}.
Objects such as these that are marshalled between different service points (clients or servers) have been placed in a maven project called DBankBusinessObjects. All other projects are dependent on this project. This project must be compiled and installed into your local maven repo so that dependent projects can locate the build artifact (mvn install).
Maven project hierarchy
Top maven project is a maven module project bringing together the service layer and reusable java artifact - DBankAppllicationRoot
DBankBusinessObjects maven project contains objects that will be marshalled between service points
DBankDBTier_ws_exp maven project (_exp implies exposed) is the service layer that exposes services to the database. The example code has a rest api to create, find, and update users in the users table
Exposing the services
The next section outlines the way I have exposed the services using the jersey api.
@Path("/user") public class UserRestServices { private String genPassword() @GET @Path("/sayhello") @Produces(MediaType.TEXT_HTML) public String sayHtmlHelloTest() // Test entry point, call this from url in browser to make sure service up and running @GET @Path("/cu/{user}") @Produces(MediaType.APPLICATION_JSON) public Response createUser(@PathParam("user") String userId) // create a user from url in browser, calls __createUser @POST @Path("/cu") @Consumes(MediaType.APPLICATION_JSON) public Response createUserFromObject(User usr) // create a user from java marshalled object, calls __createUser private Response __createUser( User usr ) // actually makes call to DB @DELETE @Path("/du") @Consumes(MediaType.APPLICATION_JSON) public Response deleteUser(User us) // deletes a user if they exist in DB @PUT @Path("/uu") @Consumes(MediaType.APPLICATION_JSON) public Response updateUser(User us) // updates a user if they exist in DB @GET @Path("/fu/{user}/{pwd}") @Produces(MediaType.APPLICATION_JSON) public Response findUser(@PathParam("user") String userId, @PathParam("pwd") String pwd) // finds a user if they exist in the DB }
The URIs I have exposed are
<base uri>/user/cu
<base uri>/user/cu/{userid}
<base uri>/user/fu
<base uri>/user/fu/{userid}/{pwd}
<base uri>/user/uu
<base uri>/user/uu/{userid}
<base uri>/user/du/
<base uri>/user/du/{userid}
The URIs that take a parameter can be used directly from the browser or a HTML type form (this includes javascript). On a slightly tangential note, I have marked the createUser() @Path("/cu/{param}") method with a GET, even though it's actually a POST under HTTP definitions, this because if you attempt to inoke this method from the browser's URL field, the browser will send a GET and not a POST command. Marking the method as a POST will cause the server to return a HTTP 405 - method not allowed.
Using both <base url>/user/xx/{param} and <base url>/user/xx is not necessary. I have done this so I can test the service's methods without having to write a Java client. When I program the client side I will only be using the <base url>/user/xx URI form.
The URIs that don't have a parameter are used in service to service interactions passing JSON strings. For each of these interactions, the simplest way to specify the contract is through a Java class. For example the @POST /cu entry point has the following Java class
package com.celestial.dbbusinessobjects; /** * * @author Selvyn */ public class User { private String userID; private String userPwd; public User() {} public User( String userid, String pwd ) { this.userID = userid; this.userPwd = pwd; } public String getUserID() { return userID; } public void setUserID(String itsUserID) { this.userID = itsUserID; } public String getUserPwd() { return userPwd; } public void setUserPwd(String itsUserPwd) { this.userPwd = itsUserPwd; } @Override public String toString() { return new StringBuffer("itsUserID:").append(this.userID) .append("itsUserPwd:").append(this.userPwd).toString(); } }
I know that under strict ReSTful architecture, there should be no hint of the service being provided in the URI naming (in other words avoid rpc style naming conventions), to this end I have mapped eah URI to one and only one HTTP command.
Under the hood
Let's exam the following service entry points
Example code
@GET
@Path("/cu/{user}")
@Produces(MediaType.APPLICATION_JSON)
public Response createUser(@PathParam("user") String userId)
{
User usr = new User(userId, genPassword());
return __createUser(usr);
}
@POST
@Path("/cu")
@Consumes(MediaType.APPLICATION_JSON)
public Response createUserFromObject(User usr)
{
return __createUser(usr);
}
Essentially I have created two entry points to the same service __createUser() on the same interface class UserRestService. I am going to repeat this pattern for all the other services I want to expose. This is a simple case of refactoring the interfaces methods.
The __createUser() method looks like this
private Response __createUser( User usr ) { Response resp; try { DBConnector connector = DBConnector.getConnector(); connector.loadAndConnect(); UserHandler.getLoader().addUser(connector.getConnection(), usr); resp = Response.status(200).entity(usr).build(); } catch (IOException ex) { resp = Response.status(500).entity(new SimpleErrorMessage("CANNOT ADD USER - SERVER PROBLEM")).build(); Logger.getLogger(UserRestServices.class.getName()).log(Level.SEVERE, null, ex); } catch (ResponseException ex) { Logger.getLogger(UserRestServices.class.getName()).log(Level.SEVERE, null, ex); resp = Response.status(500).entity(ex.getResponse()).build(); } catch (SQLException ex) { resp = Response.status(409).entity(new SimpleErrorMessage(usr.getUserID() + " CANNOT BE ADDED - ALREADY EXIST")).build(); Logger.getLogger(UserRestServices.class.getName()).log(Level.SEVERE, null, ex); } return resp; }
I didn't use Java 8 exception multi catch statements as I wanted to report different messages back to the client.
The call at line 6 is to a singleton. We could have used non singleton implementation and this would have worked to, but backend implementation works in a non webservice context too, such as with JSPs.
Notice the error codes that I am returning in the reponse, see http://www.restapitutorial.com/httpstatuscodes.html for valid codes
Client Code
Here we present several examples of client side code
package com.celestial.jaxrs.client; import com.celestial.dbankbusinessobjects.User; import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig; import com.sun.jersey.api.json.JSONConfiguration; /** * * @author Selvyn */ public class TestServices { public static void main(String[] args) { try { User usr = new User("David", "PP-WW-QQ"); ClientConfig clientConfig = new DefaultClientConfig(); clientConfig.getFeatures().put( JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE); Client client = Client.create(clientConfig); WebResource webResource = client .resource("http://localhost:8084/dbwstier/dbrest/user/fu"); ClientResponse response = webResource.accept("application/json") .type("application/json").post(ClientResponse.class, usr); if (response.getStatus() != 201) { throw new RuntimeException("Failed : HTTP error code : " + response.getStatus()); } String output = response.getEntity(String.class); System.out.println("Server response .... \n"); System.out.println(output); } catch (Exception e) { e.printStackTrace(); } } }
The next examples only show the body changed code
User us = new User("penny", "leonard"); ClientConfig clientConfig = new DefaultClientConfig(); clientConfig.getFeatures().put( JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE); Client client = Client.create(clientConfig); WebResource webResource = client .resource("http://localhost:8084/dbwstier/dbrest/user/du"); ClientResponse response = webResource.accept("application/json") .type("application/json").delete(ClientResponse.class, us); if (response.getStatus() != 200) { throw new RuntimeException("Failed : HTTP error code : " + response.getStatus()); } String output = response.getEntity(String.class); System.out.println("Server response .... \n"); System.out.println(output);
User us = new User("David", ""); ClientConfig clientConfig = new DefaultClientConfig(); clientConfig.getFeatures().put( JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE); Client client = Client.create(clientConfig); WebResource webResource = client .resource("http://localhost:8084/dbwstier/dbrest/user/cu"); ClientResponse response = webResource.accept("application/json") .type("application/json").post(ClientResponse.class, us); if (response.getStatus() != 201) { throw new RuntimeException("Failed : HTTP error code : " + response.getStatus()); } String output = response.getEntity(String.class); System.out.println("Server response .... \n"); System.out.println(output);
To update a record we are going to use HTTP PUT command
User oldUser = new User("David", "ppp"); User newUser = new User("David", "PP-XX-YY"); UserMap umap = new UserMap( oldUser, newUser ); ClientConfig clientConfig = new DefaultClientConfig(); clientConfig.getFeatures().put( JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE); Client client = Client.create(clientConfig); WebResource webResource = client .resource("http://localhost:8084/dbwstier/dbrest/user/uu"); ClientResponse response = webResource.accept("application/json") .type("application/json").put(ClientResponse.class, umap); if (response.getStatus() != 200) { throw new RuntimeException("Failed : HTTP error code : " + response.getStatus()); } String output = response.getEntity(String.class); System.out.println("Server response .... \n"); System.out.println(output);
Notice the new class UserMap at line 28
public class UserMap { public User oldUser; public User newUser; public UserMap(){} public UserMap( User oldU, User newU ) { oldUser = oldU; newUser = newU; } }
This will be a carrier object, holding two objects of type User.
Let's see the full server side service interface