Cómo leer varias entradas (archivo) con el mismo nombre desde un formulario multiparte con Jersey?


He desarrollado con éxito un servicio, en el que leo los archivos subidos en forma de varias partes en Jersey. Aquí hay una versión extremadamente simplificada de lo que he estado haciendo:

@POST
@Path("FileCollection")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(@FormDataParam("file") InputStream uploadedInputStream,
        @FormDataParam("file") FormDataContentDisposition fileDetail) throws IOException {
    //handle the file
}

Esto funciona bien, pero se me ha dado un nuevo requisito. Además del archivo que estoy cargando, tengo que manejar un número arbitrario de recursos. Supongamos que estos son archivos de imagen.

Pensé que simplemente le proporcionaría al cliente un formulario con una entrada para el archivo, una entrada para la primera imagen y un botón para permitir añadir más entradas al formulario (usando AJAX o simplemente JavaScript).

<form action="blahblahblah" method="post" enctype="multipart/form-data">
   <input type="file" name="file" />
   <input type="file" name="image" />
   <input type="button" value="add another image" />
   <input type="submit"  />
</form>

Para que el usuario pueda agregar el formulario con más entradas para imágenes, como esta:

<form action="blahblahblah" method="post" enctype="multipart/form-data">
   <input type="file" name="file" />
   <input type="file" name="image" />
   <input type="file" name="image" />
   <input type="file" name="image" />
   <input type="button" value="add another image" />
   <input type="submit"  />
</form>

Esperaba que fuera lo suficientemente simple para leer los campos con el mismo nombre que una colección. Lo he hecho con éxito con las entradas de texto en MVC. NET y pensé que no sería más difícil en Jersey. Resulta que estaba equivocado.

Al no haber encontrado tutoriales sobre el tema, comencé experimentar.

Para ver cómo hacerlo, simplifiqué el problema a simples entradas de texto.

<form action="blahblabhblah" method="post" enctype="multipart/form-data">
   <fieldset>
       <legend>Multiple inputs with the same name</legend>
       <input type="text" name="test" />
       <input type="text" name="test" />
       <input type="text" name="test" />
       <input type="text" name="test" />
       <input type="submit" value="Upload It" />
   </fieldset>
</form>

Obviamente, necesitaba tener algún tipo de colección como parámetro de mi método. Esto es lo que probé, agrupados por tipo de colección.

Array

Al principio, comprobé si Jersey era lo suficientemente inteligente para manejar una matriz simple: {[15]]}

@POST
@Path("FileCollection")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(@FormDataParam("test") String[] inputs) {
    //handle the request
}

Pero la matriz no se inyectó como se esperaba.

MultiValuedMap

Habiendo fracasado miserablemente, recordé que MultiValuedMap los objetos podían ser manejados fuera de la caja.

@POST
@Path("FileCollection")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadFile(MultiValuedMap<String, String> formData) {
    //handle the request
}

Pero tampoco funciona. Esta vez, tengo una excepción

SEVERE: A message body reader for Java class javax.ws.rs.core.MultivaluedMap, 
and Java type javax.ws.rs.core.MultivaluedMap<java.lang.String, java.lang.String>, 
and MIME media type multipart/form-data; 
boundary=----WebKitFormBoundaryxgxeXiWk62fcLALU was not found.

Me dijeron que esta excepción podría eliminarse al incluir la biblioteca mimepull, por lo que agregué la siguiente dependencia a mi pom:

    <dependency>
        <groupId>org.jvnet</groupId>
        <artifactId>mimepull</artifactId>
        <version>1.3</version>
    </dependency>

Desafortunadamente el problema persiste. Probablemente es cuestión de elegir el lector de cuerpo correcto y usar diferentes parámetros para el genérico. No estoy seguro de cómo hacer esto. Quiero consume entradas de archivo y texto, así como algunos otros (principalmente valores Long y clases de parámetros personalizados).

FormDataMultipart

