Composition y Custom Exporter en JSF2 – Caso práctico

1
7051

En este tutorial veremos como mostrar en una misma celda de una tabla JSF una lista de valores separados por comas. Además veremos qué debemos hacer para exportar dicho contenido a una hoja de cálculo con el soporte de Primefaces Extensions.

Índice de contenidos

1. Introducción

En este tutorial trataremos un caso muy concreto que nos podemos encontrar a la hora de trabajar con Datatables JSF. Imaginemos que el contenido de una de las celdas de la tabla viene dado por una lista de objetos y lo que queremos mostrar es un campo concreto de dichos objetos, separando cada uno de los valores por comas. Una solución podría ser modificar la clase de negocio utilizada para rellenar dicha tabla, teniendo un método que nos transforme el contenido de la lista a un string. Sin embargo, en este tutorial veremos como hacerlo a nivel de vista, evitando acoplar nuestra capa de negocio con una necesidad concreta de la capa de vista.

Además, veremos como customizar el exporter a Excel de Primefaces para que nos genere un fichero de hoja de cálculo con el contenido tal y como lo vemos en la tabla.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

    • Hardware: Portátil MacBook Pro 15’ (2 GHz Intel Core i7, 8 GB 1333 MHz DDR3)
    • Sistema Operativo: Mac OS X Yosemite 10.10.4
    • Entorno de desarrollo: Eclipse Java EE IDE, Mars Release (4.5.0)
    • JSF 2.1
    • Primefaces 5.3
    • Apache Maven 3.3.3

3. Desarrollar un servicio de ejemplo

Como suele ser habitual, para ver este tipo de cosas, lo mejor es que tratemos con un ejemplo sencillo. Y también como siempre, en primer lugar, incluimos en nuestro pom.xml las dependencias necesarias:

pom.xml
...

	<dependencies>
		<dependency>
			<groupId>javax.faces</groupId>
			<artifactId>jsf-api</artifactId>
			<version>2.1</version>
		</dependency>
		<dependency>
			<groupId>org.primefaces</groupId>
			<artifactId>primefaces</artifactId>
			<version>5.3</version>
		</dependency>
		<dependency>
			<groupId>org.primefaces.extensions</groupId>
			<artifactId>primefaces-extensions</artifactId>
			<version>4.0.0</version>
		</dependency>
		<dependency>
			<groupId>org.glassfish</groupId>
			<artifactId>javax.faces</artifactId>
			<version>2.2.12</version>
		</dependency>
		<dependency>
			<groupId>javax.el</groupId>
			<artifactId>el-api</artifactId>
			<version>2.2</version>
		</dependency>
		<dependency>
			<groupId>org.apache.poi</groupId>
			<artifactId>poi</artifactId>
			<version>3.13</version>
		</dependency>
		<dependency>
			<groupId>org.apache.poi</groupId>
			<artifactId>poi-ooxml</artifactId>
			<version>3.13</version>
		</dependency>
	</dependencies>

...

En este caso, vamos a crearnos un servicio que genere una serie de personas con una serie de hobbies que serán mostrados en una tabla JSF. Para ello, nos creamos un nuevo proyecto maven preparado para JSF y desplegable en servidor.

En primer lugar vamos a crearnos nuestros POJOs, en primer lugar tendremos una clase Hobby que definimos de la siguiente manera:

Hobby.java
package com.autentia.tutoriales.uirepeat.domain;

import java.io.Serializable;

public class Hobby implements Serializable {

    private final String id;

    private final String description;

    public Hobby(String id, String description) {
        this.id = id;
        this.description = description;
    }

    public String getId() {
        return id;
    }

    public String getDescription() {
        return description;
    }

}

Y por otro lado, tendremos una clase Person, que tendrá como atributo una lista de la clase Hobby que definimos de la siguiente manera:

Person.java
package com.autentia.tutoriales.uirepeat.domain;

import java.io.Serializable;
import java.util.List;

public class Person implements Serializable {

    private final int id;

    private final String name;

    private List hobbies;

    public Person(int id, String name, List hobbies) {
        this.id = id;
        this.name = name;
        this.hobbies = hobbies;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public List getHobbies() {
        return hobbies;
    }

}

Una vez definido nuestro dominio, implementamos nuestra capa de negocio. En este caso, se trata de un servicio que genera personas con unos ciertos hobbies que luego serán consumidas por nuestra capa de vista. Por tanto, definimos nuestro PersonService de la siguiente manera:

PersonService.java
package com.autentia.tutoriales.uirepeat.service;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.faces.bean.ApplicationScoped;
import javax.faces.bean.ManagedBean;

import com.autentia.tutoriales.uirepeat.domain.Hobby;
import com.autentia.tutoriales.uirepeat.domain.Person;

@ManagedBean(name = "personService")
@ApplicationScoped
public class PersonService implements Serializable {

    private static final int NUMBER_OF_HOBBIES = 3;

    private static final String[] names;

    private static final String[] hobbys;

    static {
        names = new String[5];
        names[0] = "Lucía";
        names[1] = "Pedro";
        names[2] = "María";
        names[3] = "Jose";
        names[4] = "Laura";
    }

    static {
        hobbys = new String[5];
        hobbys[0] = "Lectura";
        hobbys[1] = "Manualidades";
        hobbys[2] = "Pintura";
        hobbys[3] = "Deportes";
        hobbys[4] = "Nuevas tecnologías";
    }

    public PersonService() {

    }

    public List generatePersons(int numberOfPersons) {
        List persons = new ArrayList();
        for (int i = 0; i < numberOfPersons; i++) {
            persons.add(new Person(i, names[i], getRandomHobbies()));
        }
        return persons;
    }

    private List getRandomHobbies() {
        List hobbies = new ArrayList();
        for (int i = 0; i < NUMBER_OF_HOBBIES; i++) {
            Hobby hobbie = getRandomHobbyInstance();
            if (!hobbies.contains(hobbie)) {
                hobbies.add(hobbie);
            }
        }
        return hobbies;
    }

    private Hobby getRandomHobbyInstance() {
        String name = hobbys[(int)(Math.random() * 5)];
        return new Hobby(name, name);
    }

}

A partir de este momento, tenemos la información necesaria que queremos plasmar en una tabla JSF. En el siguiente apartado veremos como tratar la lista de hobbies.

4. Mostrar contenido en una tabla JSF

Cuando queremos desarrollar nuestra capa de vista, en la que queremos tener una tabla en la que se muestre el nombre de la persona y sus hobbies separados por comas, el mayor problema viene derivado de esta segunda parte, ¿cómo mostrar dichos hobbies en una misma celda separados por comas?, además hacerlo a nivel de vista. Lo que vamos a hacer es definirnos un componente por composición JSF, de esta forma, podremos reutilizarlo en otras vistas. Para ello, nos podemos crear una carpeta components dentro de la carpeta webapp/resources de nuestro proyecto, en la que definimos nuestro componente JSF customizado al que hemos llamado commaSeparatedEntities.xhtml:

commaSeparatedEntities.xhml
<html xmlns="http://www.w3.org/1999/xhtml"
	xmlns:h="http://java.sun.com/jsf/html"
	xmlns:f="http://java.sun.com/jsf/core"
	xmlns:ui="http://java.sun.com/jsf/facelets"
	xmlns:composite="http://java.sun.com/jsf/composite">

<h:head>

	<title>This will not be present in rendered output</title>

</h:head>

<composite:interface>
	<composite:attribute name="entities" type="java.util.List"
		required="true" />


	<composite:attribute name="property" type="java.lang.String"
		required="true" />
</composite:interface>

<composite:implementation>
	<ui:repeat value="#{cc.attrs.entities}" var="entity" varStatus="status">
		<h:outputText value="#{entity[cc.attrs.property]}" />
		<h:outputText value=", " rendered="#{!status.last}" />
	</ui:repeat>
</composite:implementation>

</html>

No entraré en detalle de como definir un componente por composición, ya que mi compañero de Autentia Jose Manuel Sánchez lo trata en detalle en este tutorial.

A grandes rasgos, vemos una primera parte en la que definimos los parámetros que debe recibir el componente, en este caso el atributo name que recibirá la lista de objetos a recorrer, en nuestro caso una lista de hobbies, y por otro lado, el atributo property, que indica el atributo que queremos pintar en pantalla.

Por otro lado tenemos la implementación del componente por composición. En este caso, utilizamos otros tres componentes JSF para definirlo, uirepeat que recorre la lista de objetos pasada a través del atributo entities, y dos outputText dentro de ese bucle. En el primero de ellos, mostraremos el valor que nos interese pintar de cada uno de los objetos de la lista, y en el segundo incluiremos una carácter coma y un espacio. Como es lógico, al encontrarnos en el último valor a mostrar, no queremos que se incluya la coma. Este detalle lo controlamos con la variable status que nos permite controlar ciertos parámetros del bucle. En este caso utilizamos el campo last, que será cierto cuando nos encontremos al final de la lista. De esta forma podemos pedir al segundo outputText que se renderice siempre que no nos encontremos al final del bucle.

Definido nuestro componente por composición, vamos a usarlo en la vista de nuestro ejemplo, que tendrá la siguiente pinta:

commaSeparatedEntities.xhml
<html xmlns="http://www.w3c.org/1999/xhtml"
	xmlns:f="http://java.sun.com/jsf/core"
	xmlns:h="http://java.sun.com/jsf/html"
	xmlns:p="http://primefaces.org/ui"
	xmlns:component="http://java.sun.com/jsf/composite/components"
	xmlns:pe="http://primefaces.org/ui/extensions">

<h:head>
</h:head>

<h:body>

	<p:dataTable id="hobbiesTable" var="person"
		value="#{hobbiesView.persons}">
		<p:column>
			<f:facet name="header">
				<h:outputText value="Name" />
			</f:facet>
			<h:outputText value="#{person.name}" />
		</p:column>
		<p:column>
			<f:facet name="header">
				<h:outputText value="Hobbies" />
			</f:facet>
			<component:uirepeat entities="#{person.hobbies}"
				property="description">
			</component:uirepeat>
		</p:column>
	</p:dataTable>

</h:body>
</html>

Como podemos observar, tenemos una tabla que recorre una lista de personas (cada una con sus hobbies), y se nos muestra en una primera columna el nombre de la persona y en la segunda sus hobbies. Como vemos, hemos conseguido mostrar dicha lista en una misma celda utilizando nuestro uirepeat definido como un componente por composición. Como vemos, solo debemos indicar los dos parámetros de dicho componente necesarios para mostrar la información en el formato que hemos decidido. Por tanto, como entities tenemos la lista de hobbies, y como property el valor que queremos imprimir, en este caso description.

Otro detalle a tener en cuenta es que hemos definido en nuestros espacios de nombre xmlns:component="http://java.sun.com/jsf/composite/components" para poder hacer referencia a nuestro componente por composición.

Si desplegamos nuestro servicio en un servidor y accedemos a la página commaSeparatedEntities.xhtml el resultado será el siguiente:

5. Exportar contenido a hoja de cálculo

Ya podemos visualizar el contenido de la tabla de la forma que queríamos, pero ¿qué pasa si hacemos esta tabla exportable a Excel? Pues que si utilizamos el exporter de JSF nos encontraremos que el contenido de las celdas de hobbies no es el esperado. Si queremos que dicho contenido sea el que visualizamos en nuestra página, debemos crear un exporter customizado, con el soporte de Primefaces Extensions, siguiendo los siguientes pasos:

  • Dentro de la carpeta webapp/resources crearnos una carpeta META-INF (si no existe previamente), y a su vez crearnos otra carpeta dentro de META-INF llamada service. Dentro de la carpeta service debemos crearnos un fichero llamado org.primefaces.extensions.component.exporter.ExporterFactory
    El contenido de dichero fichero ha de ser el siguiente (para nuestro ejemplo concreto):

    org.primefaces.extensions.component.exporter.ExporterFactory
    com.autentia.tutoriales.component.CustomExporterFactory
    

    Lo que estamos definiendo es el lugar donde encontramos nuestra fábrica de exporter. En nustro casos en el paquete del proyecto com.autentia.tutoriales.component.CustomExporterFactory

  • El siguiente paso es implementar dicha fábrica tal y como se muestra en el siguiente bloque de código:

    CustomExporterFactory.java
    package com.autentia.tutoriales.component;
    
    import javax.faces.FacesException;
    import javax.faces.context.FacesContext;
    
    import org.primefaces.extensions.component.exporter.ExcelExporter;
    import org.primefaces.extensions.component.exporter.Exporter;
    import org.primefaces.extensions.component.exporter.ExporterFactory;
    
    public class CustomExporterFactory implements ExporterFactory {
    
        static public enum ExporterType {
            XLSX
        }
    
        public Exporter getExporterForType(String type) {
    
            Exporter exporter = null;
    
            FacesContext context = FacesContext.getCurrentInstance();
    
            try {
                ExporterType exporterType = ExporterType.valueOf(type.toUpperCase());
    
                switch (exporterType) {
    
                case XLSX:
                    exporter = new ExcelCustomExporter();
                    break;
    
                default: {
                    exporter = new ExcelExporter();
                    break;
                }
    
                }
            } catch (IllegalArgumentException e) {
                throw new FacesException(e);
            }
    
            return exporter;
        }
    
    }
    

    Como se puede observar, este extiende de la clase de primefaces extensions ExporterFactory, y con él podemos definir un comportamiento particular para casos concretos. En el ejemplo que nos atañe, queremos exportar a excel y que cuando lo haga sea a través de un exporter customizado al que hemos llamado ExcelCustomExporter y que veremos a continuación. Por tanto, hemos implementado la lógica necesaria para que al encontrarnos ante un exporter a XLSX (Hoja de cálculo Excel), utilice nuestro exporter customizado.

