@FormParam and Optional<T>

647 views
Skip to first unread message

Olve Hansen

unread,
Jul 31, 2014, 8:18:21 AM7/31/14
to dropwiz...@googlegroups.com
Just noticed that Optional<T> doesn't work with @FormParam.


Any suggestions for simple solutions? I'll also try to make time for a PR in the not too distant future...

Regards, 
Olve

Kris Massey

unread,
Aug 5, 2014, 11:50:36 AM8/5/14
to dropwiz...@googlegroups.com
Hi Olve,

I've worked around this problem as below. All of the code is within the Dropwizard/Jersey source code, I just lifted the bits I needed.

Essentially copy the OptionalQueryParamInjectableProvider.class and create on for the @FormParam annotaiton 


import com.google.common.base.Optional;
import com.sun.jersey.api.ParamException;
import com.sun.jersey.api.core.HttpContext;
import com.sun.jersey.api.core.HttpRequestContext;
import com.sun.jersey.api.model.Parameter;
import com.sun.jersey.api.representation.Form;
import com.sun.jersey.core.header.MediaTypes;
import com.sun.jersey.core.spi.component.ComponentContext;
import com.sun.jersey.core.spi.component.ComponentScope;
import com.sun.jersey.core.spi.component.ProviderServices;
import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;
import com.sun.jersey.server.impl.model.method.dispatch.FormDispatchProvider;
import com.sun.jersey.server.impl.model.parameter.multivalued.ExtractorContainerException;
import com.sun.jersey.server.impl.model.parameter.multivalued.MultivaluedParameterExtractor;
import com.sun.jersey.server.impl.model.parameter.multivalued.MultivaluedParameterExtractorFactory;
import com.sun.jersey.server.impl.model.parameter.multivalued.StringReaderFactory;
import com.sun.jersey.spi.inject.Injectable;
import com.sun.jersey.spi.inject.InjectableProvider;

import javax.ws.rs.FormParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.Provider;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
 * User: kris massey
 */
@Provider
public class OptionalFormParamInjectableProvider implements InjectableProvider<FormParam, Parameter> {
    private static class FormParamInjectable extends AbstractHttpContextInjectable<Object> {
        private final MultivaluedParameterExtractor extractor;
        private final boolean decode;

        private FormParamInjectable(MultivaluedParameterExtractor extractor,
                                     boolean decode) {
            this.extractor = extractor;
            this.decode = decode;
        }

        @Override
        public Object getValue(HttpContext context) {

            Form form = getCachedForm(context);

            if (form == null) {
                form = getForm(context);
                cacheForm(context, form);
            }

            try {
                return  Optional.fromNullable(extractor.extract(form));
            } catch (ExtractorContainerException e) {
                throw new ParamException.FormParamException(e.getCause(),
                        extractor.getName(), extractor.getDefaultStringValue());
            }
        }

        private void cacheForm(final HttpContext context, final Form form) {
            context.getProperties().put(FormDispatchProvider.FORM_PROPERTY, form);
        }

        private Form getCachedForm(final HttpContext context) {
            return (Form) context.getProperties().get(FormDispatchProvider.FORM_PROPERTY);
        }

        private HttpRequestContext ensureValidRequest(final HttpRequestContext r) throws IllegalStateException {
            if (r.getMethod().equals("GET")) {
                throw new IllegalStateException(
                        "The @FormParam is utilized when the request method is GET");
            }

            if (!MediaTypes.typeEquals(MediaType.APPLICATION_FORM_URLENCODED_TYPE, r.getMediaType())) {
                throw new IllegalStateException(
                        "The @FormParam is utilized when the content type of the request entity "
                                + "is not application/x-www-form-urlencoded");
            }
            return r;
        }

        private Form getForm(HttpContext context) {
            final HttpRequestContext r = ensureValidRequest(context.getRequest());
            return r.getFormParameters();
        }

    }

    private static class OptionalExtractor implements MultivaluedParameterExtractor {
        private final MultivaluedParameterExtractor extractor;

        private OptionalExtractor(MultivaluedParameterExtractor extractor) {
            this.extractor = extractor;
        }

        @Override
        public String getName() {
            return extractor.getName();
        }

        @Override
        public String getDefaultStringValue() {
            return extractor.getDefaultStringValue();
        }

        @Override
        public Object extract(MultivaluedMap<String, String> parameters) {
            return Optional.fromNullable(extractor.extract(parameters));
        }
    }

    private final ProviderServices services;
    private MultivaluedParameterExtractorFactory factory;

    public OptionalFormParamInjectableProvider(@Context ProviderServices services) {
        this.services = services;
    }

    @Override
    public ComponentScope getScope() {
        return ComponentScope.PerRequest;
    }

    @Override
    public Injectable<?> getInjectable(ComponentContext ic,
                                       FormParam a,
                                       Parameter c) {
        if (isExtractable(c)) {
            final OptionalExtractor extractor = new OptionalExtractor(getFactory().get(unpack(c)));
            return new FormParamInjectable(extractor, !c.isEncoded());
        }
        return null;
    }

    private boolean isExtractable(Parameter param) {
        return (param.getSourceName() != null) && !param.getSourceName().isEmpty() &&
                param.getParameterClass().isAssignableFrom(Optional.class) &&
                (param.getParameterType() instanceof ParameterizedType);
    }

    private Parameter unpack(Parameter param) {
        final Type typeParameter = ((ParameterizedType) param.getParameterType()).getActualTypeArguments()[0];
        return new Parameter(param.getAnnotations(),
                param.getAnnotation(),
                param.getSource(),
                param.getSourceName(),
                typeParameter,
                (Class<?>) typeParameter,
                param.isEncoded(),
                param.getDefaultValue());
    }

    private MultivaluedParameterExtractorFactory getFactory() {
        if (factory == null) {
            final StringReaderFactory stringReaderFactory = new StringReaderFactory();
            stringReaderFactory.init(services);

            this.factory = new MultivaluedParameterExtractorFactory(stringReaderFactory);
        }

        return factory;
    }
}


 
Then you need to register it with the Jersey Environment in you Application configuration as below:

environment.jersey().getResourceConfig().getClasses().add(OptionalFormParamInjectableProvider.class);

Does the trick for my needs, hopefully someone might find it helpful!

Olve Hansen

unread,
Aug 6, 2014, 7:32:05 AM8/6/14
to dropwiz...@googlegroups.com
I'll test that - looks good. Basically a copy of the QueryParam in dw? This could go into a PR more or less unchanged I guess..

Thanks!

Olve

Kris Massey

unread,
Aug 6, 2014, 10:04:11 AM8/6/14
to dropwiz...@googlegroups.com
Almost a copy. It's partly from the QueryParam and partly from somewhere else (can't remember exactly where! More than likely the FormParamInjectable)

Before creating a PR I'd want to do some refactoring to split the common code into external classes. Hopefully I'll get a chance to do it later this week.

Sent from my iPhone
--
You received this message because you are subscribed to a topic in the Google Groups "dropwizard-user" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/dropwizard-user/wDl9BOm_evQ/unsubscribe.
To unsubscribe from this group and all its topics, send an email to dropwizard-us...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Kris Massey

unread,
Aug 8, 2014, 9:02:59 AM8/8/14
to dropwiz...@googlegroups.com
Reply all
Reply to author
Forward
0 new messages