REST
The seed-rest-core
and its companion modules provides support for exposing REST resources using JAX-RS. SeedStack implements
the JAX-RS standard through the Jersey 2 implementation.
REST support requires the following dependency in your project:
<dependency>
<groupId>org.seedstack.seed</groupId>
<artifactId>seed-rest-jersey2</artifactId>
</dependency>
Show version
dependencies {
compile("org.seedstack.seed:seed-rest-jersey2:3.15.1")
}
Configuration
The following configuration options are available:
rest:
# The base path of REST resources which will prefix all resource paths
path: (String)
# Set of Jersey features to enable
features:
- (Class<?>)
# The base URI used for building hypermedia params
baseParam: (String)
# The base URI used for building hypermedia rel
baseRel: (String)
# If true, a JSON-Home resource describing REST entry-points will be served on REST base path
jsonHome: (boolean)
# Allows to specify custom Jersey properties (key: property name, value: property value)
jerseyProperties:
key: (String)
# If JSP support is enabled, the path where JSP files are located
jspPath: (String)
exceptionMapping:
# If true, security exceptions are automatically mapped to adequate HTTP responses
security: (boolean)
# If true, all exceptions not already mapped are mapped to a 500 HTTP response
all: (boolean)
To dump the rest
configuration options:
mvn -q -Dargs="rest" seedstack:config
Base path
REST resources are exposed on /
by default. This is fine for micro-services only serving REST APIs but you can choose
to avoid potential conflicts with other application resources by changing the base path with the path
configuration property.
Jersey features
The following features are automatically enabled when discovered on the classpath:
- Multipart. Being included as a dependency to the
seed-rest-jersey2
module so it is always enabled unless explicitly excluded from the classpath. - JSP. This feature is enabled when the
org.glassfish.jersey.ext:jersey-mvc-jsp
dependency is present in the classpath. The path where the JSP files must be located is controlled by thejspPath
configuration property.
Other features must be explicitly enabled by:
- Adding the dependency if necessary.
- Adding the fully qualified class name to the
features
configuration property.
Jersey properties
You can specify custom Jersey 2 properties in the properties
option. They will be passed to the Jersey runtime as is.
The reference of all Jersey 2 properties can be found here.
Hypermedia settings
The baseRel
, baseParam
and jsonHome
control the behavior of the hypermedia engine.
Exception mapping
SeedStack default exception mapping is controlled by the following configuration properties:
security
: if trueAuthenticationException
andAuthorizationException
security exceptions thrown from a JAX-RS resource are respectively mapped to a 401 and a 403 HTTP responses. This is enabled by default.all
: if true any exception thrown from a JAX-RS resource not already mapped by another exception mapper is mapped to a 500 HTTP response (without stacktrace to avoid leaking technical information). This is enabled by default.
If you need to display exception stacktraces during development, disable those exception mappers. Be sure to re-enable them for production systems.
Usage
SeedStack automatically detects:
- JAX-RS resources (concrete classes annotated with
@Path
) - JAX-RS providers (concrete classes annotated with
Provider
)
These classes are injectable and interceptable:
@Path("/products")
public class ProductsResource {
@Inject
private CatalogService catalogService;
@GET
@Path("/{id}")
@Produces({MediaType.APPLICATION_JSON})
public ProductRepresentation findProduct(@PathParam("id") long productId) {
Optional<Product> product = catalogService.findProduct(productId);
if (!product.isPresent()) {
throw new NotFoundException("Product " + productId + " cannot be found");
}
return new ProductRepresentation(product.get());
}
@POST
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public Response createProduct(ProductRepresentation pr, @Context UriInfo uriInfo) {
Product newProduct = catalogService.addProduct(
pr.getDesignation(),
pr.getPrice());
URI newUri = new URI(
uriInfo.getRequestUri().toString() +
"/" +
newProduct.getId());
return Response.created(newUri).entity(newProduct).build();
}
}
You can find more information about how to work with JAX-RS in this guide.
Hypermedia
SeedStack implements hypermedia with two main features:
- JSON-HOME which allows to discover an application or micro-service global REST resources.
- Hypertext Application Language (HAL) which is a popular media-type for representing hypermedia-capable resources.
JSON-HOME
To ease REST API discovery, SeedStack exposes an API home resource with the JSON-HOME media type. This is similar to a Website homepage but for REST APIs.
The goal of the JSON-HOME media type is to expose an home resource which provides all the entry points of the application’s API. It tells the client developer what it can do and give him hints on how to use the resources.
The following example shows a JSON-HOME resource with two entry points «products» and «product». For the «products» resource, the JSON-HOME provides just an href indicating the URI of the resource. But for the «product» resource, the JSON-HOME provides an href-template instead.
{
"resources": {
"http://example.org/rel/products": {
"href": "/products",
"hints": {
"allow": ["POST"],
"formats": {
"application/json": {}
},
"accept-patch": ["application/json-patch"]
}
},
"http://example.org/rel/product": {
"href-template": "/products/{productId}",
"href-vars": {
"productId": "http://example.org/params/productId"
},
"hints": {
"allow": ["GET"],
"formats": {
"application/json": {}
},
"accept-patch": ["application/json-patch"]
}
}
}
}
To expose a JAX-RS resource in the JSON-HOME resource, annotate the resource with @Rel
and set the home attribute to true
. The annotation can be on the class or the method.
@Path("/products")
public class ProductsResource {
@Inject
private CatalogService catalogService;
@GET
@Path("/{id}")
@Produces({MediaType.APPLICATION_JSON, "application/hal+json"})
@Rel(value = "product", home = true)
public ProductRepresentation findProduct(@PathParam("id") long productId) {
Optional<Product> product = catalogService.findProduct(productId);
if (!product.isPresent()) {
throw new NotFoundException("Product " + productId + " cannot be found");
}
return new ProductRepresentation(product.get());
}
@POST
@Produces({MediaType.APPLICATION_JSON, "application/hal+json"})
@Rel(value = "product", home = true)
public Response createProduct(ProductRepresentation pr, @Context UriInfo uriInfo) {
Product newProduct = catalogService.addProduct(
pr.getDesignation(),
pr.getPrice());
URI newUri = new URI(
uriInfo.getRequestUri().toString() +
"/" +
newProduct.getId());
return Response.created(newUri).entity(newProduct).build();
}
}
Hypertext Application Language (HAL)
Beyond providing an «API homepage», you have to provide a way to navigate between these pages. That’s the role of the HAL+JSON media type, which establishes conventions for expressing hypermedia controls. An HAL representation looks like this:
{
"_links": {
"self": { "href": "/products" },
"next": { "href": "/products?page=2" },
"find": { "href": "/products{?id}", "templated": true }
},
"_embedded": {
"products": [{
"_links": {
"self": { "href": "/products/123" },
"status": { "href": "/products/123/status" }
},
"price": 30.00,
"designation": "Product 123"
}, {
"_links": {
"self": { "href": "/products/124" },
"status": { "href": "/products/124/status" }
},
"price": 20.00,
"designation": "Product 124"
}]
},
"totalProductCount": 245,
"latestCatalogUpdate": "2017-02-08"
}
It is a JSON representation with just two reserved keywords:
_links
: this property is used to share links to other resources. Links are represented as a set of keys and values. The keys represents a relation type (rel) and the value a link object. The only required value of the link object ishref
which can be an URI (/products/123
) or an URI template (/products{?id}
). By convention, a resource always returns aself
link with its own URI. However, the propery_links
is optional. For more information on link object see the section 5 of the specification._embedded
: this property is a set of keys and values. Keys are relation types and values can be a resource object or an array of resource objects. The embedded resources can be full or partial representations of a resource.
Building hypermedia links
Concatenating strings for building URI can quickly become painful and error-prone. With SeedStack, you can inject the
RelRegistry
interface which can greatly simplify the task. This registry tracks
all the resources annotated by @Rel
and can build hypermedia links in two forms:
- An expanded form, where all variables are sent already resolved to the client,
- A templated form, where the URI template is sent as-is to the client.
Consider the following resource:
@Path("/products")
public class ProductsResource {
@GET
@Rel(value = "products") // defines the resource rel
@Produces({MediaType.APPLICATION_JSON, "application/hal+json"})
public List<ProductRepresentation> listProducts(
@QueryParam("pageIndex") @DefaultValue("0") Integer pageIndex,
@QueryParam("pageSize") @DefaultValue("10") Integer pageSize) {
}
}
The expanded link is created as follows:
Link link = relRegistry.uri("products") // specifies the target resource rel
.set("pageIndex", pageIndex)
.set("pageSize", pageSize);
Which will be serialized to the following form:
{
"href": "/products?pageIndex=0&pageSize=10"
}
The templated link can be created as follows:
Link link = relRegistry.uri("products") // specifies the target resource rel
.templated();
Which will be serialized to the following form:
{
"href": "/products{?pageIndex,pageSize}",
"templated": true
}
Note that SeedStack will automatically use the Servlet context path and the REST prefix to build HAL links, alleviating the need for you to build any URI manually.
Creating HAL representations
SeedStack provides two options to simplify the creation of HAL representations. First, you can take an existing representation
and transform it to an HAL representation using the HALBuilder
:
@Path("/products")
public class ProductsResource {
@Inject
private CatalogService catalogService;
@Inject
private RelRegistry relRegistry;
@GET
@Path("/{id}")
@Produces({MediaType.APPLICATION_JSON, "application/hal+json"})
@Rel(value = "product", home = true)
public HalRepresentation findProduct(@PathParam("id") long productId) {
// Find the product
Optional<Product> product = catalogService.findProduct(productId);
if (!product.isPresent()) {
throw new NotFoundException("Product " + productId + " cannot be found");
}
// Build the product representations
ProductRepresentation productRepresentation = new ProductRepresentation(product);
List<ProductRepresentation> relatedProducts = catalogService
.findRelatedProducts(product)
.stream()
.map(ProductRepresentation::new)
.collect(toList());
// Create an HAL representation from product representations
return HalBuilder
.create(product)
.self(relRegistry.uri("products").set("id", product.getId()))
.link("find", relRegistry.uri("product").templated())
.embedded("related", relatedProducts);
}
}
The second option is to make your representation extends HalRepresentation
:
public class ProductHalRepresentation extends HalRepresentation {
private long id;
private String designation;
ProductHalRepresentation() {
}
public ProductHalRepresentation(RelRegistry relRegistry, Product product, List<Product> relatedProducts) {
this.id = product.getId();
this.designation = product.getDesignation();
self(relRegistry.uri("products").set("id", product.getId()));
link("find", relRegistry.uri("find").templated());
embedded(
"products",
relatedProducts.stream()
.map(relatedProduct -> new ProductHalRepresentation(relRegistry, relatedProduct, new ArrayList<>()))
.collect(toList())
);
}
}