lunes, 24 de septiembre de 2012

SpringMVC + JQuery Datatables

Datatables es uno de los plugins de JQuery por excelencia a la hora de mostrar un listado de valores en una tabla. Soporta ordenación por columnas, paginación, filtrado de datos, etc... Y es muy flexible a la hora de ser alimentado de datos, puede hacerse por Html, Ajax, JavaScript Array, Desdel servidor Ajax+Json, etc....

Desde luego para kien empiece en esto lo mejor q puede hacer antes de nada es leer la documentación tanto de jquery como de datatables para empaparse bien del funcionamiento de la API.

Mezclar dicha API con nuestro proyecto Spring en principio no tiene mayor problema (una vez q se ha estudiado su funcionamiento y lo que se quiere lograr), dicho a lo rápido, en nuestro proyecto añadimos las dependencias relativas a JSON; en el jsp hacemos referencia a los archivos .js y .css q nos hemos descargado o bien apuntamos a su URL si están onLine; y finalmente en nuestra clase controller añadimos la etiqueta @ResponseBody en el método que retornará la colección de datos a través de JSON.

¿Que es JSON?, cuando usamos AJAX javascript llama al servidor y el servidor devuelve un XML con los datos a pintar. Si en lugar de eso, devolvemos un JSONObject, en javascript con evaluate() tienes un objeto y podemos usarlo sin necesidad de parseo.

Dicho esto y sin más preludios pasamos a ver las partes clave de código:

1º  Creamos un proyecto web:
$ mvn archetype:create -DarchetypeArtifactId=maven-archetype-webapp -DgroupId=org.nak.testJQuery -DartifactId=testJQuery

En el pom.xml  me decanté por las librerías de google para json, llamadas gson... Como todo, esto q propongo se puede mejorar, usando las librerías jackson y con algunas anotaciones quizás se pueda hacer algo más elegante ya que construyen el JSONObject de forma autmática, pero por desgracia, por la documentación y blogs que he visto no he terminado de controlar su funcionamiento :), asi que optaré por construir el JSONObject de forma manual por ahora, así luce mi archivo pues:

<dependencies>
  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>3.8.1</version>
   <scope>test</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-core</artifactId>
   <version>3.1.2.RELEASE</version>
   <type>jar</type>
   <!-- <version>${org.springframework.version}</version> -->
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-webmvc</artifactId>
   <version>3.1.2.RELEASE</version>
   <!-- <version>${org.springframework.version}</version> -->
  </dependency>
  <dependency>
   <groupId>javax.servlet</groupId>
   <artifactId>servlet-api</artifactId>
   <version>2.5</version>
  </dependency>
  <dependency>
   <groupId>taglibs</groupId>
   <artifactId>standard</artifactId>
   <version>1.1.2</version>
  </dependency>
  <dependency>
   <groupId>log4j</groupId>
   <artifactId>log4j</artifactId>
   <version>1.2.16</version>
  </dependency>
  <!-- jar propios acceso a datos y llamada ws -->
  <dependency>
   <groupId>testNak</groupId>
   <artifactId>dataTest</artifactId>
   <version>1.3</version>
  </dependency>

  <!-- del otro jar para correr el jetty -->

  <!-- Para poder usar hibernate con spring -->
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-orm</artifactId>
   <version>3.1.2.RELEASE</version>
  </dependency>
  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring</artifactId>
   <version>2.5.6</version>
  </dependency>
  <!-- Hibernate framework -->
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate</artifactId>
   <version>3.2.7.ga</version>
  </dependency>
  <!-- Para usar anotaciones con hibernate -->
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-annotations</artifactId>
   <version>3.4.0.GA</version>
  </dependency>
  <!-- mysql conector -->
  <dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>5.1.21</version>
  </dependency>
  <!-- Log4j y Sfl4j -->
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-api</artifactId>
   <version>1.5.8</version>
  </dependency>
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-log4j12</artifactId>
   <version>1.5.8</version>
   <scope>runtime</scope>
  </dependency>

  <dependency>
   <groupId>com.google.code.gson</groupId>
   <artifactId>gson</artifactId>
   <version>2.2.2</version>
  </dependency>

 </dependencies>


Están las dependencias del proyecto web SpringMVC q estamos haciendo, las q acceden a la capa de datos, el jar q hicimos para el acceso a dichos datos, las dependencias de los log... y la última de todas es la q mencionábamos de gson.


2º De la propia web de DataTables nos podemos bajar el API con los css y javaScripts necesarios para montar el artilugio y los alojamos en una carpeta que cuelge directamente de "webapp", podrían llamarse por ejemplo "scripts" y "themes", aunke en el código q voy a poner tengan otros nombres, fruto de multiples pruebas.

Este sería el aspecto del .jsp donde cargaremos la tabla:

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>

        <link href="cssxample/demo_page.css" rel="stylesheet" type="text/css" />
        <link href="cssxample/demo_table.css" rel="stylesheet" type="text/css" />
        <link href="cssxample/demo_table_jui.css" rel="stylesheet" type="text/css" />
        <link href="cssxample/base/jquery-ui.css" rel="stylesheet" type="text/css" media="all" />
        <link href="cssxample/smoothness/jquery-ui-1.7.2.custom.css" rel="stylesheet" type="text/css" media="all" />
        <script src="scriptsxample/jquery.js" type="text/javascript"></script>
        <script src="scriptsxample/jquery.dataTables.min.js" type="text/javascript"></script>

 <script type="text/javascript" charset="utf-8">
 $(document).ready(function () {
  $('#myTable').dataTable(
   "bServerSide" : true,
   "sAjaxSource" : 'doajax.do',
    "bProcessing": true,
             "sPaginationType": "full_numbers",
             "bJQueryUI": true,
             "aoColumns" : [ {
              mDataProp : 'nombre'
          }, {
                mDataProp : 'rut'
          },  {
                mDataProp : 'apellidoMaterno'
          },  {
                mDataProp : 'apellidoPaterno'
          } ]

  });
 });

 </script>



<title>Insert title here</title>
</head>
<body>
 
 
 <table id="myTable">
  <thead>
   <tr>
    <th>nombre</th>
    <th>rut</th>
    <th>apellidoPaterno</th>
    <th>apellidoMaterno</th>
   </tr>
  </thead>
  <tbody />
 </table>

</body>
</html>

Todo dentro de la etiketa <head> las llamadas a los javaScripts y hojas de estilo y luego la función donde parametrizamos el dataTable:

bServerSide: Indica al plugin que hay que sacar la información del lado del servidor.
sAjaxSource: Define la URL que debe ser convocada por el plug-in para tomar datos.
bProcessing (opcional): se utiliza para mostrar el
mensaje "Processing mientras la llamada AJAX se está ejecutando.
bJQueryUI: aplicación de estilos estándar jQueryUI.
sPagination: instruir a los plug-in para generar paginación con números en lugar de dos botones de anterior, siguiente, como se genera por omisión.
aoColumns: Dado que en nuestro caso viene el nombre de los campos junto a su valor correspondiente, debemos hacer un mapeo:
 "aaData":[{"nombre":"Perico","rut":1,"apellidoMaterno":"Moya","apellidoPaterno":"Palotes"}
...]

3º La clase controller captará la llamada url "doajax.do" y retornará un String con lo necesario para cargar la tabla:
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.nak.dataTest.SpringContext;
import org.nak.dataTest.model.Persona;
import org.nak.dataTest.service.PersonaManager;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.google.gson.Gson;
import com.google.gson.JsonObject;


@Controller
public class PersonaController { 

 private static Logger logger = Logger.getLogger(PersonaController.class);

 private SpringContext context;

 private PersonaManager personaManager;

 public PersonaController() {
  super();
  context = SpringContext.getInstance();
  personaManager = (PersonaManager) context.getBean("personaManager");

 }

 @RequestMapping(value = "/doajax.do", method = RequestMethod.GET)
 protected @ResponseBody String doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  
  JQueryDataTableParamModel param = DataTablesParamUtility.getParam(request);
  
  String sEcho = param.sEcho;
  int iTotalRecords; // total number of records (unfiltered)
  int iTotalDisplayRecords; //value will be set when code filters companies by keyword
  
  iTotalRecords = personaManager.obtenerPersonas().size();
  List personas = new LinkedList();
  
  for(Persona p : personaManager.obtenerPersonas()){
   if (p.getNombre().toUpperCase().contains(param.sSearch.toUpperCase()) ||
    p.getApellidoPaterno().toUpperCase().contains(param.sSearch.toUpperCase()) ||
    p.getApellidoMaterno().toUpperCase().contains(param.sSearch.toUpperCase())){
     personas.add(p);
   }
  }
  iTotalDisplayRecords = personas.size();  // number of companies that match search criterion should be returned
  final int sortColumnIndex = param.iSortColumnIndex;
  final int sortDirection = param.sSortDirection.equals("asc") ? -1 : 1;
  
  Collections.sort(personas, new Comparator(){
   public int compare(Persona o1, Persona o2) {
    switch(sortColumnIndex){
    case 0:
     return o1.getNombre().compareTo(o2.getNombre()) * sortDirection;
    case 2:
     return o1.getApellidoPaterno().compareTo(o2.getApellidoPaterno()) * sortDirection;
    case 3:
     return o1.getApellidoMaterno().compareTo(o2.getApellidoMaterno()) * sortDirection;
    }
    return 0;
   }
  });
  
