lunes, febrero 25, 2013

Control de acceso a datos automatizado con PrimeFaces (Paso a paso)

Estimad@s,

siguiendo con los "minitutos" de PrimeFaces veremos ahora como mejorar la tabla con datos de ventas que hemos desarrollado en este tutorial y también haciendo uso del "pseudo" sistema de control de acceso que desarrollamos en este tutorial.

Modificando la lógica de validación.

La idea general es que en base al usuario que se haya logueado se filtren los datos sin modificar la vista en absoluto. Para conseguir este objetivo comenzaremos modificando "LoginBean.java" agregando al principio de la clase el siguiente código:

public class LoginBean implements Serializable {
  private static final long serialVersionUID = -2152389656664659476L;


  private static String [][] users;
  static {
    users=new String[101][2];
    users[0][0]="admin";
    users[0][1]="admin";
    for (int t=1;t<101;t++){
      users[t][0]="Cliente "+t;
      users[t][1]="Clave "+t;
    }
  }


  private static boolean usuarioValido(String nombre, String clave) {
    if(nombre == null || clave == null)
      return false;
    for(int t=0; t<users.length; t++) {
      if(users[t][0].equals(nombre) && users[t][1].equals(clave))
      return true;
    }
    return false;
  }
...
...
  public void login(ActionEvent actionEvent) {
    RequestContext context = RequestContext.getCurrentInstance();
    FacesMessage msg = null;
    if (usuarioValido(nombre,clave)) {
      logeado = true;
      msg = new FacesMessage(FacesMessage.SEVERITY_INFO, "Bienvenid@", 
nombre);

    } else {
      logeado = false;
      msg = new FacesMessage(FacesMessage.SEVERITY_WARN, "Login Error", 
"Credenciales no válidas");
    }
...
...

Bien, el código nuevo reemplaza el algoritmo de verificación de cuenta, antes solo admitía el usuario "admin" con la clave "admin", ahora sigue admitiendo al mismo usuario, pero además se crean 100 cuentas más cuyos nombres son "Cliente 1", "Cliente 2", ... , "Cliente 100" y cuyas respectivas claves son: "Clave 1", "Clave 2", ... , "Clave 100". Hay que tener en cuenta que se generan con espacios en blanco, tanto el nombre como la clave de cada uno de esos usuarios.
Algo fundamental para lo que veremos más adelante, es que los nombres de usuario generados, coinciden exactamente con los nombres de clientes que tenemos en la base de datos.

La validación en si la derivamos a un método llamado usuarioValido(), el método es por demás sencillo. Luego reemplazamos la lógica del if(...) que se encuentra en el método login() para hacer uso del nuevo algoritmo de validación.


Aplicando el filtrado automático.

Hasta aquí no hemos hecho nada para que los datos se filtren automáticamente, a esto lo haremos en el componente que mantiene las listas de ventas en el servidor "VentasBean.java":

Lo primero es hacer que la lista ya no se genere para todos igual, para ello comentaremos el llamando en el constructor de la clase:

public VentasBean() {
  //processList(null);
}


Lo siguiente es modificar la sentencia sql con la que se obtienen los datos para añadirle un parámetro de filtrado:


private String sql = "SELECT year(fecha) as anio, month(fecha) as mes, zona, cliente, sum(importe*cantidad) as ventas FROM dw_ventasfact v INNER JOIN clientes c ON v.idCliente=c.idCliente INNER JOIN zonas z ON z.idZona=c.idZona WHERE cliente like ? GROUP BY zona, cliente, anio, mes ORDER BY anio,mes,zona,cliente";



Filtramos las ventas por nombre de cliente, utilizaremos el nombre del usuario logueado para reemplazar el parámetro ?, en el caso de ser "admin" el que se loguea se utilizar se utilizará el comodín '%' lo que implica que "admin" verá la lista completa. A esto lo hace el método que encapsula la lista con las ventas getVentas() que al inicio crea la lista solo si no existe, esto mejora la respuesta, ya que solo se calcula una vez por usuario, la desventaja es que si los datos cambian, este cambio no se verá reflejado.

public List<Venta> getVentas() {
  if(ventas==null){
    HttpSession session = (HttpSession) FacesContext.getCurrentInstance()
                                       .getExternalContext().getSession(false);

    LoginBean loginBean = (LoginBean) session.getAttribute("loginBean");
    String parametroNombre=loginBean.getNombre();
    if(parametroNombre.equals("admin")){
      parametroNombre="%";
    }
    processList(new String[]{parametroNombre});
  }

  return ventas;
}

El método ahora funciona como un singleton por sesión, esto es, solo existirá una lista por sesión de usuario. Con las tres líneas que siguen obtenemos el nombre del usuario logueado:

HttpSession session = (HttpSession) FacesContext.getCurrentInstance()                                   .getExternalContext().getSession(false);

LoginBean loginBean = (LoginBean) session.getAttribute("loginBean");
String parametroNombre=loginBean.getNombre();

Luego, si se trata de "admin" colocamos en el nombre el comodín '%', de esta manera la variable parametroNombre contendrá el nombre del usuario logueado o, si se trata de "admin", el comodín, en otras palabras, contendrá el valor exacto que esperamos en el parámetro de la consulta. Finalmente llamamos a processList() con ese valor de parámetro.

Ahora si probamos logearnos, por ejemplo con "Cliente 3"/"Clave 3", veremos que la lista se filtra automáticamente como se ve en la siguiente figura:



Podemos probar con cualquier "Cliente 1" a "Cliente 100" o "admin" para ver la lista completa. El resto sigue funcionando como antes.

Conclusión:

Lo que hemos visto demuestra la potencia del encapsulamiento y la las capas de abstracción que promueven los frameworks como JSF y PrimeFaces. Aún falta mucho por recorrer y, definitivamente, muchas mejorar por implementa.


Eso es todo, recuerden que disponen del proyecto eclipse completo en: https://github.com/magm3333/workspace-pftuto
Saludos

Mariano

viernes, febrero 22, 2013

Pantalla principal con menú estilo dock con PrimeFaces (Paso a paso)

Estimad@s,

seguimos con PrimeFaces, ahora vamos a crear nuestra pantalla principal, la cual contendrá un menú al estilo Dock:


Implementaremos en esta ocasión, las tres opciones disponibles Ventas|Gauge|Logout, luego, en el futuro, tendremos que agregar alguno más.
Lo primero es eliminar código que ya no usaremos, se trata de la opción de logout de "gauge.xhtml", se deberá eliminar el código que coloco en color rojo y lo que coloco en color azul, lo que coloco en color azul, en realidad lo eliminaremos de este archivo y lo pegaremos en otro.

<html xmlns="http://www.w3c.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:p="http://primefaces.org/ui">
<h:head>
</h:head>
<h:body>
<p:commandLink id="logout" actionListener="#{loginBean.logout}"
style="margin-right:20px;" oncomplete="logout(xhr, status, args)">
<h:outputText value="logout" />
</p:commandLink>
<h:form id="formGauge">
<p:poll interval="2" update="gauge" />
<p:meterGaugeChart id="gauge" value="#{gaugeBean.meterGaugeModel}"
showTickLabels="false" labelHeightAdjust="110"
intervalOuterRadius="130"
seriesColors="66cc66, 93b75f, E7E658, cc6666"
style="width:400px;height:250px" title="Gauge Personalizado"
label="km/h" />
</h:form>
</h:body>
<script type="text/javascript">
//<![CDATA[
function logout(xhr, status, args) {

setTimeout(function() {
window.location = 'login.xhtml';
}, 500);
}
//]]>
</script>
</html>

Luego cambiaremos parte de una línea en LoginBean.java (lo que está en negrita es lo que debe quedar):

...

context.addCallbackParam("estaLogeado", logeado);
if (logeado)
context.addCallbackParam("view", "menu.xhtml");
}
...

Luego agregamos el archivo "menu.xhtml":


<html xmlns="http://www.w3c.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:p="http://primefaces.org/ui">
<h:head>
</h:head>
<h:body>
<p:layout fullPage="true">
<p:layoutUnit position="center">
<iframe id="frame" src="ventas.xhtml"
style="width: 100%; height: 100%; text-align: center;"
seamless='seamless' />
</p:layoutUnit>

</p:layout>
<h:form id="form">
<p:dock>
<p:menuitem value="Ventas" icon="/images/ventas.png"
url="javascript:cambioPagina('ventas.xhtml')" />
<p:menuitem value="Gauge" icon="/images/gauge.jpg"
url="javascript:cambioPagina('gauge.xhtml')" />
<p:menuitem value="Logout" icon="/images/logout.png"
actionListener="#{loginBean.logout}"
oncomplete="logout(xhr, status, args)" />
</p:dock>
</h:form>
</h:body>
<script type="text/javascript">
//<![CDATA[
var actual = 'ventas.xhtml';
function cambioPagina(pagina) {
if (pagina != actual) {
$('#frame').attr('src', pagina);
actual=pagina;
}
}
function logout(xhr, status, args) {

setTimeout(function() {
window.location = 'login.xhtml';
}, 500);

}
//]]>
</script>
</html>

El código importante está en negrita y lo examinamos a continuación:

Los tags a continuación:
<p:layout fullPage="true">
  <p:layoutUnit position="center">
    <iframe id="frame" src="ventas.xhtml"
      style="width: 100%; height: 100%; text-align: center;"
      seamless='seamless' />
  </p:layoutUnit>
</p:layout>

Generan un área de contenido al centro que a su vez contiene un iframe que será el contenedor que muestre la página seleccionada, por defecto es "ventas.xhtml", que más decir de este iframe, que no tiene bordes (seamless='seamless'), que ocupa el 100% de ancho y alto.

Luego tenemos:
<h:form id="form">
  <p:dock>
    <p:menuitem value="Ventas" icon="/images/ventas.png"
      url="javascript:cambioPagina('ventas.xhtml')" />
    <p:menuitem value="Gauge" icon="/images/gauge.jpg"
      url="javascript:cambioPagina('gauge.xhtml')" />
    <p:menuitem value="Logout" icon="/images/logout.png"
      actionListener="#{loginBean.logout}"
      oncomplete="logout(xhr, status, args)" />
   </p:dock>
</h:form>

El tag dock permite armar un menú al estilo Dock, por defecto en la parte inferior de la pantalla, aunque esto se puede cambiar sin problemas, un menu dock está compuesto de items (menuitem) con un nombre (atributo value) y un icono, luego podemos hacer que ejecute acciones al hacerle click, en este caso hay dos tipos de acciones, en el caso de Ventas y Gauge se ejecuta una función javascript que carga una página en el iframe, en el caso del logout, solo he trasladado la lógica del comando remoto que estaba en "gauge.xhtml" a un menuitem, esto ya lo he explicado en su momento.

La función cambioPagina() es muy sencilla:
var actual = 'ventas.xhtml';
function cambioPagina(pagina) {
  if (pagina != actual) {
    $('#frame').attr('src', pagina);
    actual=pagina;
  }
}
Solo carga la página recibida como argumento en el iframe utilizando un selector de jQuery (con PrimeFaces disponemos siempre de jQuery en el lado del cliente), esto ocurre siempre y cuando la página no se encuentre carga, para eso se utiliza la variable auxiliar actual.

Eso es todo, recuerden que disponen del proyecto eclipse completo en: https://github.com/magm3333/workspace-pftuto

La imágenes de los iconos están aquí, aquí y aquí.
Saludos

Mariano

miércoles, febrero 20, 2013

Una tabla con datos de una base de datos MySQL con PrimeFaces (Paso a paso)

Hola gente,

una vez más con PrimeFaces, esta vez iremos un poco más allá, crearemos una tabla que mostrará datos de una base de datos MySQL. Además está tabla permitirá ordenar y filtrar y paginar los datos. Un "chiche!".

Como en el tutorial anterior, nos basaremos en el proyecto inicial creado en el post: "Primeros pasos con PrimeFaces, Eclipse y Tomcat (Paso a paso)"

Vamos a comenzar con una modificando el Filtro (LoginFilter.java) y nos aseguramos que la nueva versión contenga las líneas que muestro en negrita a continuación:

private boolean noProteger(String urlStr) {
  /*
  * Este es un buen lugar para colocar y programar todos los patrones que
  * creamos convenientes para determinar cuales de los recursos no
  * requieren protección. Sin duda que habría que crear un mecanizmo tal
  * que se obtengan de un archivo de configuración o algo que no requiera
  * compilación.
  */
  if (urlStr.indexOf("/login.xhtml")!= -1)
    return true;

  if (urlStr.indexOf("/javax.faces.resource/") != -1)
    return true;
  return false;
}


Luego en la clase LoginBean

  FacesContext.getCurrentInstance().addMessage(null, msg);
  context.addCallbackParam("estaLogeado", logeado);
  if (logeado)
    context.addCallbackParam("view", "ventas.xhtml");
}


A raíz de esta modificación, ya estoy arrepentido de haber hecho el post anterior ya que la intención fue dar una idea genérica de como se hace para proteger recursos mediante algún sistema de control de acceso con artefactos Web Java, pero por simplista he caído en lo burdo, a este sistema le falta mucho y las falencias son grandes y es debido a que no se trata de un tema trivial, veré si en el futuro me redimo y vemos algo un poco más adecuado a la realidad, por ejemplo Spring Security, en fin ya veremos las ganas y el tiempo que son dos cosas que no regalan ni se compran.

Bien hecha la mea culpa comencemos.

Creando un DataSource accesible mediante JNDI

Para definir un recurso de este tipo, solo debemos crear un archivo xml llamada "context.xml" en la carpeta WebContent/META-INF cuyo contenido debe ser:

<?xml version="1.0" encoding="UTF-8"?>
<Context>
  <Resource auth="Container" description="BD Practico" 

     name="jdbc/practico" type="javax.sql.DataSource" 
     password="xxxxx" driverClassName="com.mysql.jdbc.Driver"
     maxIdle="2" maxWait="5000" validationQuery="select 1"
     username="root" url="jdbc:mysql://localhost:3306/practico" maxActive="4" />
  <WatchedResource>WEB-INF/web.xml</WatchedResource>
  <WatchedResource>META-INF/context.xml</WatchedResource>
