Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@bowenwr
Created September 5, 2014 18:44
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save bowenwr/57e39ea14f7eede0d804 to your computer and use it in GitHub Desktop.
Save bowenwr/57e39ea14f7eede0d804 to your computer and use it in GitHub Desktop.
Jersey Pagination Example for Jersey Mailing List Request
public class HelperResource {
public static void setRequestOptions(ContainerRequestContext requestContext, RequestOptions requestOptions) {
requestContext.setProperty("requestOptions", requestOptions);
}
public static boolean isBodyRequested(ContainerRequestContext requestContext) {
// Do not return a body for head methods, but we might want to calculate paging / headers, etc.
// For now this is getting rewritten as GET by Jersey, but it might be changed later:
// https://java.net/jira/browse/JERSEY-2460
String method = requestContext.getMethod();
if(requestContext.getProperty("originalMethod") != null) {
method = requestContext.getProperty("originalMethod").toString();
}
return (! method.equalsIgnoreCase(HttpMethod.HEAD));
}
}
public final class InjectionBinder extends AbstractBinder {
/**
* Implement to provide binding definitions using the exposed binding
* methods.
*/
@Override
protected void configure() {
bind(RequestOptionsValueFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class);
bind(RequestOptionsValueFactoryProvider.InjectionResolver.class).to(new TypeLiteral<InjectionResolver<RequestOptionsParam>>() {}).in(Singleton.class);
}
}
@ApplicationPath("/")
public class MyApplication extends ResourceConfig {
@SuppressWarnings("unchecked")
@Inject
public MyApplication(ServiceLocator serviceLocator) {
// This won't run by itself, do things like map your service packages, object mapper, injection, etc.
// Register your filter for putting request options in the response
register(RequestOptionsResponseFilter.class);
// Exception mapper for pagination
register(PaginationExceptionMapper.class);
// Injection binder for putting RequestOptions into our services
register(new InjectionBinder());
}
}
public class Pagination {
public static final int MAXIMUM_LIMIT = 100;
public static final int DEFAULT_LIMIT = 25;
public static final int OFFSET_FIRST_RECORD = 0;
public static final String OFFSET_NAME = "offset";
public static final String LIMIT_NAME = "limit";
public static final String HEADER_LIMIT = "X-Limit";
public static final String HEADER_OFFSET = "X-Offset";
public static final String HEADER_TOTAL_RECORDS = "X-Total-Records";
private Integer offset = OFFSET_FIRST_RECORD;
private Integer limit = DEFAULT_LIMIT;
private Integer total = null;
private static final Logger logger = LoggerFactory.getLogger(Pagination.class);
public Pagination() {}
public Pagination(HttpServletRequest request) {
parse(request);
}
public Pagination(Integer offset, Integer limit) {
setLimit(limit);
setOffset(offset);
}
public Pagination(Integer offset, Integer limit, Integer total) {
setLimit(limit);
setOffset(offset);
setTotal(total);
}
public Integer getLimit() {
return limit;
}
public Integer getOffset() {
return offset;
}
public Integer getTotal() {
return total;
}
public Pagination parse(final HttpServletRequest request) {
parseFromHeaders(request);
return this;
}
public void parseFromHeaders(final HttpServletRequest request) {
Enumeration<?> headers = request.getHeaderNames();
while(headers.hasMoreElements()) {
String key = (String) headers.nextElement();
String value = request.getHeader(key);
if(key.equalsIgnoreCase(HEADER_OFFSET) && (! isOffsetInitialized)) {
try {
offset = Integer.parseInt(value);
if(offset < OFFSET_FIRST_RECORD || limit > MAXIMUM_LIMIT) {
logger.debug("Offset exceeded acceptable range (>= {}). Offset was {}", OFFSET_FIRST_RECORD, value);
throw new PaginationException(OFFSET_NAME + " exceeded acceptable range (>= " + OFFSET_FIRST_RECORD + "). " + OFFSET_NAME + " was " + value);
}
isOffsetInitialized = true;
} catch(NumberFormatException|NullPointerException e) {
logger.debug("Error parsing offset from {}", value, e);
throw new PaginationException("Error parsing " + OFFSET_NAME + " from supplied value: " + value);
}
} else if(key.equalsIgnoreCase(HEADER_LIMIT) && (! isLimitInitialized)) {
try {
limit = Integer.parseInt(value);
if(limit < 1 || limit > MAXIMUM_LIMIT) {
logger.debug("Limit exceeded acceptable range (1 - {}). Limit was {}", MAXIMUM_LIMIT, value);
throw new PaginationException(LIMIT_NAME + " exceeded acceptable range (1 - " + MAXIMUM_LIMIT + "). Supplied " + LIMIT_NAME + " was " + value);
}
isLimitInitialized = true;
} catch(NumberFormatException|NullPointerException e) {
logger.debug("Error parsing limit from {}", value, e);
throw new PaginationException("Error parsing " + LIMIT_NAME + " from supplied value: " + value);
}
}
}
}
public void setLimit(Integer limit) {
this.limit = limit;
}
public void setOffset(Integer offset) {
this.offset = offset;
}
public void setTotal(Integer total) {
this.total = total;
}
@Override
public String toString() {
return "Pagination {" + LIMIT_NAME + ": " + limit + ", " + OFFSET_NAME + ": " + offset + (total == null ? "" : (", total available records: " + total)) + "}";
}
}
public class PaginationException extends WebApplicationException {
// Arbitrary, auto-generated
private static final long serialVersionUID = -2864569265777303842L;
public PaginationException(String message) {
super(message);
}
public PaginationException(String message, Exception e) {
super(message, e);
}
}
@Provider
public class PaginationExceptionMapper implements ExceptionMapper<PaginationException> {
@Override
public Response toResponse(PaginationException exception) {
// Maps a pagination exception to a HTTP 400 (meaning the client provided bad info such as non-numeric values)
return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).type(MediaType.TEXT_PLAIN).build();
}
}
public class RequestOptions {
public static final String HEADER_RECURSION = "X-Recursion";
public static final String RECURSION_ENABLED = "true";
public static final String RECURSION_DISABLED = "false";
private Pagination pagination;
private boolean recursive = false;
public RequestOptions() {
this.pagination = new Pagination();
this.recursive = false;
}
public RequestOptions(HttpServletRequest request) {
this.pagination = new Pagination();
this.recursive = false;
parse(request);
}
public RequestOptions(Pagination pagination) {
this.pagination = pagination;
}
public RequestOptions(Pagination pagination, boolean recursive) {
this.pagination = pagination;
this.recursive = recursive;
}
public Pagination getPagination() {
return pagination;
}
public boolean isRecursive() {
return recursive;
}
public void setRecursive(boolean recursive) {
this.recursive = recursive;
}
public void setPagination(Pagination pagination) {
this.pagination = pagination;
}
public Invocation.Builder apply(Invocation.Builder builder) {
if(pagination != null) {
builder.header(Pagination.HEADER_OFFSET, pagination.getOffset());
builder.header(Pagination.HEADER_LIMIT, pagination.getLimit());
}
return builder;
}
@SuppressWarnings("unchecked")
public RequestOptions parse(final HttpServletRequest request) {
String recursion = request.getHeader(HEADER_RECURSION);
if(recursion != null && recursion.trim().equalsIgnoreCase(RECURSION_ENABLED)) {
recursive = true;
} else {
for (Iterator<Map.Entry<String, String[]>> iterator = request.getParameterMap().entrySet().iterator(); iterator.hasNext();) {
Map.Entry<String, String[]> entry = iterator.next();
String key = entry.getKey();
String value = entry.getValue()[0];
if(key.equalsIgnoreCase("recursive") && value.trim().equalsIgnoreCase(RECURSION_ENABLED)) {
recursive = true;
}
}
}
setPagination(new Pagination(request));
return this;
}
@Override
public String toString() {
return ("Request Options: {" + pagination.toString() + ", Recursion: " + recursive + "}");
}
}
public class RequestOptionsResponseFilter implements ContainerResponseFilter {
private static final Logger logger = LoggerFactory.getLogger(RequestOptionsResponseFilter.class);
@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException {
if(requestContext.getProperty("requestOptions") != null && (requestContext.getProperty("requestOptions") instanceof RequestOptions)) {
RequestOptions requestOptions = (RequestOptions) requestContext.getProperty("requestOptions");
if(requestOptions.getPagination() != null) {
logger.debug("Adding pagination information to response: {}", requestOptions.getPagination());
responseContext.getHeaders().add(Pagination.HEADER_TOTAL_RECORDS, requestOptions.getPagination().getTotal());
responseContext.getHeaders().add(Pagination.HEADER_LIMIT, requestOptions.getPagination().getLimit());
responseContext.getHeaders().add(Pagination.HEADER_OFFSET, requestOptions.getPagination().getOffset());
}
}
}
}
@Singleton
public final class RequestOptionsValueFactoryProvider extends AbstractValueFactoryProvider {
/**
* Injection resolver for {@link RequestOptionsParam} annotation. Will create a
* Factory Provider for the actual resolving of the {@link RequestOptions}
* object.
*/
@Singleton
static final class InjectionResolver extends ParamInjectionResolver<RequestOptionsParam> {
/**
* Create new {@link RequestOptionsParam} annotation injection resolver.
*/
public InjectionResolver() {
super(RequestOptionsValueFactoryProvider.class);
}
}
/**
* Factory implementation for resolving request-based attributes and other
* information.
*/
private static final class RequestOptionsValueFactory extends AbstractContainerRequestValueFactory<RequestOptions> {
@Context
private ResourceContext context;
/**
* Fetch the RequestOptions object from the request. Since
* HttpServletRequest is not directly available, we need to get it via
* the injected {@link ResourceContext}.
*
* @return {@link RequestOptions} stored on the request, or NULL if no
* object was found.
*/
public RequestOptions provide() {
final HttpServletRequest request = context.getResource(HttpServletRequest.class);
return new RequestOptions(request);
}
}
/**
* {@link RequestOptionsParam} annotation value factory provider injection
* constructor.
*
* @param mpep
* multivalued parameter extractor provider.
* @param injector
* injector instance.
*/
@Inject
public RequestOptionsValueFactoryProvider(MultivaluedParameterExtractorProvider mpep, ServiceLocator injector) {
super(mpep, injector, Parameter.Source.UNKNOWN);
}
/**
* Return a factory for the provided parameter. We only expect
* {@link RequestOptions} objects being annotated with {@link RequestOptionsParam}
* annotation
*
* @param parameter
* Parameter that was annotated for being injected
* @return {@link RequestOptionsValueFactory} if parameter matched
* {@link RequestOptions} type
*/
@Override
public AbstractContainerRequestValueFactory<?> createValueFactory(Parameter parameter) {
Class<?> classType = parameter.getRawType();
if (classType == null || (!classType.equals(RequestOptions.class))) {
// Not logging this as it will match anything in the resource methods like @NotNull or @Valid annotations
return null;
}
return new RequestOptionsValueFactory();
}
}
@Path("samples")
public class SampleResource {
@Context UriInfo ui;
@Context ContainerRequestContext requestContext;
@RequestOptionsParam RequestOptions requestOptions;
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("foo")
public Response getFoo() {
List<Foo> results = null;
int count = 0;
// This is fake code, the general idea is calculate the number of foo to be returned and apply pagination. We are assuming some sort of DAO or other data access object which would actually get our data
persistenceManager.beginTransaction();
try {
count = dao.countFoo();
requestOptions.getPagination().setTotal(count); // Assign our total, we can include this in the response to the client via headers
HelperResource.setRequestOptions(requestContext, requestOptions); // Write this to request context so our response filter can access it later
// For HEAD requests, we do not need to process the body results, its a waste
if(count > 0 && HelperResource.isBodyRequested(requestContext)) {
results = dao.listFoo(requestOptions); // The dao knows how to access our pagination info and use it to limit results
}
persistenceManager.commitTransaction();
} catch(Exception e) {
persistenceManager.rollbackTransaction();
throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR, e);
}
if(results == null || results.size() == 0) {
logger.debug("No foo found");
return HelperResource.emptySet();
}
return Response.ok(results).build(); // Return the response, our response filter will apply pagination so the user knows the total records, etc.
}
}
@notsoluckycharm
Copy link

Great example to learn from! QQ: did you forget to share the code for @RequestOptionsParam ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment