Created
September 5, 2014 18:44
-
-
Save bowenwr/57e39ea14f7eede0d804 to your computer and use it in GitHub Desktop.
Jersey Pagination Example for Jersey Mailing List Request
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) + "}"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 + "}"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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. | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great example to learn from! QQ: did you forget to share the code for @RequestOptionsParam ?