JAX-RS server-side endpoints

We have looked at a simple GET request on a JAX-RS simple resource. In order to create a useful business application, we need the other web request methods, namely POST, PUT, and DELETE.

JAX-RS common server annotation

JAX-RS defines annotations for defining server side REST style endpoint, which are found in the Java package javax.ws.rs.

The following is a table of these annotations:

Annotation

Description

@Path

Defines the relative URI path for the REST style Java class and configures where it is installed and mapped.

@GET

Defines a method on the Java class REST style resource to accept HTTP GET requests.

@POST

Defines a method on the Java class REST style resource to accept HTTP POST requests.

@PUT

Defines a method on the Java class REST style resource to accept HTTP PUT requests.

@DELETE

Defines a method on the Java class REST style resource to accept HTTP DELETE requests.

@HEAD

Defines a method on the Java class REST style resource to accept HTTP HEAD requests.

@TRACE

Defines a method on the Java class REST style resource to accept HTTP TRACE requests.

@PathParam

Defines a URI fragment parameter that permits the developer to extract from the REST request into the resource class. URI path parameters are extracted from the request URI, and the parameter name must match information found in the URI path template variables specified in the @Path class-level annotation.

@QueryParam

Defines a (CGI-style) query parameter that permits the developer to extract from the REST request.

@Consumes

Specifies MIME media representations that the REST style consumes from the client. Typically, this is useful for a REST style uploading application, which only accepts a certain media type of document.

@Produces

Specifies the MIME media representation that the REST style endpoint produces for the client.

@Provider

Specifies extra resource information that is useful for extending the capabilities of the JAX-RS runtime with custom features for your application. This annotation can be seen as a sort of factory object or virtual constructor for building filters, interceptors, and dynamic features.

Obviously, a Java method can only accept one of the HTTP request annotations. It is illegal, for instance, to annotate a single method with @GET and @POST; and the JAX-RS provider will generate an error.

Defining JAX-RS resources

Let's look at a more realistic example of JAX-RS service endpoint. We will develop a REST style endpoint that can accept a registered set of users. The service allows a client to register a user with a log-in name, first and last name, and a secret code. Admittedly, this service is contrived, however this shows how to implement a JAX-RS service completely and we will see how to test it thoroughly. The following table shows the REST style URI for the service:

URI

Purpose

<mywebapp>/rest/users

Refers to list of registered users

<mywebapp>/rest/users/pilgrimp

Refers to a specific user

<mywebapp>/rest/users/goslingj

Refers to another specific user

Where <mywebapp> is the placeholder for URL http://localhost/mywebapp/

The following code shows our data value class, User:

package je7hb.jaxrs.basic;

public final class User implements Comparable<User> {
  private final String loginName, firstName, lastName,secretName; 

  public User(String loginName, String firstName,String lastName, int secretCode) {
    this.loginName = loginName;
    this.firstName = firstName;
    this.lastName = lastName;
    this.secretCode = secretCode;
  }

  public String getLoginName() { return loginName; }
  public String getFirstName() { return firstName; }
  public String getLastName() { return lastName; }
  public int getSecretCode() { return secretCode; }

  @Override
  public int compareTo(User ref) {
    return loginName.compareTo(ref.loginName);
  }
  
  // hashcode(), equals(), toString() methods omitted
}

For storage of the user, we will rely on Singleton EJB, which is called UserRegistry. The following code explains it:

package je7hb.jaxrs.basic;
/* ... imports omitted */

@Singleton
@Startup
public class UserRegistry {
  private ConcurrentMap<String,User> registeredUsers= new ConcurrentHashMap<>();

  public void addUser( User user ) {
    registeredUsers.put( user.getLoginName(), user );
  }

  public void removeUser( User user ) {
    registeredUsers.remove(user.getLoginName());
  }

  public User findUser( String loginName ) {
    return registeredUsers.get(loginName);
  }

  public List<User> getUsers( ) {
    List<User> users = new ArrayList<>(registeredUsers.values());
    Collections.sort(users);
    return users;
  }

  @PostConstruct
  public void postConstruct() { /* ... */ }
  @PreDestroy
  public void preDestroy() {  /* ... */ }
}