  if(personas.size()< param.iDisplayStart + param.iDisplayLength) {
   personas = personas.subList(param.iDisplayStart, personas.size());
  } else {
   personas = personas.subList(param.iDisplayStart, param.iDisplayStart + param.iDisplayLength);
  }
  
  
   JsonObject jsonResponse = new JsonObject();  
   jsonResponse.addProperty("sEcho", sEcho);
   jsonResponse.addProperty("iTotalRecords", iTotalRecords);
   jsonResponse.addProperty("iTotalDisplayRecords", iTotalDisplayRecords);   
   Gson gson = new Gson();
   jsonResponse.add("aaData", gson.toJsonTree(personas));
   
   System.out.println("Retorno:: "+jsonResponse.toString());
   return jsonResponse.toString();
 }

}

JQueryDataTableParamModel es una especie de bean que contiene las diferentes propiedades del plug-in:
public class JQueryDataTableParamModel {

 /// 
    /// Request sequence number sent by DataTable, same value must be returned in response
    ///        
    public String sEcho;

    /// 
    /// Text used for filtering
    /// 
    public String sSearch;

    /// 
    /// Number of records that should be shown in table
    /// 
    public int iDisplayLength;

    /// 
    /// First record that should be shown(used for paging)
    /// 
    public int iDisplayStart;

    /// 
    /// Number of columns in table
    /// 
    public int iColumns; 

    /// 
    /// Number of columns that are used in sorting
    /// 
    public int iSortingCols;
    
    /// 
    /// Index of the column that is used for sorting
    /// 
    public int iSortColumnIndex;
    
    /// 
    /// Sorting direction "asc" or "desc"
    /// 
    public String sSortDirection;

    /// 
    /// Comma separated list of column names
    /// 
    public String sColumns;

}

DataTablesParamUtility carga los valores que se han pedido en la request:
import javax.servlet.http.HttpServletRequest;

public class DataTablesParamUtility {

 public static JQueryDataTableParamModel getParam(HttpServletRequest request)
 {
  if(request.getParameter("sEcho")!=null && request.getParameter("sEcho")!= "")
  {
   JQueryDataTableParamModel param = new JQueryDataTableParamModel();
   param.sEcho = request.getParameter("sEcho");
   param.sSearch = request.getParameter("sSearch");
   param.sColumns = request.getParameter("sColumns");
   param.iDisplayStart = Integer.parseInt( request.getParameter("iDisplayStart") );
   param.iDisplayLength = Integer.parseInt( request.getParameter("iDisplayLength") );
   param.iColumns = Integer.parseInt( request.getParameter("iColumns") );
   param.iSortingCols = Integer.parseInt( request.getParameter("iSortingCols") );
   param.iSortColumnIndex = Integer.parseInt(request.getParameter("iSortCol_0"));
   param.sSortDirection = request.getParameter("sSortDir_0");
   return param;
  }else
   return null;
 }
}

Mientras que PersonaManager es el manejador que hicimos en el HibernateDAO y q hemos importado desdel pom en este proyecto, encargado en este caso de devolvernos una colección en forma de lista tipada.

4º Finalmente la configuracción del web.xml y el mapeador de Spring no presentan diferencias de lo que sería cualquier otro proyecto hecho con Spring Web MVC:
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
 id="WebApp_ID" version="2.5">
 <display-name>Archetype Created Web Application</display-name>

 <!-- utilizamos el servlet de spring MVC -->
 <servlet>
  <servlet-name>NakSpringMVC</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
 </servlet>
 <!-- declaracion de beans utilizados por spring MVC y asociacion entre vistas 
  y controladores -->
 <context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/NakSpringMVC-servlet.xml</param-value>
 </context-param>

 <!-- Creates the Spring Container shared by all Servlets and Filters -->
      <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>

 <servlet-mapping>
  <servlet-name>NakSpringMVC</servlet-name>
  <url-pattern>*.do</url-pattern>
 </servlet-mapping>

 <welcome-file-list>
  <welcome-file>index.jsp</welcome-file>
 </welcome-file-list>
</web-app>

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
 xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
 xmlns:jee="http://www.springframework.org/schema/jee" xmlns:lang="http://www.springframework.org/schema/lang"
 xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx"
 xmlns:util="http://www.springframework.org/schema/util"
 xsi:schemaLocation="http://www.springframework.org/schema/beans 
  http://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd
  http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
  http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
  http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd
  http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
  http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

 
 <context:component-scan base-package="org.nak.testJQuery.controller" />
 
 <mvc:annotation-driven />
 
 <bean id="jspViewResolver"
  class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass"
   value="org.springframework.web.servlet.view.JstlView" />
  <property name="prefix" value="/WEB-INF/jsp/" />
  <property name="suffix" value=".jsp" />
 </bean>

</beans>


Este debe ser el resultado final:











Y aki adjunto el código fuente, veréis en el más jsp, librerías y controllers a parte de lo que os he comentado arriba, la razón de ello es que probé varias fórmulas para conseguir esto, todas son válidas y os pueden servir de apoyo.


No hay comentarios: