Search code examples
javajerseyjax-rsjersey-2.0

How do I extend Jersey's param annotations?


As far as I can see, Jersey does not support deep-object parameters (parameters in the form of ?type[n1]=v1&type[n2]=v2).

Is it possible to add this as extension? And if so, how?

My idea is to have an annotation similar to @QueryParam, let's say @DeepObjectParam, and I would use it to annotate a field like this:

   @GET
   public Response(@DeepObjectParam("type") Map<String, String> type) {
       // ...
   }

And have Jersey inject the map.


Solution

  • Depending on the version of Jersey you are using, the interface that you need to implement will be different. In Jersey 2.0-2.25.1, the class is ValueFactoryProvider and 2.26+ it's ValueParamProvider.

    With both classes, the implementation will be similar. There is one method to implement which takes a Parameter argument. We use this Parameter to check whether this provider is able to handle this type of parameter. If the check passes, then the method should return either a Factory or a Function (depending on the version) that provides the actual argument. If the check fails, it should return null.

    For example, if the parameter is annotated with @DeepObjectParam and the parameter type is Map, then we should check for these two things.

    @Override
    public Function<ContainerRequest, ?> getValueProvider(Parameter param) {
    
        if (param.isAnnotationPresent(DeepObjectParam.class) && isStringStringMap(param)) {
            return new DeepParamFunction(param);
        }
        return null;
    }
    
    

    Here, the DeepParamFunction is a Function that takes a single ContainerRequest argument. It will do the parsing of the query parameter and then return the Map.

    After you've implemented the required class, you need to register it with Jersey. Again, depending on which version of Jersey you are using, the registration will be different (but similar). In both cases you need to register an AbstractBinder with the ResourceConfig

    register(new AbstractBinder() {
        @Override
        protected void configure() {
            bind(DeepObjectParamProvider.class)
                    // 2.0-2.25.1 you will use ValueFactoryProvider.class
                    .to(ValueParamProvider.class)
                    .in(Singleton.class);
        }
    });
    

    For both versions of Jersey, you will use the same AbstractBinder class, but the imports will be different. In 2.0-2.25.1, you will look for hk2 in the package name. In 2.26, you will look for jersey in the package name. The other difference is in the to() method. In 2.0-2.25.1 you will use ValueFactoryProvider and 2.26+, you will use ValueParamProvider.

    Here is an example implementation of the ValueParamProvider (for Jersey 2.26+). The implementation for ValueFactoryProvider will be very similar

    public class DeepObjectParamProvider implements ValueParamProvider {
    
        @Override
        public Function<ContainerRequest, ?> getValueProvider(Parameter param) {
    
            if (param.isAnnotationPresent(DeepObjectParam.class) && isStringStringMap(param)) {
                return new DeepParamFunction(param);
            }
            return null;
        }
    
        private static boolean isStringStringMap(Parameter param) {
            if (!param.getRawType().equals(Map.class)) {
                return false;
            }
            ParameterizedType type = (ParameterizedType) param.getType();
            Type[] genericTypes = type.getActualTypeArguments();
            return genericTypes[0].equals(String.class) && genericTypes[1].equals(String.class);
        }
    
        @Override
        public PriorityType getPriority() {
            // Use HIGH otherwise it might not be used
            return Priority.HIGH;
        }
    
        private static class DeepParamFunction implements Function<ContainerRequest, Map<String, String>> {
    
            private final Parameter param;
    
            private DeepParamFunction(Parameter param) {
                this.param = param;
            }
    
            @Override
            public Map<String, String> apply(ContainerRequest request) {
                Map<String, String> map = new HashMap<>();
    
                DeepObjectParam anno = param.getAnnotation(DeepObjectParam.class);
                String paramName = anno.value();
                MultivaluedMap<String, String> params = request.getUriInfo().getQueryParameters();
                params.forEach((key, list) -> {
                    // do parsing of params
                });
    
                return map;
            }
        }
    }
    

    For a complete running (2.26+) example, take a look at this post. For versions earlier than 2.26, I've refactored that example and posted it to this Gist.

    P.S.

    While implementing the provider and debugging, don't be surprised when the method is called more than once. What happens is that on startup, Jersey will validate all the resource methods and make sure that all the parameters are able to be processed. How Jersey does this is by passing each Parameter to all the providers until one is reached that doesn't return null. So the more resource methods you have, the more times your provider will be called. See this post for more elaboration.