As a reminder, this is a stateless session EJB, which is annotated with @Startup, because we want the bean instance to be immediately available as soon as the web application is deployed. We also annotate the class with @Singleton to ensure only the EJB instance is available for the entire application.

Now let's look at the following code of the REST style resource, which is implemented by the class called RegisteredUserResource:

package je7hb.jaxrs.basic;
import javax.ejb.*;
import javax.ws.rs.*;

@Path("/users")
@Stateless
public class RegisteredUserResource {
  @EJB
  private UserRegistry userRegistry;

  @GET
  @Produces("text/csv")
  public String listUsers() {
    StringBuilder buf = new StringBuilder();
    for ( User user : userRegistry.getUsers()) {
      buf.append( user.getLoginName()+","+user.getFirstName()+",");
      buf.append( user.getLastName()+","+user.getSecretCode()+"
");
    }
    return buf.toString();
  }

  @GET
  @Path("{id}")
  @Produces("text/csv")
  public String getUser( @PathParam("id") String loginName ) {
    User user = userRegistry.findUser(loginName);
    if ( user == null ) {
      return "";
    }
    StringBuilder buf = new StringBuilder();
    buf.append( user.getLoginName()+","+user.getFirstName()+",");
    buf.append( user.getLastName()+","+user.getSecretCode()+"
");
    return buf.toString();
  }

  @POST
  @Path("{id}")
  public void addUser( @PathParam("id") String loginName,@FormParam("firstName") String fname,@FormParam("lastName") String lname,@FormParam("secretCode") int code )
  {
    User user = new User(loginName,fname,lname,code);
    userRegistry.addUser(user);
  }

  @PUT
  @Path("{id}")
  public void amendUser( @PathParam("id") String loginName,@FormParam("firstName") String fname,@FormParam("lastName") String lname,@FormParam("secretCode") int code )
  {
    User user = userRegistry.findUser(loginName);
    if ( user == null ) {
      throw new UnknownUserException("unknown login name: ["+loginName+"]");
      }
      else {
        User user2 = new User(user.getLoginName(), fname, lname, code );
        userRegistry.addUser(user2);
      }
  }

  @DELETE
  @Path("{id}")
  public void deleteUser( 
  @PathParam("id") String loginName) {
    User user = userRegistry.findUser(loginName);
    if ( user == null ) {
      throw new UnknownUserException("unknown login name: ["+loginName+"]");
    }
    else {
      userRegistry.removeUser(user);
    }
  }
}

The class implements all of the four HTTP web request methods. It is surprising that the JAX-RS resource, RegisteredUserResource, itself is written as a Stateless session EJB. The reason for this is to do with the progression of the initial JAX-RS specification 1.0 predated the Context and Dependency Inject and EJB instance facilities and at the time of writing was not clear JAX-RS 2.0 will work. Nonetheless, the procedure is solid and it is the intention of the standard JavaEE 7 to support JAX-RS, CDI and EJB injection.

The class RegisteredUserResource injects the singleton EJB UserRegistry and therefore can make use of the store. The class is annotated with the relative REST style URI path/users.

The listUsers() method has @Produces annotation that is different, text/csv stands for comma-separated values, which is a file format supported by popular spreadsheet programs such Microsoft Excel and Libre Office. Hence, this method generates comma-delimited output for each user in the registry.

The method getUser() introduces us to URI path variables. Variables are denoted with the braces ({ }). The annotation @PathParam("{id}") adds a variable to the current path and permits a method to be extracted from the URL template. This is the way to process a REST style by identifier and the annotation is applied to the method argument. This method attempts to find the User record by log-in name. If it can retrieve the object getUser(), it returns a CSV representation otherwise the output is empty text. This method is important for testing the REST resource as we shall see later.

In order to add a new user to the registry, there is @POST annotation on the addUser () method. Here is a new annotation @javax.ws.rs.FormParam, which specifically retrieves HTTP form parameters from the incoming request. They correspond directly with HTML Form submissions. The @FormParam requires the name of the form request parameter and we apply them to the method arguments. The JAX-RS implementation injects the form parameters and the identifier variable during invocation of the method. The addUser() method simply constructs a new User record and adds it to the registry. There is no side effect and output response rendered for a POST. The JAX-RS implementation will respond with a HTTP Response Code 200 on successful execution of the method.

The method amendUser() is almost the same as the addUser(), because it uses the same parameters with @PathParam and @FormParm annotations. The difference is the amendment assuming that a User record already exists in the registry. It saves a new version of the User record into the registry. The amendment is associated with the @PUT annotation and associated HTTP PUT requests.

The method deleteUser() is the last method and this annotated with the @Delete annotation and associated HTTP DELETE requests. This method only requires the path parameter to identifier the specify user to delete.

Tip

Prefer to specify the MIME type

You should seriously consider specifying the media type with @Produces for all methods of the REST resource, especially if you are passing multiple MIME types. If you write a custom resource method that can return more than one MIME content type with ResponseBuilder, then it is definitely helpful to the client to set media type.

You may be wondering about the custom exception UnknownUserException. If an arbitrary exception is raised during the JAX-RS request on the server side, then the client (or user) sees a HTTP Response 500 code (Forbidden). This is probably not the error you want users or developers to see.

The following is the code for the exception class:

package je7hb.jaxrs.basic;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.*;

public class UnknownUserException 
extends WebApplicationException {
  public UnknownUserException(String message) {
    super( Response.status(Response.Status.NOT_FOUND).
    entity(message).type(MediaType.TEXT_PLAIN_TYPE).build());
  }
}

This custom exception extends the WebApplicationException exception. In the constructor, we make use of the JAX-RS Response builder object to generate a HTTP 404 error (NOT_FOUND), add our message string as the entity and set the media type to text/plain MIME content. This is the key to building custom JAX-RS messages, and I hope you will not see many of these error message in your applications.

Testing JAX-RS resources

The obvious way to test a JAX-RS resource, in a clean, concise, and solid way, is to deploy the resource to a container and then have a test harness that invokes each of the endpoints. By running inside a container we can have a high degree of confidence that the final code will run in a production environment.

Let us now look at how to write a test for all of these processes. The following is the code for the unit test RegisteredUserResourceTest. Be warned, it is fairly big for now:

package je7hb.jaxrs.basic;
/* ... imports omitted */

public class RegisteredUserResourceTest {
  public static final String ROOT_RES_PATH ="http://localhost:8080/mywebapp/rest/users";
  public static final String USER_RES_PATH =ROOT_RES_PATH+"/pilgrimp";
  private static SimpleEmbeddedRunner runner;

  @BeforeClass
  public static void beforeAllTests() throws Exception {
    WebArchive webArchive = ShrinkWrap.create(WebArchive.class, "testusers.war")
    .addClasses(RegisteredUserResource.class, 
    /* . . . */    }

  /* ... */    

  @Test
  public void shouldAddOneUser() throws Exception {
    Map<String,String> params = new HashMap<String,String>() {
        {
        put("firstName",    "Peter");
        put("lastName",     "Pilgrim");
        put("secretCode",   "19802014");
        }
      };

      List<String> output = makePostRequest(new URL(USER_RES_PATH), params);
      assertTrue(output.isEmpty());
      List<String> lines = makeGetRequest( new URL(USER_RES_PATH) );
      assertFalse(lines.isEmpty());
      assertEquals("pilgrimp,Peter,Pilgrim,19802014", lines.get(0));
    }

  /* ... */        
}

The class RegisteredUserResourceTest is fairly involved and more than that code is devoted to creating the environment for the test. Having said all of that, the test does work.

The first part of the call is the refactoring of the ShrinkWrap packaging and the embedded container launch to the JUnit methods beforeAllTests() and afterAllTests(). These are static methods so that they are only invoked when the class is loaded, just before the series unit test method is executed and after the test has run in the class.

The unit test makes use of a utility class WebMethodUtils, which has a series of static methods to make HTTP request method calls to a remote server. The utility class uses the JDK classes, javax.net.URL, javax.net.HttpURLConnection also standard I/O. (In order to save space, the WebMethodUtils class is not shown, but you can access it from the book's website.)

The first test, the method shouldAddOneUser(), creates a HTTP POST request of a user with form data. A literal hash map collection is created with name and value pairs to simulate form request data. A HTTP POST request is made to the JAX RS Resource with the form parameters. This is the responsibility of the static call to makePostRequest() with the URL http://localhost:8080/mywebapp/rest/users/pilgrimp. There should be no output and there is an assertion for empty text. Next, the unit test method makes a call to makeGetRequest() with the same URL. We should expect the output from the REST style to be comma delimited as "pilgrim,Peter,Pilgrim,19802014", which of course the actual output matches to. In this way, we validate that information was stored by the JAX-RS service. The following code shows the validation:

@Test
public void shouldAmendOneUser() throws Exception {
  shouldAddOneUser();

  Map<String,String> params2 = new HashMap<String,String>(){
    {
      put("firstName",    "Pierre");
      put("lastName",     "Pilgrim");
      put("secretCode",   "87654321");
    }
  };

  List<String> output = makePutRequest(new URL(USER_RES_PATH), params2);
  assertTrue(output.isEmpty());
  List<String> lines = makeGetRequest(new URL(USER_RES_PATH) );
  assertFalse(lines.isEmpty());
  assertEquals("pilgrimp,Pierre,Pilgrim,87654321",lines.get(0));
}

The second test method shouldAmendOneUser() follows a similar principle and it executes HTTP POST to insert the record followed by a HTTP PUT and then a HTTP GET. The test validates the operation of the PUT method by editing the user record from the client side. In this case, the first name is changed to Pierre and the secret code to a different number. The GET request validates the data has been changed by the endpoint. The following code shows what we just read:

@Test
public void shouldDeleteOneUser() throws Exception {
  shouldAddOneUser();

  List<String> output = makeDeleteRequest(new URL(USER_RES_PATH));
  assertTrue(output.isEmpty());
  List<String> lines = makeGetRequest(new URL(USER_RES_PATH) );
  assertTrue(lines.isEmpty());
}

The third test method shouldDeleteOneUser() creates a HTTP POST method with a user and invokes the HTTP DELETE method. The GET request validates the data has been removed. The output text should be blank.

In the book's source code, you will see the fourth and final test method shouldAddTwoUsers() should verify that the JAX-RS Resource can maintain more than one User record. There, we create two different User records with HTTP POST web request. In this test method, we invoke the HTTP GET with the parent URL http://localhost:8080/mywebapp/rest/users and we validate the CSV output. The list of users is sorted by the login name, which you can see in the UserRegistry class, namely the getUsers() method.

Here is a little to the wise, the unit test, although it follows a Behavioral-Driven Design pattern, which is nice, is not the best way to achieve the result. We will see later how to achieve better testing with the JAX-RS Client API.

Path URI variables

As we have seen in the previous section, path variables are placeholders inside a URL template that represent a value that changes on the request. Path variables are denoted within brace characters for example, "/users/{id}". A Path variable can be a simple name or it can be a regular expression.

The Path variable as a simple name is defined by combination of alphabetic and numerical characters. In fact, the path variable can be any character apart from spaces, backslash and special terminal characters. The URI variable must match the regular "[^/]+?". For best practice, it is probably best to stick to the Java identifier syntax.

The following is an example of simple variable conventions:

@Path("/users/{username47}")
public class ACMEUserResource {
  @GET
  @Products("text/xml")
  public String getUserList( ) {
    // username is null, so return a list collection
    /* ... */
  }

  @GET
  @Products("text/xml")
  public String getUser (@PathParam("username47") String username ) {
    /* ... */
  }
}

In the example class we just saw, ACMEUserResource is annotated with a path URI template with one variable, which is called username47. There are two methods, namely, getUser() and getUserList(). The getUser() method accepts a single argument, a named path parameter /users/{username47}. JAX-RS will invoke this method if there is a matching URL request such as /users/jamesdean. On the other hand, if the incoming URL request was just defined as /users then JAX-RS, instead, will choose the getUserList() method, because the method does not require a path parameter.

Path variable can also be defined with custom regular expressions to further restrict the characters that can be matched. The following is an example of the same JAX-RS resource, where we restrict the match to lowercase and uppercase alphanumeric and underscore characters.

@Path("/users/{username47: [a-zA-Z_][a-zA-Z_0-9]*}")
public class ACMEUserResource { /* ... */ }

A @Path value can have leading or trailing slash character (/). Given the regular expression, the JAX-RS runtime parses the template for matching URI path elements. In this case, username47 accepts a path element that can start with an underscore character. A path name can start with a leading or trailing slash character.

It is possible to have a URI path template with more than one variable. Each variable name must be surrounded with braces. The following is an example of a widget REST style resource for a warehouse business.

@Path("/widgets/{range}/{manufacturer}/{productId}")
public class ACMEInventoryResource {
  @PathParam("range") 
  private String range;
  @PathParam("manufacturer") 
  private String manufacturer;
  @PathParam("productId") 
  private String productId;
  
  /* ... */
}

This class ACMEInventoryResource accepts a resource with three variables and the JAX-RS provider will attempt to activate it on matching URL requests. The developer of this resource must take into account that perhaps one, two, or three of the parameters may or may not be selected.

JAX-RS annotations for extracting field and bean properties

JAX-RS has several annotations for extracting field and bean properties from in the incoming HTTP request. We have already seen some of them such as @PathParam, which extracts data from the URI template path and @FormParam, which extracts data from form request parameters.

JAX-RS has some additional annotations to extract further data from the HTTP request.

Annotation

Description

@Context

Injects JAX-RS context information into the class field and bean property of method parameter.

@CookieParam

Extracts data from cookies declared in the request header.

@FormParam

Extracts data from form request data in a POST and PUT request and where the content type is encoded with application/x-www-form-urlencoded.

@HeaderParam

Extracts the data value from a HTTP header parameter.

@MatrixParam

Extracts the data value from a URI matrix parameter.

@PathParam

Extracts the data value from a URI template parameter.r

@QueryParam

Extracts the data value from a URI query parameter, which is the same as the old fashion CGI query parameter.

@DefaultValue

Injects a default value into the class field and bean property of method parameter when the JAX-RS runtime cannot find an appropriate value.

Extracting query parameters

The annotation @javax.ws.js.QueryParam allows data values to be extracted from the query component of the incoming request URI, the web request.

Let us look at a JAX-RS resource that demonstrates the use of @QueryParam. The business case is a website that delivers job search for contractors and permanent staff. For this example, we show only the contract search for candidate. We allow contractors to search for jobs by minimum and maximum rate, the currency, and also allow the unit rate to be set. For example, contract can be set by hours per day, a daily rate, or sometimes a weekly rate.

The following is the code for the JobSearchService REST style resource:

@Path("/contracts")
public class JobSearchService {
  @GET
  public String getJobs(
    @QueryParam("ccy")      String ccy,
    @QueryParam("unitrate") String unitrate,
    @QueryParam("minrate")  int minrate,
    @QueryParam("maxprice") int maxrate)
  {
    /*...*/
  }
}

The resource is invoked by a URI template matching the /contracts. The JAX-RS runtime calls the method getJobs() with a HTTP GET request. In order to fulfill the request the URI must be supplied with all of the expected query parameters.

The following URIs match this resource.

/contracts?ccy=GBP&unitrate=PER_DAY&minrate=250&maxrate=750
/contracts?maxrate=470&minrate=325&ccy=GBP&unitrate=PER_DAY
/contracts?&unitrate=PER_HOUR&ccy=EUR&minrate=30&maxrate=90

It is an interesting note and a nice technique that query parameters can be combined with @DefaultValue annotations.

Extracting matrix parameters

Matrix parameters are a form of URI pattern that contains name and value pairs. The form of the URI is as follows "/something;param1=value1;param2=value2". The URI pattern contains name and value pair separated with an equal character (:) and the pairs delimited by the semi-colon character (;).

The following is an example of JAX-RS resource that makes use of @javax.ws.js.MatrixParam annotation:

@Path("/admin")
public class ValuationService {
  @GET
  @Path("{customer}")
  public String getValue(
    @MatrixParam(«price») String price,
    @MatrixParam(«quantity») int quantity)
  {
    return String.format(
      "Customer [%s] want price [%s] at quantity: [%d]"customer, price, quantity );
  }
}

This class ValuationService responds to the URL pattern such as /admin/janet_fripps. The JAX-RS runtime provider will invoke this class given the matching URI and the method getValue().

For the URI pattern /admin/janet_fripps, the method generates the following text:

Customer janet_fripps wants price null at quantity null. 

For the URI pattern /admin/janet_fripps;price=24.99, the method generates the following text:

Customer janet_fripps wants price 24.99 at quantity null. 

For the URI pattern /admin/janet_fripps;price=24.99;quantity=12, the method generates the following text:

Customer janet_fripps wants price 24.99 at quantity 12. 

For the alternative URI pattern: /admin/mark_webber;quantity=7;price=39.99, the method generates the following text:

Customer mark_webber wants price 39.99 at quantity 7. 

Using default values

JAX-RS permits default values to be defined for path variable on the class field and bean property or method argument. The @javax.ws.rs.DefaultValue annotation specifies a default value, if the metadata is not present in the request.

The following is an example of the annotation in action:

@Path("/aston/{year}/{model}/{engine}")
public class CarProductResource {
  @DefaultValue("2014") @PathParam("year")private String range;
  @DefaultValue("Solar") @PathParam("model")private String model;
  @DefaultValue("2155") @PathParam("engine")private int engineCc;
  
  /* ... */
}

This CarProductResource class is a fictional example resource for a British car manufacturer and it caters for the firm's idea of organizing their business of selling cars around the combination of year, model, and an engine size. Here, we have gone through the trouble of ensuring that all three parameters are always set to a value, even if one or more parameters are missing from the web request to the resource.

Extracting form parameters

JAX-RS extracts form parameters with the annotation @javax.ws.rs.FormParam. Form parameters are submitted from a web client and encoded by the browser in standard format. They are normally sent with a HTTP POST or PUT request.

We already have seen how to extract form parameters in the UserRegistry example earlier in this chapter. The following is this code again for perusal.

@Path("/users") 
@Stateless
public class RegisteredUserResource {
  @POST @Path("{id}")
  @Consumes("application/x-www-form-urlencoded")
  public void addUser( @PathParam("id") String loginName,
    @FormParam("firstName") String fname,
    @FormParam("lastName") String lname,
    @FormParam("secretCode") int code )
    {
      User user = new User(loginName,fname,lname,code);
      userRegistry.addUser(user);
    }
  
  /* ... */
}

The @Consumes annotation on the resource method, directly stipulates how this method will behave, the MIME content, it will only be triggered by the JAX-RS runtime to act on HTML form requests.

There is an alternative way to access form parameters generically. In this case, we do need the @Consumes annotation and must use the JAX-RS specific javax.ws.js.core.MultivaluedMap collection. The multi-value map is a map collection of keys to a list of values. Each dictionary key can map to more than one value, which is an allowed feature of the HTTP specification.

Here is an alternative implementation of the addUser() method that demonstrates the generic form parameter logic:

@Path("/users")
@Stateless
public class RegisteredUserResource {
  /* ... */
  @POST
  @Path("{id}")
  @Consumes("application/x-www-form-urlencoded")
  public void addUser( @PathParam("id") String loginName,MultivaluedMap<String,String> formParams)
  {
    User user = new User(
      formParams.getFirst("firstName"),
      formParams.getFirst("lastName"),
      formParams.getFirst("secretCode"));
    userRegistry.addUser(user);
  }
  
  /* ... */
}

It is interesting to note, we call getFirst() to retrieve the value of the key from the multi-value map.

Field and bean properties

When the JAX-RS runtime instantiates a resource at runtime, it will also inject values into the fields of the resource and JavaBeans. It will inject values into method parameter before invocation of the matched resource method after URI path matching. The runtime will pay attention particularly to the following annotations: @CookieParam, @Context, @FormParam, @HeaderParam, @MatrixParam, @PathParam, @QueryParam.

The JAX-RS runtime perform injection at object creation time and therefore the annotations are checked for incompatible contextual scope, but the standard does not enforce the restriction, instead it recommends that the runtime warn the developer when the annotation is used in a problematic scope.

The following are the rules for the injection of parameter values:

  • The runtime will apply conversion for an object type V for which javax.ws.js.ext.ParamConverter is available via registered javax.ws.js.ext.ParamConverterProvider.
  • Injection applies automatically to primitive types.
  • Types that have a constructor with a single String argument.
  • Types that have a static method named valueOf() or fromString() with a single String argument and also return an instance of the type. If both methods are available, then for a non-enumerated type the runtime must choose valueOf(), otherwise for an enumerated type the runtime chooses fromString().
  • The type is a specific Java collection and a generic type List<T>, Set<T>, or SortedSet<T>.
  • For any of these injection values, the developer can choose to annotate the injection point with a @DefaultValue.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.156.235