quinta-feira, 2 de dezembro de 2010

Implementando o pattern Composite View com Tags JSP

Update 1: Atualizei o nome deste post, uma vez que "Templates estilo Django para páginas JSP" não refletia muito o conteúdo. O código também está disponível no repositório de códigos-fonte http://code.ronoaldo.net/composite-view/

Uma das ferramentas do Django que eu admiro é a sua engine de templates, pois com ele produzimos layouts uniformes para toda a sua aplicação web de maneira estruturada.

O problema


Em Django, você define um template base, que geralmente contém o cabeçalho de suas páginas, e define blocos onde o conteúdo será inserido. Nas páginas de conteúdo, você utiliza uma tag para indicar que você está estendendo o template base, e preenche apenas os blocos de conteúdo a serem alterados.

Isso funciona como uma herança de templates, onde o template filho apenas sobrescreve os blocos de conteúdo do template pai, ao mesmo tempo que permite ter um blocos de conteúdo padrão em um só arquivo. E mais, você pode criar uma hierarquia de quantos níveis forem necessários (e razoáveis!), proporcionando reuso inclusive de sua marcação HTML.

O problema aparece quando você tenta realizar algo semelhante em outras tecnologias com as quais acaba trabalhando. Estamos portando uma aplicação Django para o AppEngine, e por restrições diversas, a tecnologia escolhida foi Java.


Optamos por causar o mínimo de overhead, e escolhemos usar apenas as tecnologias estritamente necessárias. Além do mais, temos poucas interfaces distintas, uma meia dúzia de jsps já resolveria o problema, e criamos uma interface administrativa utilizando o Google Web Toolkit, que nos permitiu criar uma interface rica e reutilizar  diversos códigos de validação server-side em uma versão client-side.

Por questões de simplicidade, geralmente criamos blocos reutilizáveis em JSP da seguinte maneira. No arquivo cabecalho.jsp:
<h1>Cabecalho</h1>

No arquivo rodape.jsp:
<i>Todos os direitos reservados</i>

No arquivo index.jsp:
<%@ include file="cabecalho.jsp" %>
Conteúdo
<%@ include file="rodape.jsp" %>

Obviamente, isso chega a ofender. Pesquisando um pouco, encontrei algumas soluções, quase todas baseadas no pattern Composite View. Basta fazer uma busca e você vai encontrar implementações, mas a maioria delas dá muito trabalho e não é tão intuitiva quanto a solução Django.

Resolvi desenvolver algo um pouco mais simples, a partir da seguinte idéia: criar uma custom tag que define um bloco, e criar uma outra tag que permite extender um template (outra página JSP). A tag que define um bloco, o registra no contexto da requisição e salva um buffer com o conteúdo do mesmo, e em seguida, passa o controle da página para o template pai.

A idéia seria utilizar nossas tags de template da seguinte forma:

No arquivo base.jsp, temos a definição dos blocos:
<%@ page language="java" pageEncoding="utf-8" %> 
<%@ taglib prefix="t" uri="/templates.tld" %>
<html>
<head>
<title>
<t:block name="title">Título padrão</t:block>
</title>
</head>
</body>
<h1> 
<t:block name="header">Cabeçalho Padrão</t:block>
</h1>
<div id="left-box">  
<t:block name="left">Navegação</t:block>
</div>
<div id="main-box">
<t:block name="main">Bloco de conteúdo</t:block> 
</div>
</body> 
</html>

E na página index.jsp, teríamos apenas:
<%@page language="java" pageEncoding="utf-8" %>
<%@taglib prefix="t" uri="/templates.tld" %>
<t:extends template="base.jsp">
<t:block name="main">
 Conteúdo da Index 
</t:block>
</t:extends>  

Implementação


Primeiro, vamos à definição da tag block:

package net.ronoaldo.tools.templateutils.tags;

import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;

import javax.servlet.jsp.JspContext;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.JspFragment;
import javax.servlet.jsp.tagext.SimpleTagSupport;

/**
 * Tag simples que define um bloco de template.
 * 
 * @author Ronoaldo Pereira &lt;ronoaldo@ronoaldo.net&gt;
 */
public class Block extends SimpleTagSupport {

 /**
  * Chave para recuperar o buffer, no escopo da requisição.
  */
 private static final String BLOCK_REGISTRY_KEY = Block.class.getName()
   + "-BLOCK_REGISTRY_KEY";

 /**
  * {@link Logger} para depuração.
  */
 protected Logger logger = Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);

 /**
  * Nome do bloco.
  */
 private String blockName;

 /**
  * Setter para o atributo name, que identifica unicamente o
  * bloco.
  * 
  * @param name
  *            o nome do bloco.
  */
 public void setName(String name) {
  this.blockName = name;
 }

 /**
  * Realiza a implementação da Tag propriamente dita.
  */
 @Override
 public void doTag() throws JspException, IOException {
  if (withinExtendsBlock()) {
   updateBlockContent();
  } else {
   renderBlock();
  }
 }

 /**
  * Atualiza o conteúdo em cache do bloco, caso ele ainda não tenha sido
  * definido.
  * 
  * @throws JspException
  * @throws IOException
  */
 private void updateBlockContent() throws JspException, IOException {
  // Renderiza o bloco caso ele ainda não exista
  if (getRegistry().get(blockName) != null)
   return;

  StringWriter sw = new StringWriter();
  JspFragment body = getJspBody();
  if (body != null) {
   body.invoke(sw);
  }
  getRegistry().put(blockName, sw.toString());
  logger.info(String.format("Content for block %s updated to %s",
    blockName, sw.toString()));
 }

 /**
  * Renderiza o bloco na página JSP.
  * 
  * @throws JspException
  * @throws IOException
  */
 private void renderBlock() throws JspException, IOException {
  // Insere o bloco na página
  updateBlockContent();
  JspWriter out = getJspContext().getOut();
  out.print(getRegistry().get(blockName));
 }

 /**
  * Identifica se esta tag {@link Block} está dentro de uma tag
  * {@link Extends}.
  * 
  * @return
  */
 private boolean withinExtendsBlock() {
  return (getParent() instanceof Extends);
 }

 /**
  * Recupera ou cria um registro no escopo da requisição, para armazenar os
  * buffers dos blocos da página a ser exibida.
  * 
  * @return um {@link Map} contendo os valores associados ao nome do bloco.
  */
 private Map<String, String> getRegistry() {
  JspContext ctx = getJspContext();

  @SuppressWarnings("unchecked")
  Map<String, String> registry = (Map<String, String>) ctx.getAttribute(
    Block.BLOCK_REGISTRY_KEY, PageContext.REQUEST_SCOPE);

  if (registry == null) {
   registry = new HashMap<String, String>();
   ctx.setAttribute(Block.BLOCK_REGISTRY_KEY, registry,
     PageContext.REQUEST_SCOPE);
  }

  return registry;
 }
}

Esta tag é bem simples. Neste caso, estamos utilizando uma implementação baseada em um Map<String, String>, para armazenar apenas um valor para o bloco durante o processamento de todas as tags das páginas envolvidas.

Se utilizarmos apenas esta Tag, já conseguimos criar um efeito bem interessante. Basta definir os blocos antes de qualquer outra coisa, e finalizar a página com a diretiva <%@include %>. Isso já nos dá o resultado esperado, exceto para aninhar páginas.

Para uma implementação mais completa, vamos definir a tag Extends:

package net.ronoaldo.tools.templateutils.tags;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.SimpleTagSupport;

/**
 * Tag simples que realiza um include após realizar o processamento de
 * seu conteúdo.
 * 
 * @author Ronoaldo Pereira &lt;ronoaldo@ronoaldo.net&gt;
 */
public class Extends extends SimpleTagSupport {

 /**
  * Nome do template a ser utilizado para inclusão.
  */
 private String template;

 /**
  * Setter para que o atributo template funcione.
  * 
  * @param template
  *            o nome do template a ser extendido.
  */
 public void setTemplate(String template) {
  this.template = template;
 }

 /**
  * Implementação da Tag.
  */
 @Override
 public void doTag() throws JspException, IOException {
  // Processa o body (definição de blocos)
  getJspBody().invoke(null);

  // Realiza o include do template
  try {
   PageContext pageContext = (PageContext) getJspContext();
   pageContext.include(template);
  } catch (ServletException e) {
   throw new JspException(e);
  }
 }

}

Para finalizar com chave de ouro, basta agora realizar a implementação de um arquivo tld:

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE taglib PUBLIC
 "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
 "http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">
<taglib>
 <tlib-version>1.2</tlib-version>
 <jsp-version>2.0</jsp-version>
 <short-name>template-utils</short-name>
 <description>Several utilities for templating with JSP and JSTL</description>

 <tag>
  <name>extends</name>
  <tag-class>net.ronoaldo.tools.templateutils.tags.Extends</tag-class>
  <body-content>scriptless</body-content>
  <attribute>
   <name>template</name>
   <required>true</required>
   <type>java.lang.String</type>
   <description>Indicates the parent template to extends from.</description>
  </attribute>
 </tag>

 <tag>
  <name>block</name>
  <tag-class>net.ronoaldo.tools.templateutils.tags.Block</tag-class>
  <body-content>scriptless</body-content>
  <attribute>
   <name>name</name>
   <required>true</required>
   <type>java.lang.String</type>
   <description>The block name, unique across all template and its extensions</description>
  </attribute>
 </tag>

</taglib>

Você pode até criar um pequeno jar com estas classes e este arquivo taglib.tld dentro de META-INF, e ele pode ser incluído no seu diretório WEB-INF/lib como biblioteca reutilizável.