Skip to content

Instantly share code, notes, and snippets.

@vicly
Last active September 27, 2017 07:59
Show Gist options
  • Save vicly/3a9e7cc4a1ce6c7a2d9237568a6e5880 to your computer and use it in GitHub Desktop.
Save vicly/3a9e7cc4a1ce6c7a2d9237568a6e5880 to your computer and use it in GitHub Desktop.
[Jersey note] #REST #Jersey
@GET
@Consumes("application/json")
@Produces("application/json")
@ManagedAsync
public void addBook(Book book, @Suspended final AsyncResponse response) {
  ListenableFuture<Book> bookFuture = dao.addBookAsync(book);
  Futures.addCallback(bookFuture, new FutureCallback<Book>() {
    public void onSuccess(Book addedBook) {
      response.resume(addedBook);
    }
    public void onFailure(Throwable thrown) {
      response.resume(thrown);
    }
  });
}
@Context Request request;

@GET
@Path("{id}")
@Produces("application/json")
public Response getBook(@PathParam("id") String id) {

  Book book = dao.getBookById(id);
  if (book != null) {
    EntityTag etag = EntityTag(String.valueOf(book.getLastModifiedMillis()));
    Response.ResponseBuilder rb = request.evaluatePreconditions(entityTag);
    if (rb != null) {
      // not changed
      return rb.build();
    } else {
      // changed
      return Response.ok().tag(entityTag).entity(book).build();
    }
  } else {
    // 404
    ...
  }
}

Request

EntityTag etag = new EntityTag("the-last-entity-etag");
Response response = target("books").path(book_id).request().header("If-Match", entityTag).get();

Execution flow

Pre-Matching Request Filter

  [resource method matching]

Post-Matching Request Filter

Reader Interceptor
  
  [resource method executed]

Response Filter

Writer Interceptor

Filters and Interceptors

Filters

  • modify headers, the entity and other request/response parameters
  • executed regarless of whether there is an entity to read or write
  • can abort a request, preventing the resource method from executing

Interceptors

  • primarily used to manipulate the entity (via input/output stream)
  • change properties to affect choice of Message Body Reader/Writer
  • ONLY executed when there is an entity to read or write

Jersey built-in filters

  • logging
  • jaxrs to spring bridge
  • cross-site request forgery protection
  • HTTP method overrides
  • URI-based content negotiation

Http method override

// enable it
register(HttpMethodOverrideFilter.class);

// test code: _method=PATCH + POST
Response response = target("books").path(book_id).queryParam("_method", "PATCH").request().post();

Uri content negotiation

Register filter

Map<String, MediaType> map = ...;
map.put("xml", MediaType.APPLICATION_XML);
map.put("json", MediaType.APPLICATION_JSON);
UriConnegFilter ucnFilter = new UriConnegFilter(map, null);
register(ucnFilter);

Make the call

Response response = target("books.json").request().get();

Custom Filter

Request Filter

@Provider // if you use auto-scan
public class MyRequestFilter implements ContainerRequestFilter {
  
  // @PreMatching if you want a pre-matching filter
  public void filter(ContainerRequestContext reqCtx) throws IOException {
    ...
  }
}

Response filter

@Provider // if you use auto-scan
public class PoweredByFilter implements ContainerResponseFilter {
  public void filter(ContainerRequestContext reqCtx,
                     ContainerResponseContext resCtx)
                     throws IOException {
    resCtx.getHeaders().add("X-Powered-By", "Vic");
  }
}

Name Binding

Define how your filter/interceptor applies.

  1. define annotation
@javax.ws.rs.NameBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PoweredBy {
  String value() default "";
}
  1. annotate filter class
@Provider
@PoweredBy
public class PoweredByFilter implements ContainerResponseFilter {
  public void filter(ContainerRequestContext requestContext,
                     ContainerResponseContext responseContext)
                     throws IOException {
    for (Annotation a : responseContext.getEntityAnnotations()) {
      if (a.annotationType() == PoweredBy.class) {
        String value = ((PoweredBy) a).value();
        responseContext.getHeaders().add("X-Powered-By", value);
      }
    }
  }
}
  1. annotate resource method to apply the fiter
