Статьи

Поддержка прозрачного PATCH в JAX-RS 2.0

Метод PATCH является одним из наименее любимых методов HTTP, потому что до недавнего времени не было стандартного формата PATCH. Это уже давно стандартизировано для JSON, поэтому существует немало библиотек, которые сделают вам тяжелую работу. Для целей этого блога я собираюсь использовать json-patch, хотя было бы легко адаптировать эту конкретную реализацию к выбранной вами библиотеке исправлений.

В обычном режиме можно получить ресурсы и классы компонентов. В этом примере кода у нас есть простой ресурс, который знает, как вернуть исходный объект, и ресурс, который позволяет вам выполнить метод PATCH. Обратите внимание, что метод patch просто принимает объект bean, это связано с тем, что мы собираемся немного поработать над предварительной обработкой патча.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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(List list) {
    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;
  }
 
}

Таким @PATCH аннотация @PATCH — это то, что мы должны создать для этого примера, к счастью, JAX-RS содержит расширенную метааннотацию для этой цели. Мы также собираемся использовать @NameBinding как этот пример использует JAX-RS 2.0, чтобы мы могли подключить наш фильтр в одно мгновение.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
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 {
}

Итак, вот реализация ReaderInterceptor которая будет обрабатывать входящий поток и заменять его исправленной версией. Обратите внимание, что класс аннотируется @PATCH также для того, чтобы заставить магию @NamedBinding работать, а также что отсутствует много обработки ошибок, поскольку это простой POC.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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");
    }
 
  }
}

Итак, после развертывания вы можете начать играть с данными, поэтому исходное сообщение:

1
2
3
4
5
6
7
8
{
  "list" : [
    "one",
    "two"
  ],
  "message" : "message",
  "title" : "title"
}

Поэтому, если вы примените следующий патч, результат будет:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
[
  {
    "op" : "replace",
    "path" : "/message",
    "value" : "otherMessage"
  },
  {
    "op" : "add",
    "path" : "/list/-",
    "value" : "three"
  }
]
 
{
  "list" : [
    "one",
    "two",
    "three"
  ],
  "message" : "otherMessage",
  "title" : "title"
}

Этот пример показывает, что довольно просто добавить поддержку PATCH в ваши классы, следуя простому шаблону кодирования и используя простую аннотацию. Таким образом, поддержка PATCH становится тривиальной, поскольку реализация может просто делегировать существующий метод PUT.

Обновление: Мирсолав Фукса из команды Джерси напомнил мне, что для того, чтобы эта реализация соответствовала RFC PATCH, она должна предоставить заголовок Accept-Patch когда клиент выполняет запрос OPTIONS. Вы можете сделать это с помощью простого CotnainerResponseFilter:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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")); 
      }
    }
  }
}