Paginacion (y II)

El otro día comenté en Paginacion (I) como obtener resultados paginados desde la base de datos, mostrando dos formas distintas para hacerlo.
El resultado final es que se devolvía un objeto Page que contenía una coleccion de resultados (los beans de la página) y algunos valores numéricos con información de la página, como el tamaño de la misma, el número de página y el número total de páginas (última página) y elementos (último elemento).

Veamos antes de continuar, un ejemplo de utilización del metodo “getPage” de obtencion de páginas. Esto sería el código de un Servlet cualquiera que recibe como parametros “page” y “size” en su url la página a mostrar y el tamaño de la página. Despues el objeto Page devuelto se introduce en la request con el nombre “vuelos”.
Además, se muestra un “truquillo” para recoger el parametro “page” y “size” sin muchos quebraderos de cabeza. Es el método getParameterInt.

public static final int DEFAULT_PAGE_SIZE = 20;

protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
    int numPage = getParameterInt(req, "page", 1);
    int pageSize = getParameterInt(req, "size", DEFAULT_PAGE_SIZE);

    Page pageVuelos = getPage(numeroPagina, pageSize);

    request.setAttribute("vuelos", pageVuelos);
    req.getRequestDispatcher("/mipagina.jsp").forward(req, res);
}

public int getParameterInt(HttpServletRequest req, String name, int defaultValue) {
    try {
        return Integer.parseInt(req.getParameter(name));
    } catch (NumberFormatException e) {
        // Si llegamos hasta aqui es que número recibido como parámetro es null o no es númerico.
        // Por lo tanto, retornamos el valor por defecto
        return defaultValue;
    }
}

A partir de aqui, el trabajo consiste, desde el JSP, recoger el objeto Page que hemos dejado en el atributo “vuelos” y pintar, con un Iterator (o con lo que se quiera) la coleccion de objetos que en el método getResults()

Una vez pintado la lista de objetos de nuestra página, queremos que el usuario pueda elegir cualquier otra página para poder navegar por todo el conjunto de resultados real. Esto se puede hacer de varias maneras. Podríamos, por ejemplo, hacer un bucle que pintase todas las páginas posibles:

     <%
        Page vuelos = (Page)request.getAttribute("vuelos");
        int n = 1;
        for (n=1; n <= vuelos.getLastPage(); n++) {
        %>
           <a href="/consultaVuelos?page=<%=n%>"><%=n%></a>
        <%
        }
     %>


Pero esto puede no resultar muy elegante. Sobre todo si tenemos 1000 páginas ?nos pintaría 1000 enlaces! Lo ideal sería (para mi gusto) mostrar la primera página (1), la última (getLastPage()), la actual (getPage()), dos anteriores (getPage()-1 y getPage()-2), dos posteriores (getPage()+1 y getPage()+2) y dos saltos de 5 páginas llamados “-5” y “+5” (getPage()-5 y getPage()+5). Por ejemplo, si estuvieramos en la página 16 y hubiera 40 páginas, que pintase algo parecido a:


[Primero][-5][14][15] 16 [17][18][+5][Ultimo]

De esta manera, tendríamos siempre, como máximo, 9 enlaces.
Esto lo haremos con una etiqueta JSP, la cual recogerá directamente el objeto Page desde nuestra request y creará los enlaces correspondientes, asegurándose de no pintar nunca enlaces que nos pudieran llevar más allá de la última página o de la primera, ni pintar enlaces repetidos, etc.
Esto requerirá hacer algunas validaciones, sobre todo al calcular las páginas anteriores y posteriores a la actual. Cada vez que se pinte un enlace, validaremos si supera o no la última (o primera) página antes de pintarlo. También habra que controlar que ningún enlace este repetido (es decir, que nos lleve a la misma página). Por ejemplo, si [Primero] y [-5] van a la página 1, solo habrá que pintar el enlace de [Primero].
Alguno podrá pensar que este requerirá hacer muchas comparaciones, ?verdad? Pues no, no haremos ni una sola: nuestro navegador eliminará automáticamente los enlaces repetidos y nunca se pasará de la primera ni la última página el solito. Pero no nos adelantemos, vamos a empezar por el principio y vamos a crear primero una etiqueta JSP para pintar nuestro navegador.

Primero haremos nuestro taglib:

<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee web-jsptaglibrary_2_0.xsd"
    version="2.0">
    <description>Navegador de paginas</description>
    <tlib-version>1.0</tlib-version>
    <short-name>NavigationTag</short-name>
    <tag>
        <name>pagination</name>
        <tag-class>PaginationTag</tag-class>
        <body-content>JSP</body-content>
        <attribute>
          <name>attribute</name>
          <required>true</required>
          <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
          <name>parameter</name>
          <required>false</required>
          <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
          <name>baseUrl</name>
          <required>true</required>
          <rtexprvalue>true</rtexprvalue>
        </attribute>
    </tag>
</taglib>

Nuestro taglib necesita tiene dos parametro obligatorios y uno opcional.

  • “attribute” (obligatorio) Teniendo en cuenta que hemos recogido un objeto Page desde nuestro servlet, hemos tenido que pasarselo a la JSP a través de la request con algun nombre. Este parametro indica el nombre del atributo de la request en el cual hemos depositado nuestro objeto Page.
  • “baseUrl” (obligatorio) La url base a partir de la cual se generarán los enlaces. Suele ser el contextPath + servlet. Puede llevar incluidos algunos parametros fijos, por ejemplo baseUrl=”/Vuelos/consultaVuelos?ordenar=FECHA”. A esta url se le añadira el parametro de la página . En funcion de si la url ya tiene o no tiene parametros, se añadira “?” o “&”.
  • “parameter” (opcional) Indica el nombre del parametro que define la página . Por defecto es “page”

Ahora el codigo fuente de nuestra etiqueta

PaginationTag.java

import javax.servlet.jsp.tagext.*;
import javax.servlet.jsp.*;
import java.io.*;
import java.util.*;

public class PaginationTag extends TagSupport {

    private static class Link implements Comparable {

        private String text;
        private int page;

        public Link(int page, int lastPage, String text) {
            this.page = Math.max(Math.min(page, lastPage), 1);
            this.text = text;
        }

        public Link(int page, int lastPage) {
            this.page = Math.max(Math.min(page, lastPage), 1);
            this.text = String.valueOf(page);
        }

        public String getLinkText(int currentPage, String prefix) {
            if (currentPage != page) {
                // Solo pintamos enlace cuando no es la página  actual
                    return ("[<a class=\"pagination\" href=\"" + baseUrl + prefix + parameter + "=" + l.page + "\">" + l.text + "</a>]");
            } else {
                // Si es la página  actual, solo pintamos el texto
                return ("[" + page + "]");
            }
        }