</Context>

Sin entrar en mucho detalle acerca de la definición del pool de conexiones, solo diremos que los datos principales que contiene este archivo son aquellos referidos a los que requiere el driver JDBC los cuales he resaltado en negrita, un atributo fundamental es el nombre de recurso que estamos creado que es "jdbc/practico" y colocar la password correcta.
Una vez que disponemos de este archivo debemos colocar en el classpath el driver JDBC de MySQL que pueden descargar desde aquí: http://cdn.mysql.com/Downloads/Connector-J/mysql-connector-java-5.1.23.zip, una vez que descarguen este archivo deben descomprimir solo mysql-connector-java-5.1.23-bin.jar que se encuentra dentro del zip y copiarlo o linkearlo (como ya hemos visto) dentro de la carpeta WebContent/WEB-INF/lib

Obteniendo una conexión del pool y alacenandola en el contexto global de la aplicación

Ya hemos realizado la configuración necesaria para que Tomcat cree un pool de conexiones al iniciar, es nuestro trabajo ahora obtener una referencia a ese pool y pedirle una conexión a la base de datos para ser utilizada en la aplicación. Es buena práctica hacer esto una sola vez y al inicio de la aplicación. Afortunadamente la especificación web de java nos provee de una serie de herramientas para poder hacerlo, en este caso haremos uso de los métodos callback de control de ciclo de vida, también se los llama listeners, usaremos particularmente el listener a nivel de contexto, el cual posee dos métodos que son llamados por el contenedor (tomcat) cuando se inicia y cuando finaliza.

Crear un listener es muy sencillo, se trata de una clase java que debe implementar para este caso la interface ServletContextListener y estar marcado con la anotación @WebListener, el código se muestra a continuación:

package ar.com.magm.web.listeners;

import java.sql.Connection;
import java.sql.SQLException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import javax.sql.DataSource;

@WebListener
public class InitListener implements ServletContextListener {
  public InitListener() { }

  public void contextInitialized(ServletContextEvent sce) {
    DataSource fuenteDatos = null;
    Context ctx;
    try {
      ServletContext sc = sce.getServletContext();
      ctx = new InitialContext();

      fuenteDatos = (DataSource) ctx.lookup("java:comp/env/jdbc/practico");

      Connection cn = fuenteDatos.getConnection();

      sc.setAttribute("datasource", cn);

    } catch (NamingException e) {
      throw new RuntimeException(e.getMessage());
    } catch (SQLException e) {
      throw new RuntimeException(e.getMessage());
    }
  }


  public void contextDestroyed(ServletContextEvent sce) { }
}

En las líneas:
ServletContext sc = sce.getServletContext();
ctx = new InitialContext();
fuenteDatos = (DataSource) ctx.lookup("java:comp/env/jdbc/practico");

Obtenemos una referencia al recurso mediante JNDI y el nombre que establecimos en el archivo de configuración.

La línea:
Connection cn = fuenteDatos.getConnection();
Obtiene la conexión.

La línea: 
sc.setAttribute("datasource", cn);
almacena en el contexto global de la aplicación la instancia de la conexión con el nombre datasource.

Una vez que contamos con este listener es muy sencillo acceder desde la aplicación a esta conexión.

Bien hasta aquí no hemos tocado un tema importante, a que base de datos nos conectamos, pues bien es una base de datos que he utilizado desde hace años para diversos ejemplos y la pueden descargar desde aquí, el archivo es BDDump.sql y se encuentra dentro de archivos.zip. Una vez descargado el zip y descomprimido el archivo .sql, y desde una consola escribimos:

