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));
}