jersey icon indicating copy to clipboard operation
jersey copied to clipboard

Custom Java type for consuming request parameters

Open strangelookingnerd opened this issue 6 years ago • 8 comments

I recently upgraded from Jersey 2.25.1 to 2.28 and am facing problems with a @FormParam that is a list of custom java type.

REST endpoint looks like this:

@POST
@Path("/person")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Produces(MediaType.APPLICATION_JSON)
public Response getPerson(@FormParam("attributes") final List<Attr> attributes) {
	// 
}

The Attr class has "toString" and "valueOf" implemented - according to the documentation (https://jersey.github.io/documentation/latest/jaxrs-resources.html#d0e2271) the java type must have a static method named valueOf OR fromString. However I found that I required both for this to work after the upgrade. Before the upgrade it worked just fine.

I tried to implement: fromString only -> does not work valueOf only -> does not work fromString & valueOf -> works fine

Stacktrace:

org.glassfish.jersey.server.ParamException$FormParamException: HTTP 400 Bad Request
	at org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider$FormParamValueProvider.apply(FormParamValueParamProvider.java:141) ~[jersey-server-2.28.jar:?]
	at org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider$FormParamValueProvider.apply(FormParamValueParamProvider.java:83) ~[jersey-server-2.28.jar:?]
	at org.glassfish.jersey.server.spi.internal.ParamValueFactoryWithSource.apply(ParamValueFactoryWithSource.java:50) ~[jersey-server-2.28.jar:?]
	at org.glassfish.jersey.server.spi.internal.ParameterValueHelper.getParameterValues(ParameterValueHelper.java:64) ~[jersey-server-2.28.jar:?]
	...
Caused by: org.glassfish.jersey.internal.inject.ExtractorException: Error un-marshalling JAXB object of type: class com.test.Attr.
	at org.glassfish.jersey.jaxb.internal.JaxbStringReaderProvider$RootElementProvider$1.fromString(JaxbStringReaderProvider.java:172) ~[jersey-media-jaxb-2.28.jar:?]
	at org.glassfish.jersey.server.internal.inject.AbstractParamValueExtractor.convert(AbstractParamValueExtractor.java:116) ~[jersey-server-2.28.jar:?]
	at org.glassfish.jersey.server.internal.inject.AbstractParamValueExtractor.fromString(AbstractParamValueExtractor.java:107) ~[jersey-server-2.28.jar:?]
	at org.glassfish.jersey.server.internal.inject.CollectionExtractor.extract(CollectionExtractor.java:64) ~[jersey-server-2.28.jar:?]
	at org.glassfish.jersey.server.internal.inject.CollectionExtractor$ListValueOf.extract(CollectionExtractor.java:83) ~[jersey-server-2.28.jar:?]
	at org.glassfish.jersey.server.internal.inject.FormParamValueParamProvider$FormParamValueProvider.apply(FormParamValueParamProvider.java:138) ~[jersey-server-2.28.jar:?]
	...

strangelookingnerd avatar Jun 06 '19 12:06 strangelookingnerd

I was trying to implement the described situation using a list of Attr(s). And when Attr is implemented with a single String argument constructor, then with the only static method fromString(String input), then with the only static method valueOf(String input) it works just fine.

I mean the Attr class always contains one of those - constructor or fromString or valueOf. It never contains more than one of possible solutions.

But if the Attr class does not contain any of described possibilities, request fails.

Could you please provide a reproducer for your case?

senivam avatar Jun 07 '19 07:06 senivam

Thanks for your reply. My Attr class does not have a single String argument constructor but only a "valueOf". However this should still work according to the documentation. I will create a reproducer and report back.

strangelookingnerd avatar Jun 07 '19 14:06 strangelookingnerd

Problem is reproducable with jersey-formparam-test.zip run with mvn clean test I figured that it might have something to do with the @XmlRootElement annotation on my Attr class. Once this is removed or jersey-media-json-jackson.jar it added to the classpath it works just fine.

strangelookingnerd avatar Jun 08 '19 09:06 strangelookingnerd

I've tried your reproducer as is. As per your instructions running mvn clean test and it works (without any modifications).

I presume you've set it up to fail as is, because jersey-media-json-jackson is commented out as well as its feature registration in the App. And there is @XmlRootElement on top of the Attr. I'm using Oracle JDK 1.8.0_211.

Afterwards I was trying it inside Jersey 2.29-SNAPSHOT (current master) and it works as well. Inside IntelliJ idea IDE (using Oracle JDK 1.8.0_181 nightly) it runs again.

It works even with Oracle JDK 9.0.2 except it fails to find JAXB, but still test passes.

Do you use it inside some application server? Or how does it fail? Or is it supposed to modify the reproducer to make the test fail? May be some environment influences behavior of the reproducer?

senivam avatar Jun 20 '19 12:06 senivam

Sorry for the late response. I found that this issue comes up sporadically. Sometimes it just works and sometimes it fails. E.g. running it in debug mode works great, running it regulary fails - so my thought is that it might be something related to the classloader.

However the error is thrown if (by whatever reason) the request parameters are processed by JaxbStringReaderProvider$RootElementProvider as the unmarshaller is unable to handle the non-XML input. If the parameters are processed by ParamConverters$TypeValueOf everything is just fine (as the Attr class does have a proper valueOf method). Which of the two converters is chosen seems to be decided by which of them is "found" first (by the classloader I suppose?). Is there any way to force the class MultivaluedParameterExtractorFactory to always use the TypeValueOf converter? Or any other workaround?

strangelookingnerd avatar Jul 23 '19 14:07 strangelookingnerd

Did you manage to find a solution to this problem? I am facing the exact same issue randomly. Sometimes my query parameters are being processed by the JaxbStringReaderProvider in which case it fails. When the parameter is processed by ParamConverter$TypeValueOf it succeeds. I did a remote debug and see that the flow comes into AbstractParamValueExtractor's convert method where in some executions it uses JaxbStringReaderProvider , and if i restart next time it may use ParamConverter$TypeValueOf. Using jersey version 2.27 , tried downgrading to 2.26 also. Removing @XmlRootElement from my class is not an option because i support both XML and JSON.

K-ous avatar Nov 06 '19 10:11 K-ous

@K-ous My solution was to implement and register a custom ParamConverterProvider which will be used over the build in Converters and handles the processing to my needs.

@Singleton
@Custom
public class AttrParamConverterProvider implements ParamConverterProvider {
    @Override
    public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType,
            final Annotation[] annotations) {
        if (Attr.class.isAssignableFrom(rawType)) {
            return new ParamConverter<T>() {
                @Override
                public T fromString(final String value) {
                    if (value == null) {
                        throw new IllegalArgumentException(
                                LocalizationMessages.METHOD_PARAMETER_CANNOT_BE_NULL("value"));
                    }
                    return (T) Attr.valueOf(value);
                }

                @Override
                public String toString(final T value) {
                    if (value == null) {
                        throw new IllegalArgumentException(
                                LocalizationMessages.METHOD_PARAMETER_CANNOT_BE_NULL("value"));
                    }
                    return value.toString();
                }
            };
        }
        return null;
    }
}

strangelookingnerd avatar Nov 07 '19 09:11 strangelookingnerd

Thanks , yes this is what i ended up doing myself. However i did not put the @Singleton annotation. Any problems i might get if i dont add that ? I have many WebResources in my code across many different web applications and i think the getConverter is called by Jersey during provider scanning phase , in my case i see it being called once for each web application.

K-ous avatar Nov 18 '19 10:11 K-ous