$ mysql --user=UnUsuarioVálido --password=LaClave < BDDump.sql

Esto creará la base de datos practico en la instancia de MySQL.

Creando la vista del usuario

Ahora crearemos la vista del usuario, se trata como ya hemos visto antes de un archivo .xhtml, en este caso "ventas.xhtml", en este archivo crearemos una tabla en la cual mostraremos información de ventas que saldrán de la siguiente consulta SQL:
SELECT 
  YEAR(fecha) AS anio, 
  MONTH(fecha) AS mes, 
  zona, 
  cliente, 
  SUM(importe*cantidad) AS ventas 
FROM 
  dw_ventasfact v INNER JOIN clientes c ON v.idCliente=c.idCliente 
                  INNER JOIN zonas z ON z.idZona=c.idZona 
GROUP BY 
  zona, 
  cliente, 
  anio, 
  mes 
ORDER BY 
  anio,
  mes,
  zona,
  cliente


Para nuestro caso una venta tiene los atributos que se muestran en la imagen, o sea: año, mes, zona, cliente e importe de venta. Para representar cada venta (cada hecho), o cada fila que retorne la consulta, crearemos una clase java.
Ahora veremos el código de la vista, o sea del archivo "ventas.xhtml":


<html xmlns="http://www.w3c.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:p="http://primefaces.org/ui">
<h:head></h:head>
<h:body style="text-align:center">
  <h:form>


    <p:dataTable id="tablaDeDatos" var="venta"
      value="#{ventasBean.ventas}" widgetVar="tablaDeVentas"
      emptyMessage="No hay ventas con este criterio de filtrado"
      filteredValue="#{ventasBean.ventasFiltradas}" paginator="true"
      rows="10" style="width:800px">

      <f:facet name="header">
        <p:outputPanel>
          <h:outputText value="Buscar en todos:" />
          <p:inputText id="globalFilter" onkeyup="tablaDeVentas.filter()" 
style="width:150px" />

        </p:outputPanel>
      </f:facet>

      <p:column id="zonaCol" filterBy="#{venta.zona}" headerText="Zona"
        filterMatchMode="exact" filterOptions="#{ventasBean.zonasOptions}">
        <h:outputText value="#{venta.zona}" />
      </p:column>

      <p:column id="clienteCol" filterBy="#{venta.cliente}"
        headerText="Cliente" filterMatchMode="startsWith">
        <h:outputText value="#{venta.cliente}" />
      </p:column>


      <p:column id="anioCol" filterBy="#{venta.anio}" headerText="Año"
        filterMatchMode="startsWith">
        <h:outputText value="#{venta.anio}" />
      </p:column>

      <p:column id="mesCol" filterBy="#{venta.mesLetra}" headerText="Mes"
        filterMatchMode="exact" filterOptions="#{ventasBean.mesesOptions}">
        <h:outputText value="#{venta.mesLetra}" />
      </p:column>

      <p:column id="ventasCol" headerText="Importe Venta" style="text-align:right">
        <h:outputText value="#{venta.ventaFormat}"/>
      </p:column>

    </p:dataTable>
  </h:form>

</h:body>
</html>

El resultado final se verá así:


Bien, ese es todo el código de la vista del cliente, la analizaremos por partes:

Este tag:
    <p:dataTable id="tablaDeDatos" var="venta"
      value="#{ventasBean.ventas}" widgetVar="tablaDeVentas"
      emptyMessage="No hay ventas con este criterio de filtrado"
      filteredValue="#{ventasBean.ventasFiltradas}" paginator="true"
      rows="10" style="width:800px">
define la tabla de datos que se llama "tablaDeDatos" (id="tablaDeDatos"), los valores que se mostrarán en la tabla se obtendrán de: ventasBean.ventas (value="#{ventasBean.ventas}") que es una lista de instancias de Venta que es la clase java que representa solo una venta y que aún no hemos creado, por cada instancia que se recorra de la lista se crea una variable llamada venta (var="venta"). La lista que se encuentra en ventasBean.ventas contiene todas las ventas que obtenemos de la consulta SQL, este componente permite que se apliuen filtros sobre esa lista y lo implementa utilizando una segunda lista, esta es: ventasBean.ventasFiltradas (filteredValue="#{ventasBean.ventasFiltradas}") lo bueno de esto es que no tenemos que preocuparnos mas que por definir esta lista en el server, PrimeFaces la mantiene por nosotros. La lista se paginará automáticamente y mostrará 10 filas por página, el ancho de la lista será de 800 píxeles (paginator="true" rows="10" style="width:800px"). También se podrá realizar una búsqueda global (en cualquier columna), está búsqueda y filtrado se produce en el cliente, por ello debemos definir el nombre que tendrá el elemento html (el widget) en el cliente y este será: tablaDeVentas (value="#{ventasBean.ventas}"). Por último diremos que si no existen items ante algún criterio de búsqueda se mostrará el mensaje: "No hay ventas con este criterio de filtrado" (emptyMessage="No hay ventas con este criterio de filtrado").

El filtro que mencioné en el último párrafo se fine con el tag:
 <p:inputText id="globalFilter" onkeyup="tablaDeVentas.filter()"   
   style="width:150px" />
El widget tablaDeVentas posee automáticamente un método llamada filter(), al cual se llamará cada vez que finalice la presión de una tecla (onkeyup), el método de filtrado descartará todas las filas en las cuales no se encuentre en algún lugar el valor que escribamos en el campo de entrada con id="globalFilter", esto es siempre así, el id debe ser ese y ningún otro, esto la verdad que no me gusta demasiado, pero en fin...

Ahora solo resta definir las columnas que mostraremos en esta tabla de datos.

La columna que muestra la zona se define el el tag:

<p:column id="zonaCol" filterBy="#{venta.zona}" headerText="Zona"
  filterMatchMode="exact" filterOptions="#{ventasBean.zonasOptions}">
  <h:outputText value="#{venta.zona}" />
</p:column>

Para todas las columnas definiremos un id, en este caso id="zonaCol", también el texto que se mostrará en la cabecera, en este caso: headerText="Zona", también definiemos cual es el componente que renderizará el valor en cada celda, en este caso lo hacemos con el tag:
<h:outputText value="#{venta.zona}" />
donde podemos ver que se utiliza el atributo zona del bean venta, recordemos que este bean lo definimos cuando definimos la tabla (var="venta"). 
No entraré en detalle sobre estos atributos para las próximas columnas.

Particularmente para la columna zona definimos un filtro que será una lista desplegable que contiene todas las zonas posibles, a esto lo hacemos en: filterOptions="#{ventasBean.zonasOptions}", el bean ventasBean poseerá una lista especial llamada zonasOptions que otorgará la lista de zonas que mencionamos. Además definimos que cada vez que se selccione una zona se realice una búsqueda por exactamente igual en: filterMatchMode="exact". El filtro en acción se ve así:


La columna que muestra el cliente se define el el tag:

<p:column id="clienteCol" filterBy="#{venta.cliente}"
  headerText="Cliente" filterMatchMode="startsWith">
  <h:outputText value="#{venta.cliente}" />
</p:column>

La diferencia más grande (amén del valor que se muestra) es el filtro, solo hemos definido que se buscará todos aquellos valores que comiencen con: filterMatchMode="startsWith", si no se define otra cosa se utiliza un campo de texto en el cual debemos escribir el criterio de filtrado como se ve en la siguiente imagen:


El resto de las columnas están definidas utilizando conceptos que hemos explicado ya, por ello omitiré explicaciones al respecto.

El bean que representa una venta.

Como ya he comentado antes, crearemos una clase java que representará cada venta y que instanciaremos en el bean que posee las listas de ventas más adelante.

La clase se llama: ar.com.magm.model.Venta y el código es:

package ar.com.magm.model;

import java.io.Serializable;
import java.text.DecimalFormat;

public class Venta implements Serializable {
  private static final long serialVersionUID = 8060348552656940209L;

  public static long getSerialversionuid() {
    return serialVersionUID;
  }

  private int anio;
  private String cliente;
  private int mes;
  private String mesLetra;
  private double venta;
  private String zona;

  public Venta(String zona, String cliente, int anio, int mes,
               String mesLetra, double venta) {
    super();
    this.zona = zona;
    this.cliente = cliente;
    this.anio = anio;
    this.mes = mes;
    this.mesLetra = mesLetra;
    this.venta = venta;
  }

  public int getAnio() {
    return anio;
  }
  public String getCliente() {
    return cliente;
  }
  public int getMes() {
    return mes;
  }
  public String getMesLetra() {
    return mesLetra;
  }
  public double getVenta() {
    return venta;
  }

  public String getVentaFormat() {
    DecimalFormat df = new DecimalFormat("0.00");
    return df.format(venta);
  }

  public String getZona() {
    return zona;
  }
}

No hay mucho que decir de este bean, solo que tenemos un par de getters especiales, por un lago uno que nos retorna el nombre del mes getMesLetra() y el importe formateado getVentaFormat().

El bean manejado por faces que mantiene las listas de ventas.

Bien, es hora de crear el controlador que es una clase llamada ar.com.magm.web.primefaces.VentasBean y cuya configuración en faces-config.xml es: 

<managed-bean>
  <managed-bean-name>ventasBean</managed-bean-name>
  <managed-bean-class>ar.com.magm.web.primefaces.VentasBean</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

Notemos que el scope de este bean es session, si la lista fuese común a todos los usuarios, el scope correcto sería application ya que no sería necesario crear más instancias. En algún post futuro haremos uso de esto.

El código completo del bean es el siguiente:

package ar.com.magm.web.primefaces;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.faces.context.FacesContext;
import javax.faces.model.SelectItem;
import javax.servlet.ServletContext;
import ar.com.magm.model.Venta;

public class VentasBean implements Serializable {
  private static final long serialVersionUID = -6690574219803425728L;

  private String[] meses = new String[] { "Enero", "Febrero", "Marzo",
    "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre",
    "Octubre", "Noviembre", "Diciembre" };

  private String sql = "SELECT year(fecha) as anio, month(fecha) as mes, zona, cliente, sum(importe*cantidad) as ventas FROM dw_ventasfact v INNER JOIN clientes c ON v.idCliente=c.idCliente INNER JOIN zonas z ON z.idZona=c.idZona GROUP BY zona, cliente, anio, mes ORDER BY anio,mes,zona,cliente";
  private List<Venta> ventas;
  private List<Venta> ventasFiltradas;
  private List<String> zonas;

  public VentasBean() {
    processList(null);
  }

  public SelectItem[] getMesesOptions() {
    SelectItem[] r = new SelectItem[13];
    r[0] = new SelectItem("", "Todos");
    for (int t = 0; t < meses.length; t++)
      r[t + 1] = new SelectItem(meses[t], meses[t]);
    return r;
  }

  public List<Venta> getVentas() {
    return ventas;
  }

  public List<Venta> getVentasFiltradas() {
    return ventasFiltradas;
  }

  public SelectItem[] getZonasOptions() {
    SelectItem[] r = new SelectItem[zonas.size() + 1];
    r[0] = new SelectItem("", "Todas");
    for (int t = 0; t < zonas.size(); t++)
      r[t + 1] = new SelectItem(zonas.get(t), zonas.get(t));
    return r;
  }

  private void processList(Object args[]) {
    ventas = new ArrayList<Venta>();
    zonas = new ArrayList<String>();
    ServletContext sc = (ServletContext) FacesContext.getCurrentInstance()
                                        .getExternalContext().getContext();
    Connection cn = (Connection) sc.getAttribute("datasource");
    try {
      PreparedStatement pst = cn.prepareStatement(sql);
      if (args != null) {
        for (int t = 0; t < args.length; t++) {
          pst.setObject(t + 1, args[t]);
        }
      }
      ResultSet rs = pst.executeQuery();
      while (rs.next()) {
        String zona = rs.getString("zona");
        Venta venta = new Venta(zona, rs.getString("cliente"),
                                rs.getInt("anio"), rs.getInt("mes"),
                                meses[rs.getInt("mes") - 1], 
                                rs.getDouble("ventas"));
        ventas.add(venta);
        if (!zonas.contains(zona))
          zonas.add(zona);
      }
    } catch (SQLException e) {
      e.printStackTrace();
    }
  }

  public void setVentasFiltradas(List<Venta> ventasFiltradas) {
    this.ventasFiltradas = ventasFiltradas;
  }
}

Como pueden ver en el constructor se llama al método que llena la lista de ventas (List<Venta> ventasprocessList(), lo hace en base a los datos obtenidos de la consulta sql además llena una lista con las zonas (List<String> zonas). También es necesario mencionar como obtenemos la conexión que almacenamos en el contexto general, primero obtenemos el contexto:
ServletContext sc = (ServletContext) FacesContext.getCurrentInstance()
                                     .getExternalContext().getContext();
Luego usamos el contexto para obtener la conexión:
Connection cn = (Connection) sc.getAttribute("datasource");

La lista List<Venta> ventasFiltrada junto a los métodos setVentasFiltradas() y getVentasFiltradas() es lo único que necesitamos para que los filtros se procesen correctamente del lado del server.

El método getVentas() es el fundamental, el que provee los datos iniciales de la lista de ventas y el que se utiliza para generar las listas filtradas.

Por último los métodos getMesesOptions() y getZonasOptions() retornan un arreglo de objetos SelectItem que utilizará el componente tabla para renderizar los filtros de las columnas de zona y mes.

Bien, esto es todo por ahora, recuerden que disponen del proyecto eclipse completo en: https://github.com/magm3333/workspace-pftuto
Saludos

Mariano





domingo, febrero 17, 2013

Instalando Graphite en Ubuntu 12.04 LTS (Power script by Ale!)

Hola Gente,

ni más ni menos que lo que reza el titulo.

Debo agradecer antes que nada al creador del script, Gracias Ale!, a popósito visiten su blog, es genial.

No tengo mucha que decir, solo que este script ha sido probado en Ubuntu 12.04 recién instalado.

Pasos a seguir para instalar graphite mediante el script:

1) descargar el script desde aquí
2) una vez descargado y desde una consola:
chmod +x instalar_graphite.sh

y luego

sudo ./instalar_graphite.sh

Solo deberán establecer la clave para el superusuario de django.

3) Generar una métrica:

echo "test.metric:1|c" | nc -w 1 -u 127.0.0.1 8125

4) Ver como quedó ingresando con el browser a: http://127.0.0.1 y luego seleccionar: Graphite.stats.test.metric




Bueno, ya estamos listos para disfrutar de Graphite!

Saludos

Mariano

jueves, febrero 14, 2013

Login y control de acceso básico con PrimeFaces (Paso a paso)

Hola Gente,

vamos a ver como hacer un formulario de login y un control de acceso utilizando PrimeFaces.
No usaremos bases de batos ni lógicas complejas, solo se trata de que un usuario no pueda acceder a ningún recurso sino no está logueado en el sistema.

Nos basamos en el post anterior "Primeros pasos con PrimeFaces, Eclipse y Tomcat (Paso a paso)", ya no entraré en tanto detalle de como realizar ciertas tareas que se encuentran explicadas en el post anterior.


Creando la vista:

Debemos crear un archivo en "WebContent", o sea en el root de nuestra aplicación, llamado "login.xhtml"

El contenido es el siguiente:

<html xmlns="http://www.w3c.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:p="http://primefaces.org/ui">
<h:head>
</h:head>
<h:body style="text-align:center">
  <p:growl id="mensajes" showDetail="true" life="2000" />
  <h:form>
    <p:panel header="Login" style="width:300px">
      <h:panelGrid columns="2" cellpadding="5">
        <h:outputLabel for="username" value="Usuario:" />
        <p:inputText value="#{loginBean.nombre}" id="username"
           required="true" label="username" />
        <h:outputLabel for="password" value="Clave:" />
        <p:password value="#{loginBean.clave}" id="password" required="true"
           label="password" />
        <f:facet name="footer">
          <p:commandButton id="loginButton" value="Login"
             actionListener="#{loginBean.login}" update=":mensajes"
             oncomplete="manejarLogin(xhr, status, args)" />

        </f:facet>
      </h:panelGrid>
    </p:panel>
  </h:form>
</h:body>
<script type="text/javascript">
  //<![CDATA[
  function manejarLogin(xhr, status, args) {
    if (!args.validationFailed && args.estaLogeado) {
      setTimeout(function() {
        window.location = args.view;
      }, 500);
    }
  }

//]]>
</script>
</html>


En un navegador, este código produce la siguiente vista:



Analicemos ahora un poco el código:

<p:growl id="mensajes" showDetail="true" life="2000" />

Se utiliza para mostrar mensajes emergentes, los generaremos desde loginBean, por ejemplo:


<p:inputText value="#{loginBean.nombre}" id="username"
   required="true" label="username" />


Este tag representa el campo que contiene el nombre del usuario que una vez que sea transmitido (submit) establecerá el valor en un bean, loginBean, del lado del servidor mediante el llamado al método setNombre() por el atributo nombre. Es un campo obligatorio, esto es, no puede estar en blanco.

<p:password value="#{loginBean.clave}" id="password" required="true"
   label="password" />


Este tag anterior trabaja similar al de nombre pero con la clave, además el campo no mostrará lo que se tipea.

Por último:

<p:commandButton id="loginButton" value="Login"
   actionListener="#{loginBean.login}" update=":mensajes"
   oncomplete="manejarLogin(xhr, status, args)" />


