Upload en JSF 2.0
Después de varias semanas desconectado por motivo de trabajo (parece que ahora sí trabajo más que antes) retomo (y espero que sea más seguido) los artículos de este blog que, según veo las estadísticas, a varios desarrolladores ayudan.
Y bueno, estuve haciendo una aplicación con JSF 2.0, y una de las característica que debería tener es que permita anexar archivos. Los Servlet 3.0 - que son parte de Java EE 6 - permiten cargar archivos de manera nativa. Lo que mostraré en este post es justamente cómo utilizar el Upload de Servlet 3.0 desde una aplicación web con JSF 2.0. No usaré extensiones de JSF como el ICEFaces o el MyFaces, porque quiero hacerlo de la manera más genérica posible, de tal manera que no afecte a la implementación de JSF que estén usando.
Supongamos que nuestra aplicación debe registrar una bitácora de trabajo, y en cada registro permita anexar archivos, para que después se pueda descargar.
Nuestra aplicación web...
... se llamará "BitacoraWeb"...
... y se ejecutará en GlassFish V3...
... con soporte para JSF 2.0
Y listo...
Comencemos ahora sí. (Para la siguiente haré un post bien explicado sobre cómo crear aplicaciones web en NetBeans para ahorrarme estos pasos)
Para registrar la bitácora necesitamos de una base de datos. Lo mostraré aquí con MySQL, pero - naturalmente - ustedes pueden utilizar la que más se acomode.
Asumiré - para este post - que existe la base de datos "bitacora" asignado al usuario "bitacora" con contraseña "bitacora"
Ahora, necesitamos una entidad llamada "Entrada" que tendrá las siguientes propiedades:
Y la Clase
Ahora, necesitamos el manejador de la entidad
Seleccionamos las Entidades que queremos administrar...
... seleccionamos donde queremos que se creen...
... y listo.
Ahora, crearemos un ManagedBean de alcance "Session" llamado BitacoraBean. Este es el que mostrará los datos en el formulario.
No voy a detallar cada parte del código, porque es algo complejo. Además, el código colgado ya tiene sus propios comentarios. Pero sí voy a explicar partes principales de las clases principales.
En la parte inferior del JSF hay un formulario que es donde se registrará la bitácora. Pero también hay una parte para agregar los archivos. Para esto utilicé otra página para cargar los adjuntos.
Por ello existe el botón "Cargar archivo". Lo que hace este botón es abrir un JSP que tiene un formulario para cargar cualquier archivo.
El boton "recargar" no hace nada. Solo que cuando se llama, se recarga la página, y por tanto, muestra los contenidos de la tabla de adjuntos. (Por ahora lo oculté, porque sino se confundiría con lo que quiero explicar)
El formulario de carga es bastante simple: (archivo form.jsp)
El servlet que es apuntado por este formulario es lo principal. Veamos por qué:
Ahora bien, el boton "Recargar" es utilizado justo por el JavaScript que devuelve el Servlet. La parte que contiene para mostrar los adjuntos (o sea, lo que oculté hace un rato) es como sigue:
Como se ve, aquí se muestra los adjuntos tienen tener un botón (con una "X") para quitarlo de la bitácora que se va a registrar. Este botón llama al método
Por ello, cuando se hace clic en el botón "X", busca el objeto asociado (getRowData()), lo quita de los adjuntos de la entrada, y devuelve null para que recargue la página.
Listo....!!
Ah, falta mostrar los registros de la bitácora, y que muestre los archivos. Aquí el código:
Cada archivo adjunto que se está mostrando está apuntado por un enlace, que es un servlet. Y este servlet es el siguiente:
El código fuente de proyecto se puede descargar desde aquí:
http://java.net/projects/apuntes/downloads/download/web/BitacoraWeb.tar.gz
Y bueno, estuve haciendo una aplicación con JSF 2.0, y una de las característica que debería tener es que permita anexar archivos. Los Servlet 3.0 - que son parte de Java EE 6 - permiten cargar archivos de manera nativa. Lo que mostraré en este post es justamente cómo utilizar el Upload de Servlet 3.0 desde una aplicación web con JSF 2.0. No usaré extensiones de JSF como el ICEFaces o el MyFaces, porque quiero hacerlo de la manera más genérica posible, de tal manera que no afecte a la implementación de JSF que estén usando.
Supongamos que nuestra aplicación debe registrar una bitácora de trabajo, y en cada registro permita anexar archivos, para que después se pueda descargar.
Nuestra aplicación web...
... se llamará "BitacoraWeb"...
... y se ejecutará en GlassFish V3...
... con soporte para JSF 2.0
Y listo...
Comencemos ahora sí. (Para la siguiente haré un post bien explicado sobre cómo crear aplicaciones web en NetBeans para ahorrarme estos pasos)
Para registrar la bitácora necesitamos de una base de datos. Lo mostraré aquí con MySQL, pero - naturalmente - ustedes pueden utilizar la que más se acomode.
Asumiré - para este post - que existe la base de datos "bitacora" asignado al usuario "bitacora" con contraseña "bitacora"
Ahora, necesitamos una entidad llamada "Entrada" que tendrá las siguientes propiedades:
@Entity
public class Entrada implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column
private String descripcion;
@ElementCollection
private List<Adjunto> adjuntos;
public void addAdjunto(Adjunto adjunto) {
getAdjuntos().add(adjunto);
}
public List<Adjunto> getAdjuntos() {
if (adjuntos == null) {
adjuntos = new ArrayList<Adjunto>();
}
return adjuntos;
}
//... métodos get y set por cada propiedad
Y la Clase
Adjunto
tendrá la siguiente estructura:
@Embeddable
public class Adjunto implements Serializable {
@Lob
private byte[] contenido;
@Column
private String tipoContenido;
@Column
private String nombreArchivo;
public Adjunto() {
}
public Adjunto(byte[] contenido, String tipoContenido, String nombreArchivo) {
this.contenido = contenido;
this.tipoContenido = tipoContenido;
this.nombreArchivo = nombreArchivo;
}
//.. métodos set y get de las propiedades.
Ahora, necesitamos el manejador de la entidad
Entrada
. Con el NetBeans, esto se nos será muy fácil: basta con seleccionar desde la opción "File > New" la opción "Persistence > Session Beans For Entity Classes"Seleccionamos las Entidades que queremos administrar...
... seleccionamos donde queremos que se creen...
... y listo.
Ahora, crearemos un ManagedBean de alcance "Session" llamado BitacoraBean. Este es el que mostrará los datos en el formulario.
No voy a detallar cada parte del código, porque es algo complejo. Además, el código colgado ya tiene sus propios comentarios. Pero sí voy a explicar partes principales de las clases principales.
En la parte inferior del JSF hay un formulario que es donde se registrará la bitácora. Pero también hay una parte para agregar los archivos. Para esto utilicé otra página para cargar los adjuntos.
//...
<h:form id="form">
<h:panelGrid columns="2">
<h:outputLabel value="Descripción" for="descripcion" />
<h:inputTextarea id="descripcion" value="#{bitacoraBean.entradaActual.descripcion}" />
<h:outputText value="Adjuntos"/>
<h:panelGroup>
<h:commandButton value="Recargar..." id="recargar">
<f:ajax render="adjuntos" />
</h:commandButton>
<h:button value="Agregar" onclick="return abrirAdjuntos()"/>
<!-- aqui va el codigo donde se muestra los adjuntos -->
</h:panelGroup>
</h:panelGrid>
<h:commandButton action="#{bitacoraBean.guardarEntrada}" value="Guardar"/>
</h:form>
//...
Por ello existe el botón "Cargar archivo". Lo que hace este botón es abrir un JSP que tiene un formulario para cargar cualquier archivo.
El boton "recargar" no hace nada. Solo que cuando se llama, se recarga la página, y por tanto, muestra los contenidos de la tabla de adjuntos. (Por ahora lo oculté, porque sino se confundiría con lo que quiero explicar)
El formulario de carga es bastante simple: (archivo form.jsp)
//...
<h1>Cargar archivo</h1>
<form action="<c:url value='/servlet/upload' />" method="POST" enctype="multipart/form-data">
Archivo:
<input type="file" name="archivo" value="" />
<button type="submit">Cargar</button>
</form>
//...
El servlet que es apuntado por este formulario es lo principal. Veamos por qué:
@WebServlet(name = "UploadServlet", urlPatterns = {"/servlet/upload"})
public class UploadServlet extends HttpServlet {
/**
* Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Part filePart = request.getPart("archivo"); //obtengo el archivo adjunto
String nombreArchivo = extraerNombre(filePart.getHeader("content-disposition")); //extraigo el nombre
InputStream is = filePart.getInputStream(); //obtengo el Stream
long size = filePart.getSize(); //... el tamaño
byte[] buffer = new byte[(int) size]; //.. creo el buffer
is.read(buffer); //.. leo el buffer en un solo bloque
is.close(); //... cierro el buffer
String mimeType = filePart.getContentType(); //... obtengo el tipo de archivo
Adjunto adjunto = new Adjunto(buffer, mimeType, nombreArchivo); //... creo el objeto ajdjunto
BitacoraBean bitacoraBean = (BitacoraBean) request.getSession().getAttribute("bitacoraBean"); //obtengo el bean
Entrada entradaActual = bitacoraBean.getEntradaActual(); //.. obtengo la entrada actual que está con el formulario
entradaActual.addAdjunto(adjunto); //... adjunto el objeto del archivo
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
out.println("<html><head>"); //imprimo en el HTML para ejecutar un javascript
out.println("<script type=\"text/javascript\">");
out.println("window.opener.document.getElementById(\"form:recargar\").click()"); //busco el boton para recargar
out.println("window.close()"); //cierro ventana
out.println("</script>");
out.println("</head></html>");
} finally {
out.close();
}
}
/**
* Método para extrar el nombre físico del archivo
* @param header Parametro del nombre, con toda la ruta completa en el cliente
* @return Devuelve solo el nombre del archivo
*/
static private String extraerNombre(String header) {
String[] parts = header.split(";");
for (String part : parts) {
if (part.trim().startsWith("filename=")) { //busco todos los que comienzan con filename
String[] $parts = part.split("="); //separo el nombre
StringBuilder $sb = new StringBuilder($parts[1]);
String fn = $sb.substring(1, $sb.length() - 1); //el segundo es el nombre
File f = new File(fn); //creo una entrada de archivo
return f.getName(); //devuelvo el nombre del archivo
}
}
return "";
}
Ahora bien, el boton "Recargar" es utilizado justo por el JavaScript que devuelve el Servlet. La parte que contiene para mostrar los adjuntos (o sea, lo que oculté hace un rato) es como sigue:
<h:dataTable id="adjuntos" value="#{bitacoraBean.entradaActual.adjuntos}" var="adjunto" binding="#{bitacoraBean.adjuntosDataTable}">
<h:column>
<h:outputText value="#{adjunto.nombreArchivo}" />
</h:column>
<h:column>
<h:commandButton value="X" action="#{bitacoraBean.quitarAdjunto}" />
</h:column>
</h:dataTable>
Como se ve, aquí se muestra los adjuntos tienen tener un botón (con una "X") para quitarlo de la bitácora que se va a registrar. Este botón llama al método
quitarAdjunto
. Pero para que funcione, se ha creado un binding llamado adjuntosDataTable
. El método de quitarAdjunto es como sigue:
//...
public HtmlDataTable getAdjuntosDataTable() {
return adjuntosDataTable;
}
public void setAdjuntosDataTable(HtmlDataTable adjuntosDataTable) {
this.adjuntosDataTable = adjuntosDataTable;
}
public String quitarAdjunto(){
Adjunto adjunto = (Adjunto) adjuntosDataTable.getRowData();
entradaActual.quitarAdjunto(adjunto);
return null;
}
//...
Por ello, cuando se hace clic en el botón "X", busca el objeto asociado (getRowData()), lo quita de los adjuntos de la entrada, y devuelve null para que recargue la página.
Listo....!!
Ah, falta mostrar los registros de la bitácora, y que muestre los archivos. Aquí el código:
<h2>Entradas de Bitácora</h2>
<h:dataTable border="1" value="#{bitacoraBean.entradas}" var="entrada">
<h:column>
<f:facet name="header">ID</f:facet>
#{entrada.id}
</h:column>
<h:column>
<f:facet name="header">Descripción</f:facet>
#{entrada.descripcion}
</h:column>
<h:column>
<f:facet name="header">Adjuntos</f:facet>
<h:dataTable value="#{entrada.adjuntos}" var="adjunto">
<h:column>
<h:outputLink value="#{facesContext.externalContext.requestContextPath}/servlet/upload/abrir" >
<h:outputText value="#{adjunto.nombreArchivo}" />
<f:param name="id" value="#{entrada.id}" />
<f:param name="archivo" value="#{adjunto.nombreArchivo}" />
</h:outputLink>
</h:column>
</h:dataTable>
</h:column>
</h:dataTable>
Cada archivo adjunto que se está mostrando está apuntado por un enlace, que es un servlet. Y este servlet es el siguiente:
//...
@WebServlet(name = "MostrarArchivoServlet", urlPatterns = {"/servlet/upload/abrir"})
public class MostrarArchivoServlet extends HttpServlet {
@EJB
private EntradaFacade entradaFacade;
/**
* Processes requests for both HTTP <code>GET</code> and <code>POST</code> methods.
* @param request servlet request
* @param response servlet response
* @throws ServletException if a servlet-specific error occurs
* @throws IOException if an I/O error occurs
*/
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String $idEntrada = request.getParameter("id"); //obtengo el ID del parametro
String archivo = request.getParameter("archivo"); //obtengo el nombre del archivo del parametro
Long idEntrada = Long.parseLong($idEntrada); //convierto el ID a Long...
Entrada entrada = entradaFacade.find(idEntrada);//.. para buscarlo en la base de datos
if (entrada == null) { //si no existe...
response.setStatus(HttpServletResponse.SC_NOT_FOUND); //... devuelvo un mensaje de q no existe
return; //... y termina
}
List<Adjunto> adjuntos = entrada.getAdjuntos(); //ahora, obtenemos todos los adjuntos de la entrada
Adjunto adjuntoMostrar = null; //el adjunto actual
for (Adjunto $adjunto : adjuntos) { //recorro todos los adjuntos
if ($adjunto.getNombreArchivo().equals(archivo)); //comparamos el nombre, y si existe...
adjuntoMostrar = $adjunto; //.. lo capturamos
}
if (adjuntoMostrar == null) { //si recorrio toda la lista y no encontro nada...
response.setStatus(HttpServletResponse.SC_NOT_FOUND); //... devolvemos un mensaje que q no existe
return; //.. y termina
}
OutputStream out = response.getOutputStream();
try {
response.setContentType("application/octet-stream"); //...preparamos el tipo para q se descargue la adjunto
response.setHeader("Content-Disposition", "attachment;filename=" + adjuntoMostrar.getNombreArchivo().replaceAll(" ", "_")); //preparando el 'download' al navegador
response.setContentLength(adjuntoMostrar.getContenido().length); //ponemos el tamaño...
out.write(adjuntoMostrar.getContenido()); //... y vaciamos el contenido
} finally {
out.close(); //y cerramos el flujo
}
}
//...
Código fuente
El código fuente de proyecto se puede descargar desde aquí:
http://java.net/projects/apuntes/downloads/download/web/BitacoraWeb.tar.gz
Yo trabajo con alguien más en un empresa y esta otra persona me alega que JSF es mil veces mejor que JSP por el hecho de que él usa PrimeFaces y ese Framework le genera todo!
ردحذفClaro yo no estoy de acuerdo con él porque al generar todo dependo de PrimeFaces para hacer las cosas, entonces eso me hace sentir desconfiado y temereso a que me salga uno de esos errores que suelen salir que uno dice !#.@! :).
Que opinan ustedes?
Claro yo utilizo jquuery, jsp y los beans para mi son clases con constructores y propiedades.
yo tambien utilizo JQuery. Solo que en este post quise juntar dos tecnologias: JSF 2.0 y Servlet 3.0. PrimeFaces y otros framework tiene un costo en configurar el framework. Por mi, si no viene con el IDE (y si es dificil de configurarlo en el IDE), no lo utilizo. Porque no me gusta luego decir a mi equipo "por favor, hagan esto y tal para que funcione". Es cuestion de practicidad. Ahora estoy considerando en utilizar Maven.
ردحذفJSF no es tan malo: ahorra mucho esfuerzo en seguir el hilo de las peticiones, lo que me resultaba muy engorroso en Struts. Aún no he probado SpringMVC... dicen que es bueno justo para el MVC y la seguridad... pero cuando se quiere utilizar con otra tecnologia aparecen los problemas.
Una consulta la estructura de la BD cual es para poder hacer correr el ejemplo
ردحذفLa estructura se crea automaticamente.
ردحذفMagnifico tutorial está sublime. Pero tengo un problema al ejecutar el boton guardar y no que pasa: at facade.__EJB31_Generated__EntradaFacade__Intf____Bean__.create(Unknown Source)
ردحذفque debo hacer para que funcione.
El tuto esta muy bien... y el codigo mejor (porque si funciona), solo tengo un comentario, yo lo corri me estaba dando unos problemas pero era porque si habia creado la BD pero no con el usuario y pass.. que se indicaba... sino que con root. Bueno corrigiendo eso en el archivo de persistencia, solucionado.
ردحذفCuando me corrio la aplicación la primera vez, escogi un archivo que tenia un nombre muy largo y al parecer eso provoco la exception. Seleccione otro y listo cargo el primer file a la BD. Cuando intente cargar otro, me dio otro error al parecer llave duplicada, pero como soy nuevo en esto de Faces, no se donde buscar la linea de codigo que genera el id de la entrada? o esto se define a nivel de la BD, declarando el campo auto numerico o con secuencia????? Comentarios al respecto... muy agradecido.