JAX-RS
Content
HTTP Method and URI Matching
JAX-RS Injection
JAX-RS Content Handlers
Server Responses and Exception Handling
HATEOAS
Http Content Negotiation
Scaling JAX-RS Apps
HTTP Method and URI Matching
Binding HTTP methods
@Target({ElementType.Method})
@HttpMethod(HttpMethod.GET)
@Retention(RetentionPolicy.RUNTIME)
public @interface GET {
}
- JAX-RS defines 5 annotations that map to specific HTTP operations
- (GET, PUT, POST, DELETE, HEAD)
- @GET does not mean anything
- JAX-RS looks for meta-annotation @HTTPMethod
- Developers may create new annotations that bind to other methods
@Path Annotation
// Maps GET /orders/unpaid to getUnpaidOrders()
@Path("/orders")
public class OrderResource {
@GET
@Path("unpaid")
public String getUnpaidOrders() {
// ...
}
}
- Can have complex matching expressions
- Can be used on methods (object factory for subresources)
- Defines a URI matching pattern for incoming HTTP requests
- Can be placed on class or several methods
- To receive a request at least an HttpMethod annotation is required
@Path Annotation #2
- Usually value is simple string, but also more complex expressions allowed
- Template parameters
- @Path("{id}")
- @Path("{firstname}-{lastname}")
- Regular Expressions
- @Path("{id : \\d+}") // only digits
- @Path("{id: .+}/address")
- /customers/max/muster/address
@Path Annotation #3
- Most specific match wins algorithm
- Number of literal characters in the full URI
- Number of template expressions
- Number of non-default expressions (RE)
- E.g.:
- 1: /customers/{id}/{name}/address
- 2: /customers/{id:.+}/address
- 3: /customers/{id}/address
- 4: /customers/{id:.+}
- 1-3 more literal characters than 4
- 1 has most num of template expressions
- 2 has more non-default expressions than 3
@Path Annotation #4
- Encoding
- URI specification only allows certain characters
- a-zA-z allowed
- 0-9 allowed
- _-!.~'()+ allowed
- ,;:$&+=?/\[]@ allowed but reserved
- All other characters must be encoded using % followed by two digit hex num
- Hex num corresponds to hex num in ASCII table
- (ASCII table)
- roy&fielding - roy%26fielding
- When creating @Path expressions, strings may be encoded (optional)
- If char is illegal, JAX-RS will try to encode
@Path Annotation #5
- Matrix paramters
- Specified in URI, name-value pairs within path
- http://example.cars.com/mercedes/e55;color=black/2006
- Delimited by ; character
- Here after e55, name of matrix param is color, value is black
- Not the same as query params
- Query params always at the end of URI
- Matrix parameters represent attributes of certain segments
- Matrix parameters ignored when matching requests
- Illegal to specify in @Path
- @Path("e55/{year}") would match /e55;color=black/2006
- Not part of matching process, but used to get info
Subresource Locators
- So far static path matching
- JAX-RS allows dynamic dispatch
- Subresource locators are Java methods annotated with @Path, but without HttpMethod
- Returns an object that is a JAX-RS annotated service
- E.g.: customer db is partitioned into different databases based on geographic location
- Finding the correct db and db operations should be decoupled
Subresource Locators #2
@Path("/customers")
public class CustomerDBHandler {
@Path("{database}-db")
public CustomerResource getDatabase(@PathParam("database") String db) {
return locateCustomerResource(db);
}
private CustomerResource locateCustomerResource(String db) {
// ...
}
}
public class CustomerResource {
@POST
@Consumes("application/xml")
public Response createCustomer(...) {
// ...
}
// ...
}
Subresource Locators #3
- CustomerDBHandler does not service any http requests directly
- Processes database identifier part of URI
- Delegates http method to subresource
- Subresource not annotated with @Path
- Not root resource anymore
- Must not be registered in Application class
- Full dynamic dispatch
- Subresource locators may return any object
- JAX-RS provider will introspect class for resource methods that can handle request
Request Matching Tips
@Path("/a")
public class Resource1 {
@GET
@Path("/b")
public Response get(){}
}
- E.g.: GET /a/b
- Matching algorithm will find best matching class first and then continue dispatch
- Resource1 is chosen due to precedence rules
- Remainder is matched within class Resource1
- E.g.: OPTIONS /a/b will return 405 "Method Not Allowed"
- If path annotations are the same for more classes, reflection over all classes
@Path("/{any: .+}")
public class Resource2 {
@GET
public Response get(){}
@OPTIONS
public Response options(){}
}
JAX-RS Injection
JAX-RS Injection
- @javax.ws.rs.PathParam
- @javax.ws.rs.MatrixParam
- @javax.ws.rs.QueryParam
- @javax.ws.rs.FormParam
-
@javax.ws.rs.HeaderParam
-
@javax.ws.rs.CookieParam
-
@javax.ws.rs.core.Context
@PathParam
@Path("/customers")
public class CustomerResource {
@Path("{id}")
@GET
@Produces("application/xml")
public StreamingOutput getCustomer(@PathParam("id") int id) {
// ...
}
@Path("{first}-{last}")
@GET
@Produces("application/xml")
public StreamingOutput getCustomer(
@PathParam("first") String firstName,
@PathParam("last") String lastName) {
// ...
}
}
PathSegment and Matrix Parameters
public interface PathSegment {
String getPath();
MultivaluedMap<String, String> getMatrixParameters();
}
@Path("/cars/{make}")
public class CarResource {
@GET
@Path("/{model}/{year}")
@Produces("image/jpeg")
public Jpeg getPicture(
@PathParam("make") String make,
@PathParam("model") PathSegment car,
@PathParam("year") String year) {
String carColor = car.getMatrixParameters().getFirst("color");
}
}
Multiple PathSegments
@Path("/cars/{make}")
public class CarResource {
@GET
@Path("/{model : .+}/year/{year}")
@Produces("image/jpeg")
public Jpeg getPicture(
@PathParam("make") String make,
@PathParam("model") List<PathSegment> car,
@PathParam("year") String year) {
// ...
}
}
- GET /cars/mercedes/e55/amg/year/200
- PathSegments
- e55
- amg
URI Information
public interface UriInfo {
public String getPath();
public String getPath(boolean decode);
public List<PathSegment> getPathSegments();
public List<PathSegment> getPathSegments(boolean decode);
public MultivaluedMap<String, String> getPathParameters();
public MultivaluedMap<String, String> getPathParameters(boolean decode);
// ...
}
@Path("/cars/{make}")
public class CarResource {
@GET
@Path("/{model}/{year}")
@Produces("image/jpeg")
public Jpeg getPicture(@Context UriInfo info) {
String make = info.getPathParameters().getFirst("make");
PathSegment model = info.getPathSegments().get(1);
String color = model.getMatrixParameters().getFirst("color");
}
}
@MatrixParam
@Path("/{make}")
public class CarResource {
@GET
@Path("/{model}/{year}")
@Produces("image/jpeg")
public Jpeg getPicture(
@PathParam("make") String make,
@PathParam("model") String model,
@MatrixParam("color") String color) {
// ...
}
}
@QueryParam
@Path("/customers")
public class CustomerResource {
@GET
@Produces("application/xml")
public String getCustomers(
@QueryParam("start") int start,
@QueryParam("size") int size) {
// ...
}
}
// Programmatic Query Param
@Path("/customers")
public class CustomerResource {
@GET
@Produces("application/xml")
public String getCustomers(@Context UriInfo info) {
String start = info.getQueryParameters().getFirst("start");
String size = info.getQueryParameters().getFirst("size");
// ...
}
}
@FormParam
@Path("/customers")
public class CustomerResource {
@POST
public void createCustomer(
@FormParam("firstname") String first,
@FormParam("lastname") String last) {
// ...
}
}
@HeaderParam
@Path("/myservice")
public class MyService {
@GET
@Produces("text/html")
public String get(@HeaderParam("Referer") String referer) {
// ...
}
}
public interface HttpHeaders {
public List<String> getRequestHeader(String name);
public MultivaluedMap<String, String> getRequestHeaders();
// ...
}
@Path("/myservice")
public class MyService {
@GET
@Produces("text/html")
public String get(@Context HttpHeaders headers) {
String referer = headers.getRequestHeader("Referer").get(0);
for (String header : headers.getRequestHeaders().keySet()) {
System.out.println("This header was set: " + header);
}
// ...
}
}
@CookieParam
@Path("/myservice")
public class MyService {
@GET
@Produces("text/html")
public String get(@CookieParam("customerId") int custId) {
// ...
}
}
public class Cookie {
public String getName() {...}
public String getValue() {...}
public int getVersion() {...}
public String getDomain() {...}
public String getPath() {...}
// ...
}
@Path("/myservice")
public class MyService {
@GET
@Produces("text/html")
public String get(@CookieParam("customerId") Cookie custId) {
// ...
}
}
JAX-RS Content Handlers
Built-in Content Marshalling
- javax.ws.rs.core.StreamingOutput
- java.io.InputStream, java.io.Reader
- java.io.File
- byte[]
- String, char[]
- MultivaluedMap<String, String>
- javax.xml.transform.Source
javax.ws.rs.core.StreamingOutput
// Callback interface
public interface StreamingOutput {
void write(OutputStream os) throws IOException, WebApplicationException;
}
//
@Path("/")
public class ResourceX {
@GET
@Produces("text/plain")
StreamingOutput get(){
// anonymous inner class implementation
return new StreamingOutput() {
public void write(OutputStream os)
throws IOException, WebApplicationException {
os.write("Hi".getBytes());
}
}
}
}
- Allows streaming of raw response bodies
- JAX-RS runtime calls the write method and supplies the OutputStream whenever ready
- Here anonymous inner class implementation
java.io.InputStream
@Path("/")
public class Resource {
@PUT
public void putInfo(InputStream inputStream) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1000];
int wasRead = 0;
do {
wasRead = inputStream.read(buffer);
if(wasRead > 0) {
baos.write(buffer, 0, wasRead);
}
} while(wasRead > -1);
byte[] bytes = boas.toByteArray();
String input = new String(bytes);
System.out.println(input);
}
}
java.io.Reader
@Path("/")
public class Resource {
@PUT
public void putInfo(Reader reader) {
LineNumberReader lineReader = new LineNumberReader(reader);
do {
String line = lineReader.readLine();
if(line != null) System.out.println(line);
} while(line != null);
}
@GET
@Path("{file: .+}")
@Produces("text/plain")
public InputStream getFile(@PathParam("file") String path) {
FileInputStream is = new FileInputStream(path);
return is;
}
}
- InputStream or Reader may be used as return type
- JAX-RS will read from InputStream into buffer and write out to response OutputStream
java.io.File
@Path("/")
public class Resource {
@GET
@Path("{file: .+}")
@Produces("text/plain")
public InputStream getFile(@PathParam("file") String path) {
return new File(path);
}
@POST
public void post(File file) {
Reader reader = new Reader(new FileInputStream(file));
LineNumberReader lineReader = new LineNumberReader(reader);
do {
String line = lineReader.readLine();
if (line != null)
System.out.println(line);
} while (line != null);
}
}
byte[]
@Path("/")
public class Resource {
@GET @Produces("text/plain")
public byte[] get() {
return "Hi".getBytes();
}
@POST
@Consumes("text/plain")
public void post(byte[] bytes) {
System.out.println(new String(bytes));
}
}
- Must specify the @Consumes annotation so that JAX-RS knows how to set Content-Type Header
String, char[]
@Path("/")
public class Resource {
@GET
@Produces("application/xml")
public String get() {
return "<customer><name>Max Muster</name></customer>";
}
@POST
@Consumes("text/plain")
public void post(String str) {
System.out.println(str);
}
}
- JAX-RS also handles character encoding (e.g. UTF-8)
MultivaluedMap<String, String>
@Path("/")
public class Resource {
@POST
@Consumes("application/x-www-form-urlencoded")
@Produces("application/x-www-form-urlencoded")
public MultivaluedMap<String,String> post(
MultivaluedMap<String, String> form) {
return form;
}
}
- Used to process Html form data
- @FormParam annotation can be used to inject single form parameter
javax.xml.transform.Source
@Path("/transform")
public class TransformationService {
@POST
@Consumes("application/xml")
@Produces("application/xml")
public String post(Source source) {
TransformerFactory tFactory = TransformerFactory.newInstance();
Transformer transformer = tFactory.newTransformer(new StreamSource("foo.xsl"));
StringWriter writer = new StringWriter();
transformer.transform(source, StreamResult(writer));
return writer.toString();
}
}
- XML input or output
- Used to perform XSLT transformation on input documents
- XSL (EXtensible Stylesheet language)
- XSLT (XSL Transformations)
JAXB
JAXB
@XmlRootElement(name="customer")
@XmlAccessorType(XmlAccessType.FIELD)
public class Customer {
@XmlAttribute
protected int id;
@XmlElement
protected String fullname;
public Customer() {}
public int getId() { return this.id; }
public void setId(int id) { this.id = id; }
public String getFullName() { return this.fullname; }
public void setFullName(String name} { this.fullname = name; }
}
- Older specification, not defined by JAX-RS
- JAXB annotation framework, maps Java classes to XML and XML schema
- Work directly with Java object, instead of abstract representation
JAXB #2
- @XmlRootElement(name="customer")
- defines root XML element
- name parameter defines the name of the element
- @XmlAttribute
- defines attribute on element
- <customer id="42342">...</customer>
- @XmlElement
- Define child element
- Embed other JAXB annotated classes
- Also more complex mappings possible
- JAXB command-line tools to generate JAXB annotated classes from XML schema
JAXBContext
Customer customer = new Customer();
customer.setId(42);
customer.setName("Max Muster");
JAXBContext ctx = JAXBContext.newInstance(Customer.class);
StringWriter writer = new StringWriter();
ctx.createMarshaller().marshal(customer, writer);
String custString = writer.toString();
customer = (Customer)ctx.createUnmarshaller().unmarshal(new StringReader(custString));
- JAXBContext instances introspect Java classes to understand structure of annotated class
- Used as factories for Marshaller/Unmarshaller interfaces
- Marshaller: Java object -> XML
- Unmarshaller: XML -> Java object
JAXB JAX-RS Handlers
@Path("/customers")
public class CustomerResource {
@GET
@Path("{id}")
@Produces("application/xml")
public Customer getCustomer(@PathParam("id") int id) {
Customer cust = findCustomer(id);
return cust;
}
@POST
@Consumes("application/xml")
public void createCustomer(Customer cust) {
// ...
}
}
- JAX-RS specification requires to support marshalling/unmarshalling of annotated classes with
- @XmlRootElement
- @XmlType
- or objects wrapped inside JAXBElement
Server Responses and Exception Handling
Sucessful Responses
- Range: 200-399
- Create / Get Customer
- 200 "OK"
- 204 "No Content"
- 204 is sent when message body is empty
- 200 contains a body
Error Responses
- Range: 400-599
- E.g. mistyped URI
- 404 "Not found"
- E.g. unsupported media type
- 406 "Not Acceptable"
- E.g. client invokes valid URI, but method not supported
- 405 "Method Not Allowed"
- JAX-RS returns an Allow header with the list of possible methods
Complex Responses
public abstract class Response {
public abstract Object getEntity();
public abstract int getStatus();
public abstract MultivaluedMap<String, Object> getMetadata();
// ...
}
- Response object created via ResponseBuilder
public abstract class Response {
// ...
public static ResponseBuilder status(Status status) {...}
public static ResponseBuilder status(int status) {...}
public static ResponseBuilder ok() {...}
public static ResponseBuilder ok(Object entity) {...}
public static ResponseBuilder ok(Object entity, MediaType type) {...}
public static ResponseBuilder serverError() {...}
public static ResponseBuilder created(URI location) {...}
public static ResponseBuilder noContent() {...}
public static ResponseBuilder notModified() {...}
public static ResponseBuilder seeOther(URI location) {...}
public static ResponseBuilder temporaryRedirect(URI location) {...}
public static ResponseBuilder notAcceptable(List<Variant> variants) {...}
public static ResponseBuilder fromResponse(Response response) {...}
// ...
}
Complex Responses #2
public static abstract class ResponseBuilder {
public abstract Response build();
public abstract ResponseBuilder clone();
public abstract ResponseBuilder status(int status);
public ResponseBuilder status(Status status) {...}
public abstract ResponseBuilder entity(Object entity);
public abstract ResponseBuilder type(MediaType type);
public abstract ResponseBuilder variant(Variant variant);
public abstract ResponseBuilder language(String language);
public abstract ResponseBuilder location(URI location);
public abstract ResponseBuilder contentLocation(URI location);
public abstract ResponseBuilder tag(String tag);
public abstract ResponseBuilder lastModified(Date lastModified);
public abstract ResponseBuilder cacheControl(CacheControl cacheControl);
public abstract ResponseBuilder expires(Date expires);
public abstract ResponseBuilder header(String name, Object value);
public abstract ResponseBuilder cookie(NewCookie... cookies);
}
- Response.ok(..): returns initialized ResponseBuilder with status code 200 "OK"
- ResponseBuilder is a factory, that is used to create a Response
Complex Resources #3
@Path("/textbook")
public class TextBookService {
@GET
@Produces("text/plain")
public Response getBook() {
String book = "A book...";
ResponseBuilder builder = Response.ok(book);
builder.language("fr")
.header("Some-Header", "some value");
return builder.build();
}
}
Returning Cookies
public class NewCookie extends Cookie {
public static final int DEFAULT_MAX_AGE = −1;
public NewCookie(String name, String value) {}
public NewCookie(String name, String value, String path,
String domain, String comment,
int maxAge, boolean secure) {}
public NewCookie(String name, String value, String path,
String domain, int version, String comment,
int maxAge, boolean secure) {}
public NewCookie(Cookie cookie) {}
public NewCookie(Cookie cookie, String comment,
int maxAge, boolean secure) {}
public static NewCookie valueOf(String value)
throws IllegalArgumentException {}
public String getComment() {}
public int getMaxAge() {}
public boolean isSecure() {}
public Cookie toCookie() {}
}
Returning Cookies #2
@Path("/")
public class MyService {
@GET
public Response get() {
NewCookie cookie = new NewCookie("key", "value");
ResponseBuilder builder = Response.ok("hi", "text/plain");
return builder.cookie(cookie).build();
}
}
Status Enum
public enum Status {
OK(200, "OK"),
CREATED(201, "Created"),
ACCEPTED(202, "Accepted"),
NO_CONTENT(204, "No Content"),
MOVED_PERMANENTLY(301, "Moved Permanently"),
SEE_OTHER(303, "See Other"),
NOT_MODIFIED(304, "Not Modified"),
TEMPORARY_REDIRECT(307, "Temporary Redirect"),
BAD_REQUEST(400, "Bad Request"),
UNAUTHORIZED(401, "Unauthorized"),
FORBIDDEN(403, "Forbidden"),
NOT_FOUND(404, "Not Found"),
NOT_ACCEPTABLE(406, "Not Acceptable"),
CONFLICT(409, "Conflict"),
GONE(410, "Gone"),
PRECONDITION_FAILED(412, "Precondition Failed"),
UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
SERVICE_UNAVAILABLE(503, "Service Unavailable");
public enum Family {
INFORMATIONAL, SUCCESSFUL, REDIRECTION,
CLIENT_ERROR, SERVER_ERROR, OTHER
}
public Family getFamily()
public int getStatusCode()
public static Status fromStatusCode(final int statusCode)
}
Status Enum #2
- Each status enum value is associated with specific family of Http response codes
- 100 Range: Informational
- 200 Range: Successful
- 300 Range: Sucessful but redirect
- 400 Range: Client Error
- 500 Range: Server Error
- Response.status(..) and ResponseBuilder.status(..) accept Status enum
Exception Handling
- Either return correct Response or throw Exception
- Exceptions are handled by JAX-RS, if exception mapper exists
- Checked Exceptions (subclass of java.lang.Exception)
- Unchecked Exceptions (subclass of java.lang.RuntimeException)
- If not handled by exception mapper, propagated up to servlet container
- WebApplicationException can be thrown without exception mapper
WebApplication Exception
public class WebApplicationException extends RuntimeException {
public WebApplicationException() {...}
public WebApplicationException(Response response) {...}
public WebApplicationException(int status) {...}
public WebApplicationException(Response.Status status) {...}
public WebApplicationException(Throwable cause) {...}
public WebApplicationException(Throwable cause, Response response) {...}
public WebApplicationException(Throwable cause, int status) {...}
public WebApplicationException(Throwable cause, Response.Status status) {...}
public Response getResponse() {...}
}
- Built-in JAX-RS unchecked exception
- Initialize with a status code or response object
WebApplication Exception #2
@Path("/customers")
public class CustomerResource {
@GET
@Path("{id}")
@Produces("application/xml")
public Customer getCustomer(@PathParam("id") int id) {
Customer cust = findCustomer(id);
if (cust == null) {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
return cust;
}
}
- JAX-RS calls the getResponse method of a WebApplication Exception
- Must be set, otherwise JAX-RS returns generic 500 "Internal Server Error"
Exception Mapping
public interface ExceptionMapper<E extends Throwable> {
Response toResponse(E exception);
}
@Provider
public class EntityNotFoundMapper implements ExceptionMapper<EntityNotFoundException> {
public Response toResponse(EntityNotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).build();
}
}
- Implement and register instances of ExceptionMapper
- @Provider annotation marks class as component
- In JPA
- javax.persistence.EntityNotFoundException
- thrown if certain object not found in DB
Exception Mapping #2
- JAX-RS can also handle exception inheritance
- First try to find exact class, then find superclass of exception
- ExceptionMappers are registered with the JAX-RS run-time using deployment API (soon)
Exception Hierarchy
BadRequestException 400 Malformed message
NotAuthorizedException 401 Authentication failure
ForbiddenException 403 Not permitted to access
NotFoundException 404 Couldn’t find resource
NotAllowedException 405 HTTP method not supported
NotAcceptableException 406 Client media type requested not supported
NotSupportedException 415 Client posted media type not supported
InternalServerErrorException 500 General server error
ServiceUnavailableException 503 Server is temporarily unavailable or busy
- JAX-RS 2.0 introduces several exceptions for various Http error conditions
@Path("/customers")
public class CustomerResource {
@GET
@Path("{id}")
@Produces("application/xml")
public Customer getCustomer(@PathParam("id") int id) {
Customer cust = findCustomer(id);
if (cust == null) {
throw new NotFoundException());
}
return cust;
}
}
Exception Hierarchy #2
- BadRequestException
- client sends something server cannot interpret
- Also thrown if failed to convert header or cookie value
- NotAuthorizedException should be used to implement own authentication protocol
- should write a WWW-Authenticate Header (e.g. OAuth bearer token is required)
- WWW-Authentication: Bearer
- throw new NotAuthorizedException("Bearer")
- ForbiddenException when client is authorized but not permitted
- NotAcceptableException when client demands a format, which the server does not produce
Exception Hierarchy #3
- NotSupportedException when client posts unsupported format
- InternalServerErrorException for generic server side error
- ServiceUnavailableException server is temp unavailable
- client may retry request
public ServiceUnavailableException(Long retryAfter) {}
public ServiceUnavailableException(Date retryAfter) {}
HATEOAS
HATEOAS
- Web: information is connected via hyperlinks
- Html forms allow state change
- HATEOAS and web services
- Data format describes how to change app state
- Embed links in XML/JSON
- Atom Syndication Format: RFC to implement HATEOAS
- Atom is an XML-based document format that describes lists of related information known as "feeds." Feeds are composed of a number of items, known as "entries," each with an extensible set of attached metadata.
- Atom ~ next evolution of RSS
Atom Links
<customers>
<link rel="next"
href="http://example.com/customers?start=2&size=2" type="application/xml"/>
<customer id="123">
<name>Bill Burke</name>
</customer>
<customer id="332">
<name>Roy Fielding</name>
</customer>
</customers>
- Atom link is xml element with specific attributes
- rel: link relationship (e.g. "next")
- href: URI
- type: media type
- hreflang: language of data
Advantages
<customers>
<link rel="next"
href="http://example.com/customers?start=2&size=2" type="application/xml"/>
<customer id="123">
<name>Bill Burke</name>
</customer>
<customer id="332">
<name>Roy Fielding</name>
</customer>
</customers>
- Location transparency
- few URIs are published
- the rest is embedded in the data
- Decoupling interaction details
- /customers?start={start}&size={size}
- Increased amount of predefined knowlegde
- If service changes, interface is broken
- Instead embed info within document
Advantages #2
PUT /orders/333 HTTP/1.1
Content-Type: application/xml
<order id="333">
<customer id="123">...</customer>
<amount>$99.99</amount>
<cancelled>true</cancelled>
<order-entries>
...
</order-entries>
</order>
Better:
<order id="333">
<customer id="123">...</customer>
<amount>$99.99</amount>
<cancelled>false</cancelled>
<link rel="cancel"
href="http://example.com/orders/333/cancelled"/>
<order-entries>
...
</order-entries>
</order>
- Reduced state transition errors (e.g. order)
Advantages #3
- Reduced state transition errors
- Some operations may not be allowed in certain states
- Embed possible operations in response
- W3C standardized relationship names
(Name) (Descr)
previous A URI that refers to the immediately preceding document
in a series of documents.
next A URI that refers to the immediately following document
in a series of documents.
edit A URI that can be retrieved, updated, and deleted.
payment A URI where payment is accepted. It is meant as a general way
to facilitate acts of payment.
...
Link Headers vs Atom Links
HTTP/1.1 200 OK
Content-Type: application/xml
Link: <http://example.com/orders/333/cancelled>; rel=cancel
<order id="333">
...
</order>
- Instead of atom link, use link header
- No need to parse xml
- HEAD method may be used
HATEOAS and JAX-RS
- HATEOAS defined by application
- JAX-RS does not provide a HATEOAS framework but has useful tools
- UriBuilder, UriInfo, Link, Link headers
UriBuilder
public abstract class UriBuilder {
public static UriBuilder fromUri(URI uri)
throws IllegalArgumentException
public static UriBuilder fromUri(String uri)
throws IllegalArgumentException
public static UriBuilder fromPath(String path)
throws IllegalArgumentException
public static UriBuilder fromResource(Class<?> resource)
throws IllegalArgumentException
public static UriBuilder fromLink(Link link)
throws IllegalArgumentException
// ...
}
- Instantiated from static helper methods
- Specify URI piece by piece
public abstract UriBuilder clone();
public abstract UriBuilder uri(URI uri)
throws IllegalArgumentException;
public abstract UriBuilder scheme(String scheme)
throws IllegalArgumentException;
public abstract UriBuilder schemeSpecificPart(String ssp)
throws IllegalArgumentException;
public abstract UriBuilder userInfo(String ui);
// ..
UriBuilder #2
UriBuilder builder = UriBuilder.fromPath("/customers/{id}");
builder.scheme("http")
.host("{hostname}")
.queryParam("param={param}");
// http://{hostname}/customers/{id}?param={param}
UriBuilder clone = builder.clone();
URI uri = clone.build("example.com", "333", "value");
// http://example.com/customers/333?param=value
// Map usage
Map<String, Object> map = new HashMap<String, Object>();
map.put("hostname", "example.com");
map.put("id", 333);
map.put("param", "value");
UriBuilder clone = builder.clone();
URI uri = clone.buildFromMap(map);
UriBuilder #3
@Path("/customers")
public class CustomerService {
@Path("{id}")
public Customer getCustomer(@PathParam("id") int id) {
//...
}
}
// ..
UriBuilder builder = UriBuilder.fromResource(CustomerService.class);
builder.host("{hostname}")
builder.path(CustomerService.class, "getCustomer");
// http://{hostname}/customers/{id}
UriBuilder #4
URI uri = UriBuilder.fromUri("/{id}").build("a/b");
// /a%2Fb
// Encode methods
public abstract URI build(Object[] values, boolean encodeSlashInPath)
throws IllegalArgumentException, UriBuilderException
public abstract URI buildFromMap(Map<String, ?> values, boolean encodeSlashInPath)
throws IllegalArgumentException, UriBuilderException
// Template methods
public abstract UriBuilder resolveTemplate(String name, Object value);
public abstract UriBuilder resolveTemplate(String name, Object value,
boolean encodeSlashInPath);
public abstract UriBuilder resolveTemplateFromEncoded(String name,
Object value);
// ...
String original = "http://{host}/{id}";
String newTemplate = UriBuilder.fromUri(original)
.resolveTemplate("host", "localhost")
.toTemplate();
Relative URIs with UriInfo
public interface UriInfo {
public URI getRequestUri();
public UriBuilder getRequestUriBuilder();
public URI getAbsolutePath();
public UriBuilder getAbsolutePathBuilder();
public URI getBaseUri();
public UriBuilder getBaseUriBuilder();
// ...
}
@Path("/customers")
public class CustomerService {
@GET
@Produces("application/xml")
public String getCustomers(@Context UriInfo uriInfo) {
UriBuilder nextLinkBuilder = uriInfo.getAbsolutePathBuilder();
nextLinkBuilder.queryParam("start", 5);
nextLinkBuilder.queryParam("size", 10);
URI next = nextLinkBuilder.build();
//... set up the rest of the document ...
}
// ...
}
// http://example.com/jaxrs/customers?start=5&size=10
UriInfo #2
public interface UriInfo {
// ...
public List<String> getMatchedURIs();
public List<String> getMatchedURIs(boolean decode);
}
@Path("/customers")
public class CustomerDatabaseResource {
@Path("{database}-db")
public CustomerResource getDatabase(@PathParam("database") String db) { // ... }
}
public class CustomerResource {
public CustomerResource(Map db) {}
@GET
@Path("{id}")
@Produces("application/xml")
public StreamingOutput getCustomer(@PathParam("id") int id) { // ...}
}
// getMatchedURIs
// http://example.com/customers
// http://example.com/customers/usa-db
// http://example.com/customers/usa-db/333
Building Links and Link Headers
public abstract class Link {
public abstract URI getUri();
public abstract UriBuilder getUriBuilder();
public abstract String getRel();
public abstract List<String> getRels();
public abstract String getTitle();
public abstract String getType();
// ...
public static Builder fromUri(URI uri);
public static Builder fromUri(String uri);
public static Builder fromUriBuilder(UriBuilder uriBuilder);
// ...
}
Link link = Link.fromUri("http://{host}/root/customers/{id}")
.rel("update").type("text/plain").build("localhost", "1234");
// <http://localhost/root/customers/1234>; rel="update"; type="text/plain"
Links #2
@Path
@GET
Response get() {
Link link = Link.fromUri("a/b/c").build();
Response response = Response.noContent()
.links(link)
.build();
return response;
}
JAXB and Links
// JAXB provides XmlAdapter for Link class
@XmlRootElement
public class Customer {
private String name;
private List<Link> links = new ArrayList<Link>();
@XmlElement
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@XmlElement(name="link")
@XmlJavaTypeAdapter(Link.JaxbAdapter.class)
public List<Link> getLinks() {
return links;
}
}
Http Content Negotiation
Intro
- Handle variety of clients and platforms
- Different clients prefer different formats
- Java - XML
- JavaScript - JSON
- Ruby - YAML
- Clients need internationalized data
- Different versions of API
- Http supports content negotiation (conneg)
- content-type, encoding, language
Content Type
GET http://example.com/stuff
Accept: application/xml, application/json
- stuff needs to be formatted in XML or JSON
- 406 "Not Acceptable" if not possible
- Otherwise server determines a format type and returns response
- Wildcards and media type properties may also be used
GET http://example.com/stuff
Accept: text/*, text/html;level=1
Preference Ordering
GET http://example.com/stuff
Accept: text/*, text/html;level=1, */*, application/xml
GET http://example.com/stuff
Accept: text/*;q=0.9, */*;q=0.1, audio/mpeg, application/xml;q=0.5
- text/html;level=1
- application/xml
- text/*
- */*
- q MIME type property
- text/*;q=0.9
- If q not provided, it is implicitely 1
Language/Encoding Negotiation
// Language
GET http://example.com/stuff
Accept-Language: en-us, es, fr
GET http://example.com/stuff
Accept-Language: fr;q=1.0, es;q=1.0, en;q=0.1
// Encoding
GET http://example.com/stuff
Accept-Encoding: gzip, deflate
GET http://example.com/stuff
Accept-Encoding: gzip;q=1.0, compress;0.5; deflate;q=0.1
- ISO-639/ISO-3166 for language
- Content-Encoding header specifies message encoding
JAX-RS and CONNEG
// Method Dispatching according to Accept header
@Path("/customers")
public class CustomerResource {
@GET
@Path("{id}")
@Produces("application/xml")
public Customer getCustomerXml(@PathParam("id") int id) {
// ...
}
@GET
@Path("{id}")
@Produces("text/plain")
public String getCustomerText(@PathParam("id") int id) {
// ...
}
@GET
@Path("{id}")
@Produces("application/json")
public Customer getCustomerJson(@PathParam("id") int id) {
// ...
}
}
JAXB and CONNEG
@Path("/service")
public class MyService {
@GET
@Produces({"application/xml", "application/json"})
public Customer getCustomer(@PathParam("id") int id) {
// ...
}
}
Complex Negotiation
public interface HttpHeaders {
public List<MediaType> getAcceptableMediaTypes();
public List<Locale> getAcceptableLanguages();
// ...
}
@Path("/myservice")
public class MyService {
@GET
public Response get(@Context HttpHeaders headers) {
MediaType type = headers.getAcceptableMediaTypes().get(0);
Locale language = headers.getAcceptableLanguages().get(0);
Object responseObject = ...;
Response.ResponseBuilder builder = Response.ok(responseObject, type);
builder.language(language);
return builder.build();
}
}
- Media type matching via @Produces annotation
- But no @ProduceLanguage or @ProduceEncoding annotations
- Look up in header values
- Ordered by q values
Variant Processing
public interface Request {
Variant selectVariant(List<Variant> variants) throws IllegalArgumentException;
// ...
}
@Path("/myservice")
public class MyService {
@GET
Response getSomething(@Context Request request) {
List<Variant> variants = new ArrayList<Variant>();
variants.add(new Variant(
MediaType.APPLICATION_XML_TYPE,
"en", "deflate"));
variants.add(new Variant(
MediaType.APPLICATION_XML_TYPE,
"es", "deflate"));
// Pick the variant
Variant v = request.selectVariant(variants);
Object entity = ...; // get the object you want to return
ResponseBuilder builder = Response.ok(entity);
builder.type(v.getMediaType())
.language(v.getLanguage())
.header("Content-Encoding", v.getEncoding());
return builder.build();
}
}
Variant Builder
@Path("/myservice")
public class MyService {
@GET
Response getSomething(@Context Request request) {
Variant.VariantListBuilder vb = Variant.VariantListBuilder.newInstance();
vb.mediaTypes(MediaType.APPLICATION_XML_TYPE,
MediaType.APPLICATION_JSON_TYPE)
.languages(new Locale("en"), new Locale("es"))
.encodings("deflate", "gzip").add();
List<Variant> variants = vb.build();
// Pick the variant
Variant v = request.selectVariant(variants);
Object entity = ...; // get the object you want to return
ResponseBuilder builder = Response.ok(entity);
builder.type(v.getMediaType())
.language(v.getLanguage())
.header("Content-Encoding", v.getEncoding());
return builder.build();
}
}
Negotiation by URI Patterns
// /customers/en-US/xml/3323
// /customers/3323.xml.en-US
@Path("/customers/{id}.{type}.{language}")
@GET
public Customer getCustomer(@PathParam("id") int id,
@PathParam("type") String type, @PathParam("language") String language) {
// ...
}
- Some clients do not support conneg
- Embed conneg info in URI
Scaling JAX-RS Apps
Intro
- 100000+ websites
- 1 billion requests per day
- TB/h data download
- Amazon: 1 million+ transactions per day
- Caching is key concept in web
Caching
- Browser stores images and static text in memory or disk
- Speeds up page rendering
- Reduces server load
- Proxy cache
- Middlemen between browser and server
- Reduce load on master servers
- E.g. Content delivery networks (CDN)
- For REST servers
- GET /someStuff
- Read-only and idempotent
Http Caching
@Path("/customers")
public class CustomerResource {
@Path("{id}")
@GET
@Produces("application/xml")
public Response getCustomer(@PathParam("id") int id) {
Customer cust = findCustomer(id);
ResponseBuilder builder = Response.ok(cust, "application/xml");
Date date = Calendar.getInstance(TimeZone.getTimeZone("GMT"))
.set(2014, 5, 15, 16, 0);
builder.expires(date);
return builder.build();
}
}
HTTP/1.1 200 OK
Content-Type: application/xml
Expires: Tue, 15 May 2014 16:00 GMT
<customer id="123">...</customers>
Cache-Control
- For HTTP 1.1 caching was extended
- Revalidation feature
- Expires header deprecated
- Now Cache-Control header
- List of directives
- Control who can cache, how and how long
Cache-Control #2
- private
- Only client may cache data
- Not CDN or proxy is allowed to cache
- public
- Caching allowed also for proxies
- no-cache
- response is not cached, unless revalidated with server
- no-store
- Browser may store cache on disk
- Available after restart
- no-store directive does not allow this kind of caching
Cache-Control #3
- no-transform
- Sometimes cached data is compressed or transformed to save space on disk
- max-age
- amount of seconds the cache is valid
- If expires and max-age is present, max-age takes precedence
- s-maxage
- amout of seconds the cache is valid for proxies
Cache-Control #4
@Path("/customers")
public class CustomerResource {
@Path("{id}")
@GET
@Produces("application/xml")
public Response getCustomer(@PathParam("id") int id) {
Customer cust = findCustomer(id);
CacheControl cc = new CacheControl();
cc.setMaxAge(300);
cc.setPrivate(true);
cc.setNoStore(true);
ResponseBuilder builder = Response.ok(cust, "application/xml");
builder.cacheControl(cc);
return builder.build();
}
}
HTTP/1.1 200 OK
Content-Type: application/xml
Cache-Control: private, no-store, max-age=300
<customers>...</customers>
- Only client may cache
- Not allowed to store on disk
- valid for 300 seconds
Revalidation
HTTP/1.1 200 OK
Content-Type: application/xml
Cache-Control: max-age=1000
Last-Modified: Tue, 15 May 2013 09:56 EST
<customer id="123">...</customer>
GET /customers/123 HTTP/1.1
If-Modified-Since: Tue, 15 May 2013 09:56 EST
- When cache expires, client may revalidate
- For revalidation the client needs
- Last-Modified and/or
- ETag header
- If modified: new response with 200, "OK"
- If not modified: 304, "Not Modified"
Revalidation #2
HTTP/1.1 200 OK
Content-Type: application/xml
Cache-Control: max-age=1000
ETag: "3141271342554322343200"
<customer id="123">...</customer>
GET /customers/123 HTTP/1.1
If-None-Match: "3141271342554322343200"
- id, usualy MD5 hash
- Strong ETag
- hash changes on every change of data
- Weak ETag
- W/ prefix
- Allows revalidation if minor changes
Revalidation #3
public class EntityTag {
public EntityTag(String value) {
//...
}
public EntityTag(String value, boolean weak) {
//...
}
public static EntityTag valueOf(String value)
throws IllegalArgumentException {
//...
}
public boolean isWeak() {
//...
}
public String getValue() {
//...
}
}
JAX-RS conditional GETs
@Path("/customers")
public class CustomerResource {
@Path("{id}")
@GET
@Produces("application/xml")
public Response getCustomer(@PathParam("id") int id,
@Context Request request) {
Customer cust = findCustomer(id);
EntityTag tag = new EntityTag(
Integer.toString(cust.hashCode()));
CacheControl cc = new CacheControl();
cc.setMaxAge(1000);
ResponseBuilder builder = request.evaluatePreconditions(tag);
if (builder != null) {
builder.cacheControl(cc);
return builder.build();
}
// Preconditions not met!
builder = Response.ok(cust, "application/xml");
builder.cacheControl(cc);
builder.tag(tag);
return builder.build();
}
}
Concurrency
HTTP/1.1 200 OK
Content-Type: application/xml
Cache-Control: max-age=1000
ETag: "3141271342554322343200"
Last-Modified: Tue, 15 May 2013 09:56 EST
<customer id="123">...</customer>
PUT /customers/123 HTTP/1.1
If-Match: "3141271342554322343200"
If-Unmodified-Since: Tue, 15 May 2013 09:56 EST
Content-Type: application/xml
<customer id="123">...</customer>
- If data has changed in meantime
- 412, "Precondition Failed"
Concurrency #2
@Path("/customers")
public class CustomerResource {
@Path("{id}")
@PUT
@Consumes("application/xml")
public Response updateCustomer(@PathParam("id") int id,
@Context Request request, Customer update ) {
Customer cust = findCustomer(id);
EntityTag tag = new EntityTag(
Integer.toString(cust.hashCode()));
Date timestamp = ...; // get the timestamp
ResponseBuilder builder =
request.evaluatePreconditions(timestamp, tag);
if (builder != null) {
// Preconditions not met!
return builder.build();
}
... perform the update ...
builder = Response.noContent();
return builder.build();
}
}
References
RESTful Java with JAX-RS 2.0, Bill Burke
Thank you for your attention!
JAX-RS
By dinony
JAX-RS
Java API for RESTful Web Services
- 459