Es el botón que envía los campos al server (nombre y clave) y ejecuta el método login() de loginBean, una vez que se ejecuta al método mencionado se actualiza el componente de mensajes emergentes para mostrar los mensajes que se hayan acumulado, además ejecuta el script cliente manejarLogin(xhr, status, args). Las líneas más importantes del método son:

if (!args.validationFailed && args.estaLogeado) {

Nos aseguramos que el proceso de validación no haya fallado y que el usuario esté logueado. Si el usuario está logueado correctamente se ejecuta:

window.location = args.view;
donde args, es una selección de parámetros que recibiremos del servidor, en este caso nos envía el parámetro view con el nombre de la vista que debe ser cargada. A esto lo hacemos con un retardo para que se puedan mostrar los mensajes emergentes de bienvenida.


Creando el componente del servidor

Ya hemos visto como se realiza esta operación, debemos crear una clase java llamada ar.com.magm.web.primefaces.LoginBean, que debe ser luego agregada al faces-config.xml como bean de sesión, el código que para hacerlo es:

<managed-bean>
  <managed-bean-name>loginBean</managed-bean-name>
  <managed-bean-class>ar.com.magm.web.primefaces.LoginBean</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

Para poder copiar el código anterior, es necesario seleccionar la pestaña "Source" del editor del archivo de configuración de faces.

El código de la clase recién creada es el siguiente:

package ar.com.magm.web.primefaces;

import java.io.Serializable;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import javax.servlet.http.HttpSession;
import org.primefaces.context.RequestContext;

public class LoginBean implements Serializable {
  private static final long serialVersionUID = -2152389656664659476L;
  private String nombre;
  private String clave;
  private boolean logeado = false;

  public boolean estaLogeado() {
    return logeado;
  }

  public String getNombre() {
    return nombre;
  }

  public void setNombre(String nombre) {
    this.nombre = nombre;
  }

  public String getClave() {
    return clave;
  }

  public void setClave(String clave) {
    this.clave = clave;
  }

  public void login(ActionEvent actionEvent) {
    RequestContext context = RequestContext.getCurrentInstance();
    FacesMessage msg = null;
    if (nombre != null && nombre.equals("admin") && clave != null
        && clave.equals("admin")) {
      logeado = true;
      msg = new FacesMessage(FacesMessage.SEVERITY_INFO, "Bienvenid@",
nombre);
    } else {
      logeado = false;
      msg = new FacesMessage(FacesMessage.SEVERITY_WARN, "Login Error",
                             "Credenciales no válidas");
    }

    FacesContext.getCurrentInstance().addMessage(null, msg);
    context.addCallbackParam("estaLogeado", logeado);
    if (logeado)
      context.addCallbackParam("view", "gauge.xhtml");
  }

  public void logout() {
    HttpSession session = (HttpSession)
FacesContext.getCurrentInstance() 
                                        .getExternalContext().getSession(false);
    session.invalidate();
    logeado = false;
  }
}

Además de los gettes y setters para nombre y clave y el getter para saber si el usuario está logueado, tenemos dos métodos más: login() y logout(), analicemos login().

    if (nombre != null && nombre.equals("admin") && clave != null
        && clave.equals("admin")) {
      logeado = true;
      msg = new FacesMessage(FacesMessage.SEVERITY_INFO, "Bienvenid@", 
nombre);

Si el nombre de usuario y contraseña son correctos y además poseen el valor "admin", consideramos que el usuario es válido y lo logueamos ejecutando:

logeado = true;

Luego establecemos el mansaje de bienvenida del que hablamos antes:

msg = new FacesMessage(FacesMessage.SEVERITY_INFO, "Bienvenid@", nombre);

Aclaración: si alguno se está preguntando porque el único usuario es "admin" y la clave es "admin" y ambos valores están "hardcodeados" y no se pueden cambiar, la respuesta es que he simplificado esa tarea para no hacer más complejo el ejemplo, además no resulta muy complejo obtenerlo de una base de datos por ejemplo.

Si el usuario no está logueado:

    } else {
      logeado = false;
      msg = new FacesMessage(FacesMessage.SEVERITY_WARN, "Login Error",
                             "Credenciales no válidas");
    }

Simplemente lo aseguramos estableciendo el valor de la variable logeado en falso y estableciendo un mensaje de advertencia al usuario.

Finalmente enviamos el mensaje que establecimos antes:

FacesContext.getCurrentInstance().addMessage(null, msg);

Agregamos un parámetro que será utilizado en el script del cliente:

context.addCallbackParam("estaLogeado", logeado);

Si el usuario ha podido loguearse sin inconvenientes, le enviamos como parámetro la próxima vista.

if (logeado)
  context.addCallbackParam("view", "gauge.xhtml");


El método logout() que aún no hemos utilizado realiza lo siguiente:

Obtiene la sesión del usuario:

HttpSession session = (HttpSession) FacesContext.getCurrentInstance()
                                    .getExternalContext().getSession(false);


La invalida:

session.invalidate();

y desloguea al usuario:

logeado = false;


Agregando la opción logout a "gauge.xhtml"

Editamos "gauge.xhtml" y agregamos el siguiente código inmediatamente después del body:

<h:body>
  <p:commandLink id="logout" actionListener="#{loginBean.logout}"
      style="margin-right:20px;" oncomplete="logout(xhr, status, args)">
    <h:outputText value="logout" />
  </p:commandLink>


Se agregará un link como el que se ve en la siguiente figura:


El link ejecutará el método loginBean.logout y una vez completado ejecutará el script del cliente logout(....)

El script del cliente debe agregarse entre el cierre del tag body y el cierre del tag html como sigue:


</h:body>
<script type="text/javascript">
  //<![CDATA[
  function logout(xhr, status, args) {
    setTimeout(function() {
      window.location = 'login.xhtml';
    }, 500);
  }
//]]>
</script>

</html>


Como se puede observar solo hacemos un redirect a la página de login.

Controlando el acceso a todos los recursos

Hasta aquí hemos creado los componentes necesarios para permitir al usuario presentar sus credenciales y que sean comprobadas y almacenadas en una sesión, también permitimos al usuario terminar su sesión, pero aún falta algo muy importante ya que hasta aquí debemos confiar en que todos los usuarios son buenos y pasan primero por el proceso de login antes de peticionar cualquier otro recurso. En fin, definitivamente no podemos confiar en esto, debemos crear un mecanismo tal que si un usuario o proceso requiere un recurso sin que se hayan validado sus credenciales no pueda acceder al mismo. A esto lo hacemos con el uso de un filtro que es un componente que se ejecuta antes de que se maneje la petición final en el server y puede "torcer" el flujo de ejecución normal si es necesario.

Creando el filtro:

Para crear el filtro presionamos Ctrl+N en la vista "Project Explorer" y seleccionamos "Filter" de la categoría "Web" y presionamos el botón "Next >"

Completamos la primer pantalla con:

Java package: ar.com.magm.web.filters
Class name: LoginFilter



Presionamos "Next >"

Luego cambiamos el URL Pattern por "/*" para que este filtro se ejecute ante cualquier petición.



y presionamos "Finish"

El código que debe tener el método doFilter(..) se encuentra a continuación y está explicado con comentarios en el mismo código:

public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
  HttpServletRequest req = (HttpServletRequest) request;
  HttpServletResponse res = (HttpServletResponse) response;

  // Obtengo el bean que representa el usuario desde el scope sesión
  LoginBean loginBean = (LoginBean) req.getSession().getAttribute("loginBean");

  //Proceso la URL que está requiriendo el cliente
  String urlStr = req.getRequestURL().toString().toLowerCase();
  boolean noProteger = noProteger(urlStr);
  System.out.println(urlStr + " - desprotegido=[" + noProteger + "]");

  //Si no requiere protección continúo normalmente.
  if (noProteger(urlStr)) {
    chain.doFilter(request, response);
    return;
  }

  //El usuario no está logueado
  if (loginBean == null || !loginBean.estaLogeado()) {
    res.sendRedirect(req.getContextPath() + "/login.xhtml");
    return;
  }

  //El recurso requiere protección, pero el usuario ya está logueado.
  chain.doFilter(request, response);
}

He separado la lógica de excepción de control de recursos en un método muy sencillo que se encuentra a continuación y que también forma parte del filtro el cual no requiere mucha explicación.

private boolean noProteger(String urlStr) {

/*
 * Este es un buen lugar para colocar y programar todos los patrones que
 * creamos convenientes para determinar cuales de los recursos no
 * requieren protección. Sin duda que habría que crear un mecanizmo tal
 * que se obtengan de un archivo de configuración o algo que no requiera
 * compilación.
 */
  if (urlStr.endsWith("login.xhtml"))
    return true;
  if (urlStr.indexOf("/javax.faces.resource/") != -1)
    return true;
  return false;
}

Este filtro completa este pequeño sistema de control de acceso y logueo, espero que les sea útil.

El workspace completo de este post, del anterior y de futuros posts que tengan que ver con el tema se encuentra en github en la siguiente dirección: https://github.com/magm3333/workspace-pftuto

Tutorial anterior: http://jmagm.blogspot.com/2013/02/primeros-pasos-con-primefaces-eclipse-y.html
Próximo tutorial: http://jmagm.blogspot.com/2013/02/una-tabla-con-datos-de-una-base-de.html

Saludos

Mariano

Etiquetas

pentaho (45) java (35) eclipse (23) jdbc (14) curso (13) tomcat (13) primefaces (12) db2 (11) mondrian (10) review (10) jsf (9) openI (9) pdi (9) prd (9) libro (8) plugin (8) musql (7) struts (7) javascript (6) spring (6) cdf (5) ctools (5) instalar (5) linux (5) mysql (5) data studio (4) hibernate (4) ireport (4) jasper (4) meteor (4) videocurso (4) eglu (3) eglubi (3) elearning (3) graphite (3) grupo eglu (3) jboos tools (3) mexico (3) openbits (3) packt (3) python (3) undec (3) websphere (3) applet (2) cde (2) dao (2) db2university (2) exelearning (2) flexigrid (2) hadoop (2) iua (2) kettle (2) moodle (2) node (2) olap (2) osbi (2) pivot4j (2) scorm (2) sql (2) stpivot (2) actionscript (1) amazon (1) autenticacion (1) avanzado (1) base de datos (1) big data (1) bigdata (1) bodoc (1) cambiar (1) ccc (1) cdc (1) chat (1) cloud (1) coffeescript (1) control de acceso (1) corti (1) csv (1) cuba (1) curso meteor undec (1) dashboard (1) datamart (1) dataptix.tv (1) datasource (1) datatable (1) db2 ExpressC (1) demonio (1) distancia (1) driver (1) driver jdbc (1) eglufiltertwolist (1) encapsulamiento (1) especialización (1) etl (1) excepciones (1) export (1) faces (1) federación (1) filas afectadas (1) filtertwolist (1) filtrado (1) flegrid (1) flex (1) google (1) google viz (1) hostname (1) html (1) i18n (1) ibm (1) identidad (1) indignación (1) instancias (1) inteligencia de negocios (1) jee (1) jpivot (1) l10n (1) la azada (1) la zaga de los confines (1) layout (1) liberado (1) libre (1) libro promoción (1) lob (1) marktplace (1) menu (1) meteor node javascript google oauth autenticacion (1) mobile (1) mongoDB (1) node.js (1) oauth (1) olap4j (1) open source (1) orm (1) persistencia (1) personalizada (1) prd5 (1) psw (1) publicidad (1) rad6 (1) recursividad (1) reporting (1) rock (1) saiku (1) script (1) servicio (1) sessiontimeout (1) sourceforge (1) spinneta (1) sqlserver (1) ssl (1) taller (1) troyanx (1) ubuntu (1) ucc (1) ui (1) web (1) web console (1) xampp (1) xml (1) xpath (1)

Seguidores