A per normal lets get the resource and bean classes out of the way. In this example code we have a simple resource that knows how to return the original object and one that allows you to perform the PATCH method. Note that the patch method just accepts the bean object, this is because of some magic we are going to do in a little bit to pre-process the patch.
import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("service") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class Service { @GET public Bean get() { return new Bean(true); } @PATCH @Consumes("application/json-patch+json") public Bean patch(Bean input) { System.out.println(input.getMessage() + " " + input.getTitle()); return input; } } import java.util.ArrayList; import java.util.List; public class Bean { private String title = "title"; private String message = "message"; private List<String> list = new ArrayList<String>(); public Bean() { this(false); } public Bean(boolean init) { if (init) { title = "title"; message = "message"; list.add("one"); list.add("two"); } } public void setList(Listlist) { this.list = list; } public List getList() { return list; } public void setTitle(String title) { this.title = title; } public String getTitle() { return title; } public void setMessage(String message) { this.message = message; } public String getMessage() { return message; } }
So the
@PATCH
annotation is something we have to create for this example, luckily JAX-RS contains a extension meta-annotation for this purpose. We are also going to use @NameBinding
as this example is using JAX-RS 2.0 so we can connect up our filter in a moment.import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.ws.rs.HttpMethod; import javax.ws.rs.NameBinding; @Target({ ElementType.METHOD, ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @HttpMethod("PATCH") @Documented @NameBinding public @interface PATCH { }
So here is the implementation of the
ReaderInterceptor
that will process the incoming stream and replace it with the patched version. Note that the class is annotated with @PATCH
also in order to make the @NamedBinding
magic work and also that there is a lot of error handling that is missing as this is a simple POC.import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.fge.jsonpatch.JsonPatch; import com.github.fge.jsonpatch.JsonPatchException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import javax.ws.rs.GET; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedHashMap; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; import javax.ws.rs.ext.ReaderInterceptor; import javax.ws.rs.ext.ReaderInterceptorContext; import org.glassfish.jersey.message.MessageBodyWorkers; @Provider @PATCH public class PatchReader implements ReaderInterceptor { private UriInfo info; private MessageBodyWorkers workers; @Context public void setInfo(UriInfo info) { this.info = info; } @Context public void setWorkers(MessageBodyWorkers workers) { this.workers = workers; } @Override public Object aroundReadFrom( ReaderInterceptorContext readerInterceptorContext) throws IOException, WebApplicationException { // Get the resource we are being called on, // and find the GET method Object resource = info.getMatchedResources().get(0); Method found = null; for (Method next : resource.getClass().getMethods()) { if (next.getAnnotation(GET.class) != null) { found = next; break; } } if (found != null) { // Invoke the get method to get the state we are trying to patch // Object bean; try { bean = found.invoke(resource); } catch (Exception e) { throw new WebApplicationException(e); } // Convert this object to a an aray of bytes ByteArrayOutputStream baos = new ByteArrayOutputStream(); MessageBodyWriter<? super Object> bodyWriter = workers.getMessageBodyWriter(Object.class, bean.getClass(), new Annotation[0], MediaType.APPLICATION_JSON_TYPE); bodyWriter.writeTo(bean, bean.getClass(), bean.getClass(), new Annotation[0], MediaType.APPLICATION_JSON_TYPE, new MultivaluedHashMap<String, Object>(), baos); // Use the Jackson 2.x classes to convert both the incoming patch // and the current state of the object into a JsonNode / JsonPatch ObjectMapper mapper = new ObjectMapper(); JsonNode serverState = mapper.readValue(baos.toByteArray(), JsonNode.class); JsonNode patchAsNode = mapper.readValue( readerInterceptorContext.getInputStream(), JsonNode.class); JsonPatch patch = JsonPatch.fromJson(patchAsNode); try { // Apply the patch JsonNode result = patch.apply(serverState); // Stream the result & modify the stream on the readerInterceptor ByteArrayOutputStream resultAsByteArray = new ByteArrayOutputStream(); mapper.writeValue(resultAsByteArray, result); readerInterceptorContext.setInputStream( new ByteArrayInputStream( resultAsByteArray.toByteArray())); // Pass control back to the Jersey code return readerInterceptorContext.proceed(); } catch (JsonPatchException e) { throw new WebApplicationException( Response.status(500).type("text/plain").entity(e.getMessage()).build()); } } else { throw new IllegalArgumentException("No matching GET method on resource"); } } }
So once you have this deployed you can start playing with the data, so the original message is:
{ "list" : [ "one", "two" ], "message" : "message", "title" : "title" }
So if you apply the following patch, the result returned is:
[ { "op" : "replace", "path" : "/message", "value" : "otherMessage" }, { "op" : "add", "path" : "/list/-", "value" : "three" } ] { "list" : [ "one", "two", "three" ], "message" : "otherMessage", "title" : "title" }
This example shows it is relatively trivial to add PATCH support to your classes by following a simple coding pattern and using a simple Annotation. In this way PATCH support becomes trivial as the implementation can just delegate to your existing PUT method.
Update: Mirsolav Fuksa from the Jersey team reminded me that in order for this implementation to comply with the PATCH RFC it should provide the
Accept-Patch
header when the client performs an OPTIONS request. You can do this with a simple CotnainerResponseFilter:import java.io.IOException; import java.util.Collections; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; import javax.ws.rs.ext.Provider; @Provider public class OptionsAcceptHeader implements ContainerResponseFilter { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { if ("OPTIONS".equals(requestContext.getMethod())) { if (responseContext.getHeaderString("Accept-Patch")==null) { responseContext.getHeaders().put( "Accept-Patch", Collections.<Object>singletonList("application/json-patch+json")); } } } }
4 comments:
Hi,
Just what I was lookin for.
Is it possible to do this in JAX-RS 1.x?
If so, know of any examples?
Thanks
Hi,
It should be possible to port this code as the interceptors and similar were available as internal API's in Jersey 1.x. Would have to be a DIY job though. :-)
Gerard
Is there a corollary for the JAX-RS 2 client api?
Brant,
The client model is more complicate because it has to deal with caching and retrying if there is a model miss match. It is something I am looking at though, perhaps a post in the future....
Gerard
Post a Comment