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 :
- Have a static method named
valueOf
orfromString
that accepts a single String argument (see, for example,Integer.valueOf(String)
andjava.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.
So this can be accomplished with a
ParamConverter
/ParamConverterProvider
. We just need to inject aResourceInfo
. From there we can obtain the resourceMethod
, and just do some reflection. Below is an example implementation that I've tested and works for the most part.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 withjavax.inject.Provider
, which allows us to retrieve the object lazily. When we actually doget()
it, it will be within a request scope.The thing to be cautious about is that it
get()
must be called inside thefromString
method of theParamConverter
. ThegetConverter
method of theParamConverterProvider
is called many times during application load, so we cannot try and call theget()
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.If you don't do this, a call to
Parameter.getName()
will result inargX
,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:
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
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:
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:
And in your application 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: