Search code examples
javajerseyjax-rs

Get Jersey to work with Optional parameters


I'm trying to get Jersey to work with Optional parameters. I have a very simple web service:

    @Path("helloworld")
    public static class HelloWorldResource {
        public static final String CLICHED_MESSAGE = "Hello World!";

        @GET
        @Produces("text/plain")
        public String getHello(@QueryParam("maybe") Optional<String> maybe) {
            return CLICHED_MESSAGE;
        }
    }

And a simple harness:

    public static void main(String[] arg) throws IOException {
        ResourceConfig config = new ResourceConfig(HelloWorldResource.class);

        String baseUri = "http://localhost:8080/api/";
        HttpServer server = GrizzlyHttpServerFactory
                .createHttpServer(URI.create(baseUri), config, false);
        server.start();
    }

However I get the following error:

Exception in thread "main" org.glassfish.jersey.server.model.ModelValidationException: Validation of the application resource model has failed during application initialization.
[[FATAL] No injection source found for a parameter of type public java.lang.String com.mercuria.odyssey.server.GrizllyOptional$HelloWorldResource.getHello(java.util.Optional) at index 0.; source='ResourceMethod{httpMethod=GET, consumedTypes=[], producedTypes=[text/plain], suspended=false, suspendTimeout=0, suspendTimeoutUnit=MILLISECONDS, invocable=Invocable{handler=ClassBasedMethodHandler{handlerClass=class com.mercuria.odyssey.server.GrizllyOptional$HelloWorldResource, handlerConstructors=[org.glassfish.jersey.server.model.HandlerConstructor@a3d9978]}, definitionMethod=public java.lang.String com.mercuria.odyssey.server.GrizllyOptional$HelloWorldResource.getHello(java.util.Optional), parameters=[Parameter [type=class java.util.Optional, source=maybe, defaultValue=null]], responseType=class java.lang.String}, nameBindings=[]}']
    at org.glassfish.jersey.server.ApplicationHandler.initialize(ApplicationHandler.java:555)
    at org.glassfish.jersey.server.ApplicationHandler.access$500(ApplicationHandler.java:184)
    at org.glassfish.jersey.server.ApplicationHandler$3.call(ApplicationHandler.java:350)
    at org.glassfish.jersey.server.ApplicationHandler$3.call(ApplicationHandler.java:347)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
    at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
    at org.glassfish.jersey.internal.Errors.processWithException(Errors.java:255)
    at org.glassfish.jersey.server.ApplicationHandler.<init>(ApplicationHandler.java:347)
    at org.glassfish.jersey.server.ApplicationHandler.<init>(ApplicationHandler.java:311)
    at org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpContainer.<init>(GrizzlyHttpContainer.java:337)
    at org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory.createHttpServer(GrizzlyHttpServerFactory.java:140)
    at com.mercuria.odyssey.server.GrizllyOptional.main(GrizllyOptional.java:33)

I presume I need to do something about so that Jersey knows how to handle Optional parameters, but I've no idea what!


Solution

  • So parameter types that are allowed as a @xxxParam, you need to meet one of these requirements:

    • Be a primitive type

    • Have a constructor that accepts a single String argument

    • Have a static method named valueOf() or fromString() that accepts a single String argument (see, for example, Integer.valueOf(String))

    • Have a registered implementation of ParamConverterProvider JAX-RS extension SPI that returns a ParamConverter instance capable of a "from string" conversion for the type.

    • Be List<T>, Set<T> or SortedSet<T>, where T satisfies 2, 3 or 4 above. The resulting collection is read-only.

    So in this case of Optional, going down the list; it's not a primitive; it doesn't have a String constructor; it doesn't have a static valueOf() or fromString()

    So basically, the only option left is to implement a ParamConverter/ParamConverterProvider pair for it. Dropwizard (a framework built on top of Jersey) has a good implementation for it. I will post it here in case the link ever goes dead

    import org.glassfish.hk2.api.ServiceLocator;
    import org.glassfish.jersey.internal.inject.Providers;
    import org.glassfish.jersey.internal.util.ReflectionHelper;
    import org.glassfish.jersey.internal.util.collection.ClassTypePair;
    
    import javax.inject.Inject;
    import javax.inject.Singleton;
    import javax.ws.rs.ext.ParamConverter;
    import javax.ws.rs.ext.ParamConverterProvider;
    import java.lang.annotation.Annotation;
    import java.lang.reflect.Type;
    import java.util.List;
    import java.util.Optional;
    import java.util.Set;
    
    @Singleton
    public class OptionalParamConverterProvider implements ParamConverterProvider {
        private final ServiceLocator locator;
    
        @Inject
        public OptionalParamConverterProvider(final ServiceLocator locator) {
            this.locator = locator;
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType, final Annotation[] annotations) {
            if (Optional.class.equals(rawType)) {
                final List<ClassTypePair> ctps = ReflectionHelper.getTypeArgumentAndClass(genericType);
                final ClassTypePair ctp = (ctps.size() == 1) ? ctps.get(0) : null;
    
                if (ctp == null || ctp.rawClass() == String.class) {
                    return new ParamConverter<T>() {
                        @Override
                        public T fromString(final String value) {
                            return rawType.cast(Optional.ofNullable(value));
                        }
    
                        @Override
                        public String toString(final T value) {
                            return value.toString();
                        }
                    };
                }
    
                final Set<ParamConverterProvider> converterProviders = Providers.getProviders(locator, ParamConverterProvider.class);
                for (ParamConverterProvider provider : converterProviders) {
                    final ParamConverter<?> converter = provider.getConverter(ctp.rawClass(), ctp.type(), annotations);
                    if (converter != null) {
                        return new ParamConverter<T>() {
                            @Override
                            public T fromString(final String value) {
                                return rawType.cast(Optional.ofNullable(value).map(s -> converter.fromString(value)));
                            }
    
                            @Override
                            public String toString(final T value) {
                                return value.toString();
                            }
                        };
                    }
                }
            }
    
            return null;
        }
    }
    

    Note, if you are using a Jersey version 2.26+, instead of injecting ServiceLocator you will use InjectionManager instead. Also the argument that accepts a locator, you will need to change the the manager.

    With this class, you just need to register it with your Jersey application.