        /**
         * Metodo para que los Link repetidos se eliminen del TreeSet, ya que el metodo
         * equals por defecto que hereda de java.lang.Object, compara los hashCode.
         * @return
         */
        public int hashCode() {
            return page;
        }
        /**
         * Metodo que mantiene los Link ordenados en el TreeSet
         * @param o
         * @return
         */
        public int compareTo(Object o) {
            int thisVal = this.page;
            int anotherVal = ((Link) o).page;
            return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1));
        }
    }

    private String attribute;
    private String baseUrl;
    private String parameter;

    public String getAttribute() { return attribute; }

    public void setAttribute(String attribute) { this.attribute = attribute; }

    public String getBaseUrl() { return baseUrl; }

    public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }

    public String getParameter() { return parameter; }

    public void setParameter(String parameter) { this.parameter = parameter; }

    public int doEndTag()
        throws JspException {
        try {
            JspWriter w = pageContext.getOut();

            Page search = (Page) pageContext.getRequest().getAttribute(attribute);

            // En funcion si la url ya tiene parametros "/servlet?parametro1=a" o
            // no los tiene "/servlet", concatenamos un "?" o un "&".
            // Recordemos que en las url, el prefijo "?" sirve para el primero parametro y
            // que los siguientes parametros necesitan el prefijo "&"
            String prefix = baseUrl.indexOf("?") > -1 ? "&" : "?";

            int lastPage = search.getLastPage();
            int page = search.getPage();

            // Por defecto, el nombre del parametro para las paginas, si no viene definido
            // es "page"
            if (parameter == null) {
                parameter = "page";
            }

            // No cambiar el orden de insercion, ya que define la prioridad en caso
            // de páginas repetidas. Por ejemplo, si la página  actual y la ultima
            // coinciden, entonces tiene prioridad la página  actual. Y asi con el
            // resto.
            TreeSet al = new TreeSet();
            al.add(new Link(page, lastPage));
            al.add(new Link(1, lastPage, "primero"));
            al.add(new Link(lastPage, lastPage, "ultimo"));
            al.add(new Link(page + 1, lastPage));
            al.add(new Link(page - 1, lastPage));
            al.add(new Link(page + 2, lastPage));
            al.add(new Link(page - 2, lastPage));
            al.add(new Link(page + 5, lastPage, "+5"));
            al.add(new Link(page - 5, lastPage, "-5"));

            for (Iterator i = al.iterator(); i.hasNext();) {
                Link l = (Link)i.next();
                w.print(l.getLinkText(page, prefix));
            }
            return EVAL_PAGE;
        } catch (IOException ioe) {
            throw new JspException(ioe.getMessage());
        }
    }

}

El truco consiste en tener una clase interna llamada Link. Es interna y privada porque solo tiene sentido dentro de nuestra clase PaginationTag, por lo que no la creamos en un archivo Link.java ni la hacemos accesible.
Esta clase Link encapsula un único enlace dentro del navegador con solo 2 atributos: la página a la que enlaza y el texto que muestra (por defecto es la propia página, pero podría ser un texto o una etiqueta HTML con una imágen, por ejemplo).
Es necesario pasar estos dos atributos en el constructor, más el número de la última página (solo para validar).

Entonces, para no “pasarnos” de la última página ni de la primera, tenemos este cálculo.

    this.page = Math.max(Math.min(page, lastPage), 1);

Que es mucho más optimo que empezar con if(page > lastPage) page = lastPage … etc.

Nuestra clase Link implementa el metodo hashCode(), el cual retorna el número de página. Por lo tanto, el hash de un objeto Link es su página.

    public int hashCode() {
        return page;
    }

El método por defecto de equals() de cualquier clase se limita a comparar sus hashCode (ya que así es como está implementado en la clase Object, de la cual heredan todos los objetos de Java). Dado que Link hereda de Object y no sobreescribe el método equals(), comparar dos objetos Link es comparar sus hashCode.

Nota: También podríamos haber sobreescrito el método equals() de Link de esta manera (un poco más larga):

    public boolean equals(Object o){
        return (((Link)o).page == page);
    }

Además, la clase Link también implementa la interfaz Comparable y el método compareTo(), con el fin de poder comparar dos elementos del mismo tipo y establecer un orden. El sistema de ordenación será también por página, de manera que si ordenamos una colección de objetos Link, estarán ordenados por página de menor a mayor.
Para no comernos el coco, copiamos el código del método compareTo de la clase java.lang.Integer y lo adaptamos a nuestra clase.

    public int compareTo(Object o) {
        int thisVal = this.page;
        int anotherVal = ((Link) o).page;
        return (thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1));
    }

Por otro lado, dentro de nuestra etiqueta, creamos un TreeSet para guardar todos los Link que vamos a pintar. Recordemos que TreeSet es un Set (conjunto) y, como tal, no permite objetos repetidos. Además, TreeSet tiene la peculiaridad de mantener sus elementos ordenados (ya que internamente los guarda en un arbol y no en una tabla hash, como HashSet).
Esto nos aporta dos ventajas que hemos aprovechado con nuestra clase Link:

  • Una es que podemos añadir tantos Link como queramos que, en el caso de que se repitan, el nuevo añadido sobreescribirá al antiguo (dos Link se toman como repetidos si son iguales, es decir, si apuntan a la misma página). Para ello Link implementa hashCode()
  • Y otra es que siempre se mantienen ordenados, por lo que podemos añadirlos en cualquier orden, que luego serán pintados de menor a mayor. Para ello Link implementa Comparable/compareTo.

En nuestra etiqueta, vamos a añadir tantos Link como queramos, pero en orden de menor a mayor prioridad: primero los menos necesarios y después los más necesarios..

    TreeSet al = new TreeSet();
    al.add(new Link(1, lastPage, "primero"));
    al.add(new Link(lastPage, lastPage, "ultimo"));
    al.add(new Link(page + 1, lastPage));
    al.add(new Link(page - 1, lastPage));
    al.add(new Link(page + 2, lastPage));
    al.add(new Link(page - 2, lastPage));
    al.add(new Link(page + 5, lastPage, "+5"));
    al.add(new Link(page - 5, lastPage, "-5"));

    al.add(new Link(page, lastPage));

Dado que el constructor de Link limita la página a valores comprendidos entre 1 y lastPage, crear por encima o por debajo de estos valores supone crear objetos repetidos que apunten a 1 o lastPage.
Así, según los vamos añadiendo, si un objeto Link está repetido (o sea, apunta a la misma página), el TreeSet lo sustiuirá por el nuevo que acabmos de añadir.
Vemos, por tanto, el orden de prioridad que hemos establecido: Primero van los enlaces “primero” y “ultimo”. Así, si existe otro enlace que apunta a la misma página que la primera o la última, estos desaparecerán. Después van los enlaces directos a páginas anteriores y posteriores y por último van los enlaces con saltos (+5 y -5. El enlace a la página actual puede ir en cualquier sitio porque nunca estará repetido.
Podemos crear tantos enlaces como queramos: los enlaces que superen 1 y lastPage quedarán como repetidos y serán eliminados según son añadidos.
Por ejemplo, podemos tener un navegador mucho mas grande como éste:

    al.add(new Link(1, lastPage, "primero"));
    al.add(new Link(lastPage, lastPage, "ultimo"));
    al.add(new Link(page + 3, lastPage));
    al.add(new Link(page + 2, lastPage));
    al.add(new Link(page + 1, lastPage));
    al.add(new Link(page - 1, lastPage));
    al.add(new Link(page - 2, lastPage));
    al.add(new Link(page - 3, lastPage));
    al.add(new Link(page + 5, lastPage, "+5"));
    al.add(new Link(page - 5, lastPage, "-5"));
    al.add(new Link(page + 10, lastPage, "+10"));
    al.add(new Link(page - 10, lastPage, "-10"));

    al.add(new Link(page, lastPage));

Finalmente, TreeSet mantiene los Link ordenados de menor a mayor y podemos pintarlos iterando el TreeSet y utilizando el metodo getLinkText() de la clase Link.

    for (Iterator i = al.iterator(); i.hasNext();) {
        Link l = (Link)i.next();
        w.print(l.getLinkText(page, prefix));
    }

Y desde nuestro JSP, solo tenemos que incluir nuestro taglib y utilizar esta etiqueta sin preocuparnos que sucede en su interior. Así:

<%@taglib uri="pagination" prefix="util"%>

     <%
        Page vuelos = (Page)request.getAttribute("vuelos");
     %>

     <util:pagination attribute="vuelos" baseUrl="/consulaVuelos"/>

Espero que este tutorial haya ayudado a alguien al menos en dos cosas: una es la utilidad que supone listar objetos página dos en si misma y otra, al aprendizaje del buen uso de las coleciones Java como TreeSet y la correcta implementación de métodos básicos como equals(), compareTo() y hashCode() para optimizar nuestro trabajo.
Comentarios, sugerencias y críticas serán bien recibidas. Que lo disfruteis.