2 de marzo de 2009

Creación de Reportes con JasperRepots y iReports - Parte 2: Uso de DataSources Personalizados

En el primer post de la serie de JasperReports hablé de cómo crear una biblioteca para trabajar con JasperReports y cómo crear un reporte en múltiples formatos. Además hicimos un ejemplo de creación de reportes usando una conexión JDBC directa a nuestra base de datos.

Ahora veremos cómo generar nuestros reportes sin hacer uso de una conexión. Esto puede ser muy útil en los casos en los que ya tengamos los datos en memoria y no necesitemos realizar una conexión a la base de datos; o en el caso en que, por alguna razón ajena a nosotros, no podamos obtener la conexión.

Para poder hacer esto se nos proporciona una interface: "net.sf.jasperreports.engine.JRDataSource". Esta interface tiene solo dos métodos:

  • getFieldValue(JRField jrField)
  • next()

Explicaré estos métodos un poco más adelante, por ahora comencemos a generar todo lo necesario para tener nuestro reporte.

Creamos un nuevo proyecto en NetBeans (menú "File -> New Project..."). En la ventana de nuevos proyectos seleccionamos la categoría "Java" y como tipo de proyecto seleccionamos "Java Application" y hacemos clic en el botón "Next". En la siguiente ventana introducimos el nombre de nuestro proyecto y dejamos seleccionada las opciones "Create Main Class" y "Set as Main Project".

Para que podamos pasar datos a nuestro reporte es necesario que utilicemos un datasource. En el ejemplo anterior veíamos que el datasource que usábamos era la base de datos y la conexión a la misma. En esta ocasión el datasource será una de nuestras clases. Esta clase deberá implementar la interface "JRDataSource" y contendrá la lógica para pasar los datos correspondientes a nuestro reporte.

Creamos una nueva clase haciendo clic derecho sobre el nodo "Source Packages" de la ventana "Projects", o sobre el paquete que ya tenemos creado, y en el menú contextual que se abre seleccionamos la opción "New -> Java Class...".



En la ventana que se abre escribimos como en nombre de la nueva clase "Participante" y presionamos el botón "Finish". Con esto veremos en la ventana de nuestro editor la nueva clase "Participante".

Esta clase "Partcipante" representará los datos que se mostrarán en el reporte, tal como en el ejemplo anterior cada uno de los registros de la tabla "participantes" representaba una fila en el reporte final, cada objeto participante que creemos representará una fila en el nuevo reporte.

Agreguemos los siguientes atributos a nuestra clase:


private int id;
private String nombre;
private String username;
private String password;
private String comentarios;


Además agregamos sus getters y sus setters y dos constructores, uno sin argumentos y uno que reciba como argumentos todas las propiedades declaradas anteriormente.

Si están utilizando el NetBeans para seguir este tutorial pueden generar tanto los setters y los getters de forma automática presionando las teclas "Alt + Insert", con lo cual se abrirá un menú contextual con las opciones necesarias para hacer esto:



Al terminar, su clase debe verse más o menos así:


public class Participante
{
    private int id;
    private String nombre;
    private String username;
    private String password;
    private String comentarios;

    public Participante()
    {
    }
    
    public Participante(int id, String nombre, String username, String password, String comentarios)
    {
        this.id = id;
        this.nombre = nombre;
        this.username = username;
        this.password = password;
        this.comentarios = comentarios;
    }

    public String getComentarios()
    {
        return comentarios;
    }

    public void setComentarios(String comentarios)
    {
        this.comentarios = comentarios;
    }

    public int getId()
    {
        return id;
    }

    public void setId(int id)
    {
        this.id = id;
    }

    public String getNombre()
    {
        return nombre;
    }

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

    public String getPassword()
    {
        return password;
    }

    public void setPassword(String password)
    {
        this.password = password;
    }

    public String getUsername()
    {
        return username;
    }

    public void setUsername(String username)
    {
        this.username = username;
    }
}


Ahora, antes de seguir con nuestro código Java, pasaremos a crear la plantilla de nuestro reporte con iReport, por lo que abrimos esta herramienta.

En esta ocasión no haremos uso de un wizard para crear el reporte, sino que lo haremos desde cero, y aprovecharé para explicar algunos conceptos básicos sobre los reportes conforme los vayamos necesitando.

Para crear el nuevo reporte vamos el menú "File -> New...". En la ventana que se abre seleccionamos uno de los formatos "Blank" y presionamos el botón "Open this Template":



Después introducimos el nombre del reporte, yo lo llamaré "reporte2.jrxml", y la ubicación en la que se guardará. Les recomiendo que en esta ocasión guarden el reporte en el directorio raíz del proyecto de NetBeans que acabamos de crear. Hacemos clic en el botón "Next" y se nos mostrará un mensaje de felicitación por haber creado un nuevo reporte (^_^!). Cuando presionen el botón "Finish" verán una hoja dividida en 7 u 8 diferentes porciones horizontales llamadas bandas.



Cuando generamos nuestro reporte final con datos, cada una de estas bandas se comporta de una forma distinta. Algunas aparecen solo al principio o al final del reporte, otras aparecen con cada nueva fila que se le agrega, otras solo al final de los datos, etc.

La plantilla de reportes está dividida en 10 bandas predefinidas. Una banda siempre tiene el mismo ancho que la página. Sin embargo el alto de algunas de las bandas puede variar durante la fase de llenado de datos, aún si establecimos un alto en el momento del diseño.

Las bandas existentes son:

  • Background
  • Title
  • Page Header
  • Column Header
  • Detail 1
  • Column Footer
  • Page Footer
  • Last Page Footer
  • Summary
  • No Data

Explicaré algunas de estas bandas conforme las vayamos usando en los tutoriales correspondientes.

Lo primero que haremos es agregar texto a nuestro reporte. En iReport existen dos tipos de texto: texto estático y texto dinámico (también llamado expresiones). El texto estático es aquel que no cambia, mientras que las expresiones son como etiquetas que le indican a JasperReports que debe reemplazarlas por algún valor al momento de generar el reporte final.

Existen 3 tipos de expresiones:

  • Campos (fields) representados con "$F{nombre_campo}".
  • Variables representadas por "$V{nombre_variable}".
  • Parámetros representados por "$P{nombre_parámetro}".

Cada uno de estos tipos de expresiones tiene un uso particular:

Los campos ("$F{}") le dicen al reporte dónde colocará los valores obtenidos a través del datasource. Por ejemplo, nuestro objeto "Partcipante" tiene un atributo llamado "username". Usando una expresión de campo indicamos en cuál parte o sección del reporte debe aparecer el valor de ese atributo usando "$F{username}". Esto quedará más claro un poco más adelante.

Los parámetros ("$P{}") son valores que usualmente se pasan al reporte directamente desde el programa que crea el JasperPrint del reporte (en nuestra aplicación Java). Aunque también existen algunos parámetros internos que podemos leer pero no modificar. Para hacer uso de estos parámetros simplemente indicamos el nombre del parámetro en el lugar que queremos colocarlo. Pueden encontrar los nombres y significados de los parámetros internos en la documentación de JasperReports.

Las variables ("$V{}") son objetos usados para almacenar valores como los resultados de cálculos. Al igual que con los parámetros, JasperReports tiene algunas variables internas que podemos leer. Pueden encontrar los nombres y significados de las variables la documentación de JasperReports.

Cada uno de estos elementos tiene un nombre, un tipo (que debe corresponder con un tipo de objeto Java como String o Integer), y una descripción opcional. Además deben ser registrados para poder ser usados en tiempo de diseño y que puedan ser entendidos al momento de compilar el reporte, y por lo tanto para que nuestro reporte funcione correctamente en tiempo de ejecución. Esto lo veremos un poco más adelante.

Regresemos a donde nos quedamos. Queremos agregar un texto estático a nuestro reporte a modo de título. Para esto debemos agregar un elemento llamado "Static Text". Los elementos que podemos agregar a nuestros reportes están en una ventana llamada "Palette", que se encuentra en la parte derecha de iReport, bajo la categoría "Report Elements". Si no pueden ver la ventana "Palette" presionen las teclas "Ctrl + Shift + 8":



