How do you map multiple query parameters to the fields of a bean on Jersey GET request?
A service class has a @GET
operation that accepts multiple parameters. These parameters are passed in as query parameters to the @GET
service call.
@GET
@Path("find")
@Produces(MediaType.APPLICATION_XML)
public FindResponse find(@QueryParam("prop1") String prop1,
@QueryParam("prop2") String prop2,
@QueryParam("prop3") String prop3,
@QueryParam("prop4") String prop4, ...)
The list of these parameters are growing, so I would like to place them into a single bean that contains all these parameters.
@GET
@Path("find")
@Produces(MediaType.APPLICATION_XML)
public FindResponse find(ParameterBean paramBean)
{
String prop1 = paramBean.getProp1();
String prop2 = paramBean.getProp2();
String prop3 = paramBean.getProp3();
String prop4 = paramBean.getProp4();
}
How would you do this? Is this even possible?
Solution 1:
In Jersey 2.0, you'll want to use BeanParam to seamlessly provide what you're looking for in the normal Jersey style.
From the above linked doc page, you can use BeanParam to do something like:
@GET
@Path("find")
@Produces(MediaType.APPLICATION_XML)
public FindResponse find(@BeanParam ParameterBean paramBean)
{
String prop1 = paramBean.prop1;
String prop2 = paramBean.prop2;
String prop3 = paramBean.prop3;
String prop4 = paramBean.prop4;
}
And then ParameterBean.java
would contain:
public class ParameterBean {
@QueryParam("prop1")
public String prop1;
@QueryParam("prop2")
public String prop2;
@QueryParam("prop3")
public String prop3;
@QueryParam("prop4")
public String prop4;
}
I prefer public properties on my parameter beans, but you can use getters/setters and private fields if you like, too.
Solution 2:
Try something like this. Use UriInfo to get all the request parameters into a map and try to access them. This is done inplace of passing individual parameters.
// showing only the relavent code
public FindResponse find( @Context UriInfo allUri ) {
MultivaluedMap<String, String> mpAllQueParams = allUri.getQueryParameters();
String prop1 = mpAllQueParams.getFirst("prop1");
}
Solution 3:
You can use com.sun.jersey.spi.inject.InjectableProvider
.
import java.util.List;
import java.util.Map.Entry;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import org.springframework.beans.BeanUtils;
import com.sun.jersey.api.core.HttpContext;
import com.sun.jersey.api.model.Parameter;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;
@Provider
public final class ParameterBeanProvider implements InjectableProvider<QueryParam, Parameter> {
@Context
private final HttpContext hc;
public ParameterBeanProvider(@Context HttpContext hc) {
this.hc = hc;
}
@Override
public ComponentScope getScope() {
return ComponentScope.PerRequest;
}
@Override
public Injectable<ParameterBean> getInjectable(ComponentContext ic, final QueryParam a, final Parameter c) {
if (ParameterBean.class != c.getParameterClass()) {
return null;
}
return new Injectable<ParameterBean>() {
public ParameterBean getValue() {
ParameterBean parameterBean = new ParameterBean();
MultivaluedMap<String, String> params = hc.getUriInfo().getQueryParameters();
// Populate the parameter bean properties
for (Entry<String, List<String>> param : params.entrySet()) {
String key = param.getKey();
Object value = param.getValue().iterator().next();
// set the property
BeanUtils.setProperty(parameterBean, key, value);
}
return parameterBean;
}
};
}
}
In your resource you just have to use @QueryParam("valueWeDontCare")
.
@GET
@Path("find")
@Produces(MediaType.APPLICATION_XML)
public FindResponse find(@QueryParam("paramBean") ParameterBean paramBean) {
String prop1 = paramBean.getProp1();
String prop2 = paramBean.getProp2();
String prop3 = paramBean.getProp3();
String prop4 = paramBean.getProp4();
}
The provider will be automatically called.
Solution 4:
You can create a custom Provider.
@Provider
@Component
public class RequestParameterBeanProvider implements MessageBodyReader
{
// save the uri
@Context
private UriInfo uriInfo;
// the list of bean classes that need to be marshalled from
// request parameters
private List<Class> paramBeanClassList;
// list of enum fields of the parameter beans
private Map<String, Class> enumFieldMap = new HashMap<String, Class>();
@Override
public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType)
{
return paramBeanClassList.contains(type);
}
@Override
public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException
{
MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
Object newRequestParamBean;
try
{
// Create the parameter bean
newRequestParamBean = type.newInstance();
// Populate the parameter bean properties
for (Entry<String, List<String>> param : params.entrySet())
{
String key = param.getKey();
Object value = param.getValue().iterator().next();
// set the property
BeanUtils.setProperty(newRequestParamBean, key, value);
}
}
catch (Exception e)
{
throw new WebApplicationException(e, 500);
}
return newRequestParamBean;
}
public void setParamBeanClassList(List<Class> paramBeanClassList)
{
this.paramBeanClassList = paramBeanClassList;
}
Solution 5:
You might want to use the following approach. This is a very standard-compliant solution and there are no hacks in there. The above solution also works but is somewhat hacky because it suggests it deals only with request body whereas it extracts the data from the context instead.
In my case I wanted to create an annotation which would allow to map query parameters "limit" and "offset" to a single object. The solution is as follows:
@Provider
public class SelectorParamValueFactoryProvider extends AbstractValueFactoryProvider {
public static final String OFFSET_PARAM = "offset";
public static final String LIMIT_PARAM = "limit";
@Singleton
public static final class InjectionResolver extends ParamInjectionResolver<SelectorParam> {
public InjectionResolver() {
super(SelectorParamValueFactoryProvider.class);
}
}
private static final class SelectorParamValueFactory extends AbstractContainerRequestValueFactory<Selector> {
@Context
private ResourceContext context;
private Parameter parameter;
public SelectorParamValueFactory(Parameter parameter) {
this.parameter = parameter;
}
public Selector provide() {
UriInfo uriInfo = context.getResource(UriInfo.class);
MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
SelectorParam selectorParam = parameter.getAnnotation(SelectorParam.class);
long offset = selectorParam.defaultOffset();
if(params.containsKey(OFFSET_PARAM)) {
String offsetString = params.getFirst(OFFSET_PARAM);
offset = Long.parseLong(offsetString);
}
int limit = selectorParam.defaultLimit();
if(params.containsKey(LIMIT_PARAM)) {
String limitString = params.getFirst(LIMIT_PARAM);
limit = Integer.parseInt(limitString);
}
return new BookmarkSelector(offset, limit);
}
}
@Inject
public SelectorParamValueFactoryProvider(MultivaluedParameterExtractorProvider mpep, ServiceLocator injector) {
super(mpep, injector, Parameter.Source.UNKNOWN);
}
@Override
public AbstractContainerRequestValueFactory<?> createValueFactory(Parameter parameter) {
Class<?> classType = parameter.getRawType();
if (classType == null || (!classType.equals(Selector.class))) {
return null;
}
return new SelectorParamValueFactory(parameter);
}
}
What you also need to do is registering it.
public class JerseyApplication extends ResourceConfig {
public JerseyApplication() {
register(JacksonFeature.class);
register(new InjectionBinder());
}
private static final class InjectionBinder extends AbstractBinder {
@Override
protected void configure() {
bind(SelectorParamValueFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class);
bind(SelectorParamValueFactoryProvider.InjectionResolver.class).to(
new TypeLiteral<InjectionResolver<SelectorParam>>() {
}).in(Singleton.class);
}
}
}
You also need the annotation itself
@Target({java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.FIELD})
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface SelectorParam {
long defaultOffset() default 0;
int defaultLimit() default 25;
}
and a bean
public class BookmarkSelector implements Bookmark, Selector {
private long offset;
private int limit;
public BookmarkSelector(long offset, int limit) {
this.offset = offset;
this.limit = limit;
}
@Override
public long getOffset() {
return 0;
}
@Override
public int getLimit() {
return 0;
}
@Override
public boolean matches(Object object) {
return false;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
BookmarkSelector that = (BookmarkSelector) o;
if (limit != that.limit) return false;
if (offset != that.offset) return false;
return true;
}
@Override
public int hashCode() {
int result = (int) (offset ^ (offset >>> 32));
result = 31 * result + limit;
return result;
}
}
Then you might use it like this
@GET
@Path(GET_ONE)
public SingleResult<ItemDTO> getOne(@NotNull @PathParam(ID_PARAM) String itemId, @SelectorParam Selector selector) {
Item item = auditService.getOneItem(ItemId.create(itemId));
return singleResult(mapOne(Item.class, ItemDTO.class).select(selector).using(item));
}