  • Debemos de crear un exporter a excel que trate de manera concreta los componentes uirepeat. Este exporter lo hemos llamado ExcelCustomExporter y se define de la siguiente manera:

    ExcelCustomExporter.java
    package com.autentia.tutoriales.component;
    
    import java.io.IOException;
    import java.io.StringWriter;
    
    import javax.faces.component.UIComponent;
    import javax.faces.component.UINamingContainer;
    import javax.faces.context.FacesContext;
    import javax.faces.context.ResponseWriter;
    
    import org.primefaces.extensions.component.exporter.ExcelExporter;
    
    public class ExcelCustomExporter extends ExcelExporter {
    
        @Override
        protected String exportValue(FacesContext context, UIComponent component) {
            if (component instanceof UINamingContainer) {
                final StringWriter stringWriter = new StringWriter();
                final ResponseWriter cachingResponseWriter = context.getRenderKit().createResponseWriter(stringWriter,
                        "text/html", "UTF-8");
                context.setResponseWriter(cachingResponseWriter);
                try {
                    component.encodeAll(context);
                } catch (IOException e) {
                }
    
                return stringWriter.getBuffer().toString().replaceAll("\\<.*?>", "");
            }
    
            return super.exportValue(context, component);
        }
    }
    

    Como vemos, extiende del exporter a Excel por defecto de primefaces, pero se diferencia en que a la hora de exportar un valor que viene dado de un un componente UINamingContainer como es el caso de nuestra columna de hobbies, muestra el valor renderizado eliminando posteriormente las etiquetas de la vista.

    En los casos en los que no se trate de ese componente, llamará al método padre que se encargará de obtener los valores a exportar de la manera habitual.

  • El último paso será incluir en nuestro fichero commaSeparatedEntities.xhtml el siguiente trozo de código (inmediatamente después de la definición de la datatable):

    commaSeparatedEntities.xhml
    
    <html xmlns="http://www.w3c.org/1999/xhtml"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:p="http://primefaces.org/ui"
    xmlns:component="http://java.sun.com/jsf/composite/components"
    xmlns:pe="http://primefaces.org/ui/extensions">
    
    ...
    
    	</p:dataTable>
    
    	<h:form>
    		<h:commandLink
    			styleClass="ui-button ui-widget ui-state-default ui-corner-all ui-button-text-only ui-priority-primary"
    			style="padding:5px;">
    			Export
    			<pe:exporter type="xlsx" target=":hobbiesTable" fileName="hobbies" />
    		</h:commandLink>
    	</h:form>
    
    </h:body>
    </html>
    

    De esta forma, incluiremos un botón que nos permitirá dar la orden de exportar la tabla a Excel. Como hemos definido un exporter customizado de excel, se llamará a dicho exporter.

Después de esta implementación, podemos exportar el contenido de la tabla obteniendo un fichero Excel de la siguiente forma:

6. Conclusiones

Espero que este tutorial os sea útil si os encontráis ante situaciones concretas como estas, en las que queremos cumplir unos requisitos concretos sin recurrir a soluciones que acoplen las diferentes capas de nuetros proyectos y así mantener un código de una mejor calidad.

Tengo que dar las gracias a mi compañero Jose Manuel Sánchez por ser él quién me proporcionó esta solución para resolver este problema, surgido de las necesidades de un proyecto. Y que además fue quién me sugirió la idea de plasmar la información en un tutorial de adictosaltrabajo.

Sin más, solo me queda decir que espero que os haya servido de ayuda este tutorial tal y como me va a servir a mí cuando pase mucho tiempo y se me haya olvidado todo esto y tenga que recurrir a él porque me habré visto en una situación igual o al menos parecida.

7. Referencias

1 COMENTARIO

  1. Que tal, antes que nada felicitarte por el articulo pero tengo una duda al momento de hacer el paso 5.. «Dentro de la carpeta webapp/resources crearnos una carpeta META-INF (si no existe previamente), y a su vez crearnos otra carpeta dentro de META-INF llamada service.»

    ..:: ¿Como seria en el caso de un proyecto «non-maven»? ::..

    Como referencia en el libro: “Learning PrimeFaces Extensions Development” de Sudheer Jonna
    En el capitulo 6: Extended Data Reporting and Image Components.
    Tema: “Understanding and implementing fully controlled custom exporter” (pagina 143) realiza el mismo procedimiento para las Extensiones de Primefaces, pero referiendose únicamente a usuarios Maven también.
    Entorno:
    -NetBeans 7.3
    -Primefaces 6.2
    -JSF 2.1
    -GlassFish 3.1
    -Windows 7

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad