JavaEE 6: Subida de imágenes al servidor (Servlet 3.0)

por Abr 5, 2020Desarrollo de Software0 Comentarios

Hace algún tiempo publiqué un proyecto de una tienda online en JavaEE a modo de proyecto didáctico. En esta ocasión me gustaría detallar una de las partes de dicho proyecto, haré una guía para subir imágenes al servidor con JavaEE 6.

Antes de la especificación 3.0 de los Servlets esta tarea se tenía que hacer usando las librerías de Apache Commons y se convertía en una tarea más larga y menos clara. Desde la nueva especificación no es necesario usar ninguna librería externa dado que nos da esta posibilidad de serie y de una manera muy sencilla.

Formulario para subir Imágenes con JavaEE 6

Lo primero que hay que hacer es una pequeña modificación en el formulario donde se quiera subir la imagen. Ahora los datos no se van a enviar en modo texto sino como un flujo binario. Teniendo esto en cuenta, el verbo HTTP usado será POST y nunca GET; de manera que la cabecera de nuestro formulario tendra una forma como la que os muestro.

<form name="pruebaImagenes" method="post" action="/admin/pruebaimg" enctype="multipart/form-data" >
</form>

Una vez que ya tenemos la estructura del formulario HTML, ya podemos añadir los campos que queramos al formulario al igual que lo hacemos normalmente. El campo para seleccionar la imagen sera de tipo file. Una vez hecho nos quedaría algo como lo que sigue.

<form name="pruebaImagenes" method="post" action="/admin/pruebaimg" enctype="multipart/form-data" >
            <b>Nombre</b> <br />
            <input type="text" name="name" /><br /><br />
            <b>Escoja una foto de producto</b> (Opcional)<br />
            <input type="file" name="foto" /><br /><br />
            <b>Detalles</b> <br />
            Puede usar etiquetas html (si usa etiquetas invalidas sera bloqueado)<br />
            <textarea name="detail"></textarea><br /><br />
            <input type="submit" name="send" value="Enviar datos" />
</form>

Procesando el formulario en el servidor

Una vez que ya tenemos el formulario HTML preparado, ya podemos pasar a la parte de servidor para procesar este formulario, es decir, crear el Servlet para procesar el formulario. Se procesa de una forma algo distinta a como estamos acostumbrados. Esto es debido, a que ahora no hemos recibido texto sino un flujo de datos y de ahí tendremos que obtener el texto normal y la fotografía. En el formulario he puesto tres tipos de campos para que veamos como procesar cada uno de ellos (texto, archivo, textarea).

En primer lugar el Servlet encargado de procesar el formulario deberá llevar la anotación @MultipartConfig. Y ahora puede haber varias preguntas clave.

  • ¿Cómo se si se ha enviado la imagen o no? (el usuario pudo no poner la imagen). Sencillo, si el Part (nombre que se le da a un campo en un formulario de este tipo) ocupa cero bits.
  • ¿Cómo se si el archivo que se ha seleccionado es una imagen? Hay un método para saber el tipo de archivo subido. Si contiene la cadena image sabremos que es una imagen (image/jpeg, etc…)
  • ¿Cómo guardamos la imagen en disco? Tendremos que obtener el InputStream del Part.
  • ¿Cómo obtenemos los campos que son de tipo texto, ahora no recibo texto? Ahora veremos que hay una manera sencilla de hacerlo. Os enseñaré los métodos de la clase estática Tools.

Ahora os muestro el código del Servlet completo, he puesto algunos comentarios en el mismo. Está basado en uno de los Servlets de tienda online. He quitado bastante código para mostrar prácticamente solo lo referente al tema que nos concierne ahora en concreto, obviamente entendiendo lo fundamental podemos modificarlo para que cumpla con la función que cada uno desee. El Servlet debe de estar dado de alta en el descriptor de despliegue y atendiendo peticiones en la ruta a la que se envía el formulario HTML.

import control.Tools;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author Juan Díez-Yanguas Barber
 */

//Anotación necesaria para procesar formulario
@MultipartConfig
public class AddServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
                response.sendError(404);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        if (validateForm(request)) {
                //Extraemos el texto del formulario
                String nombre = Tools.getcontentPartText(request.getPart("name"));
                String detalles = Tools.getContentTextArea(request.getPart("detail"));                              

                //Comprobamos si el formulario contiene o no la imagen (usamos el tamaño para comprobar si existe el campo o no)
                if (request.getPart("foto").getSize() > 0) {
                    //Nos aseguramos que el archivo es una imagen y que no excece de unos 8mb
                    if (request.getPart("foto").getContentType().contains("image") == false ||
                            request.getPart("foto").getSize() > 8388608) {
                        // TIPO DE ARCHIVO NO VALIDO
                        request.setAttribute("resultados", "Archivo no válido");
                        Tools.anadirMensaje(request, "Solo se admiten archivos de tipo imagen");
                        Tools.anadirMensaje(request, "El tamaño máximo de archivo son 8 Mb");
                        request.getRequestDispatcher("/admin/add.jsp").forward(request, response);
                        return;
                    }else{
                        //Obtenemos la ruta absoluta del sistema donde queremos guardar la imagen
                        String fileName = this.getServletContext().getRealPath("/images/products/imagen");
                        //Guardamos la imagen en disco con la ruta que hemos obtenido en el paso anterior
                        boolean ok = Tools.guardarImagenDeProdructoEnElSistemaDeFicheros(request.getPart("foto").getInputStream(), fileName);
                        if (ok == false){
                            request.setAttribute("resultados", "Fallo al guardar archivo");
                            Tools.anadirMensaje(request, "Ocurrio un error guardando la imagen");
                            request.getRequestDispatcher("/admin/add.jsp").forward(request, response);
                            return;
                        }
                    }
                }
                //TODO CORRECTO SE REDIRIGE A UNA PAGINA DE VISUALIZACIÓN
                Producto prod = new Producto(nombre, detalles);
                request.getSession().setAttribute("productoEnCursoAdd", prod);
                request.setAttribute("operation", "add");
                RequestDispatcher previsualizacion = request.getRequestDispatcher("/WEB-INF/admin/view.jsp");
                previsualizacion.forward(request, response);
        } else {
            request.setAttribute("resultados", "Formulario no válido");
            Tools.anadirMensaje(request, "El formulario recibido no tiene los campos esperados");
            request.getRequestDispatcher("/admin/addproduct.jsp").forward(request, response);
        }
    }

    //Método para validar que el formulario contiene los parámetros correctos
    private boolean validateForm(HttpServletRequest request) throws IOException, ServletException {
        if (request.getParts().size() >= 4 && request.getPart("name") != null &&
                request.getPart("detail") != null && request.getPart("send") != null) {
            return true;
        }
        return false;
    }

    @Override
    public String getServletInfo (){
        return "Servlet para añadir productos al catálogo";
    }
}

Resumiendo, lo primero que hace el servlet es comprobar que se reciben los parámetros correctos y no menos. Si la validación ha sido correcta se obtienen los campos de texto como veremos ahora. Posteriormente comprobamos si hay archivo o no, y en caso de que lo haya comprobamos que es una imagen y que el tamaño no exceda de 8 Mb, posteriormente se guarda en disco el archivo. Si queréis obtener más información sobre el archivo os animo a que investiguéis los métodos del objeto Part.

Si el Servlet recibe una petición GET se da una respuesta HTTP 404. A este Servlet se ha de acceder solo usando POST, no hay razón para acceder con GET. Así evitaremos posibles problemas y evitamos que se «hagan experimentos» con nuestra aplicación web.

Los atributos que se añaden a la petición son usados para dar el mensaje correspondiente en la jsp a la que se redirige, por lo tanto vosotros hacer caso solo al texto de los mensajes.

Ahora nos falta por terminar de explicar como obtener los campos de texto del formulario y como guardar la imagen en disco, en el Servelt aparece como una única llamada a un método que ahora paso a explicar

Para obtener el texto se puede obtener un InputStream del Part del formulario. El InputStream se puede leer de varias formas, en este caso se ha usado la clase Scanner de Java. La principal diferencia entre obtener el texto de un campo de texto y un textarea, es que en el primer caso solo hay una línea y en el segundo caso habrá que mantener los saltos de línea. Otro apunte importante sería especificar la codificación para evitar errores con caracteres como tildes, eñes o similares. Por último mencionar la manera de trabajar con el objeto Scaneer para que siempre se libere el recurso aunque haya un error.

public static String getcontentPartText(Part input) {
    Scanner sc = null;
    String content = null;
    try {
        sc = new Scanner(input.getInputStream(), "UTF-8");
        if (sc.hasNext()) {
            content = sc.nextLine();
        } else {
            content = "";
        }
        return content;
    } catch (IOException ex) {
        Logger.getLogger(Tools.class.getName()).log(Level.SEVERE, ex.getMessage());
        content = null;
    } finally {
        sc.close();
    }
    return content;
}

public static String getContentTextArea(Part input) {
    StringBuilder sb = null;
    Scanner sc = null;
    try {
        sc = new Scanner(input.getInputStream(), "UTF-8");
        sb = new StringBuilder("");
        while (sc.hasNext()) {
            sb.append(sc.nextLine());
            sb.append("\n");
        }
    } catch (IOException ex) {
        Logger.getLogger(Tools.class.getName()).log(Level.SEVERE, ex.getMessage());
        sb = null;
    } finally {
        sc.close();
    }
    if (sb == null) {
        return null
    } else {
        return sb.toString();
    }
}

Ahora ya solo falta por ver como guardar la imagen en disco. Como hemos visto se obtiene el InputStream del Part del formulario (que ya lo obtuvimos en el Servlet y se lo pasamos ahora a este método estático). También nos aseguramos de cerrar los flujos de datos en todos los casos de error y de éxito.

public static boolean guardarImagenDeProdructoEnElSistemaDeFicheros(InputStream input, String fileName)
        throws ServletException {
    FileOutputStream output = null;
    boolean ok = false;
    try {
        output = new FileOutputStream(fileName);
        int leido = 0;
        leido = input.read();
        while (leido != -1) {
            output.write(leido);
            leido = input.read();
        }
    } catch (FileNotFoundException ex) {
        Logger.getLogger(Tools.class.getName()).log(Level.SEVERE, ex.getMessage());
    } catch (IOException ex) {
        Logger.getLogger(Tools.class.getName()).log(Level.SEVERE, ex.getMessage());
    } finally {
        try {
            output.flush();
            output.close();
            input.close();
            ok = true;
        } catch (IOException ex) {
            Logger.getLogger(Tools.class.getName()).log(Level.SEVERE, "Error cerrando flujo de salida", ex);
        }
    }
    return ok;
}

Hasta aquí creo que no se me queda ningún aspecto por cubrir, únicamente añadir alguna anotación. Comentar que sería interesante para administrar las imágenes guardar sus nombres en una base de datos haciéndolas corresponder con productos por ejemplo en el caso de una tienda. Como aspectos a corregir de mi modo de hacerlo en el proyecto de la tienda sería guardar las imágenes con extensión. Puede ser que no todos los navegadores muestren la imagen correctamente, también sería interesante guardar las imágenes con nombres normales y no con códigos, esto último más que nada para el SEO (Search Engine Optimization), debido a que con nombres normales las imágenes se indexarán mejor en los buscadores.