I have some resource handle methods which contain dozens of @QueryParam
parameters with @Default
, grouped in roughly themes (pagination/ordering, filtering, authentication). This is really cumbersome and I'd like to simplify this. The good thing is that those parameters are grouped by themes (pagination, ordering, filtering, etc.) so I can reduce my whole set of parameters to 4 methods.
How can I achieve that?
Typically, I want to come from this:
@GET
public Response findAll(
@QueryParam("sort") @DefaultValue("name") List<String> sort,
@QueryParam("from") UUID fromId
) {
// Validate sort
// Validate fromId
}
To this:
@GET
public Response findAll(@Context Pagination pagination) { // Inject pagination
// Yeah, small code! Yeah, modularity!
}
// Create the pagination somewhere else.
public Pagination createPagination(@Context UriInfo uriInfo) {
Optional<UUID> fromId = extractFromId(uriInfo); // retrieve "from" from uriInfo
List<String> sort = extractSort(uriInfo); // retrieve "sort" from uriInfo
Pagination pagination = new Pagination();
pagination.setFromId(fromId);
pagination.setSort(sort);
// Validate pagination
return pagination;
}
Note: as I show in my example, I don't mind writing more code myself, but I just can't bear having too many parameters in my methods and read that wall of @QueryParam
+ @DefaultValue
.
If you are using JAX-RS 2.0, you can use the @BeanParam
, which allows you to inject arbitrary @XxxParam
annotated properties and @Context
objects into an arbitrary bean class. For example
public class Bean {
@QueryParam("blah")
String blah;
}
@GET
public Response get(@BeanParam Bean bean) {}
You can even inject into the constructor, if you want immutibility. For example
public static class Pagination {
private final List<String> sort;
private final Optional<String> from;
public Pagination(@QueryParam("sort") List<String> sort,
@QueryParam("from") Optional<String> from) {
this.sort = sort;
this.from = from;
}
public List<String> getSort() { return sort; }
public Optional<String> getFrom() { return from; }
}
If you notice the Optional
is being injected. Normally this is not possible, but I created a ParamConverter
for it. You can read more about it in this answer. It basically allows us to inject arbitrary objects, create from the String value of the parameter.
@Provider
public static class OptionalParamProvider implements ParamConverterProvider {
@Override
public <T> ParamConverter<T> getConverter(Class<T> rawType,
Type genericType,
Annotation[] annotations) {
if (Optional.class != rawType) {
return null;
}
return (ParamConverter<T>)new ParamConverter<Optional>() {
@Override
public Optional fromString(String value) {
return Optional.ofNullable(value);
}
@Override
public String toString(Optional value) {
return value.toString();
}
};
}
}
The benefit of the OptionalParamProvider
is that it allows you to use Optional
anywhere you need to inject a @FormParam
, @QueryParam
, @PathParm
, and all other @XxxParam
s (except for multitpart).
I don't know what JAX-RS implementation you are using but the above should work on all implementations. Below is a Jersey test case, using Jersey Test Framework. You can run the class like any other JUnit test.
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Optional;
import javax.ws.rs.BeanParam;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import org.junit.Test;
public class BeanParamTest extends JerseyTest {
@Provider
public static class OptionalParamProvider implements ParamConverterProvider {
@Override
public <T> ParamConverter<T> getConverter(Class<T> rawType,
Type genericType,
Annotation[] annotations) {
if (Optional.class != rawType) {
return null;
}
return (ParamConverter<T>)new ParamConverter<Optional>() {
@Override
public Optional fromString(String value) {
return Optional.ofNullable(value);
}
@Override
public String toString(Optional value) {
return value.toString();
}
};
}
}
public static class Pagination {
private final List<String> sort;
private final Optional<String> from;
public Pagination(@QueryParam("sort") List<String> sort,
@QueryParam("from") Optional<String> from) {
this.sort = sort;
this.from = from;
}
public List<String> getSort() { return sort; }
public Optional<String> getFrom() { return from; }
}
@Path("bean")
public static class PaginationResource {
@GET
public String get(@BeanParam Pagination pagination) {
StringBuilder sb = new StringBuilder();
sb.append(pagination.getSort().toString());
if (pagination.getFrom().isPresent()) {
sb.append(pagination.getFrom().get());
}
return sb.toString();
}
}
@Override
public ResourceConfig configure() {
return new ResourceConfig(PaginationResource.class)
.register(OptionalParamProvider.class);
}
@Test
public void should_return_all_sort_and_from() {
Response response = target("bean")
.queryParam("sort", "foo")
.queryParam("sort", "bar")
.queryParam("from", "baz")
.request().get();
assertEquals(200, response.getStatus());
String message = response.readEntity(String.class);
assertThat(message, containsString("foo"));
assertThat(message, containsString("bar"));
assertThat(message, containsString("baz"));
System.out.println(message);
response.close();
}
}
This is the only Maven dependency you need to run the test
<dependency>
<groupId>org.glassfish.jersey.test-framework.providers</groupId>
<artifactId>jersey-test-framework-provider-grizzly2</artifactId>
<version>2.19</version>
<scope>test</scope>
</dependency>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With