Después de más investigación, encontré la clase FormDataMultiPart. Lo he usado con éxito para extraer los valores de cadena de mi formulario

@POST
@Path("upload2")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadMultipart(FormDataMultiPart multiPart){
    List<FormDataBodyPart> fields = multiPart.getFields("test");
    System.out.println("Name\tValue");
    for(FormDataBodyPart field : fields){
        System.out.println(field.getName() + "\t" + field.getValue());
        //handle the values
    }
    //prepare the response
}

El problema es, esta es una solución a la versión simplificada de mi problema. Aunque sé que cada parámetro inyectado por Jersey se crea analizando una cadena en algún momento (no es de extrañar, es HTTP después de todo) y tengo algo de experiencia escribiendo mis propias clases de parámetros, realmente no sé cómo convertir estos campos a InputStream o File instancias para su procesamiento posterior.

Por lo tanto, antes de sumergirme en el código fuente de Jersey para ver cómo se crean estos objetos, decidí preguntar aquí si hay una manera más fácil de leer un conjunto (de tamaño desconocido) de archivos. ¿Sabes cómo resolver este enigma?

Author: toniedzwiedz, 2012-08-26

3 answers

He encontrado la solución siguiendo el ejemplo con FormDataMultipart. Resulta que estaba muy cerca de la respuesta.

La clase FormDataBodyPart proporciona un método que permite a su usuario leer el valor como InputStream (o teóricamente, cualquier otra clase, para la que esté presente un lector de cuerpo de mensaje).

Aquí está la solución final:

Formulario

La forma permanece sin cambios. Tengo un par de campos con el mismo nombre, en los que puedo colocar archivos. Es posible utilizar ambos multiple entradas de formulario (las desea cuando carga muchos archivos desde un directorio) y numerosas entradas que comparten un nombre (forma flexible de cargar un número no especificado de archivos desde una ubicación diferente). También es posible agregar el formulario con más entradas usando JavaScript.

<form action="/files" method="post" enctype="multipart/form-data">
   <fieldset>
       <legend>Multiple inputs with the same name</legend>
       <input type="file" name="test" multiple="multiple"/>
       <input type="file" name="test" />
       <input type="file" name="test" />
   </fieldset>
   <input type="submit" value="Upload It" />
</form>

Uso de serviciosFormDataMultipart

Aquí hay un método simplificado que lee una colección de archivos de un formulario multiparte. Todas las entradas con el mismo se asignan a un List y su los valores se convierten a InputStream con la getValueAs método de FormDataBodyPart. Una vez que tenga estos archivos como instancias InputStream, es fácil hacer casi cualquier cosa con ellos.

@POST
@Path("files")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response uploadMultipart(FormDataMultiPart multiPart) throws IOException{        
    List<FormDataBodyPart> fields = multiPart.getFields("test");        
    for(FormDataBodyPart field : fields){
        handleInputStream(field.getValueAs(InputStream.class));
    }
    //prepare the response
}

private void handleInputStream(InputStream is){
    //read the stream any way you want
}
 32
Author: toniedzwiedz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-29 12:25:41
@Path("/upload/multiples")
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
public Response uploadImage(@FormDataParam("image") List<FormDataBodyPart> imageDatas){
   for( FormDataBodyPart imageData : imageDatas ){
       // Your actual code.
       imageData.getValueAs(InputStream.class);
    }
}
 6
Author: Amit Sharma,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-07 16:58:39

Si alguien está tratando de hacer cuadros de entrada genéricos type=text con el mismo atributo name como yo, podrá cambiarlos a type=hidden entradas y hacer que funcionen como @FormParam("inputName") List<String> nameList en su ruta.

Obviamente, cuando se cambia a entradas ocultas, el único punto es enviar los datos al servidor sin crear un elemento de interfaz de usuario para él, por lo que tendrá que cambiar a una interfaz de usuario de visualización alternativa (por ejemplo, usé un elemento de botón para facilitar la funcionalidad de clic para eliminar).

 3
Author: Blaskovicz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-02-13 23:28:37