java-ee Java RESTful Web Services (JAX-RS)


Remarks

Unlike SOAP and the WS- stack, which are specified as W3C standards, REST is really a set of principles for designing and using web-based interface. REST / RESTful applications rely heavily on other standards:

HTTP
URI, URL
XML, JSON, HTML, GIF, JPEG, and so forth (resource representations)

The role of JAX-RS (Java API for RESTful Web Services) is to provide APIs that support building RESTful services. However, JAX-RS is just one way of doing this. RESTful services can be implemented other ways in Java, and (indeed) in many other programming languages.

Simple Resource

First of all for a JAX-RS application must be set a base URI from which all the resources will be available. For that purpose the javax.ws.rs.core.Application class must be extended and annotated with the javax.ws.rs.ApplicationPath annotation. The annotation accepts a string argument which defines the base URI.

@ApplicationPath(JaxRsActivator.ROOT_PATH)
public class JaxRsActivator extends Application {

    /**
     * JAX-RS root path.
     */
    public static final String ROOT_PATH = "/api";

}

Resources are simple POJO classes which are annotated with the @Path annotation.

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("/hello")
public class HelloWorldResource {
    public static final String MESSAGE = "Hello StackOverflow!";

    @GET
    @Produces("text/plain")
    public String getHello() {
        return MESSAGE;
    }
}

When a HTTP GET request is sent to /hello, the resource responds with a Hello StackOverflow! message.

GET method types

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("/hello")
public class HelloWorldResource {
    public static final String MESSAGE = "Hello World!";

    @GET
    @Produces("text/plain")
    public String getHello() {
        return MESSAGE;
    }

    @GET
    @Path("/{letter}")
    @Produces("text/plain")
    public String getHelloLetter(@PathParam("letter") int letter){
        if (letter >= 0 && letter < MESSAGE.length()) {
            return MESSAGE.substring(letter, letter + 1);
        } else {
            return "";
        }
    }
}

GET without a parameter gives all content ("Hello World!") and GET with path parameter gives the specific letter out of that String.

Some examples:

$ curl http://localhost/hello
Hello World!
$ curl http://localhost/hello/0
H
$ curl http://localhost/hello/4
o

Note: if you leave out the method-type annotation (e.g. the @GET above), a request method defaults to being a GET request handler.

DELETE method

import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;

@Path("hello")
public class HelloWorldResource {

    private String message = "Hello StackOverflow!";

    @GET
    @Produces("text/plain")
    public String getHello() {
        return message;
    }

    @DELETE
    public Response deleteMessage() {
        message = null;
        return Response.noContent().build();
    }
}

Consume it with curl:

$ curl http://localhost/hello
Hello StackOverflow!

$ curl -X "DELETE" http://localhost/hello


$ curl http://localhost/hello
null

POST Method

import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;

@Path("hello")
public class HelloWorldResource {
    @POST
    @Path("/receiveParams")
    public Response receiveHello(@FormParam("name") String name, @FormParam("message") String message) {
        //process parameters
        return Response.status(200).build();
    }

    @POST
    @Path("/saveObject")
    @Consumes("application/json")
    public Response saveMessage(Message message) {
        //process message json
        return Response.status(200).entity("OK").build();
    }
}

First method can be invoked through HTML form submission by sending captured input parameters. Form submit action should point to -

/hello/receiveParams

Second method requires Message POJO with getters/setters. Any REST client can call this method with JSON input as -

{"sender":"someone","message":"Hello SO!"}

POJO should have the same property as JSON to make serialization work.

public class Message {

    String sender;
    String message;

    public String getSender() {
        return sender;
    }
    public void setSender(String sender) {
        this.sender = sender;
    }
    public String getMessage() {
        return message;
    }
    public void setMessage(String message) {
        this.message = message;
    }
}

Exception Mapper

@Provider
public class IllegalArgumentExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
        
  @Override
  public Response toResponse(IllegalArgumentException exception) {
    return Response.serverError().entity("Invalid input: " + exception.getMessage()).build();
  }
}

This exception mapper will catch all IllegalArgumentExceptions thrown in the application, and show the user a clear message instead of a stacktrace.

UriInfo

In order to get information about the URI the user agent used to access your resource, you can use the @Context parameter annotation with a UriInfo parameter. The UriInfo object has a few methods that can be used to get different parts of the URI.

//server is running on https://localhost:8080,
// webapp is at /webapp, servlet at /webapp/servlet
@Path("class")
class Foo {
    