De esta ventana arrastramos el elemento "Static Text" a la banda "Title". "Title" es una banda que solo aparece en la parte superior de la primer página. Por lo que nos sirve para colocar el título del reporte y/o el nombre y logo de nuestra empresa.

Una vez que hayamos colocado el texto estático en la banda correspondiente y mientras aún esté seleccionado, modificamos sus propiedades usando la ventana de propiedades del elemento y la barra de formato de texto. También podemos abrir otra ventana que nos ayude a alinear y controlar el alto y el ancho de los elementos yendo al menú "Window -> Formatting Tools".



Ahora agregamos los encabezados de las columnas en las que se mostrarán los datos que pasaremos al reporte. Agregamos estos nombres en la banda "Column Header", la cual se repetirá en cada página antes de mostrar los datos de las columnas.

Agregaremos las columnas "Nombre", "Usuario", "Contraseña", y "Comentarios". Por lo que nuevamente arrastramos un elemento "Static Text" para cada una de las columnas. También podemos ajustar el alto de esta banda para que se ajuste al de nuestro texto, ya sea moviéndola directamente en el diseñador, o desde la ventana de propiedades:



Nuestro reporte ya casi está terminado, ahora solo nos queda agregar los campos en los que se mostrarán los datos que pasaremos al reporte en un momento. Para que estos campos puedan ser reemplazados por el valor real es necesario que usemos las expresiones (texto dinámico) de las que hablé anteriormente. Para esto definiremos un "field" para cada uno de los campos que queramos mostrar (en este caso serán los mismos campos para los que definimos las cabeceras de las columnas).

Los fields deben definirse antes de poder ser usados. Esta definición incluye el nombre del field y su tipo. Los fields (así como el resto de las expresiones) se definen en la ventana "Report Inspector" a la izquierda del diseñador del reporte. Ahí existe un nodo llamado "Fields" que es donde se encuentran los fields que hemos definido y que por lo tanto podemos usar ^-^.

Hacemos clic derecho en el nodo "Fields" de la ventana "Report Inspector". Con esto se abre un menú contextual. Seleccionamos la opción "Add Field" (la única habilitada).



Con esto se agregará un field llamado "field1" que por default es de tipo "java.lang.String". Cambiamos el nombre de este campo por "nombre" usando la ventana de propiedades. Esto es importante porque donde pongamos este campo será en donde se muestre el valor del campo nombre de nuestros objetos Participantes.



Hacemos lo mismo para el resto de los campos ("username", "password", "comentarios", "id"). Hay que tener cuidado cuando agreguemos el field "id". Si recuerdan, en la definición de la clase "Participante" la propiedad "id" está definida como "int". Sin embargo, a JasperReports solo podemos pasarle objetos para ser usados como valores, por lo que será necesario cambiar el tipo de la clase ("Field Class en la ventana de propiedades) a "java.lang.Integer" (aunque en realidad no mostraremos el "id" en este ejemplo).



Ahora debemos indicar en qué parte de nuestro reporte queremos que se muestren los valores correspondientes a los atributos de los objetos "Participante" que le pasemos. Para eso simplemente arrastramos los fields correspondientes al lugar en el que queremos que se muestren (arrastramos el field "nombre" a donde queremos que se muestre el atributo "nombre", el campo "username" en donde queremos que se muestre el atributo "username", etc.) desde el "Report Inspector". Al arrastrar los fields, iReport agregará automáticamente un encabezado para este field, solo borren el encabezado agregado =).

Estos fields los colocaremos en la banda "Detail 1" la cual se repite cada vez que recibe un nuevo objeto y coloca sus valores en la misma fila (quedará más claro cuando ejecutemos el ejemplo). Al final, el reporte debe quedar de la siguiente forma:



Por el momento esto es todo lo que necesitamos hacer en iReport para mostrar este reporte básico. Hacemos clic en el botón "Compile Report" para compilar el reporte y generar su archivo ".jasper" respectivo.

