I am reading through this example of Storage-service example in Jersey Sample provided in oracle docs.
I am just unable to understand how this PUT request is resolved by JAX-RS runtime?
curl -X PUT http://127.0.0.1:9998/storage/containers/quotes
here is the code snippet that corresponds to this request (taken from above link).
@Path("/containers")
@Produces("application/xml")
public class ContainersResource {
@Context UriInfo uriInfo;
@Context Request request;
@Path("{container}")
public ContainerResource getContainerResource(@PathParam("container") String container) {
return new ContainerResource(uriInfo, request, container);
}
@GET
public Containers getContainers() {
System.out.println("GET CONTAINERS");
return MemoryStore.MS.getContainers();
}
}
But as you can notice, there is no method with @PUT annotation
. But the getContainerResource
method is called for /containers/{container}
. In this method, a new instance of ContainerResource
is returned. I am not sure how the above PUT
request is processed. Please explain.
Here is the ContainerResource class
:
@Produces("application/xml")
public class ContainerResource {
@Context UriInfo uriInfo;
@Context Request request;
String container;
ContainerResource(UriInfo uriInfo, Request request, String container) {
this.uriInfo = uriInfo;
this.request = request;
this.container = container;
}
@GET
public Container getContainer(@QueryParam("search") String search) {
System.out.println("GET CONTAINER " + container + ", search = " + search);
Container c = MemoryStore.MS.getContainer(container);
if (c == null)
throw new NotFoundException("Container not found");
if (search != null) {
c = c.clone();
Iterator<Item> i = c.getItem().iterator();
byte[] searchBytes = search.getBytes();
while (i.hasNext()) {
if (!match(searchBytes, container, i.next().getName()))
i.remove();
}
}
return c;
}
@PUT
public Response putContainer() {
System.out.println("PUT CONTAINER " + container);
URI uri = uriInfo.getAbsolutePath();
Container c = new Container(container, uri.toString());
Response r;
if (!MemoryStore.MS.hasContainer(c)) {
r = Response.created(uri).build();
} else {
r = Response.noContent().build();
}
MemoryStore.MS.createContainer(c);
return r;
}
@DELETE
public void deleteContainer() {
System.out.println("DELETE CONTAINER " + container);
Container c = MemoryStore.MS.deleteContainer(container);
if (c == null)
throw new NotFoundException("Container not found");
}
@Path("{item: .+}")
public ItemResource getItemResource(@PathParam("item") String item) {
return new ItemResource(uriInfo, request, container, item);
}
private boolean match(byte[] search, String container, String item) {
byte[] b = MemoryStore.MS.getItemData(container, item);
OUTER: for (int i = 0; i < b.length - search.length + 1; i++) {
for (int j = 0; j < search.length; j++) {
if (b[i + j] != search[j])
continue OUTER;
}
return true;
}
return false;
}
}
This is a documented feature called sub-resource locators: https://jersey.java.net/documentation/latest/jaxrs-resources.html
@Path("{container}")
public ContainerResource getContainerResource(@PathParam("container") String container) {
return new ContainerResource(uriInfo, request, container);
}
The @Path annotation above identifies a ContainerResource as a sub-resource. Note that the ContainerResource does have an annotated PUT method that is being invoked. If you need further explanation, I can try to expand this answer.
Update
This is not easy to explain, but here is a stab at explaining it...
Let's start explaining this by looking at the various URL's your classes support. The ONLY end-point implemented by ContainersResource class is the following:
GET /containers/
This end-point is part of a top-level resource. Obviously it returns the collection/list of containers.
Now what if we want an end-point to get a specific container by id? Normal REST end-points would expose the end-point with a GET operation on the collection followed by the id as a path parameter (PathParam), so a call may look like this for container with id 27:
GET /containers/27/
One solution to do this would be as follows:
@Path("/containers")
@Produces("application/xml")
public class ContainersResource {
@Context UriInfo uriInfo;
@Context Request request;
@GET
public Containers getContainers() {
System.out.println("GET CONTAINERS");
return MemoryStore.MS.getContainers();
}
//
// This is a solution WITHOUT sub-resource...
// Note that the Path annotation is same as you have it, but
// now the HTTP method annotation is provided. Also, the
// method returns Container instead of ContainerResource
//
@Path("{container}")
@GET
public Container getContainerResource(@PathParam("container") String container) {
// Go to memory store and get specific container...
Container x = findContainer(container);
return x;
}
//
// Again, without sub-resource, we can define PUT method
// on specific container id and again define the path param
//
@Path("{container}")
@PUT
public Response putContainer(@PathParam("container") String container) {
// Process payload to build container, put into memory store
Response r = putContainer(container, ...);
return r;
}
}
Not using sub-resources causes us to have to put multiple GET, PUT, POST, DELETE methods into the same class as we promote methods from lower level resources up to the top-most resource class. It also causes us to have to define the path param for the container id multiple times. This is just 2 levels of resources (the container collection, and specific container) so it does not seem too bad, but what about as our paths grow deeper? Containers have items, so a fully accessible API may implement an endpoint that allows you to retrieve just the items from a specific collection, or even a specific item from a collection. These calls would respectively look like this:
GET /containers/27/items/ (to get the items collection)
GET /containers/27/items/9/ (to get specific item from collection)
So now, our parent resource would need to define 4 separate GET methods.
To avoid multiple GET/POST/PUT/DELETE methods in the same class we could still break these into 4 different classes each with unique Path annotations on the class, but then if some term in the path such as "containers" needed renamed, we would have to update the code in 4 places instead of one place. Also, all the path parameters along the path would have to be defined on each method.
To illustrate this, consider the ContainersResource class you provided that uses sub-resource:
@Path("/containers")
@Produces("application/xml")
public class ContainersResource {
@Context UriInfo uriInfo;
@Context Request request;
@Path("{container}")
public ContainerResource getContainerResource(@PathParam("container") String container) {
return new ContainerResource(uriInfo, request, container);
}
}
The getContainerResource method declares that there is a sub-resource identified by the path parameter "container". This could be implemented directly in the ContainerResource class as follows if we were not using sub-resources:
@Path("containers/{container}")
@Produces("application/xml")
public class ContainerResource {
@GET
public Container getContainer(
@PathParam("container") String container,
@QueryParam("search") String search) {
System.out.println("GET CONTAINER " + container + ", search = " + search);
Container c = MemoryStore.MS.getContainer(container);
// do work
return c;
}
}
Notice that I had to add the Path annotation to the class to define the end-point location and the fact that there is a path parameter. Additionally I had to add a PathParam argument to the GET method (and would have to add it to my other methods as well) to know what the container value is.
To further demonstrate this, consider if we implement the ContainerItemsResource without using sub-resources:
@Path("containers/{container}/items")
@Produces("application/xml")
public class ContainerItemsResource {
@GET
public ContainerItems getContainerItems(
@PathParam("container") String container) {
Container c = MemoryStore.MS.getContainer(container);
return c.getItems();
}
}
And the ContainerItemResource
@Path("containers/{container}/items/{item}")
@Produces("application/xml")
public class ContainerItemResource {
@GET
public ContainerItem getContainerItem(
@PathParam("container") String container,
@PathParam("item" String item) {
Container c = MemoryStore.MS.getContainer(container);
return c.getItems();
}
}
Here we go again repeating the full Path and having to redefine the container path parameter on each and every method.
Sub-resources provide an elegant solution that (1) allows each level in the path to have it's own class with single GET/PUT/POST/DELETE methods, (2) does not require redefining levels in the path in multiple places, and (3) does not require redefining query parameters on every method. Here are the four resource files using sub-resource approach (only providing GET method for illustration):
@Path("/containers")
@Produces("application/xml")
public class ContainersResource {
@Context UriInfo uriInfo;
@Context Request request;
// Define sub-resource for specific container
@Path("{container}")
public ContainerResource getContainerResource(@PathParam("container") String container) {
return new ContainerResource(container);
}
// Provide @GET, @PUT, @POST, @DELETE to get collection of containers
@GET
public Containers getContainers() {
return MemoryStore.MS.getContainers();
}
}
@Produces("application/xml")
public class ContainerResource {
@Context UriInfo uriInfo;
@Context Request request;
String container;
// Constructor allowing it to be used as sub-resource
ContainerResource(String container) {
this.container = container;
}
// Define sub-resource for items collection
@Path("items")
public ContainerItemsResource getContainerItemsResource() {
return new ContainerItemsResource(container);
}
// Provide @GET, @PUT, @POST, @DELETE to get specific container
// Notice that path params are not redefined...
@GET
public Container getContainer() {
Container c = MemoryStore.MS.getContainer(container);
return c;
}
}
@Produces("application/xml")
public class ContainerItemsResource {
@Context UriInfo uriInfo;
@Context Request request;
String container;
// Constructor allowing it to be used as sub-resource
ContainerItemsResource(String container) {
this.container = container;
}
// Define sub-resource for specific item
@Path("{item}")
public ContainerItemsResource getContainerItemsResource(@PathParam("container") String container, @PathParam("item") String item) {
return new ContainerItemResource(container, item);
}
// Provide @GET, @PUT, @POST, @DELETE to get specific container items collection
// Notice that path params are not redefined...
@GET
public ContainerItems getContainerItems() {
Container c = MemoryStore.MS.getContainer(container);
return c.getItems();
}
}
@Produces("application/xml")
public class ContainerItemResource {
@Context UriInfo uriInfo;
@Context Request request;
String container;
String item;
// Constructor allowing it to be used as sub-resource
ContainerItemResource(String container, String item) {
this.container = container;
this.item = item;
}
// Provide @GET, @PUT, @POST, @DELETE to get specific container item
// Notice that path params are not redefined...
@GET
public ContainerItem getContainerItem() {
Container c = MemoryStore.MS.getContainer(container);
return c.getItem(item);
}
}
By providing this example of four level deep resource using sub-resource, hopefully it clarifies what your code is doing. The sub-resource approach eliminates duplicated path and path param definitions through-out your resources making the code more maintainable and (opinion) easier to read.