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.
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 <ronoaldo@ronoaldo.net> */ 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 <ronoaldo@ronoaldo.net>
*/
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 WEB-INF/lib
como biblioteca reutilizável.
Bem interessante!
ResponderExcluirprocurei semanas por algo assim antes de entregar um projeto mas só encontrei agora :(
Está faltando mais pessoas fodásticas como você, que ao invés de ficar usando os include cabeçalho e include rodapé, faz o que um programador realmente deve fazer: Desenvolve uma solução completa para o problema.
ResponderExcluirParabéns pela iniciativa de tornar o mundo um lugar melhor para os programadores!!!
Daniel, pois é, eu também fiquei procurando, e demorei um pouco para encontrar. Daí a idéia de fazer algo assim.
ResponderExcluirMauricio, obrigado! Eu não poderia ficar com isso só para mim, então achei que seria interessante postar no ronoblog para melhorar a vida de mais gente.
Muito boa a iniciativa, Ronoaldo! Se um dia voltar a usar JSP, com certeza irei utilizar desse artifício para contornar o uso excessivo de includes.
ResponderExcluirQuanto a licença do código, qual é? Uma dica é que seria bastante interessante e útil para a galera se você organizasse isso em um GitHub, por exemplo.
[]'s
Obrigado pelo comentário Vitor!
ResponderExcluirQuanto a licença, Apache 2.0. Vou adicionar esta informação na postagem.
Já quanto ao compartilhamento de código, eu atualmente utilizo o Bitbucket. Vou publicar o código lá e criar um jar para facilitar a inclusão em outros projetosa.