Para você: EntityConverter para qualquer entidade e tipo de Id

Depois de ter encontrado a solução SimpleEntityConverter em http://www.rponte.com.br/tag/entity-converter/, resolvi melhorá-la, deixando-a ainda mais genérica. O próprio autor diz: "(…) É possível estende-las e até melhora-las, você é livre para isso, e dependendo da tua necessidade provavelmente será o melhor caminho, só não deixe de contribuir com o código para a comunidade (…).

1 - Não há necessidade de implementar a interface BaseEntity
2 - O id da classe pode ser String, Integer, etc. e não apenas Long como em SimpleEntityConverter, pois a classe identifica o retorno pela anotação @Id. Essa foi uma das motivações que me levaram a melhorar SimpleEntityConverter, pois algumas entidades de nossos sistemas não possuem id do tipo Long, mas String.

package net.metha.jsf.converter;

import java.lang.reflect.Field;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.persistence.Id;

/**
 * Converter para entidades JPA. 
 * Baseia-se na anotação @Id para identificar o atributo que representa a identidade da entidade.  
 * @author Flávio Henrique 
 * @version 1.0 
 * @since 05/10/2010
 */
public class EntityConverter implements Converter {

	public Object getAsObject(FacesContext ctx, UIComponent component,
			String value) {
		if (value != null) {
			return component.getAttributes().get(value);
		}
		return null;
	}

	public String getAsString(FacesContext ctx, UIComponent component,
			Object obj) {
		if (obj != null && !"".equals(obj)) {
			String id;
			try {
				id = this.getId(getClazz(ctx, component), obj);
				if (id == null){
					id = "";
				}
				id = id.trim();
				component.getAttributes().put(id,
						getClazz(ctx, component).cast(obj));
				return id;
			} catch (SecurityException e) {
				e.printStackTrace(); // seu log aqui
			} catch (IllegalArgumentException e) {
				e.printStackTrace(); // seu log aqui
			} catch (NoSuchFieldException e) {
				e.printStackTrace(); // seu log aqui
			} catch (IllegalAccessException e) {
				e.printStackTrace(); // seu log aqui
			}
		}
		return null;
	}

	private Class<?> getClazz(FacesContext facesContext, UIComponent component) {
		return component.getValueExpression("value").getType(
				facesContext.getELContext());
	}

	public String getId(Class<?> clazz, Object obj) throws SecurityException,
			NoSuchFieldException, IllegalArgumentException,
			IllegalAccessException {
		for (Field field : clazz.getDeclaredFields()) {
			if ((field.getAnnotation(Id.class)) != null) {
				Field privateField = clazz.getDeclaredField(field.getName());
				privateField.setAccessible(true);
				if (privateField.get(clazz.cast(obj)) != null) {
					return (String)field.getType()
							.cast(privateField.get(clazz.cast(obj))).toString();
				} else {
					return null;
				}
			}
		}
		return null;
	}
}

Registre o converter em faces-config.xml:

entityConverter
net.metha.jsf.converter.EntityConverter

Use e abuse:

<h:selectOneMenu id=“comboRestaurantes"
value=”#{consumoMB.restauranteOperacao}" required="true"
label=“Restaurante” converter=“entityConverter”>

10/02/2011 -> RETIFICANDO: O CÓDIGO ACIMA NÃO É THREADSAFE, USAR DESTA FORMA:

<h:selectOneMenu id="comboRestaurantes"
value="#{consumoMB.restauranteOperacao}" required="true"
label="Restaurante">
<f:converter converterId="entityConverter"/>
</selectOneMenu>

Esta solução está sendo usada em ambiente de produção.

Atenciosamente,

Flávio Henrique de Souza Almeida

Bacana. Pro pessoal que usa o JBoss Seam isso não é necessário. Basta usar a tag s:convertEntity.

Mais uma coisinha: com @EmbeddedId sua solução não funcionará, bem como subclasses cujo id esteja na superclasse.

1 curtida

Obrigado pelas sugestões.

Estamos cientes do s:converEntity do Seam, mas como a versão 2.0 não suporta CDI (out-of-the-box), estamos aguardando a versão 3.0.

Sobre o @EmbeddedId: acredito que eu não tenha dificuldade em adicionar esta funcionalidade.

Sobre herança: farei a alteração.

Suporte à herança adicionado.

