Apesar do título, o JSF 2 não vai ter um escopo Conversation ou coisa parecida como tem no Seam ou Orchestra. O que vai ter é o suporte direto a escopos personalizados, o que torna simples a criação de um escopo como Conversation.
Nesse exemplo eu desenvolvi um escopo de conversação simples, mas não é difícil melhorá-lo para suportar diversas conversações simultâneas, assimilando a idéia a gente vê que não é nenhum bicho de sete cabeças. Claro que o principal objetivo é o aprendizado de como tudo funciona, e não ficar implementando o que já tem pronto em diversos frameworks.
Vamos então ao exemplo.
@ManagedBean(name="testeBean")
@CustomScoped("#{conversacao}")
public class TesteBean {
private Integer contador;
@PostConstruct
public void init()
{
System.out.println("TesteBean.init()");
contador = 0;
}
public void incrementarContador()
{
contador++;
}
public void iniciaConversacao()
{
Conversacao.instancia().iniciar();
}
public void finalizaConversacao()
{
Conversacao.instancia().finalizar();
}
public Integer getContador() {
return contador;
}
public void setContador(Integer contador) {
this.contador = contador;
}
}
Acima está um managed bean que chamei de “beanTeste”. Coloquei um println no método init() e anotei esse método com @PostConstruct. Na prática isso quer dizer que toda vez que o managed bean for construído esse método será chamado, e consequentemente aparecerá no console. Isso é útil para vermos que o escopo de conversação realmente está funcionando. Já que estamos falando de escopo personalizado, quem faz isso é a anotação @CustomScoped, que recebe como valor uma EL que será resolvida por um EL Resolver que vamos criar.
Agora vamos ver a tela
Contador atualizado via ajax e mantido com a conversação: <h:outputText id="output3" value="#{testeBean.contador}"/>
<br/>
<h:commandButton action="#{testeBean.incrementarContador}" value="Incrementar Contador">
<f:ajax render="output3"/>
</h:commandButton>
<h:commandButton action="#{testeBean.iniciarConversacao}" value="Iniciar Conversação">
<f:ajax render="@none"/>
</h:commandButton>
<h:commandButton action="#{testeBean.finalizarConversacao}" value="Finalizar Conversação">
<f:ajax render="@none"/>
</h:commandButton>
Tanto o exemplo da tela quanto do managed bean estão simplificados para o nosso exemplo, mas a versão disponível para download contém uns exemplos para testarmos o funcionamento do ajax também.
A idéia do nosso escopo “conversacao” é o mesmo do Seam, ele funciona como request até que seja explicitamente iniciada a conversação. Quando isso ocorre, o escopo passa a ser statefull até que a conversação seja finalizada, fazendo com que ela funcione como request novamente.
Seguindo essa idéia, o contador que aparece na tela não vai sair de “1″ até que a conversação seja iniciada, pois como o escopo é request, toda vez que executamos a ação “incrementaContador” o managed bean será criado novamente (contador = 0), depois a ação será executada (contador = 1) e então a página será renderizada (exibe contador = 1). Agora se a conversação for iniciada o managed bean será mantido entre as requisições, e o contador não será zerado até que a conversação termine.
Para nosso escopo funcionar, precisamos de um ELResolver, que será quem vai conseguir dizer para o faces de onde virá os objetos relacionados ao escopo “conversacao”. Registramos nosso ELResolver no faces-config.xml assim
<?xml version='1.0' encoding='UTF-8'?>
<faces-config 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-facesconfig_2_0.xsd"
version="2.0">
<application>
<el-resolver>br.eti.gilliard.exemplojsf2.conversacao.ConversacaoELResolver</el-resolver>
</application>
</faces-config>
E a implementação fica assim
public class ConversacaoELResolver extends ELResolver {
@Override
public Object getValue(ELContext elContext, Object base, Object property) {
if (property == null) {
throw new PropertyNotFoundException("A Propriedade não pode ser nula!");
}
if (base == null) {
if(Conversacao.NOME_ESCOPO.equals(property.toString()))
{
Conversacao conversacao = Conversacao.instancia();
elContext.setPropertyResolved(true);
return conversacao;
}
Conversacao conversacao = Conversacao.instancia();
return getValue(conversacao, property.toString(), elContext);
} else if (base instanceof Conversacao) {
return getValue((Conversacao) base, property.toString(), elContext);
}
return null;
}
private Object getValue(Conversacao conversacao, String property, ELContext elContext) {
Object objeto = conversacao.get(property);
elContext.setPropertyResolved(objeto != null);
return objeto;
}
// os outros métodos foram suprimidos nesse exemplo
}
Essa é uma implementação comum de EL Resolver, onde eu uso o objeto retornado por Conversacao.instancia() para localizar as propriedades solicitadas.
Para o jsf um escopo nada mais é do que um java.util.Map, e de fato a classe Conversacao estende ConcurrentHashMap, ou seja, é um Map como pede o jsf. Fora isso os métodos get e put foram sobrescritos para funcionarem de acordo com nossa especificação de conversação, ou seja, se ela não foi iniciada tudo deve funcionar como request, agora quando a conversação é iniciada, os valores passam a ser guardados dentro da sessão do usuário, fazendo assim ficar statefull. Depois que a conversação é finalizada os valores voltam a ser guardados no request.
Antes de ver a implementação da classe Conversacao, o mais importante é entender como o mecanismo de resolução de EL funciona. Como visto no faces-config.xml, não existe nenhuma ligação da nossa implementação de ELResolver com a El “#{conversacao}” que colocamos na anotação @CustomScoped do nosso managed bean. Toda vez que uma EL é encontrada ela é passada para os ELResolvers contidos na aplicação. Obviamente existem outras implementações padrões já disponíveis, e a nossa vai entrar nessa fila. Como nenhuma das outras implementações vai saber resolver essa EL, ela acaba vindo para a nossa implementação, e então quando encontramos o objeto procurado utilizamos o método “ElContext.setPropertyResolved(boolean b)” passando true para informar que não precisa continuar perguntando para os demais ELResolver’s, pois o nosso já descobriu quem é o objeto.
Existem alguns detalhes que devemos seguir ao implementar um escopo personalizado, como o de avisar, utilizando o novo sistema de eventos do JSF 2, que estamos criando ou destruindo nosso escopo.
Além disso para fazer essa implementação suportar múltiplas conversações seria necessário apenas colocar um nível a mais de mapa na nossa implementação, onde teríamos uma identificação da conversação e então seu contexto. Em vez de um simples Map, ficaria um Map de Map
. Então para saber qual Map interno devolver a gente buscaria a conversação atual de algum contexto da nossa escolha, e poderíamos deixar um combobox sempre visível na tela para o usuária escolher a conversação que ele quer usar. Novamente nada de novo, tudo igual o funcionamento do Seam, por exemplo.
Agora sim vamos à implementação da classe Conversacao.
public class Conversacao extends ConcurrentHashMap<string,Object>{
private static final long serialVersionUID = 7556965369432050706L;
public static final String NOME_ESCOPO = "conversacao";
private static final String CONVERSACAO_ATUAL = "exemplojsf2.conversacao.ConversacaoAtual";
private boolean conversacaoNaoIniciada = true;
private Conversacao() {
}
public static Conversacao instancia()
{
Map<string, Object> sessionMap = FacesContext.getCurrentInstance().getExternalContext().getSessionMap();
Conversacao conversacao = (Conversacao) sessionMap.get(CONVERSACAO_ATUAL);
if(conversacao == null)
{
conversacao = new Conversacao();
sessionMap.put(CONVERSACAO_ATUAL, conversacao);
}
return conversacao;
}
public Object get(Object propriedade)
{
//se a conversacao nao for iniciada funciona como request
if(conversacaoNaoIniciada)
{
return pegarDoRequest(propriedade);
}
return super.get(propriedade);
}
@SuppressWarnings("unchecked")
private Object pegarDoRequest(Object propriedade)
{
Map<string, Object> requestConversation = (Map<string, Object>) FacesContext.getCurrentInstance().getExternalContext().getRequestMap().get(CONVERSACAO_ATUAL);
if(requestConversation != null)
{
return requestConversation.get(propriedade);
}
return null;
}
@Override
public Object put(String key, Object value)
{
//se a conversacao nao for iniciada funciona como request
if(conversacaoNaoIniciada)
{
return colocarNoRequest(key, value);
}
return super.put(key, value);
}
@SuppressWarnings("unchecked")
private Object colocarNoRequest(String key, Object value)
{
Map<string, Object> requestMap = FacesContext.getCurrentInstance().getExternalContext().getRequestMap();
Map<string, Object> requestConversation = (Map<string, Object>) requestMap.get(CONVERSACAO_ATUAL);
if(requestConversation == null)
{
requestConversation = new ConcurrentHashMap<string, Object>();
requestMap.put(CONVERSACAO_ATUAL, requestConversation);
return requestConversation.put(key, value);
}
return requestConversation.put(key, value);
}
public void iniciar()
{
conversacaoNaoIniciada = false;
promoverRequestParaConversacao();
notificarCriacao();
}
@SuppressWarnings("unchecked")
private void promoverRequestParaConversacao()
{
Map<string, Object> requestConversation = (Map<string, Object>) FacesContext.getCurrentInstance().getExternalContext().getRequestMap().get(CONVERSACAO_ATUAL);
super.putAll(requestConversation);
}
private void notificarCriacao()
{
ScopeContext context = new ScopeContext(NOME_ESCOPO, this);
FacesContext facesContext = FacesContext.getCurrentInstance();
facesContext.getApplication().publishEvent(facesContext, PostConstructCustomScopeEvent.class, context);
}
public void finalizar()
{
notificarFinalizacao();
conversacaoNaoIniciada = true;
rebaixarConversacaoParaRequest();
}
@SuppressWarnings("unchecked")
private void rebaixarConversacaoParaRequest()
{
Map<string, Object> requestMap = FacesContext.getCurrentInstance().getExternalContext().getRequestMap();
Map<string, Object> requestConversation = (Map<string, Object>) requestMap.get(CONVERSACAO_ATUAL);
if(requestConversation == null)
{
requestConversation = new ConcurrentHashMap<string, Object>();
requestMap.put(CONVERSACAO_ATUAL, requestConversation);
}
requestConversation.putAll(this);
this.clear();
FacesContext.getCurrentInstance().getExternalContext().getSessionMap().remove(CONVERSACAO_ATUAL);
}
private void notificarFinalizacao()
{
ScopeContext context = new ScopeContext(NOME_ESCOPO, this);
FacesContext facesContext = FacesContext.getCurrentInstance();
facesContext.getApplication().publishEvent(facesContext, PreDestroyCustomScopeEvent.class, context);
}
}
O download do exemplo pode ser feito aqui.
Com certeza deve ter algum errinho nessa implementação, mas se tiver não se desespere, por essas e outras que você certamente deve estar usando um framework mais confiável na tua aplicação do que uma implementação de “fundo de quintal”
. De qualquer forma encontrando os errinhos me diga que eu vou corrigindo.
JSF, JavaEE
JavaEE, JavaServer Faces, JSF