Sherzod Makhmudov
Sherzod Makhmudov

Reputation: 11

Jersey 2 multipart/form-data issue. InputStream is empty (available=0)

I am facing issue with jersey 2 file upload. Input stream is coming empty to server side. Using jersey 2.21, jackson 2.5.4, spring 4.1.6.RELEASE (for DI only) & spring security 4.0.2.RELEASE for security. Using JDK 1.8.0_25 and Tomcat 8.0.26.

Code:

@POST
@Path("/upload")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.MULTIPART_FORM_DATA)
public SimpleResult categoryImageUpload(
        @FormDataParam("file") InputStream file,
        @FormDataParam("file") FormDataBodyPart bodyPart) {
        return SimpleResult.success("File Uploaded successfully!!!");
}

File Details is coming in FormDataBodyPart, but InputStream is coming empty(available=0).

Jersey configuration:

@ApplicationPath("api-business")
public class BusinessApplicationConfig extends ResourceConfig {
    public BusinessApplicationConfig() {
        register(RequestContextFilter.class);
        register(MultiPartFeature.class);
        packages("com.smx.biz.api");
    }
}

dependencies in pom.xml:

    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>2.5.4</version>
    </dependency>

    <!--Jersey-->
    <dependency>
        <groupId>org.glassfish.jersey.core</groupId>
        <artifactId>jersey-server</artifactId>
        <version>2.21</version>
    </dependency>

    <dependency>
        <groupId>org.glassfish.jersey.containers</groupId>
        <artifactId>jersey-container-servlet</artifactId>
        <version>2.21</version>
    </dependency>

    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-json-jackson</artifactId>
        <version>2.21</version>
    </dependency>

    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-multipart</artifactId>
        <version>2.21</version>
    </dependency>

    <!-- Jersey + Spring -->
    <dependency>
        <groupId>org.glassfish.jersey.ext</groupId>
        <artifactId>jersey-spring3</artifactId>
        <version>2.21</version>
    </dependency>

Could somebody help with this issue? Am I missing something???

PS: Spring REST file upload code is working well & InputStream is coming. But Jersey code is not working. Using same client side code to test apis.

Working Spring REST api code:

@ResponseStatus(HttpStatus.OK)
@RequestMapping(value = "/business/upload", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
ImageItem categoryPhotoUpload(@RequestBody MultipartFile file) {
    return uploadService.uploadFile(file);
}

I want to use Jersey for apis & I don't want to use Spring REST.

Could somebody help with this issue?

Upvotes: 1

Views: 5071

Answers (3)

thomasso
thomasso

Reputation: 290

Make sure that the string inside the annotation

@FormDataParam("theSameStringUsedInAnnotation") InputStream file

exactly matches the name of the resource that you are posting.

In my case I was using ExtJs fileupload and when you define there a fileupload like:

xtype: 'filefield', name: 'theSameStringUsedInAnnotation',

you have to define the same string

Upvotes: 1

Nav_D
Nav_D

Reputation: 1

In My case I replaced @FormDataParam("file") InputStream file with @FormDataParam("file") File file then it started working fine.

Upvotes: -1

Fire For Effect
Fire For Effect

Reputation: 33

I found that when you use the @FormDataParam("file") InputStream file method, under the hood the parameter never gets processed because jersey is actually looking for a File. What happens (at least from what i have read so far) is that when the request comes in jersey does some mime checking using the mimepull library and in turn saves the incoming file as a temporary file. The issue is that if your parameter type is InputStream, jersey does not handle it because there is no ValueFactory registered for InputStream. So in order for this to work you have to do the following.

Inside FormDataParamValueFactoryProvider

Add the following implementation:

private final class InputStreamFactory extends ValueFactory<InputStream> {

    private final String name;

    public InputStreamFactory(final String name) {
        this.name = name;
    }

    @Override
    public InputStream provide() {
        LOG.info("Processing paramaeter [" + name + "]");
        final FormDataBodyPart part = getEntity().getField(name);
        final BodyPartEntity entity = part != null ? part.getEntityAs(BodyPartEntity.class) : null;

        if (entity != null) {
            try {
                // Create a temporary file.
                final File file = Utils.createTempFile();

                // Move the part (represented either via stream or file) to the specific temporary file.
                entity.moveTo(file);

                //Retreive file via a FileInputStream
                return new FileInputStream(file);
            } catch (final Exception ex) {
                // Unable to create a temporary file or move the file.
                LOG.warn("Error while processing InputStream. " + ex);
            }
        }

        return null;
    }

}

This will allow jersey to detect the InputStream.

You also have to change the createValueFactory method to reflect the new ValueFactoryProvider.

@Override
protected Factory<?> createValueFactory(final Parameter parameter) {
    final Class<?> rawType = parameter.getRawType();
    if (Parameter.Source.ENTITY == parameter.getSource()) {
        if (FormDataMultiPart.class.isAssignableFrom(rawType)) {
            return new FormDataMultiPartFactory();
        } else {
            return null;
        }
    } else if (parameter.getSourceAnnotation().annotationType() == FormDataParam.class) {
        final String paramName = parameter.getSourceName();
        if (paramName == null || paramName.isEmpty()) {
            // Invalid query parameter name
            return null;
        }

        if (Collection.class == rawType || List.class == rawType) {
            final Class clazz = ReflectionHelper.getGenericTypeArgumentClasses(parameter.getType()).get(0);

            if (FormDataBodyPart.class == clazz) {
                // Return a collection of form data body part.
                return new ListFormDataBodyPartValueFactory(paramName);
            } else if (FormDataContentDisposition.class == clazz) {
                // Return a collection of form data content disposition.
                return new ListFormDataContentDispositionFactory(paramName);
            } else {
                // Return a collection of specific type.
                return new FormDataParamValueFactory(parameter, get(parameter));
            }
        } else if (FormDataBodyPart.class == rawType) {
            return new FormDataBodyPartFactory(paramName);
        } else if (FormDataContentDisposition.class == rawType) {
            return new FormDataContentDispositionFactory(paramName);
        } else if (File.class == rawType) {
            return new FileFactory(paramName);
        } else if (InputStream.class == rawType) {
            return new InputStreamFactory(paramName);
        } else {
            return new FormDataParamValueFactory(parameter, get(parameter));
        }
    }

    return null;
}

Now... here's where it becomes a pain... if you dont pull the module source from github and and compile with the changes.... you have to basically recreate the following classes in order to reference the new class through the reference chain.

Classes:

  • FormDataParamValueFactoryProvider
  • FormDataParamInjectionFeature (References: FormDataParamValueFactoryProvider)
  • MultiPartFeature (References: FormDataParamInjectionFeature)

Once this is done, you can then use @FormDataParam("file") InputStream and it will work as expected.

Upvotes: 1

Related Questions