Podemos ver una vista previa del reporte haciendo clic en la pestaña "Preview". Asegúrense de seleccionar el "Empty datasource" antes de ver el preview, de lo contrario les aparecerá un error indicado que el documento no tiene páginas. Al final deben ver algo como esto:



Los "nulls" aparecen porque no se recuperó ningún valor que coincidiera con el nombre de ese field usando nuestro datasource (que en este caso está vacío, lo cual solucionaremos a continuación).

Regresamos al NetBeans y ahora crearemos una clase que implemente la interface "JRDataSource" de la que hablé antes. Esta clase será la que usaremos como datasource para nuestro reporte, y la que regresará los valores correspondientes a cada uno de los fields que creamos antes.

Primero agregamos a nuestro proyecto la libreria "JasperReports" que creamos en el primer tutorial de la serie de JasperReports. En esta ocasión no necesitaremos agregar el jar con el Driver de la base de datos, ya que no usaremos ninguna ^_^.

Creamos una nueva clase llamada "ParticipantesDatasource" y hacemos que esta nueva clase implemente la interface "JRDataSource". Podemos hacer que NetBeans implemente de forma automática los métodos de esta interface (proporcionando una implementación vacía) presionando las teclas "Alt + Insert" y seleccionando la opción "Implement Method" en el menú contextual que se abre.



Con esto se abrirá una ventana llamada "Generate Implement Methods" en la cual seleccionamos la interface "JRDataSource" para que se seleccionen de forma automática todos sus métodos y hacemos clic en el botón "Generate".



Con esto tendremos los dos métodos implementados y en cuyos cuerpos solo habrá:


throw new UnsupportedOperationException("Not supported yet.");


Ahora agregaremos a nuestra clase un atributo de tipo "java.util.List" llamado "listaParticipantes" que mantendrá justamente eso: la lista de los participantes de los cuales mostraremos los datos en el reporte. Inicializamos esta lista a un objeto de tipo "java.util.ArrayList", de esta forma:


private List<Participante> listaParticipantes = new ArrayList<Participante>();


También agregamos un contador llamado "indiceParticipanteActual" de tipo "int", que usaremos enseguida, y lo inicializamos a "-1"; un poco más adelante explicaré por qué este valor:


private int indiceParticipanteActual = -1;


Hasta ahora nuestra clase debe verse así:


public class ParticipantesDatasource implements JRDataSource
{
    private List<Participante> listaParticipantes = new ArrayList<Participante>();
    private int indiceParticipanteActual = -1;
    
    public Object getFieldValue(JRField jrf) throws JRException
    {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public boolean next() throws JRException
    {
        throw new UnsupportedOperationException("Not supported yet.");
    }
}


Ahora implementaremos el método getFieldValue. Este método recibe un argumento de tipo JRField. Este parámetro nos indicará por cuál de los "fields" nos está preguntando el reporte.

En la implementación de este método regresaremos los valores correspondientes a cada uno de los atributos de nuestros objetos "Participante" conforme se vayan pidiendo. Por lo que el método queda así:


public Object getFieldValue(JRField jrField) throws JRException
{ 
    Object valor = null;  

    if("nombre".equals(jrField.getName())) 
    { 
        valor = listaParticipantes.get(indiceParticipanteActual).getNombre(); 
    } 
    else if("username".equals(jrField.getName())) 
    { 
        valor = listaParticipantes.get(indiceParticipanteActual).getUsername(); 
    } 
    else if("password".equals(jrField.getName())) 
    { 
        valor = listaParticipantes.get(indiceParticipanteActual).getPassword(); 
    } 
    else if("comentarios".equals(jrField.getName())) 
    { 
        valor = listaParticipantes.get(indiceParticipanteActual).getComentarios(); 
    }
 
    return valor; 
}


Donde básicamente ocurre lo que dije anteriormente: cada vez que el reporte pregunta por el valor de un atributo del objeto "Participante" actual (al que hace referencia el contador "indiceParticipanteActual") se regresa el valor correspondiente. Esta petición se hace en base al nombre del field que creamos desde iReport.

Ahora tal vez se estén preguntando ¿en qué momento se incrementa el contador?, o ¿cómo sabe JasperReport cuántos participantes existen? Pues bien, ambas cosas ocurren gracias a la implementación del método "next()".


public boolean next() throws JRException
{
    return ++indiceParticipanteActual < listaParticipantes.size();
}


JasperReport pregunta a este método para saber si existe otro "Participante" en la lista. Este método es el primero que se llama al generar el reporte. Por lo que el contador debe comenzar en "-1", así cuando este método se llama la primera vez el contador queda en "0", y cuando se invoca al método "getFieldValue" se regresa el objeto Participante del índice adecuado.

Esto es todo lo que necesitamos para que nuestro datasource funcione para generar reportes. Agregaré un método de utilidad a esta clase, llamado "addParticipante", que me permita agregar un nuevo participante a la lista (aunque si lo prefieren pueden agregar el setter de "listaParticipantes"):


public void addParticipante(Participante participante)
{
    this.listaParticipantes.add(participante);
}


La clase "ParticipantesDatasource" queda de esta forma (omitiendo los imports):


public class ParticipantesDatasource implements JRDataSource
{
    private List<Participante> listaParticipantes = new ArrayList<Participante>();
    private int indiceParticipanteActual = -1;

    public Object getFieldValue(JRField jrf) throws JRException
    {
        Object valor = null;

        if ("nombre".equals(jrf.getName()))
        {
            valor = listaParticipantes.get(indiceParticipanteActual).getNombre();
        }
        else if ("username".equals(jrf.getName()))
        {
            valor = listaParticipantes.get(indiceParticipanteActual).getUsername();
        }
        else if ("password".equals(jrf.getName()))
        {
            valor = listaParticipantes.get(indiceParticipanteActual).getPassword();
        }
        else if ("comentarios".equals(jrf.getName()))
        {
            valor = listaParticipantes.get(indiceParticipanteActual).getComentarios();
        }

        return valor;
    }

    public boolean next() throws JRException
    {
        return ++indiceParticipanteActual < listaParticipantes.size();
    }

    public void addParticipante(Participante participante)
    {
        this.listaParticipantes.add(participante);
    }
}


Para finalizar agregaremos en nuestra clase "Main" un ciclo dentro del cual crearemos 10 objetos "Participante", los cuales iremos agregando uno a uno a un objeto de tipo "ParticipantesDatasource":


ParticipantesDatasource datasource = new ParticipantesDatasource();

for (int i = 1; i <= 10; i++) 
{ 
    Participante p = new Participante(i, "Particpante " + i, "Usuario " + i, "Pass " + i, "Comentarios para " + i);     
    datasource.addParticipante(p);  
} 


El resto del código para generar el reporte es similar al del primer tutorial de JasperReports, solo que en esta ocasión en lugar de pasar un objeto de tipo "Connection" al método "fillReport" del "JasperFillManager" pasamos nuestro objeto "ParticipantesDatasource" de esta forma:


JasperPrint jasperPrint = JasperFillManager.fillReport(reporte, null, datasource);


Al final el código de nuestra clase "Main" queda así:


public class Main 
{ 
    public static void main(String[] args) throws Exception 
    { 
        ParticipantesDatasource datasource = new ParticipantesDatasource();  

        for (int i = 1; i <= 10; i++) 
        { 
            Participante p = new Participante(i, "Particpante " + i, "Usuario " + i, "Pass " + i, "Comentarios para " + i); 
            datasource.addParticipante(p); 
        }  

        JasperReport reporte = (JasperReport) JRLoader.loadObject("reporte2.jasper");  
        JasperPrint jasperPrint = JasperFillManager.fillReport(reporte, null, datasource);  

        JRExporter exporter = new JRPdfExporter();  
        exporter.setParameter(JRExporterParameter.JASPER_PRINT, jasperPrint); 
        exporter.setParameter(JRExporterParameter.OUTPUT_FILE, new java.io.File("reporte2PDF.pdf")); 
        exporter.exportReport(); 
    }
}


Al ejecutar este código debemos terminar con un archivo llamado "reporte2PDF.pdf" en el directorio raíz de nuestro proyecto y que tiene el siguiente contenido:



Podrá no ser el reporte más bello o más útil del mundo, pero es nuestro =D.

Podemos ver que los datos importantes (los datos de los "Participantes") aparecen en la banda "Details 1" (donde colocamos los fields) y que esta banda se repite por cada uno de los participantes de la lista.

Bien, con esto vemos que podemos crear nuestros propios datasources y pasar datos para generar reportes sin la necesidad de una conexión a base de datos. Es bastante sencillo, solamente debemos proporcionar una clase que implemente la interface "JRDataSource" y que regrese los valores correspondientes a los fields del reporte.

Aunque esto ya es bastante fácil y útil, existe una forma aún más simple para crear un datasource, sin la necesitad de implementar la interface "JRDataSource". Esto es gracias a un conjunto de clases que JasperReports ya nos proporciona y realizan básicamente la misma función que la clase "ParticipantesDatasource" que acabamos de crear. Estas clases son:

