How do you map multiple query parameters to the fields of a bean on Jersey GET request?

JavaJerseyJax Rs

Java Problem Overview


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?

Java Solutions


Solution 1 - Java

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 - Java

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 - Java

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 - Java

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 - Java

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

Solution 6 - Java

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiononejigtwojigView Question on Stackoverflow
Solution 1 - JavaPatrickView Answer on Stackoverflow
Solution 2 - Javakensen johnView Answer on Stackoverflow
Solution 3 - JavaVlagorceView Answer on Stackoverflow
Solution 4 - JavaonejigtwojigView Answer on Stackoverflow
Solution 5 - JavakboomView Answer on Stackoverflow
Solution 6 - JavatakacsotView Answer on Stackoverflow