Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there any way to know which parameter is being parsed in a Jersey @__Param fromString handler?

The API I'm working with has decided to accept UUIDs as Base32 encoded strings, instead of the standard hexadecimal, dash separated format that UUID.fromString() expects. This means that I can't simply write @QueryParam UUID myUuid as a method parameter, as the conversion would fail.

I'm working around this by writing a custom object with a different fromString converter to be used with the Jersey @QueryString and @FormParam annotations. I would like to be able to access the context of the conversion in the fromString method so that I can provide better error messages. Right now, all I can do is the following:

public static Base32UUID fromString(String uuidString) {
    final UUID uuid = UUIDUtils.fromBase32(uuidString, false);
    if (null == uuid) {
        throw new InvalidParametersException(ImmutableList.of("Invalid uuid: " + uuidString));
    }
    return new Base32UUID(uuid);
}

I would like to be able to expose which parameter had the invalid UUID, so my logged exceptions and returned user errors are crystal clear. Ideally, my conversion method would have an extra parameter for details, like so:

public static Base32UUID fromString(
    String uuidString,
    String parameterName // New parameter?
) {
    final UUID uuid = UUIDUtils.fromBase32(uuidString, false);
    if (null == uuid) {
        throw new InvalidParametersException(ImmutableList.of("Invalid uuid: " + uuidString
            + " for parameter " + parameterName));
    }
    return new Base32UUID(uuid);
}

But this would break the by-convention means that Jersey finds a parsing method :

  1. Have a static method named valueOf or fromString that accepts a single String argument (see, for example, Integer.valueOf(String) and java.util.UUID.fromString(String));

I've also looked at the ParamConverterProvider that can also be registered to provide conversion, but it doesn't seem to add enough context either. The closest it provides is the an array of Annotations, but from what I can tell of the annotation, you can't backtrack from there to determine which variable or method the annotation is on. I've found this and this examples, but they don't make effective use of of the Annotations[] parameter or expose any conversion context that I can see.

Is there any way to get this information? Or do I need to fallback to an explicit conversion call in my endpoint method?

If it makes a difference, I'm using Dropwizard 0.8.0, which is using Jersey 2.16 and Jetty 9.2.9.v20150224.

like image 793
Patrick M Avatar asked Jul 20 '15 17:07

Patrick M


2 Answers

So this can be accomplished with a ParamConverter/ParamConverterProvider. We just need to inject a ResourceInfo. From there we can obtain the resource Method, and just do some reflection. Below is an example implementation that I've tested and works for the most part.

import java.lang.reflect.Type;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.annotation.Annotation;

import java.util.Set;
import java.util.HashSet;
import java.util.Collections;

import javax.ws.rs.FormParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.InternalServerErrorException;

@Provider
public class Base32UUIDParamConverter implements ParamConverterProvider {

    @Context
    private javax.inject.Provider<ResourceInfo> resourceInfo;

    private static final Set<Class<? extends Annotation>> ANNOTATIONS;

    static {
        Set<Class<? extends Annotation>> annots = new HashSet<>();
        annots.add(QueryParam.class);
        annots.add(FormParam.class);
        ANNOTATIONS = Collections.<Class<? extends Annotation>>unmodifiableSet(annots);
    }

    @Override
    public <T> ParamConverter<T> getConverter(Class<T> type, 
                                              Type type1,
                                              Annotation[] annots) {

        // Check if it is @FormParam or @QueryParam
        for (Annotation annotation : annots) {
            if (!ANNOTATIONS.contains(annotation.annotationType())) {
                return null;
            }
        }

        if (Base32UUID.class == type) {
            return new ParamConverter<T>() {

                @Override
                public T fromString(String value) {
                    try {
                        Method method = resourceInfo.get().getResourceMethod();

                        Parameter[] parameters = method.getParameters();
                        Parameter actualParam = null;

                        // Find the actual matching parameter from the method.
                        for (Parameter param : parameters) {
                            Annotation[] annotations = param.getAnnotations();
                            if (matchingAnnotationValues(annotations, annots)) {
                                actualParam = param;
                            }
                        }

                        // null warning, but assuming my logic is correct, 
                        // null shouldn't be possible. Maybe check anyway :-)
                        String paramName = actualParam.getName();
                        System.out.println("Param name : " + paramName);

                        Base32UUID uuid = new Base32UUID(value, paramName);
                        return type.cast(uuid);
                    } catch (Base32UUIDException ex) {
                        throw new BadRequestException(ex.getMessage());
                    } catch (Exception ex) {
                        throw new InternalServerErrorException(ex);
                    }
                }

                @Override
                public String toString(T t) {
                    return ((Base32UUID) t).value;
                }
            };
        }

        return null;
    }

    private boolean matchingAnnotationValues(Annotation[] annots1, 
                                             Annotation[] annots2) throws Exception {

        for (Class<? extends Annotation> annotType : ANNOTATIONS) {
            if (isMatch(annots1, annots2, annotType)) {
                return true;
            }
        }
        return false;
    }

    private <T extends Annotation> boolean isMatch(Annotation[] a1, 
                                                   Annotation[] a2, 
                                                   Class<T> aType) throws Exception {
        T p1 = getParamAnnotation(a1, aType);
        T p2 = getParamAnnotation(a2, aType);
        if (p1 != null && p2 != null) {
            String value1 = (String) p1.annotationType().getMethod("value").invoke(p1);
            String value2 = (String) p2.annotationType().getMethod("value").invoke(p2);
            if (value1.equals(value2)) {
                return true;
            }
        }

        return false;
    }

    private <T extends Annotation> T getParamAnnotation(Annotation[] annotations, 
                                                        Class<T> paramType) {
        T paramAnnotation = null;
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == paramType) {
                paramAnnotation = (T) annotation;
                break;
            }
        }
        return paramAnnotation;
    }
}

Some notes about the implementation

  • The most important part is how the ResourceInfo is injected. Since this needs to be accessed in a request scope context, I injected with javax.inject.Provider, which allows us to retrieve the object lazily. When we actually do get() it, it will be within a request scope.

    The thing to be cautious about is that it get() must be called inside the fromString method of the ParamConverter. The getConverter method of the ParamConverterProvider is called many times during application load, so we cannot try and call the get() during this time.

  • The java.lang.reflect.Parameter class I used is a Java 8 class, so in order to use this implementation, you need to be working on Java 8. If you are not using Java 8, this post may help in trying to get the parameter name some other way.

  • Related to the above point, the compiler argument -parameters needs to be applied when compiling, to be able to access the formal parameter name, as pointed out here. I just put it in the maven-cmpiler-plugin as pointed out in the link.

    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.5.1</version>
        <inherited>true</inherited>
        <configuration>
            <compilerArgument>-parameters</compilerArgument>
            <testCompilerArgument>-parameters</testCompilerArgument>
            <source>1.8</source>
            <target>1.8</target>
        </configuration>
    </plugin>
    

    If you don't do this, a call to Parameter.getName() will result in argX, X being the index of the parameter.

  • The implementation only allows for @FormParam and @QueryParam.

  • One important thing to note (that I learned the hard way), is that all exceptions that aren't handle in the ParamConverter (only applies to @QueryParam in this case), will lead to a 404 with no explanation of the problem. So you you need to make sure you handle your exception if you want a different behavior.


UPDATE

There is a bug in the above implementation:

// Check if it is @FormParam or @QueryParam
for (Annotation annotation : annots) {
    if (!ANNOTATIONS.contains(annotation.annotationType())) {
        return null;
    }
}

The above is called during model validation when getConverter is called for each parameter. The above code only works is there is only one annotation. If there is another annotation aside from @QueryParam or @FormParam, say @NotNull, it will fail. The rest of the code is fine. It does actually work under the assumption that there will be more than one annotation.

The fix to the above code, would be something like

boolean hasParamAnnotation = false;
for (Annotation annotation : annots) {
    if (ANNOTATIONS.contains(annotation.annotationType())) {
        hasParamAnnotation = true;
        break;
    }
}

if (!hasParamAnnotation) return null;
like image 152
Paul Samsotha Avatar answered Oct 27 '22 11:10

Paul Samsotha


Just to expand on peeskillets answer above, you might also consider solving the problem with dropwizard and jerseys built in bean validation. So, instead of throwing an exception from inside the factory method, you'd do this:

public class Base32UUID{
@NotNull
private final UUID uuid;
private Base32UUID(UUID uuid){ 
   this.uuid = uuid;
}
public static Base32UUID fromString(String uuidString) {
   final UUID uuid = UUIDUtils.fromBase32(uuidString, false);
   return new Base32UUID(uuid);
   }
}

In your reousource method, you make sure to annotate the parameter with @Valid, this should already be enough for dropwizard to return a descriptive error message, however if you want to customize the returned value, create and register an exceptionmapper, like so:

public class ValidationMapper implements ExceptionMapper<ConstraintViolationException>{

@Context
UriInfo uri;
@Context
private javax.inject.Provider<ResourceInfo> resourceInfo;
@Override
 public Response toResponse(ConstraintViolationException exception) {
   return Response.ok().build();
 }

}

And in your application class:

environment.jersey().register(ValidationMapper.class);

As you can see, all the required resources peeskillet injected in his paramconverter example, can be injected in the exception mapper. The bean validation approach just seems a little more appropriate to me + once set up, it can be used for validating pretty much any input anywhere in your application, not just null checks, but regular expression matches, emails, number ranges etc, and making sure the application always return an appropriate and appropriately formatted response. According to the dropwizard docs validation should work out of the box, but I had to add dropwizard-validation and jersey-bean-validation to my pom file to make it work:

<dependency>
    <groupId>io.dropwizard</groupId>
    <artifactId>dropwizard-validation</artifactId>
    <version>0.8.2</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.ext</groupId>
    <artifactId>jersey-bean-validation</artifactId>
    <version>2.19</version>
    <exclusions>
        <exclusion>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
        </exclusion>
    </exclusions>
</dependency>
like image 31
vruum Avatar answered Oct 27 '22 10:10

vruum