@GET
@PoweredBy("Vic")
public List<Book> getBooks() {...

PATH: partical update

Create Annotation

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@HttpMethod("PATCH")
public @interface PATCH {
}

Resource

@PATCH
@PATH("{id}")
public void updateBookNameTitle(@PathParam("id") String id, Book update) {
// transaction start
  Book book = dao.getById(id);
  if (book == null) // 404
  
  if (update.getTitle() != null) book.setTitle(update.getTitle());
  if (update.getName() != null) book.setTitle(update.getName());
  return book;
// transaction auto commit
}

If JerseyTest shows not support PATCH // use grizzly org.glassfish.jersey.test-framework.providers:jersey-test-framework-providers-grizzly2 org.glassfish.jersey.connectors:jersey-grizzly-connector

clientConfig.connectorProvider(new GrizzlyConnectorProvider());

Maven

<dependency>
  <groupId>org.glassfish.jersey.ext</groupId>
  <artifactId>jersey-bean-validation</artifactId>
</dependency>

Config in ResourceConfig

// enable this to return error message in body
property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);

// special case
BV_DISABLE_VALIDATE_ON_EXECUTABLE_OVERRIDE_CHECK

Validate

public class Book {
  @NotNull(message = "title is required")
  private String title;
  ...
}
@Valid // validate output
public Book addBook(@Valid @NotNull Book book) { // validate input
  ...
  return addedBook;
}

Add XML support

<dependency>
  <groupId>com.fasterxml.jackson.jaxrs</groupId>
  <artifactId>jackson-jaxrs-xml-provider</artifactId>
  <version>2.3.0</version>
</dependency>
JacksonXMLProvider xml = new JacksonXMLProvider()
        .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
        .configure(SerializationFeature.INDENT_OUTPUT, true);
register(xml);
@Produces({MediaType.APPLICATION_XML}}
public Response top10Cars() {
  ...
}

Wrap a collection under a root XML element

Given Collection<Book> top10Books();, we want below output rather than change method to BooksWrapper top10Books();

<books>
  <book>
    <id>abc</id>
    <name>xyz</name>
  </book>
</books>

Add custom MessageBodyWriter

@Provider
@Produces({MediaType.APPLICATION_XML})
public class BooksMessageBodyWriter implements MessageBodyWriter<Collection<Book>> {

  @JacksonXmlRootElement(localName = "books")
  public class BooksWrapper {
    @JacksonXmlElementWrapper(useWrapping = false)
    @JacksonXmlProperty(localName = "book")
    public Collection<Book> books;
    
    BooksWrapper(Collection<Book>) {
      this.books = books;
    }
  }
  
  @Context Providers providers;

  public long getSize(Collection<Book> books, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
    return -1;
  }
  
  public isWritable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
    return Collection.class.isAssignableFrom(type);
  }
  
  public void writeTo(Collection<Book> books, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType,
      MultivalueMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException {
      providers.getMessageBodyWriter(BooksWrapper.class, generictype, annotation, mediaType)
          .writeTo(new BooksWrapper(books), type, genericType, annotations, mediaType, httpHeaders, entityStream);
  }
}

Test

<dependency>
  <groupId>com.jcabi</groupId>
  <artifactId>jcabi-xml</artifactId>
  <version>0.7.8</version>
  <scope>test</scope>
</dependency>
@Test
public void top10Books() {
  String output = target("books/top10").request("application/xml").get().readEntity(String.class);
  XML xml = new XMLDocument(output);
  
  assertEquals("author1", xml.xpath("books/book[@id=' + book1_id + ']).get(0));
  
  assertEquals(10, xml.xpath("//book/author/text()").size());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment