Desde hace ya bastantes años hemos asistido a la aparición y evolución de frameworks Java de desarrollo web que aportan soluciones propias para el diseño de interfaces (JSF, GWT, Tapestry, Wicket, Grails, Play…). Sin embargo, la ya veterana (y estándar) tecnología JSP sigue estando plenamente vigente debido sobre todo a que es la opción preferente para el diseño de la capa de vista de dos frameworks tan populares como SpringMVC y Struts 2.
Si hacemos uso de la tecnología JSP, puede resultar interesante construir nuestras propias librerias de etiquetas para encapsular y reutilizar diversas funcionalidades, ya sea dentro del mismo proyecto o en otros distintos, sin tener que recurrir a cortar y pegar o a la inclusión de otras páginas JSP. En este artículo veremos lo fácil que resulta 😉
Nota: se usará la versión Servlet/JSP 2.5/2.1 por ser la más alta compatible con el todavía popular Tomcat 6 (estamos en 2012 ;)), pero la última versión publicada es la 3.0/2.2.
Proyecto para pruebas
Una etiqueta JSP consiste básicamente en una implementación de la interfaz Tag (o una «subinterfaz»), y en la definición de la misma en un fichero XML llamado Tag Library Descriptor (TLD para los amigos).El tld puede definir todas las etiquetas o tags que se quieran lo que permite simplificar la creación y utilización de librerias o colecciones de etiquetas.
Para implementar y probar tags, sólo necesitaremos una JSP, por lo que procedemos a crear un proyecto web con Maven extremadamente sencillo. De hecho, las únicas dependencias que necesitamos ya son proporcionadas por el contenedor de servlets o servidor JEE en el que se realice el despliegue por lo que para nosotros estas dependecias serán de tipo provided.
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.danielme.blog</groupId> <artifactId>jsp-tags-demo</artifactId> <version>0.1</version> <packaging>war</packaging> <name>jsp-tags-demo</name> <description>Ejemplo de implementación de etiquetas (tags) para JSP 2.1</description> <developers> <developer> <name>Daniel Medina</name> <url>http://danielme.com</url> </developer> </developers> <licenses> <license> <name>GPL v3</name> <url>http://www.gnu.org/licenses/gpl-3.0.html</url> </license> </licenses> <properties> <encoding>UTF-8</encoding> </properties> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>2.4</version> <configuration> <encoding>${encoding}</encoding> <source>1.6</source> <target>1.6</target> </configuration> </plugin> <!--mvn -Djetty.port=9999 jetty:run --> <!-- las versiones más actuales de este plugin requieren Maven 3 según la documentación --> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> <version>7.5.0.RC2</version> <configuration> <scanIntervalSeconds>10</scanIntervalSeconds> <scanTargets> <scanTarget>src/main/webapp/WEB-INF</scanTarget> <scanTarget>src/main/webapp/WEB-INF/web.xml</scanTarget> </scanTargets> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.2</version> <configuration> <archive> <manifestEntries> <Implementation-Version>${project.version}</Implementation-Version> <Implementation-Title>${artifactId}</Implementation-Title> <Extension-Name>${artifactId}</Extension-Name> <Built-By>Daniel Medina</Built-By> </manifestEntries> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-eclipse-plugin</artifactId> <version>2.9</version> <configuration> <projectNameTemplate>[artifactId]</projectNameTemplate> <wtpmanifest>false</wtpmanifest> <wtpapplicationxml>true</wtpapplicationxml> <wtpversion>2.0</wtpversion> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>2.5</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.1</version> <scope>provided</scope> </dependency> </dependencies> </project>
Añadimos un web.xml mínimo y una JSP como index de la web.
<?xml version="1.0" encoding="UTF-8"?> <web-app id="struts_blank" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <display-name>JSP TAGS DEMO</display-name> <welcome-file-list> <welcome-file>/jsp/index.jsp</welcome-file> </welcome-file-list> </web-app>
<%@ page contentType="text/html;charset=UTF-8"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP TAGS DEMO</title> </head> <body> <h2>Desarrollo de etiquetas JSP</h2> </body> </html>
Ya tenemos todo lo necesario para empezar a trabajar.
La primera etiqueta
Vamos a implementar la etiqueta más simple posible. Generalmente no tendremos que implementar la interfaz Tag y será suficiente con especializar una de sus implementaciones incluídas en la API. Para nuestro ejemplo nos basta con TagSupport o, más fácil todavía, SimpleTagSupport . Implementamos el método doTag que se limitará a «imprimir» el código html que queramos ver en nuestra JSP como consecuencia de la ejecución de la etiqueta.
package com.danielme.blog.jsptags; import java.io.IOException; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.SimpleTagSupport; /** * Ejemplo de etiqueta sin atributos * @author danielme.com * */ public class HelloWorldTag extends SimpleTagSupport { @Override public void doTag() throws JspException, IOException { getJspContext().getOut().print("<b>Hello world!!</b>"); } }
Para utilizar la etiqueta hay que crear el TLD correspondiente e importarlo en la JSP.Vamos a crearlo con el nombre dm.tld (es el prefijo que usaremos para todas las etiquetas que queramos definir en el mismo tld) y lo ubicaremos en el mismo directorio que el web.xml:
<?xml version="1.0" encoding="UTF-8" ?> <taglib xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd" version="2.1"> <tlibversion>1.0</tlibversion> <shortname>dm</shortname> <info>ejemplos del tutorial de introducción al desarrollo de etiquetas para JSP 2.1</info> <tag> <name>helloWorld</name> <tagclass>com.danielme.blog.jsptags.HelloWorldTag</tagclass> <info>imprime un hello world en negrita</info> <body-content>empty</body-content> </tag> </taglib>
Se han definido los siguientes valores:
- tlibversion: (obligatorio): versión que se quiera dar a la libreria de tags.
- shortname: (opcional): prefijo recomendado para las etiquetas (en la JSP se usará el prefijo que se establezca al importar el tld). Debe ser lo más corto posible, los nombres descriptivos los dejaremos para las etiquetas.
- info: (opcional): descripción de la libreria de etiquetas.
- body-content: (obligatorio para SimpleTagSupport): cuerpo de la etiqueta.Si no lo admitimos será empty, en otro caso scriptless.
Cada etiqueta se define dentro de un elemento tag. Para este ejemplo tan simple se han usado los siguientes atributos:
- name: (obligatorio): el nombre de la etiqueta.
- tagclass: (obligatorio): clase que implementa la etiqueta.
- info: (opcional): descripción de la etiqueta. Conviene definirla ya que, por ejemplo, Eclipse la mostrará como la ayuda contextual de la etiqueta.
En nuestra JSP se importa el TLD indicando su ruta y el prefijo a utilizar para sus etiquetas y ya estamos en condiciones de utilizar la etiqueta.
<%@ page contentType="text/html;charset=UTF-8"%> <%@taglib prefix="dm" uri="/WEB-INF/dm.tld"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP TAGS DEMO</title> </head> <body> <h2>Implementación de etiquetas en JSP 2.1</h2> <dm:helloWorld/> </body> </html>
Paso de parámetros
La funcionalidad proporcionada por una etiqueta puede ser configurable mediante el paso de atributos a la misma. Estos atributos pueden ser opcionales u obligatorios según nos convenga y su utilización no puede ser más sencilla: simplemente los recibimos en la clase que implementa la etiqueta a través de su correspondiente setter.
A modo de ejemplo, vamos añadir otra etiqueta a nuestra librería «dm» que recibirá dos parámetros: una cadena de texto obligatoria que se imprimirá en la JSP como resultado de la ejecución de la etiqueta, y un parámetro booleano y opcional que indicará si queremos que la cadena que recibimos se muestre en negrita.La clase queda tal que así:
package com.danielme.blog.jsptags; import java.io.IOException; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.SimpleTagSupport; /** * Ejemplo de etiqueta con atributos. * * @author danielme.com * */ public class PrintTextTag extends SimpleTagSupport { // opcional.Por omisión se considerará falso. private boolean bold = false; // obligatorio según el TLD. private String cadenaObligatoria; @Override public void doTag() throws JspException, IOException { String salida = cadenaObligatoria; if (bold) { salida = "<b>" + salida + "</b>"; } getJspContext().getOut().print(salida); } public void setBold(boolean bold) { this.bold = bold; } public void setCadenaObligatoria(String cadenaObligatoria) { this.cadenaObligatoria = cadenaObligatoria; } }
Para esta nueva etiqueta hay que incluir en el elemento tag cada uno de los atributos que se pueden utilizar.
<tag> <name>printText</name> <tagclass>com.danielme.blog.jsptags.PrintTextTag</tagclass> <info>imprime la cadena recibida en negrita (opcionalmente)</info> <body-content>empty</body-content> <attribute> <name>cadenaObligatoria</name> <description>cadena que se imprimirá como resultado de la ejecución de la etiqueta</description> <required>true</required> <rtexprvalue>true</rtexprvalue> </attribute> <attribute> <name>bold</name> <description>indica si la cadena proporcionada se mostrará en negrita</description> </attribute> </tag>
Cada etiqueta se define dentro de un elemento tag. Disponemos de los siguientes atributos:
- name: (obligatorio): el nombre del atributo.
- description: (opcional): descripción del atributo. Eclipse lo usará como ayuda contextual al editar la JSP.
- required: (opcional): indica si el atributo es obligatorio. Por omisión es false.
- rtexprvalue: (opcional): indica si el valor asignado al atributo puede ser una expresión EL que deberá ser evaluada antes de ejecutar la etiqueta. Por omisión es false.
- type: (opcional): tipo del atributo, sólo se aplica si el valor del atributo es el resultado de una expresión que no es una cadena.
Podemos probar esta etiqueta y comprobar que:
- Si no se proporciona el atributo cadenaObligatoria se lanza una excepción al compilar la JSP.
- Si el parámetro bold no es un boolean no se produce ningún error y su setter en la clase PrintTextTag no se ejecuta para evitar errores de casting. Tendremos que tener cuidado con este comportamiento a la hora de depurar errores.
- Si el parámetro bold contiene una expresión EL se lanzará una excepción ya que no permite expresiones.
- Si cadenaObligatoria contiene una expresión regular, el resultado de su evaluación será lo que recibamos en PrintTextTag.
Una prueba sin errores podría ser la siguiente:
<%@ page contentType="text/html;charset=UTF-8"%> <%@taglib prefix="dm" uri="/WEB-INF/dm.tld"%> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP TAGS DEMO</title> </head> <body> <h2>Implementación de etiquetas en JSP 2.1</h2> <dm:helloWorld/><br/> <dm:printText cadenaObligatoria="cadena obligatoria normal"/> <br/> <dm:printText cadenaObligatoria="cadena obligatoria en negrita" bold="true"/> <br/> <dm:printText cadenaObligatoria="el id de la sesión es: ${pageContext.session.id}"/> </body> </html>
Etiquetas con cuerpo
Vamos a ver cómo implementar una etiqueta con cuerpo. Este cuerpo puede ser html, expresiones o incluso el resultado de la ejecución de otras etiquetas. En concreto, vamos a implementar la etiqueta toUpperCase que, tal y como su nombre sugiere, convierte a mayúsculas el texto contenido en su cuerpo.
Volvemos a especializar la clase SimpleTagSupport, aunque si queremos mayor control también se puede utilizar BodyTagSupport.Habrá que utilizar el método getJspBody para obtener el cuerpo.
package com.danielme.blog.jsptags; import java.io.IOException; import java.io.StringWriter; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.SimpleTagSupport; /** * Ejemplo de etiqueta con cuerpo. * * @author danielme.com * */ public class ToUpperCaseTag extends SimpleTagSupport { @Override public void doTag() throws JspException, IOException { StringWriter body = new StringWriter(); getJspBody().invoke(body); getJspContext().getOut().print(body.toString().toUpperCase()); } }
En la definición del tag hay que permitir que tenga cuerpo, de lo contrario la compilación de la JSP fallará.
<tag> <name>toUpperCase</name> <tagclass>com.danielme.blog.jsptags.ToUpperCaseTag</tagclass> <info>Convierte a mayúsculas el cuerpo de la etiqueta</info> <body-content>scriptless</body-content> </tag>
Podemos probar esta nueva etiqueta usando como cuerpo otra de nuestras etiquetas.
<dm:toUpperCase> <dm:printText cadenaObligatoria="cadena de prinText convertida a mayúsculas por toUpperCase"/> </dm:toUpperCase>
El resultado final :
Etiquetas anidadas
A veces implementar una única etiqueta no es suficiente. Por ejemplo, supongamos que tenemos una etiqueta que debe recibir una lista de parámetros definida en la JSP. Podríamos pasar de forma chapucera esos parámetros como cadenas con los valores separados por comas, pero la mejor solución es implementar otra etiqueta que «colabore» con la etiqueta «padre» y le proporcione esos parámetros.
Para probar este concepto, vamos a crear una etiqueta llamada PrintListTag que imprimirá una lista HTML con el contenido de una lista de cadenas. Cada una de estas cadenas será proporcionada por una etiqueta ListElementTag incluida en su cuerpo de tal modo que:
- PrintListTag ejecutará su cuerpo, y por lo tanto, las etiquetas que contenga.
- Cada etiqueta ListElementTag contenida en el cuerpo de PrintListTag obtendrá la etiqueta padre con el método getParent. Esta etiqueta, que será siempre la misma instancia, tendrá un metodo para poder enviarle la cadena desde la etiqueta «hija» del cuerpo.
- Una vez ejecutado el cuerpo, esto es, las etiquetas hijas, ya tendremos las cadenas que estas definen en nuestro PrintListTag por lo que sólo resta generar su resultado.
Las etiquetas:
package com.danielme.blog.jsptags; import java.io.IOException; import java.util.LinkedList; import java.util.List; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.SimpleTagSupport; /** * Ejemplo de etiqueta con etiquetas "hijas" * * @author danielme.com * */ public class PrintListTag extends SimpleTagSupport { private List<String> list = new LinkedList<String>(); @Override public void doTag() throws JspException, IOException { //se "ejecuta" el cuerpo getJspBody().invoke(null); //ahora simplemente imprimimos la lista StringBuffer sb = new StringBuffer(); sb.append("<ul>"); for (String s : list) { sb.append("<li>" + s + "</li>\n"); } sb.append("</ul>"); getJspContext().getOut().print(sb.toString()); } public void addElement(String s) { list.add(s); } }
package com.danielme.blog.jsptags; import java.io.IOException; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.JspTag; import javax.servlet.jsp.tagext.SimpleTagSupport; /** * Permite definir los elementos para PrintList. * * @author danielme.com * */ public class ListElementTag extends SimpleTagSupport { private String value; @Override public void doTag() throws JspException, IOException { JspTag parent = getParent(); //asegura que la etiqueta se usa correctamente if (parent == null || ! (parent instanceof PrintListTag)) { throw new JspException("la etiqueta ListElement siempre debe estar anidada en PrintList"); } else { ((PrintListTag) parent).addElement(value); } } public void setValue(String value) { this.value = value; } }
La definción en el TLD que ya no debería suponermos problema alguno 🙂
<tag> <name>printList</name> <tagclass>com.danielme.blog.jsptags.PrintListTag</tagclass> <info>Crea una lista sin orden con los elementos anidados</info> <body-content>scriptless</body-content> </tag> <tag> <name>listElement</name> <tagclass>com.danielme.blog.jsptags.ListElementTag</tagclass> <info>Elemento de la lista</info> <body-content>empty</body-content> <attribute> <name>value</name> <description>elemento para la lista</description> <required>true</required> <rtexprvalue>true</rtexprvalue> </attribute> </tag>
Terminamos el tutorial probando la última etiqueta:
<dm:printList> <dm:listElement value="elemento 1 de la lista" /> <dm:listElement value="elemento 2 de la lista" /> </dm:printList>
Código de ejemplo
- proyecto Maven en mi cuenta de GitHub
Ahora te toca hacerla para JSF 😛
No quiero invadir tu territorio 🙂
Muy bien hecho, me gusta como lo explicas, felicidades. Hice el ejercicio hasta donde se pasan los parametros a la etiqueta. Saludos. Simple, tag, EL, .
Muy bueno