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
github.com/ronoaldo/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 <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
META-INF, e ele pode ser incluído no seu diretório 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.