public String getId(Class<?> clazz, Object obj) throws SecurityException,
			NoSuchFieldException, IllegalArgumentException,
			IllegalAccessException {
		Class<?> classAnotada = clazz;
		if(clazz.getSuperclass() != Object.class){
			classAnotada = clazz.getSuperclass();		
		}
		for (Field field : classAnotada.getDeclaredFields()) {
			if ((field.getAnnotation(Id.class)) != null) {
				Field privateField = classAnotada.getDeclaredField(field.getName());
				privateField.setAccessible(true);
				if (privateField.get(clazz.cast(obj)) != null) {
					return (String) field.getType()
							.cast(privateField.get(clazz.cast(obj))).toString();
				} else {
					return null;
				}
			}
		}
		return null;
	}

} catch (SecurityException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); }

Isso é um dos piores erros que um programador pode fazer quando trabalha com exceptions. :shock:

http://today.java.net/article/2006/04/04/exception-handling-antipatterns

Podemos gravar em um log essas exceções ou fazer com que getId() lance uma exceção personalizada (EntityConverterException).

Olá Flavio,

Muito bacana a solução e fico feliz que você tenha disponibilizado a mesma no GUJ. Acredito que não exista lugar melhor para isso! :slight_smile:

Como já é sábido por você, o JBoss Seam se utiliza dessa mesma solução, através do s:entityConvert, e provavelmente o código dele tenha coisas bem interessantes para se observar se tratando de problemas comuns, como o problema de herança já corrigido por você.

Eu havia pensado nessa solução também, mas ela acabou ficando fora do post. Contudo, o mais interessante de tudo isso são estes tipos de iniciativas em compartilhar conhecimento e código com a comunidade.

Enfim, como o garcia-jj comentou, melhore o tratamento de exceções, pois ignora-las é uma má prática. E claro, se possível disponibilize o código no GitHub :slight_smile:

Um abraço e parabéns novamente.

Usar o github é excelente, pois muitas pessoas podem fazer um fork e contribuir.

Adicionei suporte à anotação @EmbeddedId e corrigi um problema na hierarquia de classes.

package net.metha.jsf.converter;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;

/**
 * Converter para entidades JPA. Baseia-se nas anotações @Id e @EmbeddedId para identificar o
 * atributo que representa a identidade da entidade. Capaz de detectar as anotações nas classes superiores.
 * 
 * @author Flávio Henrique
 * @version 1.0.3
 * @since 05/10/2010
 */
public class EntityConverter implements Converter {

	public Object getAsObject(FacesContext ctx, UIComponent component,
			String value) {
		if (value != null) {
			return component.getAttributes().get(value);
		}
		return null;
	}

	public String getAsString(FacesContext ctx, UIComponent component,
			Object obj) {
		if (obj != null && !"".equals(obj)) {
			String id;
			try {
				id = this.getId(getClazz(ctx, component), obj);
				if (id == null) {
					id = "";
				}
				id = id.trim();
				component.getAttributes().put(id,
						getClazz(ctx, component).cast(obj));
				return id;
			} catch (SecurityException e) {
				e.printStackTrace(); // seu log aqui
			} catch (IllegalArgumentException e) {
				e.printStackTrace(); // seu log aqui
			} catch (NoSuchFieldException e) {
				e.printStackTrace(); // seu log aqui
			} catch (IllegalAccessException e) {
				e.printStackTrace(); // seu log aqui
			}
		}
		return null;
	}

	/**
	 * Obtém, via expression language, a classe do objeto.
	 *
	 * @param FacesContext facesContext
	 * 
	 * @param UICompoment compoment
	 *     
	 * @return  Class<?>
	 */
	private Class<?> getClazz(FacesContext facesContext, UIComponent component) {
		return component.getValueExpression("value").getType(
				facesContext.getELContext());
	}


	/**
	 * Retorna a representação em String do retorno do método anotado com @Id ou @EmbeddedId do objeto.
	 *
	 * @param Class<?> clazz
	 *            
	 * @return  String
	 */
	public String getId(Class<?> clazz, Object obj) throws SecurityException,
			NoSuchFieldException, IllegalArgumentException,
			IllegalAccessException {

		List<Class<?>> hierarquiaDeClasses = this.getHierarquiaDeClasses(clazz);
		
		for (Class<?> classeDaHierarquia : hierarquiaDeClasses) {
			for (Field field : classeDaHierarquia.getDeclaredFields()) {
				if ((field.getAnnotation(Id.class)) != null
						|| field.getAnnotation(EmbeddedId.class) != null) {
					Field privateField = classeDaHierarquia
							.getDeclaredField(field.getName());
					privateField.setAccessible(true);
					if (privateField.get(clazz.cast(obj)) != null) {

						return (String) field.getType()
								.cast(privateField.get(clazz.cast(obj)))
								.toString();
					}
				}
			}
		}
		return null;
	}

	/**
	 * Retorna uma lista com a hierarquia de classes, sem considerar a classe Object.class
	 *
	 * @param Class<?> clazz
	 *            
	 * @return  List<Class<?>> clazz
	 */
	public List<Class<?>> getHierarquiaDeClasses(Class<?> clazz) {

		List<Class<?>> hierarquiaDeClasses = new ArrayList<Class<?>>();
		Class<?> classeNaHierarquia = clazz;
		while(classeNaHierarquia != Object.class) {
			hierarquiaDeClasses.add(classeNaHierarquia);
			classeNaHierarquia = classeNaHierarquia.getSuperclass();
			
		}
		return hierarquiaDeClasses;
	} 
}

Estamos usando em produção EntityConverter e até agora não tivemos problemas.
A diferença básica entre EntityConverter e o s:ConvertEntity do Seam é a seguinte:

1 - s:ConvertyEntity obtém o id da entidade e realiza a procura no banco de dados utilizando JPA. Eles resolvem o problema da unidade de persistência, permitindo que o usuário defina este atributo na própria tag quando necessário. Acho essa solução interessante, ficando ainda melhor quando temos um cache de segundo nível como ehcache.

2- EntityConverter, assim como SimpleEntityConverter, altera o estado do componente no lado do servidor. A vantagem é de não precisamos acessar o EntityManager dentro do converter, mas há um custo de processamento, que em nossos sistemas, até o momento, é imperceptível.

Eu poderia alterar o código e usar a mesma estratégia do s:ConvertEntity (o código de infraestrutura já permite isso), mas se isso fosse necessário, eu utilizaria o s:ConvertEntity e não o EntityConverter.

O que eu gostaria deixar bem claro aqui é que EntityConverter foi criado pensando nas aplicações que desenvolvo, desta forma, será necessário que cada um avalie qual tipo de conversor de entidade utilizar.

Abraço

Muito bom, meus parabéns! Tanto pelas melhorias como pela iniciativa de compartilhar suas ideias conosco.

Ataxexe sugeriu a utilização de uma biblioteca de reflexão para deixar o código mais legível. A biblioteca escolhida foi a Trugger 2.7.0 que pode ser baixada em:

O método getId() foi reescrito com base no código enviado pelo colega. Reparem que não é mais necessário o método getHierarquiaDeClasses() nem o parâmetro da classe.

Para o código ficar ainda mais rápido, será necessário limar o método getClazz (evoca EL), mas eu ainda preciso dele no método getAsString() devido a necessidade de sabermos qual a classe do objeto que chega até o converter para podermos gravá-lo dentro do map. Como ainda estou conhecendo a biblioteca trugger, acredito que ela tenha a chave para esta resposta.

Ataxexe, você vê alguma solução?

Segue o código alterado. Não esqueçam de adicionar trugger-2.7.0.jar
OBS: eu fiz um teste premilinar e funcionou. Amanhã, farei outro teste colocarei em produção.

package net.metha.jsf.converter;

import static net.sf.trugger.element.Elements.element;
import static net.sf.trugger.reflection.ReflectionPredicates.annotatedWith;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.persistence.EmbeddedId;
import javax.persistence.Id;

/**
 * Converter para entidades JPA. Baseia-se nas anotações @Id e @EmbeddedId para
 * identificar o atributo que representa a identidade da entidade. Também
 * as anotações nas super classes.
 * 
 * @author Flávio Henrique
 * @version 1.0.4
 * @since 05/10/2010
 */
public class EntityConverter implements Converter {

	public Object getAsObject(FacesContext ctx, UIComponent component,
			String value) {
		if (value != null) {
			return component.getAttributes().get(value);
		}
		return null;
	}

	public String getAsString(FacesContext ctx, UIComponent component,
			Object obj) {
		if (obj != null && !"".equals(obj)) {
			String id;
		
				id = this.getId(obj);
				if (id == null) {
					id = "";
				}
				id = id.trim();
				component.getAttributes().put(id,
						getClazz(ctx, component).cast(obj));
				return id;
		}
		return null;
	}

	/**
	 * Obtém, via expression language, a classe do objeto
	 * 
	 * @param FacesContext
	 *            facesContext
	 * 
	 * @param UICompoment
	 *            compoment
	 * 
	 * @return Class<?>
	 */
	private Class<?> getClazz(FacesContext facesContext, UIComponent component) {
		return component.getValueExpression("value").getType(
				facesContext.getELContext());
	}

	/**
	 * Retorna a representação em String do retorno do método anotado com @Id ou @EmbeddedId
	 * do objeto.
	 * 
	 * @param Object obj
	 * 
	 * @return String
	 */
	public String getId(Object obj) {
		Object idValue = element()
				.thatMatches(
						annotatedWith(Id.class).or(
								annotatedWith(EmbeddedId.class))).in(obj)
				.value();
		return String.valueOf(idValue);
	}
}

O desempenho está muito bom.

Coloquei mais um sistema nosso utilizando esta classe. Para os managedBeans em escopo de visão, o desempenho é melhor ainda.

Este converter não funciona com rich:combobox. Por quê? Porque esta tag não usa selectedItens internamente, ou seja, é derivada da suggestionbox. Parece que isso foi resolvido no richfaces 4.

Assim que eu realizar o teste, eu posto aqui.

Abraço

Boa tarde,

Estou usando este converter mas não consigo obter o resultado esperado.
No método getAsObject() recebo um null. Debuguei o método getAsString() e os objetos são inseridos no map normalmente, mas quando preciso obter no método getAsObject() recebo um null.

Segue o código:

public String getAsString(FacesContext ctx, UIComponent component, Object obj)
	{
		if (obj != null && !"".equals(obj))
		{
			String id;
			
			id = this.getId(obj);
			if (id == null)
			{
				id = "";
			}
			id = id.trim();
			//Aqui insere normal no map
			component.getAttributes().put(id, getClazz(ctx, component).cast(obj));
			return id;
		}
		return null;
	}
public Object getAsObject(FacesContext ctx, UIComponent component, String value)
	{
		if (value != null)
		{
			// Aqui retorna null sempre
			return component.getAttributes().get(value);
		}
		return null;
	}

O registro do converter no faces-config:

<converter>		
       <converter-id>entityConverter</converter-id>
       <converter-class>br.com.telefonica.indra.gestaodeativos.converters.EntityConverter</converter-class>
</converter>

Página:

<h:selectOneMenu id="perfil" value="#{usuarioBean.perfil}" converter="entityConverter">

Alguma idéia?

Preciso colocar algo mais do meu código?

Atte.

Gustavo Belloni Metzner

HarryPodre, como você populou sua lista de selectedItens? Coloca aqui o código para que eu possa analisar.
Abraço

[quote=Flavio Almeida]HarryPodre, como você populou sua lista de selectedItens? Coloca aqui o código para que eu possa analisar.
Abraço
[/quote]

Flávio, este trecho do código ajuda?

List<SelectItem> itens = new ArrayList<SelectItem>();
		itens.add(new SelectItem(null, "Selecione"));
		
		for (Perfil perfil : perfis)
		{
			itens.add(new SelectItem(perfil, perfil.getDescricao()));
		}

Valeu!

Hoje eu estou bem atarefado, mas responderei com mais calma à noite.

Faça assim:

itens.add(new SelectItem(null, “”)); // string em branco no lugar de “Selecione”

Perfil tem a anotação @Id ou @EmbeddableId?

Estamos usando este converter em produção sem problemas, mas estou interessado em ajudar você a resolver seu problema.

Você está usando a tag <f:selectItems> ?

Veja um exemplo:

<h:selectOneMenu id="papelUsuario" value="#{usuarioController.usuario.papel}" disabled="#{usuarioController.formStatus.consultando}" label="Papel" required="true" converter="entityConverter">
   <f:selectItems value="#{usuarioController.listaComboPapeis}" />
</h:selectOneMenu>

Lucas Sorrentino vai te ajudar!

Estou sim.

<h:selectOneMenu id="perfil" value="#{usuarioBean.perfil}" converter="entityConverter">
    <f:selectItems value="#{usuarioBean.perfis}"/>			            			            
</h:selectOneMenu>