Yo, soy asin Yo en el universo
El Weblog de ese insignificante ser llamado David Bonilla
M2012

20041011 Monday October 11, 2004

Clases de Cocina: Barra de Progreso JAVA
Muchas veces, todos hemos deseado utilizar una Barra de Progreso para monitorizar el progreso de determinadas tareas en nuestras aplicaciones WEB. Poco a poco, vamos creando procesos mas y mas complejos (y mas y mas pesados) en nuestras aplicaciones y, la verdad, cuando el Front-End de las mismas esta basado en un navegador, tener a un usuario esperando 2,5 o 10 minutos sin indicarle QUE esta pasando no da muy buena sensacion.
A continuacion, voy a plantear una implementacion, sencilla y elegante, de una barra de progreso basada en navegador WEB y un simple mecanismo para ir marcando el progreso de una tarea que reflejara dicha barra. Como el post va para largo, lo he escrito a lo Arguiñano, asi espero que os sea mas amena su lectura. Yo, desde luego, me he divertido escribiendolo.


Hoy vamos a explicar como un implementar, en una aplicacion web JAVA, una barra de progreso rica, rica y con fundamento.
Hay muchas maneras de realizar este plato y, por supuesto, cada uno de vosotros le podeis dar vuestro propio toque personal. Yo os voy a explicar como la cocino yo, tal y como me la enseño mi madre.

Para cocinar la Barra necesitaremos los siguientes ingredientes:

- 1 Servlet bien fresco
- 150gr de JavaScript
- 1 par de clases de Java ligeras y esponjosas
- Salsa tipo "Patron Commando" para aderezar

Empezaremos por picar muy fino las classes Java. A estas clases JAVA yo les echo "Patron Commando" para que queden mas agradables al gusto.
El patron Commando, bien utilizado -como diria el Chef Albert du Vilch-, puede ser muy util combinado con el patron factoria para cambiar el funcionamiento de toda una aplicacion a golpe de Property, sin embargo, no es este hilo el lugar mas apropiado para que hablemos de el. Podeis conseguir mas informacion, por ejemplo, aqui.
Nos creamos una interfaz que implementara el Patron Commando. Esta interfaz servira para definir los metodos que necesitemos para marcar el progreso de una tarea a traves de nuestro codigo:

public interface ProgressBox {

  String task = null;

   /**
   * Devuelve la tarea que se esta monitorizando.
   * @return String el ID de la tarea
   */
   public String getTask();

   /**
   * Establece el porcentaje de finalizacion de una tarea.
   * @param percentage double el porcentaje de finalizacion de la tarea
   */
   public void setProgress(double percentage);
}

Por supuesto, si se desea, se le puede añadir a esta clase un monton de "especias" que realcen el sabor y, asi, podemos definir metodos que calculen el progreso de una tarea en base a un numero de iteracion dentro de un bucle u otro que lance un mensaje (para trasmitir un mensaje de error, por ejemplo, en vez de un porcentaje). Esto se deja a vuestro buen criterio.
Una vez que tenemos la interfaz bien picadita, vamos a empezar a amasar una clase JAVA que la implemente.

Lo bueno del patron Commando es que te permite hacer llamadas a procedimientos sin saber que es lo que realmente estas haciendo. Asi, podemos crear una clase que implemente la Interfaz y que guarde el progreso de una tarea como cada uno desee (HTTPSession, Base de Datos, Property, etc.).
Yo voy a "amasar" una clase que guardara el progreso de una tarea en una Session HTTP. Gracias a la interfaz que he definido previamente, podre distribuir este objeto por toda mi aplicacion, evitando tener que pasar como parametro el objeto Session, lo que supondria cepillarme cualquier logica de diseño en capas que puediera tener. A ver que os parece:

public class SessionProgressBox implements ProgressBox {

   public static final String SESSION_PROGRESS_KEY = "task.progress.";
   private HttpSession session = null;
   private String task = null;


   /**
   * El constructor necesita una HTTPSession
   * @param session HttpSession
   * @param task String el ID de la tarea a controlar
   */
   public SessionProgressBox(HttpSession session, String task) {
     this.session = session;
     this.task = task;
   }

   public void setProgress(double percentage) {
     this.session.setAttribute(SESSION_PROGRESS_KEY + this.task, new Double(percentage));
   }

   public String getTask() {
     return task;
   }

}

Una vez tenemos nuestra interfaz y nuestra clase, las salpimentamos y las reservamos, para utilizarlas posteriormente.

Vamos a pasar a la parte mas dificil del plato: cocinar el Servlet. El Servlet de control de progreso es la parte mas importante a realizar puesto que el generara el codigo HTML y JavaScript que pintara la barra de progreso y seguira monitorizando el progreso.
Os presento un Servlet sencillito pero, eso si hecho de muy buena materia prima. No produce codigo compatible con Explorer y Netscape, solo con el primero pero, he preferido cocinarlo asi en aras de la claridad, para mejorar el entendimiento. Seguro que si alguien necesita compatibilidad doble sabra como dar su toque personal al plato.

public class ProgressServlet extends HttpServlet {

   // Declaracion de variables
   private static final String CONTENT_TYPE = "text/html";
   private String millis2refresh = null;
   private String millis2close = null;

   /**
   * Inicializacion de variables globales
   * @throws ServletException
   */
   public void init() throws ServletException {
     millis2refresh = getServletConfig().getInitParameter("millis2refresh");
     if (mil  lis2refresh == null) {
       millis2refresh = "1500";
     }
     millis2close = getServletConfig().getInitParameter("millis2close");
     if (millis2close == null) {
     millis2close = "2000";
     }
   }

   /**
   * Proceso de una peticion HTTP de tipo GET
   * @param request HttpServletRequest
   * @param response HttpServletResponse
   * @throws ServletException
   * @throws IOException
   */
   public void doGet(HttpServletRequest request, HttpServletResponse response) throws
     ServletException, IOException {

     response.setContentType(CONTENT_TYPE);
     PrintWriter out = response.getWriter();

     try {
     // Se recuperan las variables del REQUEST
     String idProceso = request.getParameter("idProceso");
     String keyProgress = SessionProgressBox.SESSION_PROGRESS_KEY +
     idProceso;
    // Se recuperan variables de SESSION
     Double progreso = (Double) request.getSession().getAttribute(
     keyProgress);
     if (progreso == null) {
       progreso = new Double(0);
     }

     // Se compone la pagina web dinamicamente

     out.println("< html >");
     out.println("< head >");
     out.println("< title >ProgressServlet< /title >");
      out.println("< style >");
     out.println(
       "#fondobarra{position:absolute;left:100px;top:20px;background-color:#B7C3D0;}");
     out.println(
       "#barra{position:absolute;left:100px;top:20px;background-color:#3D59AB;}");
     out.println("< /style >");
     out.println("< /head >");
     out.println("< body bgcolor=\"#ffffff\" >");
     out.println(
       "< div id=\"fondobarra\" style=\"width:100px;height:10px;z-index:9;\" >< /div >");
     out.println(
       "< div id=\"barra\" style=\"width:" + progreso +
       "px;height:10px;z-index:10\" >< /div >");
     out.println("< form action=\""+request.getContextPath()+"/progressservlet\" method=\"post\" >");
     out.println("< input type=\"hidden\" name=\"progreso\" value=\"" +
       progreso + "\"/ >");
     out.println("< input type=\"hidden\" name=\"idProceso\" value=\"" +
       idProceso + "\"/ >");
     out.println("< br >");
     out.println("< br >");
     out.println(
       "< p align=\"center\" >Completado un " +
       progreso +
       "% del proceso ...< /p >");
     out.println("< /form >");
     out.println("< /body >");
     out.println("< script >");
     out.println("if(document.forms[0].progreso.value < 100){");
     out.println("setTimeout(\"document.forms[0].submit()\", " +
       millis2refresh + ")");
     out.println("}");
     out.println("else{");
       out.println("setTimeout(\"window.close()\", " + millis2close +
     ")");
     out.println("}");
     out.println("< /script >");

     out.println("< /html >");
     } catch (Exception ex) {

       out.println("< html >");
       out.println("< head >");
       out.println("< title >ProgressServlet< /title >");
       out.println("< /head >");
       out.println("< body bgcolor=\"#ffffff\" >");
       out.println("< br >");

       out.println(
     "< p align=\"center\" >Error al intentar monitorizar el progreso de una tarea: " +
       ex+"< /p >");
     out.println("< /body >");
     out.println("< /html >");
   }

}

Como podeis apreciar, es un Servlet bien sencillo, con no mucha complejidad. Pinta una barra de progreso utilizando capas (es decir, no viajan imagenes, la "pagina" generada apenas ocupa unos bytes con lo cual la percepcion de refresco, cada vez que va a consultar a servidor el progreso de una tarea, es imperceptible). Tambien escribe el codigo JavaScript necesario para que vuelva a lanzar una peticion al servidor en n milisegundos -los que le hayamos indicado en el Servlet- si el progreso es menor del 100% o que cerrara la ventana en caso contario.
El Servlet escribe el codigo HTML y JavaScript que deseis asi que, ya sabeis, si deseais darle un toque personal... a la sarten y vuelta y vuelta. Es mas, la unica concesion a las delicatessen son las dos variables de inicializacion del Servlet que indican en la configuracion del Servlet en descriptorescada cuantos milisegundos se refrescara el progreso de una tarea y cuantos segundos tardara en cerrarse la ventana una vez que se ha cumplido el 100% (variables millis2refresh y millis2close respectivamente).

He tenido que toquetar el codigo del Servlet para que el post quedara escrito para copiar y pegar (en concreto he tenido que poner un espacio al principio y al final de cada etiqueta HTML para que el weblog no se volviera loco) asi que, si encontrais algun problema en el mismo o algo que no entendais, ya sabeis donde encontrarme.

Una vez que tenemos el Servlet en el plato, procederemos a adornarlo con unas lineas de descriptor. En concreto las siguientes, que debeis colocar en el descriptor web.xml:

   < servlet >
     < servlet-name >progressservlet< /servlet-name>
     < servlet-class >
      [escribir aqui el paquetizado de vuestro servlet Ej. com.test.].ProgressServlet
    < /servlet-class >
    < init-param>
       < param-name >millis2refresh< /param-name >
       < param-value >2000< /param-value >
     < /init-param >
    < init-param>
       < param-name >millis2close< /param-name >
       < param-value >3000< /param-value >
     < /init-param >
  < /servlet >
  < servlet-mapping >
    < servlet-name >progressservlet< /servlet-name >
    < url-pattern >/progressservlet< /url-pattern >
  < /servlet-mapping >


Mmmmm... ¡Que bien huele esto! ¡Ya lo tenemos casi listo!. Un poquito mas y el plato estara cocinado.

Ya hemos montado la estructura que monitorizara el progreso de una tarea ahora vamos a integrarlo con nuestra aplicacion web.
Nos vamos a la pagina web desde donde se lanzara nuestra aplicacion. Alli, dentro de un formulario tendremos seguro un campo del formulario de tipo submit, un campo de tipo button con un evento onclick controlado o algo similar. Bien, lo que tenemos que hacer es, antes de invocar el submit del formulario, llamar a la siguiente funcion:

function barraProgreso(idProceso) {
  window.open('<%=request.getContextPath()%>/progressservlet?idProceso='+idProceso, 'BarraProgreso', 'width=300, height=100, left=500,     top=200, resizable=no, scrollbars=no');
}


Si os dais cuenta, esta funcion solo recibe un parametro que es un IDENTIFICATIVO UNICO de la tarea que quereis monitorizar. Lo ideal es que copieis esta funcion (y todas las funciones de JavaScript que utiliceis de manera general en toda vuestra aplicacion) en un fichero .js para poder importarlo en todas las paginas en donde haga falta en vez de copiar una y otra vez la misma funcion en todas las paginas.
Bien, el caso es que, lo hayais metido en un fichero .js o lo hayais copiado directamente en la pagina, ya podeis invocar a la funcion. Asi, por ejemplo:

< input type="button" value="Aceptar" onclick="barraProgreso(5);document.forms[0].submit()" />


E VOILA !!!. Una nueva ventana se abrira mostrando la barra de progreso en este caso, de la tarea "5".
Ahora solo falta marcar el porcentaje de ese progreso segun se van cumpliendo hitos de la tarea a monitorizar. Si no lo hicieramos, la barra no se moveria del 0% y la ventana que la contiene nunca se cerraria.
Vamos a mostrar, como podriamos ir indicando en nuestro codigo el progreso de nuestra tarea (como ejemplo pongo el metodo doGet de un Servlet aunque se podria hacer lo mismo en un doPost, el perform de Struts, etc...):

   public void doGet(HttpServletRequest request, HttpServletResponse response) throws
     ServletException, IOException {

     SessionProgressBox box = new SessionProgressBox(request.getSession(), "5");
     System.out.println("¡ Hola Don Pepito !");
     box.setProgress(10);
     System.out.println("¡ Hola Don Jose !");
     box.setProgress(20.5);
     System.out.println("Pasaba por su casa.");
     box.setProgress(40);
     System.out.println("Por mi casa pasa usted.");
     box.setProgress(60);
     System.out.println("¡ Adios Don Pepito !");
     box.setProgress(80.5);
     System.out.println("¡ Adios Don Jose !");
     box.setProgress(100);
  }


Asi podeis ver como he marcado progresos del 10, 20.5, 40, 60, 80.5 y 100% en mi tarea "5".

Bueno, bueno, bueno... pues ya esta el plato listo y nos ha salido... ¡ riquisimo !. Para chuparnos los dedos.
Por supuesto, que esto son solo unas directrices, cada uno podeis y debeis cocinar como mas os guste. De hecho, este plato es una version muy simplificada de la que he cocinado en mi "Resturante". Ya sabeis que el objeto SessionProgressBox, o cualquier otra implementacion de la interfaz ProgressBox, podeis propagarla por toda vuestra aplicacion.
A veces podeis monitorizar un metodo que sea llamado por dos tareas y que en cada uno de ellas, la consecucion de un determinado hito suponga un porcentaje de progreso diferente... no problemo !!!. Para eso ProgressBox tiene el utilisimo metodo getTask() que te indica la tarea que se esta monitorizando. Asi podeis, por ejemplo asignar en un hito porcentajes de consecucion distintos para la tarea "pepino" y la tarea "melon":

  if(box != null && "pepino".equals(box.getTask()) {
    box.setProgress(15);
  }
  else if(box != null && "melon".equals(box.getTask()) {
    box.setProgress(87.5);
  }


Asi, habeis visto, pasando por el mismo hito le hemos asignado un 15% de progreso a la tarea "pepino" y un 87.5% a la tarea "melon". Tambien es importante que os deis cuenta que, ademas de averiguar en que tarea esta, se ha preguntado primero si el objeto ProgressBox es nulo esto es importante por lo mismo que hemos hablado antes, a lo mejor ese hito que esta dentro de un metodo, se llama desde un primer metodo que no monitoriza el progreso de ninguna tarea y pasa el objeto ProgressBox a nulo.

Bueno... pues ya esta, espero que, por la longitud de este post, no haya cometido muchos errores (mucho del codigo lo escribo de cabeza) y que NO TODOS OS HAYAIS DORMIDO. Si el post tiene aceptacion, poco a poco ire escribiendo mas clases de cocina. Ya sabeis...

RICO, RICO... Y CON FUNDAMENTO

(2004-10-11 14:13:31.0) Permalink Comentarios [19]



Archivos
Busca en el weblog
Navegacion
  Get Firefox
   Bitacoras.com
De donde venis