  • JRBeanCollectionDataSource
  • JRJpaDataSource
  • JRBeanArrayDataSource

Las cuales ya implementan la interface "JRDataSource". Cada una funciona con distintos tipos de datos, pero en que nos interesa en este momento es "JRBeanCollectionDataSource" que puede convertir una "java.util.Collection" en un DataSource de forma automática. Modificaremos nuestro método "main" para hacer uso de esta clase.

Ahora, en vez de llenar nuestro "ParticipantesDatasource" en el ciclo simplemente llenaremos un "java.util.List":


List listaPariticipantes = new ArrayList();

for (int i = 1; i <= 10; i++) 
{ 
    Participante p = new Participante(i, "Particpante " + i, "Usuario " + i, "Pass " + i, "Comentarios para " + i); 
    listaPariticipantes.add(p);
}


Y en lugar de pasar un objeto de tipo "Connection" al método "fillReport" del "JasperFillManager" pasamos un nuevo objeto "JRBeanCollectionDataSource" construido con la lista "listaPariticipantes":


JasperPrint jasperPrint = JasperFillManager.fillReport(reporte, null, new JRBeanCollectionDataSource(listaPariticipantes));


Al final nuestra clase "Main" queda así:


public class Main
{
    public static void main(String[] args) throws Exception
    {
        List listaPariticipantes = new ArrayList();

        for (int i = 1; i <= 10; i++) 
        {
            Participante p = new Participante(i, "Particpante " + i, "Usuario " + i, "Pass " + i, "Comentarios para " + i); 
            listaPariticipantes.add(p);
        } 

        JasperReport reporte = (JasperReport) JRLoader.loadObject("reporte2.jasper");

        JasperPrint jasperPrint = JasperFillManager.fillReport(reporte, null, new JRBeanCollectionDataSource(listaPariticipantes));

        JRExporter exporter = new JRPdfExporter(); 
        exporter.setParameter(JRExporterParameter.JASPER_PRINT, jasperPrint);
        exporter.setParameter(JRExporterParameter.OUTPUT_FILE, new java.io.File("reporte2PDF_2.pdf")); 
        exporter.exportReport();
    }
}


Al ejecutar este código se genera el archivo "reporte2PDF_2.pdf" en el directorio raíz de nuestro proyecto, con el siguiente contenido:



El cual es idéntico al reporte generado con la clase "ParticipantesDatasource".

Y con esto vemos que gracias a la clase "JRBeanCollectionDataSource" no es necesario que proporcionemos nuestra propia clase que implemente la interface "JRDataSource", por lo que la generación de reportes es aún más simple ^-^.

Bien, esto es todo por ahora. Espero que este post les sea de utilidad.

En el siguiente tutorial hablaré de cómo funcionan los parámetros y las variables en JasperReport, para lo cual haremos uso del reporte que acabamos de crear.

Gracias a todos y no olviden dejar sus dudas, comentarios, y sugerencias. Todo es bienvenido.

Saludos.

Descarga los archivos de este tutorial desde aquí:

Entradas Relacionadas: