09 fev 2012

Conversor de Entidade no JSF 2 com JBoss Seam 3

Introdução

Provavelmente você já sofreu muito no JSF 1.2 ou versões anteriores devido a não existência de um conversor de entidades padrão, e assim éramos obrigados a procurar soluções para contornar isso. Encontrávamos na comunidade boas soluções como implementações próprias dos famosos “Entity Converters” ou se alguns programadores tinham a liberdade de inserir bibliotecas no projeto simplesmente utilizavam a tag <s:convertEntity> do JBoss Seam 2.

Ao migrar para o JSF 2 você ainda encontrará esse problema pois ainda não foi inserido um conversor de entidades nativo, apesar de já existir uma solução padrão para conversores de enum. Se você estava utilizando Seam 2 provavelmente vai desejar migrar ou começar um novo projeto com JBoss Seam 3 para aproveitar as inúmeras vantagens desta nova versão que melhora o funcionamento do JSF 2.
A ideia é continuar utilizando a tag <s:convertEntity> em sua aplicação, mas logo quando você inicia seus estudos do Seam 3 percebe que algumas coisas mudaram, principalmente o fato dele trabalhar agora com formato de módulos.

 

Seam Faces

O módulo do Seam 3 para trabalhar com JSF é chamado Seam Faces, ele contém diversas funcionalidades já existentes em versões anteriores e ainda traz muitos recursos novos. Para nossa surpresa a antiga tag <s:convertEntity> não foi incorporada no Seam 3, mas devido a pedidos da comunidade, a partir da versão 3.1.0 BETA 5 foi adicionada uma nova tag chamada <s: objectConverter> com o mesmo propósito e comportamento ainda mais simples.

 

Solução

O ObjectConverter é um simples conversor que pode ser usado com qualquer objeto Java, incluindo entidades JPA. Ele pode ser usado através da tag <f:converter> passando como id o valor org.jboss.seam.faces.conversion.ObjectConverter ou pela forma mais comum através da tag <s:objectConverter/>.
Esse conversor somente deve ser usado dentro de um escopo de conversação. O Managed Bean que possui o objeto a ser convertido deve conter a anotação @ConversationScoped, recurso adicionado na especificação CDI que foi inspirado no JBoss Seam 2.

Sua implementação é bem interessante porque ele não precisa de um EntityManager para recuperação dos objetos como a maioria das soluções normalmente fazem. Ele simplesmente grava seu objeto em um Map criando assim um “cache” para ele e o escopo de conversação irá garantir que no momento da recuperação o objeto ainda estará no Map.
Se você utilizar JPA para persistir o objeto após a conversão deve tomar cuidado pois o objeto recuperado estará no estado “detached”, para torná-lo “managed” novamente é necessário executar um objeto.merge() por exemplo.

 

Instalação

Será necessário baixar a biblioteca do Seam Faces (http://seamframework.org/Seam3/FacesModule).
Desse arquivo você precisará dos .jars da pasta /lib que são bibliotecas que o Seam Faces utiliza internamente, como por exemplo os módulos Seam Solder e Seam International.
Existe também uma pasta chamda /artifacts que contém a própria biblioteca do Seam Faces.
Descompacte o arquivo baixado e copie as bibliotecas da pasta /lib e /artifacts para a pasta /WEB-INF/lib do seu projeto.

Obs: Até o momento desse artigo a versão oficial para download era 3.0.2. Neste caso você precisará trocar a lib do Seam Faces 3.0.2 por um snapshot da 3.1.0 BETA 5 ou superior (https://repository.jboss.org/nexus/content/repositories/snapshots/org/jboss/seam/faces/seam-faces/), pois a versão oficial ainda não tem a implementação do conversor.
As bibliotecas dependentes (Solder, International, etc) que você copiou para sua aplicação podem continuar as mesmas.

 

Exemplo

ClienteBean.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Named
@ConversationScoped
public class ClienteBean implements Serializable {

    private Cliente cliente;

    @Inject
    private Conversation conversation;

    @PostConstruct
    public void init() {
        if ( conversation.isTransient() ) {
            conversation.begin();
        }
    }

    @PreDestroy
    public void destroy() {
        if ( !conversation.isTransient() ) {
            conversation.end();
        }
    }

    public Cliente getCliente() {
        return cliente;
    }

    public void setCliente( Cliente cliente ) {
        this.cliente = cliente;
    }

    public void salvar() {
        // servico.salvar( cliente );
    }
   
    public Collection getLista() {
        // return servico.listar();
    }
}

index.xhtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:s="http://jboss.org/seam/faces">

<h:body>
    <h:form>
        <h:outputLabel value="Cliente:" />
        <h:selectOneMenu value="#{clienteBean.cliente}">
            <f:selectItems value="#{clienteBean.lista}" var="cliente" itemLabel="#{cliente.nome}" />
            <s:objectConverter/>
        </h:selectOneMenu>
        <h:commandButton value="Salvar" action="#{clienteBean.salvar}">
            <f:param name="cid" value="#{conversation.id}" />
        </h:commandButton>
    </h:form>
</h:body>
</html>

Pronto, ao clicar no botão “Salvar” será invocado o método salvar() e dentro deste método você poderá trabalhar com o objeto Cliente normalmente.

 

 

Comments

  • Arilson
    março 16, 2012 Responder

    Vi seu código e está bem parecido com o q eu estou fazendo aqui (testando CDI), aproveitar pra te perguntar:

    Qdo é que este trecho é chamdo?

    @PreDestroy
    public void destroy() {
    if ( !conversation.isTransient() ) {
    conversation.end();
    }
    }

    Nos teste q eu tinha feito, n vi ser chamado, só consegui fechar a conversation explicitamente, o q n é nada viável.

    Falew.

  • Danilo Akamine
    março 16, 2012 Responder

    Esse trecho será chamado quando o Garbage Collector decidir que essa instância não será mais utilizada. Após um tempo sem utilizar o Managed Bean ele será invocado.
    Porém, esse método nem seria necessário pois o CDI pode excluir suas ‘conversations’ que estiverem inativas automaticamente, segundo a própria documentação do Weld:

    “The container is permitted to destroy a conversation and all state held in its context at any time in order to conserve resources.”

    A conversation possui um método setTimeout() para definir esse tempo de inatividade.

    Em uma situação real, eu descartaria essa utilização do @PostConstruct e @PreDestroy e definiria explicitamente a abertura e fechamento da Conversation em metódos que seriam chamados através de ações na tela. Acho uma boa prática, pois a ideia do @ConversationScoped é exatamente essa, deixar que o desenvolvedor diga quando deverá ser iniciado e finalizado o escopo.

  • Douglas
    março 19, 2012 Responder

    É interessante o ObjectConverter. O que achei ruim foi o fato dele necessitar de um escopo conversacional. Você conhece algo legal para escopo de página? Não acho legal a ideia de promover o escopo apenas para usar esse converter 😀

  • Danilo Akamine
    março 19, 2012 Responder

    Douglas, acredito que não exista mais nada “nativo”, só criando sua própria implementação mesmo. Tentar fazer aquele esquema de sempre que cair no método getAsObject() dar um entityManager.find() para trazer o ID solicitado ou quem sabe fazendo alguma forma para seu Bean @ViewScoped guarda um Map e o Converter acessar esse Map via FacesContext.

  • Douglas BN
    abril 3, 2012 Responder

    O meu aqui não funciona de jeito nenhum.

    O que tem de errado:

    @Named
    @ConversationScoped
    public class NewAd implements Serializable {

    /**
    *
    */
    private static final long serialVersionUID = -5359992451473202124L;

    // @Inject
    @PersistenceContext(type = PersistenceContextType.EXTENDED)
    private EntityManager entityManager;

    @Inject
    private Conversation conversation;

    @PostConstruct
    public void init() {
    if ( conversation.isTransient() ) {
    conversation.begin();
    }
    }

    @PreDestroy
    public void destroy() {
    if ( !conversation.isTransient() ) {
    conversation.end();
    }
    }

    private State state;
    private City city;

    public State getState() {
    System.out.println(“getState:”+state);
    return state;
    }
    public void setState(State state) {
    System.out.println(“getState:”+state);
    this.state = state;
    }
    public City getCity() {
    return city;
    }
    public void setCity(City city) {
    this.city = city;
    }

    public Map getAllMap() {

    List states = StateDao.findAll(entityManager);

    Map map = new LinkedHashMap(states.size());

    map.put(“– Estado –“, null);

    for(State state:states) {
    map.put(state.getName(), state);
    }

    return map;
    }

  • Danilo Akamine
    abril 3, 2012 Responder

    Douglas BN:

    deu algum erro?
    como está sua view? você está trafegando o parâmetro “cid” no submit ?
    se você não passar ele pra frente, você acaba perdendo sua referência do Conversation.

Leave a Comment