    @GET
    @Path("resource")
    @Produces(MediaType.TEXT_PLAIN)
    public Response getResource(@Context UriInfo uriInfo) {
        StringBuilder sb = new StringBuilder();
        sb.append("Path: " + uriInfo.getPath() + "\n");
        sb.append("Absolute Path: " + uriInfo.getAbsolutePath() + "\n");
        sb.append("Base URI: " + uriInfo.getBaseUri() + "\n");
        sb.append("Request URI: " + uriInfo.getRequestUri() + "\n");
        return Response.ok(sb.toString()).build();
    }
}

output of a GET to https://localhost:8080/webapp/servlet/class/resource:

Path: class/resource
Absolute Path: https://localhost:8080/webapp/servlet/class/resource#
Base URI: https://localhost:8080/webapp/servlet/
Request URI: https://localhost:8080/webapp/servlet/class/resource

SubResources

Sometimes for organizational or other reasons it makes sense to have your top level resource return a sub-resource that would look like this. (Your sub-resource does not need to be an inner class)

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("items")
public class ItemsResource {

    @Path("{id}")
    public String item(@PathParam("id") String id) {
        return new ItemSubResource(id);
    }

    public static class ItemSubResource {

        private final String id;

        public ItemSubResource(String id) {
            this.id = id;
        }
        
        @GET
        @Produces("text/plain")
        public Item item() {
            return "The item " + id;
        }
    }
}

Custom parameter converters

This is an example of how to implement custom parameter converters for JAX-RS endpoints. The example shows two classes from Java 8's java.time library.

@Provider
public class ParamConverters implements ParamConverterProvider {  
  @Override
  public <T> ParamConverter<T> getConverter(Class<T> rawType,
                                            Type genericType,
                                            Annotation[] annotations)
  {
    if (rawType == LocalDate.class)
      return (ParamConverter<T>) new ParamConverter<LocalDate>() {
        @Override
        public LocalDate fromString(String value) {
          return LocalDate.parse(value);
        }

        @Override
        public String toString(LocalDate value) {
          return null;
        }
      };
    else if (rawType == MonthDay.class)
      return (ParamConverter<T>) new ParamConverter<MonthDay>() {
        @Override
        public MonthDay fromString(String value) {
          int[] ddmm = Arrays.stream(value.split("/"))
                             .mapToInt(Integer::parseInt)
                             .toArray();
          return MonthDay.of(ddmm[1], ddmm[0]);
        }

        @Override
        public String toString(MonthDay value) {
          return null;
        }
      };
    return null;
  }
}

Name binding

Name binding is a concept that allows to say to a JAX-RS runtime that a specific filter or interceptor will be executed only for a specific resource method. When a filter or an interceptor is limited only to a specific resource method we say that it is name-bound. Filters and interceptors that do not have such a limitation are called global.

Defining a name binding annotation

Filters or interceptors can be assigned to a resource method using the @NameBinding annotation. This annotation is used as meta annotation for other user implemented annotations that are applied to a providers and resource methods. See the following example:

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
public @interface Compress {}

The example above defines a new @Compress annotation which is a name binding annotation as it is annotated with @NameBinding. The @Compress annotation can be used to bind filters and interceptor to endpoints.

Binding a filter or interceptor to an endpoint

Consider you have an interceptor that performs GZIP compression and you want to bind such interceptor to a resource method. To do it, annotate both the resource method and the interceptor, as following:

@Compress
public class GZIPWriterInterceptor implements WriterInterceptor {

    @Override
    public void aroundWriteTo(WriterInterceptorContext context)
                    throws IOException, WebApplicationException {
        final OutputStream outputStream = context.getOutputStream();
        context.setOutputStream(new GZIPOutputStream(outputStream));
        context.proceed();
    }
}
@Path("helloworld")
public class HelloWorldResource {
 
    @GET
    @Produces("text/plain")
    public String getHello() {
        return "Hello World!";
    }
 
    @GET
    @Path("too-much-data")
    @Compress
    public String getVeryLongString() {
        String str = ... // very long string
        return str;
    }
}

The @Compress is applied on the resource method getVeryLongString() and on the interceptor GZIPWriterInterceptor. The interceptor will be executed only if any resource method with such a annotation will be executed.

In above example, the interceptor will be executed only for the getVeryLongString() method. The interceptor will not be executed for method getHello(). In this example the reason is probably clear. We would like to compress only long data and we do not need to compress the short response of "Hello World!".

Name binding can be applied on a resource class. In the example HelloWorldResource would be annotated with @Compress. This would mean that all resource methods will use compression in this case.

Note that global filters are executed always, so even for resource methods which have any name binding annotations.

Documentation