eclipse-ee4j / eclipse-ee4j/jersey
FormDataParam is not seen by jersey when not the last parameter annotation
- Dominant language
- Java
- Stars
- 730
- Forks
- 382
- PR merge metrics
- No merged PRs in 30d
Description
Hi,
Jersey reports the following error (edited for readbility)
```
**error log**[FATAL] No injection source found for a parameter of type
public javax.ws.rs.core.Response com.product.resource.CustomerResourceImpl.uploadFile(java.io.InputStream) at index 0.;
source='ResourceMethod{
httpMethod=POST,
consumedTypes=[multipart/form-data],
producedTypes=[],
suspended=false,
suspendTimeout=0,
suspendTimeoutUnit=MILLISECONDS,
invocable=Invocable{
handler=ClassBasedMethodHandler{
handlerClass=class com.product.resource.CustomerResourceImpl,
handlerConstructors=[org.glassfish.jersey.server.model.HandlerConstructor@99ffd68]},
definitionMethod=public abstract javax.ws.rs.core.Response com.product.api.CustomerResource.uploadFile(java.io.InputStream),
parameters=[Parameter [type=class java.io.InputStream, source=..., defaultValue=null]],
responseType=class javax.ws.rs.core.Response
},
nameBindings=[]
}'
```
The code looks like
```
**resource definition**@Path("customers")
@Api("customers")
public interface CustomerResource {
@POST
@ApiOperation("...")
@Consumes(MediaType.MULTIPART_FORM_DATA)
Response uploadFile(
@FormDataParam("customers-file")
@ApiParam(value = "the CSV file for customer's updates", required = true)
InputStream fileAsStream
);
}
```
Note the order of the annotations, first @FormDataParam then the swagger annotation @ApiParam.
```
**resource implementation**@Component
public class CustomerResourceImpl implements CustomerResource {
@Override
public Response uploadFile(InputStream fileAsStream) {
// ...
}
}
```
Also the form data extension is correctly registered in the application.
The problem here is that jersey picks the **last annotation*** on the parameter as the source provider marker. This problem appeared with the MultipartFeature however the issue can show up for any other extension using the same mechanism. There was a similar issue #1059 but it relates to Jersey 1.x and it is fixed.
The issue is hard to diagnose form a user perspective because the log doesn't mention the annotation type as the source provider or don't mention the last annotation is used when the source is unknown.
* * *
At the very least the error message should be modified regarding that :
When no source provider are found for a resource method the error message should be tweaked.
1\. First the message template (regarding the annotation order)
```
**org.glassfish.jersey.server.model.ResourceMethodValidator#checkValueProviders** private void checkValueProviders(ResourceMethod method) {
final List> valueProviders = method.getInvocable().getValueProviders(locator);
if (valueProviders.contains(null)) {
int index = valueProviders.indexOf(null);
Errors.fatal(method, LocalizationMessages.ERROR_PARAMETER_MISSING_VALUE_PROVIDER(index, method.getInvocable()
.getHandlingMethod()));
}
}
```
ERROR_PARAMETER_MISSING_VALUE_PROVIDER refers to the following localisation key error.parameter.missing.value.provider.
2\. Regarding how the invocation is printed. The previous code pass the method method, however the annotation type is not printed as shown in the log excerpt.
```
**class Parameter** @Override
public String toString() {
return String.format("Parameter [type=%s, source=%s, defaultValue=%s]",
getRawType(), getSourceName(), getDefaultValue());
}
```
I propose to add the sourceAnnotation to get the actual annotation type.
* * *
Fixing the root of the problem may be more problematic, as the parameter factory is not extensible and is non deterministic when the source is _unknown_ as the code only takes the last annotation (not documented here [https://jersey.java.net/documentation/latest/media.html#multipart](https://jersey.java.net/documentation/latest/media.html#multipart))
```
**Parameter.create**public class Parameter implements AnnotatedElement {
// ...
public static Parameter create(
Class concreteClass,
Class declaringClass,
boolean encodeByDefault,
Class rawType,
Type type,
Annotation[] annotations) {
// ...
/**
* Create a parameter from the list of annotations. Unknown annotated
* parameters are also supported, and in such a cases the last
* unrecognized annotation is taken to be that associated with the
* parameter.
*/
for (Annotation annotation : annotations) {
if (ANNOTATION_HELPER_MAP.containsKey(annotation.annotationType())) {
ParamAnnotationHelper helper = ANNOTATION_HELPER_MAP.get(annotation.annotationType());
paramAnnotation = annotation;
paramSource = helper.getSource();
paramName = helper.getValueOf(annotation);
} else if (Encoded.class == annotation.annotationType()) {
paramEncoded = true;
} else if (DefaultValue.class == annotation.annotationType()) {
paramDefault = ((DefaultValue) annotation).value();
} else {
// Take latest unknown annotation, but don't override known annotation
if ((paramAnnotation == null) || (paramSource == Source.UNKNOWN)) {
paramAnnotation = annotation;
paramSource = Source.UNKNOWN;
paramName = getValue(annotation);
}
}
}
// ...
```
Hence depending the order of the annotation in the source, the provider may not be found. What happens with the snippet code is that this codes uses the @ApiParam annotation, that of course won't match when the provider looks for a known annotation :
```
**org.glassfish.jersey.media.multipart.internal.FormDataParamValueFactoryProvider#createValueFactory** @Override
protected Factory createValueFactory(final Parameter parameter) {
final Class rawType = parameter.getRawType();
if (Parameter.Source.ENTITY == parameter.getSource()) {
// ...
} else if (parameter.getSourceAnnotation().annotationType() == FormDataParam.class) {
// ...
}
return null;
}
```
One idea when the parameter source is _unknown_ would be to pass all annotations as possible source candidates. Then value factory provider will look at all annotations and return the value factory.
Of course if there's multiple value factory for a parameter then there's a configuration or definition problem.
#### Environment
java version "1.8.0_112"
Java(TM) SE Runtime Environment (build 1.8.0_112-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.112-b16, mixed mode)
#### Affected Versions
[2.24]
Contributor guide
Research direction
Start with Parameter.create and FormDataParamValueFactoryProvider#createValueFactory to understand how annotation order selects the source provider. Then review ResourceMethodValidator#checkValueProviders, Parameter#toString, and the error.parameter.missing.value.provider localization key. Done requires an agreed, validated fix for the annotation handling or diagnostic message.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100