54

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?

Addison
  • 7,322
  • 2
  • 39
  • 55
onejigtwojig
  • 4,771
  • 9
  • 32
  • 35
  • 3
    Starting in Jersey 2.0, I believe, you'll want to use [`BeanParam`](http://jersey.java.net/nonav/apidocs/2.0-m08/jersey/javax/ws/rs/BeanParam.html) – Patrick Feb 18 '13 at 20:07
  • 1
    @Patrick please add your comment as an answer. If new information is available then it is okay to add an answer so that users can find the new information without having to look through comments. – Jonathan Spooner Jun 25 '13 at 17:17
  • 2
    @JonathanSpooner Now that 2.0 is actually released, this seems like a better idea than it did when I first made the comment, so I took your suggestion. Thanks! – Patrick Jun 26 '13 at 00:44

6 Answers6

106

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.

Magnus Reftel
  • 967
  • 6
  • 19
Patrick
  • 2,672
  • 3
  • 31
  • 47
  • 6
    Is there any way I can avoid placing `@QueryParam` for every property in my bean, if I want them all to be available? – pavel_kazlou May 05 '14 at 13:09
  • 2
    @pavel_kazlou I don't know of a pre-defined way to say "Everything in this bean is an individual QueryParam". There does exist [this](http://fusesource.com/docs/esb/4.3/cxf_rest/RESTParametersCXF.html) from Apache CXF. You could also write your own Injectable Provider (like the [selected answer](http://stackoverflow.com/a/6124014/1449525) on this page). Lastly, you could just take the query params as a [`Map` via the `@Context UriInfo`](http://stackoverflow.com/a/5718874/1449525). Beyond that, I don't really expect that Jersey 2.x has baked in anything specific just for that. – Patrick May 07 '14 at 18:13
  • The problem with this is that this bean has to know about jaxrs. The user should not see if he's using jaxrs or something other like spring-ws. The other solution is using the method @onejigtwojig mentioned – kboom Nov 02 '14 at 07:03
25

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");
}
kensen john
  • 5,439
  • 5
  • 28
  • 36
21

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.

ROMANIA_engineer
  • 54,432
  • 29
  • 203
  • 199
Vlagorce
  • 866
  • 2
  • 6
  • 17
  • 5
    This approach to implementing POJO binding can create problems later when you try to use `@QueryParam` in other resource methods. It's better to create your own Annotation and use that instead of `@QueryParam`. – Tristan Feb 07 '14 at 19:32
  • 6
    Instead, create an annotation named QueryBeanParam: `@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface QueryBeanParam {}`. Then, implement `InjectableProvider` instead of `InjectableProvider` in ParameterBeanProvider. Finally, use `@QueryBeanParam` in your resource: `public FindResponse find(@QueryBeanParam ParameterBean paramBean) {...}` – Tristan Feb 07 '14 at 19:44
  • 1
    This seems like a ton of work for the result. Its also not visible either on the REST method or the DTO in question, so would require some digging by a developer who didn't know about it. The @BeanParam answer feels like it much more closely maps the annotation driven style of JAX-RS. – Michael Haefele Sep 30 '16 at 15:07
7

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;

    }
onejigtwojig
  • 4,771
  • 9
  • 32
  • 35
2

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));
}
kboom
  • 2,279
  • 3
  • 28
  • 43
0

I know that my answer is not applicable to the specific context. But as the WEB transport mechanism should be separated from the the core application anyway it could be the option to change to other web framework. Like Spring webmvc is doing all of this out of the box.

takacsot
  • 1,727
  • 2
  • 19
  • 30