Home > JSF, JavaEE > Conversation Scope usando @CustomScoped do JSF 2.0

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 :D . 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.

Gilliard Cordeiro JSF, JavaEE , ,

  1. May 12th, 2009 at 16:32 | #1

    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.

  2. May 12th, 2009 at 16:57 | #2

    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.

  1. May 12th, 2009 at 11:38 | #1