Como hacer un componente de JSF

2
34201

Creación: 27-06-2007

Índice de contenidos

1. Introducción

Ya hemos visto algunas cosas sobre JSF (http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=jsf). Básicamente podríamos decir que se trata de un estándar que ya forma parte de la especificación de Java EE 5, donde el desarrollo se hace en base a componentes.

Lo ideal es usar los componentes estándar o usar librerías de componentes ya construidas, como Tomahawk (http://myfaces.apache.org/tomahawk/) o ICEfaces (http://www.icesoft.com/products/icefaces.html). Pero en algunas ocasiones no encontraremos ningún componente que cubra nuestras necesidades.

Para estas ocasiones siempre nos queda la opción de construirnos nuestros propios componentes. Esto es lo que vamos a ver en este tutorial.

En concreto vamos a construir un componente que, indicándole un POJO, nos pinte un formulario con una etiqueta y un campo de entrada para cada una de las propiedades de tipo String de ese POJO.

¡¡¡ Atención, el componente que vamos a construir sólo pretende ilustrar la construcción de un componente, pero todavía le quedará mucho para poder ser usando en producción !!!

Y ya, entrando en harina, podemos adelantar que para la construcción de un componente vamos a desarrollar tres piezas:

  • el componente: esta clase es realmente el componente, y es donde estará implementada la lógica asociada.
  • el render: es la clase que se encarga de pintar el componente y de recuperar la información introducida por el usuario para «inyectarla» en el componente. El render está asociado a un medio concreto (web, wap, …), mientras el componente es único. Es decir, podemos tener varios render para un mismo componente, o incluso reescribir los render si queremos cambiar el «como se pinta».
  • el tag: lo normal (aunque no obligatorio) es que nuestro componente lo queramos usar cómodamente en páginas JSP. El tag será la clase que implemente eso, una etiquetad de JSP.

En los siguientes apartados se ira viendo como construimos cada una de estas piezas.

Vamos a aprovechar y vamos a recordar el ciclo de vida de JSF. Para el desarrollo en JSF (bien sea de aplicaciones o de componentes) es fundamental comprender y dominar las 6 fases del ciclo de vida de JSF:

  1. Restore view: Se reconstruye el árbol de componentes.
  2. Apply request values: Se leen los valores de la request y se aplican sobre los componentes. En este momento es cuando se llama a los converters. Si hay algún error en la conversión se irá directamente a la fase «render response». Si un componente tiene «immediate=true» su validación (y el procesamiento de los eventos provocados por esa validación) se hará en esta fase .
  3. Process validations: se validan todos los componentes (todos los que tienen «immediate=false», ya que los que lo tienen a true ya se validaron en la fase anterior). Si falla alguna de las validaciones se irá directamente a la fase «render response».
  4. Update model values: Los valores de los back beans de JSF se actualizan con los valores que hasta ahora estaban guardados sólo en los componentes.
  5. Invoke application: Se invoca a los métodos de los back beans.
  6. Render response: se pinta la respuesta al usuario. Se pinta el árbol de componentes.

Podéis encontrar más información en
http://java.sun.com/j2ee/1.4/docs/tutorial/doc/JSFIntro10.html.

Aquí podéis encontrar el código completo de las tres clases necesarias para construir el componente.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil Asus G1 (Core 2 Duo a 2.1 GHz, 2048 MB RAM, 120 GB HD).
  • Sistema Operativo: GNU / Linux, Debian (unstable), Kernel 2.6.21, KDE 3.5
  • Máquina Virtual Java: JDK 1.6.0-b105 de Sun Microsystems
  • Eclipse 3.2.2
  • MyFaces 1.1.5
  • Maven 2.0.7

3. El componente

A la hora de construir un componente debemos fijarnos si ya hay alguno que hace algo parecido, ya sea dentro del estándar, o de alguna librería que hayamos adquirido. Si ya tenemos un componente que hace algo parecido la mejor opción suele ser que nuestra clase extienda de ese componente para aprovechar las funcionalidades del padre. Si no encontramos ningún componente que podamos reutilizar tendremos que extender de la clase javax.faces.component.UIComponentBase.

En nuestro ejemplo vamos usar el segundo camino, extenderemos la clase UIComponentBase. Vamos a ir viendo paso a paso esta clase (recordar que en al final de la introducción tenéis acceso un tar.gz con el código completo):

 public static final String COMPONENT_TYPE = FormBeanComponent.class.getName();
        public static final String DEFAULT_RENDERER_TYPE = HtmlFormBeanRenderer.class.getName();

Estas dos constantes son cadena arbitrarias, aunque hemos decidido usar los nombres de las clases, se podría haber puesto cualquier otra cosa. Luego las utilizaremos para determinar el render que se tiene que usar para pintar el componente.

private ValueBinding beanBinding = null;

En este atributo guardaremos el bean a partir del cual vamos a pintar el formulario, y donde se guardaran los valores introducidos por el usuario (en la fase «update model values»). Es de tipo ValueBinding para poder usar lenguaje de expresiones de JSF para darle valor en la JSP.

private Map<String, String> localValues = new HashMap<String, String>();

Según hemos visto en el ciclo de vida, los valores de la request se almacenan en el componente hasta alcanzar la fase «update model values». Este atributo lo usaremos para guardar esos valores recuperados de la request. Ya hemos dicho que nuestro componente sólo trabajará con las propiedades de tipo String, así que en este mapa guardaremos como clave el nombre de la propiedad y como valor, el valor introducido por el usuario.

public FormBeanComponent() {
                setRendererType(DEFAULT_RENDERER_TYPE);
        }

Constructor de la clase. Es importante llamar a setRenderType(), este es un método de nuestro padre que permite definir cual será el identificador del render a usar. Si no hacemos esto luego JSF no será capaz de elegir el render apropiado para pintar el componente.

@Override
        public String getFamily() {
                return COMPONENT_TYPE;
        }

Este es el único método que estamos obligados a implementar por extender la clase UIComponentBase. Este método devuelve el identificador de la familia a la que pertenece este componente. JSF, en función de este valor y del tipo de render (lo hemos definido en el constructor), elegirá el render apropiado para pintar este componente.

public Object getBean() {
                final Object bean = beanBinding.getValue(FacesContext.getCurrentInstance());
                return bean;
        }

Este método devuelve el POJO que hemos asociado al componente (el POJO que usaremos para pintar el formulario y posteriormente para guardar los valores). Nótese como se extrae el valor del atributo beanBinding (dijimos que este atributo va a hacer referencia al POJO mediante lenguaje de expresiones de JSF). También hay «setter» correspondiente para fijar el valor del POJO (se puede ver en el código completo).

@Override
        public void restoreState(FacesContext context, Object state) {
                final Object values[] = (Object[])state;
                super.restoreState(context, values[0]);
                beanBinding = (ValueBinding)restoreAttachedState(FacesContext.getCurrentInstance(), values[1]);
        }

        @Override
        public Object saveState(FacesContext context) {
                List state = new ArrayList();
                state.add( super.saveState(context) );
                state.add( saveAttachedState( FacesContext.getCurrentInstance(), beanBinding ) );
                return state.toArray();
        }

Estos dos métodos vienen de la interfaz
javax.faces.component.StateHolder. La implementación
de esta interfaz permite a un componente mantener valores entre
diferentes request. Esto es necesario porque en cada request se
reconstruye todo el árbol de componentes (recordar la fase
"restore view"), es decir se crean nuevas instancias de cada
componente. Para nuestro componente es importante guardar el POJO al
que se está haciendo referencia. Como estamos sobreescribiendo
los métodos de nuestro padre, es muy importante que no se nos
olvide llamar a super.
@Override
        public void processUpdates(FacesContext facesContext) {
                final Object bean = getBean();
                final Class<?> clazz = bean.getClass();
                final Class[] parameterTypes = { String.class };

                for (Map.Entry newValue : localValues.entrySet()) {
                        final String key = newValue.getKey().toString();
                        final String setterName = "set" + key.substring(0, 1).toUpperCase() + key.substring(1);
                        Method setterMethod;
                        try {
                                final Object[] setterArguments = { newValue.getValue() };
                                setterMethod = clazz.getMethod(setterName, parameterTypes );
                                setterMethod.invoke(bean, setterArguments);

                        } catch (Exception e) {
                                e.printStackTrace();
                        }
                }
        }

Este método se va a invocar en la fase de "update modelvalues", y es el que se tiene que encargar de actualizar el backbean (en nuestro caso el POJO que tenemos referenciado con elatributo beanBinding) con los valores que estánguardados localmente en el componente (en nuestro caso los valoresque tenemos en el atributo localVlaues). Igual que estémétodo, tenemos métodos similares por si tenemos quehacer cosas en el resto de las fases (processRestoreState,processDecodes, processValidators, ...).El código lo que hace es recorrer el mapa con los valoreslocales que ha recogido el componente de la request y buscar esaspropiedades en el POJO por introspección. En cualquier caso noes más que un ejemplo y no importa tanto su implementacióncomo que quede clara la responsabilidad del método, y que estese invoca en la fase "update model values".Con esto hemos terminado con el componente (se han omitido algunosgetter y setter que podéis ver en el código completo).Cabría destacar especialmente los métodos para guardarel estado del componente entre request (restoreState,saveState) y el método que actualiza los valoresde los back beans (processUpdates).

4. El render

Esta clase será la que se encargue de recuperar losparámetros de la request http y pasárselos alcomponente, y de consultar el componente para "pintar" el HTML.Esto lo podría haber hecho el propio componente pero no esrecomendable. Lo bueno de los componentes de JSF es que podemosreutilizarlos para diferentes dispositivos, y esto lo conseguiremosteniendo diferentes renders para un sólo componente.Para implementar un render extenderemos de la clasejavax.faces.render.Renderer.

@Override
        public void decode(FacesContext facesContext, UIComponent uiComponent) {
            final FormBeanComponent formBeanComponent = (FormBeanComponent)uiComponent;

            // Se va a usar el POJO asociado al componente para buscar las propiedades,
            // pero los valores recogidos de la request se almacenaran en la variable local.
            final Object componentValue = formBeanComponent.getBean();
            final Map<String, String> localValues = formBeanComponent.getLocalValues();
            localValues.clear();

            final Class<?> clazz = componentValue.getClass();
            final Method[] methods = clazz.getMethods();
            for (Method method : methods) {
                final String methodName = method.getName();
                final String propertyName = methodName.substring(3);

                if (method.getParameterTypes().length == 0 
                        && method.getReturnType() == String.class 
                        && methodName.startsWith("get")) {
                    // Sabemos que es un geter, ahora hay que comprobar si existe el setter equivalente
                    final Class[] parameterTypes = { method.getReturnType() };
                    final Method setterMethod;
                    try {
                        setterMethod = clazz.getMethod("set" + propertyName, parameterTypes);
                    } catch (Exception e) {
                        continue; // no existe setter equivalente, así que seguimos buscando propiedades.
                    }
                    if (setterMethod.getReturnType() != Void.TYPE) {
                        continue; // no es realmente un setter ya que un setter no debería tener tipo de retorno.
                    }

                    // Se busca en la request el nombre de la propiedad
                    final String requestParameterName 
                            = propertyName.substring(0,1).toLowerCase() + propertyName.substring(1);
                    final String requestParameter 
                            = (String)JsfUtils.getRequestParametersMap().get(requestParameterName);

                    localValues.put(requestParameterName, requestParameter);
                }
            }
        }

Este método decode se encarga de extraer losparámetros de la request de http, y guardarlos en la variablelocal del componente. Igual que antes, no es tan importante laimplementación del algoritmo, como que quede claro que estemétodo es el que se llama en la fase de "apply requestvalues" para guardar los parámetros de la request en elcomponente.

@Override
        public void encodeEnd(FacesContext facesContext, UIComponent uiComponent) throws IOException {
            final ResponseWriter out = facesContext.getResponseWriter();
            final FormBeanComponent formBeanComponent = (FormBeanComponent)uiComponent;
            final Object bean = formBeanComponent.getBean();
            final Class<?> clazz = bean.getClass();
            final Object[] getterArguments = {}; 

            out.startElement("table", null);
            for (Method method : clazz.getMethods()) {
                final String methodName = method.getName();
                final String propertyName = methodName.substring(3);

                if (method.getParameterTypes().length == 0 
                        && method.getReturnType() == String.class 
                        && methodName.startsWith("get")) {
                    // Sabemos que es un geter, ahora hay que comprobar si existe el setter equivalente
                    Class[] parameterTypes = { method.getReturnType() };
                    final Method setterMethod;
                    try {
                        setterMethod = clazz.getMethod("set" + propertyName, parameterTypes);
                    } catch (Exception e) {
                        continue; // no existe setter equivalente, así que seguimos buscando propiedades.
                    }
                    final Class setterReturnType = setterMethod.getReturnType();
                    if (setterReturnType != Void.TYPE) {
                        continue; // no es realmente un setter ya que un setter no debería tener tipo de retorno.
                    }

                    // Se vuelca en la salida el campo de entrada para la propiedad
                    final String outParameterName 
                            = propertyName.substring(0,1).toLowerCase() + propertyName.substring(1);
                    out.startElement("tr", null);
                    out.startElement("td", null);
                    out.startElement("label", null);
                    out.writeAttribute("for", outParameterName, null);
                    out.writeText(outParameterName, null);
                    out.endElement("label");
                    out.endElement("td");
                    out.startElement("td", null);
                    out.startElement("input", null);
                    out.writeAttribute("type", "text", null);
                    out.writeAttribute("name", outParameterName, null);
                    try {
                        final String value = (String)method.invoke(bean, getterArguments);
                            out.writeAttribute("value", value, null);
                    } catch (Exception e) {
                        throw new IOException(e);
                    }
                    out.endElement("input");
                    out.endElement("td");
                    out.endElement("tr");
                }
            }
            out.endElement("table");
        }

Para "pintar" el componente tenemos los métodosencodeBegin, encodeChildren y encodeEnd.Estos tres métodos permiten pintar la etiqueta inicial, pintarel cuerpo o etiquetas anidadas, y pintar el final de la etiqueta. Enlos casos en los que no es necesario hacer esta separación(como en nuestro caso), se mete todo el código en el métodoencodeEnd. Nuevamente recordar que no es más queun ejemplo.

5. El tag

Como último paso vamos a crear un tag de JSP para poderusar cómodamente nuestro componente en una página JSP.Para ello vamos a extender de la clasejavax.faces.webapp.UIComponentTag. Veamos como quedanuestra clase:

private String bean = null;

El tag admitirá un parámetro donde se recogeráel lenguaje de expresiones de JSF que indica el POJO que se usaráen el componente. En el atributo bean será dondeguardemos la cadena con este lenguaje de expresiones.

 @Override
        public String getComponentType() {
                return FormBeanComponent.COMPONENT_TYPE;
        }

        @Override
        public String getRendererType() {
                return FormBeanComponent.DEFAULT_RENDERER_TYPE;
        }

Estos dos métodos estamos obligados a sobreescribirlos porextender de UIComponent. getComponentType()sirve para indicar la familia del componente, y getRenderType()el tipo de render que sabe pintarlo. Estos dos métodosdeterminarán el render que hay que usar para pintar elcomponente cuando se procese el tag. Se están usando lasconstantes que habíamos definido en la clase del componente.<

@Override
        protected void setProperties(UIComponent component) {
            super.setProperties(component);

            final Application application = FacesContext.getCurrentInstance().getApplication();
            final FormBeanComponent formBeanComponent = (FormBeanComponent)component;

            if (bean != null) {
                // Con isValueReference comprobamos si el valor del atributo puesto
                // en la JSP se trata de EL (Expresion Languaje #{ } )
                if (isValueReference(bean)) { 
                    formBeanComponent.setBeanBinding(application.createValueBinding(bean));
                }
            }
        }

Sobreescribimos este método para procesar los parámetrosindicados en el tag en la JSP. Se comprueba que el atributo beantenga valor, y que este realmente esté apuntando a un backbean. No se nos debe olvidar llamar a super. Nótesecomo el parámetro component es nuestro componenteya creado por JSF.

@Override
        public void release() {
                super.release();
                bean = null;
        }

Los tags se reutilizan, así que es muy convenientesobreescribir el método release() para noencontrarnos con sorpresas.Toda librería de tags necesita un descriptor (*.tld) paraque el servidor sepa manejarlo. Para nuestro tag tendremos elsiguiente descriptor:


1.01.2LibrerÃa de etiquetas de validación para JSFapp-jsf-validator-taglibrary_1.0app-jsf-validatorEtiquetas para JSFbeanFormComponentcom.app.web.jsf.component.FormBeanTagJSP
                        Etiqueta para usar el componente personalizado en la una JSP
                beantruefalseString

Básicamente estamos indicando el nombre del tag"beanFormComponent la clase que lo implementa, yque tiene un atributo "bean" de tipo String que esobligatorio.

6. Ejemplo de uso

Ya tenemos todas las piezas para usar nuestro componente a medida.Una JSP de ejemplo podría ser la siguiente:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<%@ taglib uri="http://myfaces.apache.org/tomahawk" prefix="t" %>
<%@ taglib uri="app-jsf-tags" prefix="a" %>



Insert title here

Arriba se incluye el taglib uri="app-jsf-tags" (paraver el significado de esta uri ver el web.xml del tar.gz con elcódigo completo), y le damos el prefijo "a".Ahora para usar la etiqueta sólo tenemos que hacer:

Donde "#{contact}" es el lenguaje de expresiones deJSF que indica el POJO que vamos a vincular al componente. Ojo"contact" deberá existir en algúnámbito (request, session, aplicación), o estar definidoel faces-config.xml.Y hablando del faces-config.xml, no se nos puede olvidar dar dealta en este fichero tanto el componente como el render que lo va apintar:


        com.app.web.jsf.component.FormBeanComponentcom.app.web.jsf.component.FormBeanComponentcom.app.web.jsf.component.FormBeanComponentcom.app.web.jsf.component.HtmlFormBeanRenderercom.app.web.jsf.component.HtmlFormBeanRenderer

Primero hemos dado de alta el componente hemos definido la clase quelo implementa y el tipo del componente (corresponde con la constanteCOMPONENT_TYPE que habíamos definido en la claseFormBeanComponent).Luego damos de alta el render, indicamos su clase, el tipo derender (corresponde con la constante DEFAULT_RENDER_TYPEque habíamos definido en la clase FormBeanComponent),y la familia de componentes que sabe pintar (corresponde con laconstante COMPONENT_TYPE que habíamos definido enla clase FormBeanComponent).

7. Conclusiones

Recordamos que este componente que hemos construido es simplementeun ejemplo, y que hay cosas que no se han tenido en cuenta (como porejemplo las validaciones, o uso de los converters). Cuandoimplementéis vuestros componentes tenéis que prestarmucha atención sobre el ciclo de vida completo.Y ya sabéis, intentar usar algo que ya exista (siempre semás fácil y más barato integrar quedesarrollar), construir a partir de algo que ya exista, y si no osqueda más remedio hacerlo de cero. Y siempre tendréisuna cuarta opción: contactar con nosotros en www.autentia.compara que os echemos una mano 😉

8. Sobre el autor

Alejandro Pérez García, Ingeniero en Informática(especialidad de Ingeniería del Software)Socio fundador de Autentia (Formación, Consultoría,Desarrollo de sistemas transaccionales)mailto:alejandropg@autentia.comAutentia Real Business Solutions S.L. - "Soporte a Desarrollo"http://www.autentia.com 

Alejandro es socio fundador de Autentia y nuestro experto en Java EE, Linux y optimización de aplicaciones empresariales. Ingeniero en Informática y Certified ScrumMaster. Seguir @alejandropgarci Si te gusta lo que ves, puedes contratarle para darte ayuda con soporte experto, impartir cursos presenciales en tu empresa o para que realicemos tus proyectos como factoría (Madrid). Puedes encontrarme en Autentia: Ofrecemos servicios de soporte a desarrollo, factoría y formación.

2 COMENTARIOS

  1. Saludos, Muy bueno el ejemplo,

    Ahora tengo una duda siguiendo tu ejemplo cree mi propio componente, el cual funciona perfecto, pero se me presento un detalle, que a incluirlo en una pagina .xhmtl con facelets el componente no funciona correctamente, ya que por lo visto el facelest no ejecuta la clase manejadora del tag es decir la que hereda de (javax.faces.webapp.UIComponentTag) si no que trabaja directamente con el component y el renderer directamente, pero a no utilizar esta clase no ejecuta el metodo setProperties(UIComponent component); y este es el cual realiza el seteo de los atributos del componente, y hace que en el renderer de un error de NULL al no ser seteado los atributos, si tienes alguna sugerencia, seria de mucha hayuda y gracias de antemano

  2. Tengo una consulta, como hago para integrar esto a paginas xhtml, segun otro articulo publicado aqui mismo (http://www.adictosaltrabajo.com/tutoriales/tutoriales.php?pagina=migrateJSF2Facelets) , hice todos los pasos, pero no encuentro la forma que los componentes en el \\\»xhtml\\\» me desplieque las propuedades que necesito asignarles, solo me despliequa el id y yo tengo varias propiedades en el tld (name, values, label,requerida, etc.) y programadas en una clase \\\»InputEntryTag\\\» pero esto no lo puedo habilitar ejmp de como se me muestra acutalmenete en le xhtml: \\\»\\\»

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