Conversation Scope usando @CustomScoped do JSF 2.0
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.
Ficou bem bacana a especficação do @CustomScoped. Agora você sabe dizer se já é algo definitivo ou ainda poderá mudar?
Muito bom o post!
Abraços.
Pelo que andei lendo acho que vai ser assim mesmo. Pessoalmente eu preferiria que já tivesse um suporte a conversação mesmo que fosse simples, mas nunca fazem tudo né :/
Apesar do @ViewScoped já melhorar muito, com um @ConversationScoped ia ficar completo, mas isso deve ficar apenas para o JCDI (WebBeans) mesmo.