Posts Tagged ‘Java’

Entity Converters pra dar e vender

Saturday, July 26th, 2008

Uma coisa que sempre aconselho aos desenvolvedores é que tentem sempre que possível trabalhar em JSF diretamente com os objetos como se estivessem em um ambiente stateful, pois um dos objetivos da tecnologia é tentar abstrair a natureza stateless do HTTP. Não que seja algo tão simples de se fazer algumas vezes, mas não é tão complexo ao ponto de abrir mão desta abstração.

Pensando no que eu disse acima, um dos problemas comuns e chatinhos quando se trabalha com SelectOneMenu (ou SelectManyMenu) e entidades em JSF ocorre quando queremos que o value do nosso SelectItem seja a própria entidade, e não o “id” da mesma. Bem, o que estou querendo dizer é exatamente isso:

public List<SelectItem> getEmpresas() {
	List<SelectItem> items = new ArrayList<SelectItem>();
	for (Empresa e : this.empresas) {
		// observem que o value do meu SelectItem é a própria entidade
		items.add(new SelectItem(e, e.getNome()));
	}
	return items;
}

Isso acaba se tornando trabalhoso pois somos obrigados a implementar um converter para cada entidade, o que particularmente eu não gosto. Levando em consideração que não queremos criar um converter para cada entidade, quais outras soluções temos?

Cenário

Antes de explicar cada solução, vou demonstrar um cenário qualquer para facilitar o entendimento, ou seja, vou demonstrar os artefatos necessários, eles seguem abaixo:

1) Nosso managed bean

public class EmpresaBean {

	private Empresa selectedEmpresa;
	private List<Empresa> empresas = new ArrayList<Empresa>();

	public EmpresaBean() {
		empresas.add(new Empresa(7, "Triadworks"));
		empresas.add(new Empresa(88, "Ivia"));
		empresas.add(new Empresa(921, "Thoughtworks"));
		empresas.add(new Empresa(15, "Caelum"));
		empresas.add(new Empresa(2, "ImproveIT"));
	}

	public List<Empresa> getEmpresas() {
		return empresas;
	}
	public Empresa getSelectedEmpresa() {
		return selectedEmpresa;
	}
	public void setSelectedEmpresa(Empresa selectedEmpresa) {
		this.selectedEmpresa = selectedEmpresa;
		System.out.println("Empresa selecionada: " + selectedEmpresa.getNome());
	}
}

2) Nossa entidade (já implementando a interface BaseEntity para a solução do SimpleEntityConverter)

public class Empresa implements BaseEntity, Serializable {

	private static final long serialVersionUID = 1L;

	private Integer codigo;
	private String nome;

	public Empresa(Integer codigo, String nome) {
		this.codigo = codigo;
		this.nome = nome;
	}

	public Long getId() {
		return new Long(codigo);
	}

	// Métodos getters e setters
	// Não esquecer os métodos equals e hashCode
}

Por favor, não esqueçam de implementar os métodos equals e hashCode, evitemos cair naquele velho probleminha, ok?

3) Nosso formulário

<h:form id="form">
	<h:panelGrid columns="2" border="1">
		<h:outputLabel value="Empresa" for="empresa"/>
		<h:column>
			<h:selectOneMenu id="empresa"
				value="#{empresaBean.selectedEmpresa}"
				converter="simpleEntityConverter" required="true"
				requiredMessage="Valor é obrigatório">
				<f:selectItem itemValue="" itemLabel="Selecione uma empresa"/>
				<t:selectItems value="#{empresaBean.empresas}" var="o" itemLabel="#{o.nome}" itemValue="#{o}"/>
			</h:selectOneMenu>
			<br/>
			<h:message for="empresa" errorStyle="color:darkred;font-size:11px;"></h:message>
		</h:column>
		<f:facet name="footer">
			<h:commandButton value="Submit"/>
		</f:facet>
	</h:panelGrid>
</h:form>

Reparem que nosso componente h:selectOneMenu possui um converter declarado, ou seja, será necessário alterar o converter de acordo com a solução escolhida.

Nada de complexo, certo? É um básico cenário comum.
Então vamos as possíveis soluções :)

EntityConverter

O componente/feature EntityConverter foi desenvolvido pelo Rogério Araújo, que é um dos coordenadores do JavaServer Faces International Group. O componente em si foi pensado inicialmente para o cenário em que é necessário carregar a entidade de um banco de dados, mas isso não impede que a entidade seja obtida de qualquer outro recurso externo, como um web service ou EJB, por exemplo.

O componente funciona perfeitamente bem e é muito simples de configurar, porém ainda assim eu o acho interessante somente em alguns casos específicos, como no caso em que a entidade precisa ser carregada do banco de dados com todos seus atributos e/ou relacionamentos ao submeter o formulário, evitando-se assim onerar o servidor com grandes entidades em memória.

Alguns desenvolvedores reclamam porque o componente -idealmente- efetua uma requisição ao bando de dados para obter a entidade, mas venhamos e convenhamos, isso não deveria ser uma preocupação, principalmente para quem vem de algum framework “action-like” como o Struts. Além do mais, se você possui algum recurso de cache na camada de persistência -como o fornecido pelo Hibernate- não deveria se preocupar tanto com isso.

Eu não pretendo explicar como configurar o componente, pois na wiki do Myfaces você tem tudo que precisa saber, além do mais o criador do componente é coordenador da Javasf e é brasileiro :)

SimpleEntityConverter

Esta solução foi baseada no caso mais comum de um converter para cada entidade, a única diferença é que tentei torna-la “genérica” para qualquer entidade, evitando-se escrever um converter para cada entidade (é esta nossa intenção, certo?).

Sendo, segue abaixo o código do nosso converter:

public class SimpleEntityConverter implements Converter {

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

	public String getAsString(FacesContext ctx, UIComponent component, Object value) {

		if (value != null
				&& !"".equals(value)) {

			BaseEntity entity = (BaseEntity) value;

			// adiciona item como atributo do componente
			this.addAttribute(component, entity);

			Long codigo = entity.getId();
			if (codigo != null) {
				return String.valueOf(codigo);
			}
		}

		return (String) value;
	}

	protected void addAttribute(UIComponent component, BaseEntity o) {
		String key = o.getId().toString(); // codigo da empresa como chave neste caso
		this.getAttributesFrom(component).put(key, o);
	}

	protected Map<String, Object> getAttributesFrom(UIComponent component) {
		return component.getAttributes();
	}

}

Como podem ver, o que basicamente ocorre é que armazenamos as entidades como atributos do nosso componente no método getAsString(), e recuperamos a entidade correta através da chave (neste caso o Id) associada a ela, no método getAsObject(). Mas quem é este BaseEntity?

public interface BaseEntity {

	public Long getId();

}

BaseEntity é uma interface com um método em comum entre as entidades, ou seja, nossas entidades precisarão implementar esta interface para que nosso converter funcione de acordo.

A solução é funcional, porém há algumas ressalvas que gostaria de explicitar. 1) Eu particularmente não gosto da idéia de “sujar” minhas entidades estendendo alguma interface ou classe para poupar código ou resolver problemas não-funcionais, mas isso é uma opinião minha. 2) Outro problema que vejo é que este converter acaba consumindo um pouco mais de memória no servidor por você está alterando o estado do componente, principalmente se sua lista de itens for muito grande, mas não é nada que você deva se preocupar inicialmente, ou seja, somente esquente a cabeça com isso se for realmente necessário.

SimpleIndexConverter

Esta solução eu tomei emprestada dos componentes do Myfaces Trinidad, que por sinal é excelente. A diferença é que eu a simplifiquei um pouco (o código para ser mais exato), logo ela não está tão robusta quanto a original (que se preocupa com casos mais específicos), mas funciona muito bem na maioria dos casos.

Sua idéia principal é utilizar o index da lista de items como chave para cada entidade, assim o que submetemos no formulário é o index da lista, e não um valor (Id?) da entidade como de costume.

O código do converter ficou grandinho, mas é possível enxuga-lo movendo alguns métodos para alguma classe utils. Segue abaixo o código do nosso SimpleIndexConverter:

public class SimpleIndexConverter implements Converter {

	private int index = -1;

	/* (non-Javadoc)
	 * @see javax.faces.convert.Converter#getAsObject(javax.faces.context.FacesContext, javax.faces.component.UIComponent, java.lang.String)
	 */
	public Object getAsObject(FacesContext ctx, UIComponent component, String value) {

		SelectItem selectedItem = this.getSelectedItemByIndex(component, Integer.parseInt(value));
		if (selectedItem != null)
			return selectedItem.getValue();

		return null;
	}

	/* (non-Javadoc)
	 * @see javax.faces.convert.Converter#getAsString(javax.faces.context.FacesContext, javax.faces.component.UIComponent, java.lang.Object)
	 */
	public String getAsString(FacesContext ctx, UIComponent component, Object value) {
		index++;
		return String.valueOf(index);
	}

	/**
	 * Obtem o SelecItem de acordo com a opção selecionada pelo usuário
	 */
	protected SelectItem getSelectedItemByIndex(UIComponent component, int index) {

		List<SelectItem> items = this.getSelectItems(component);
		int size = items.size();

		if (index > -1
				&& size > index) {
			return items.get(index);
		}

		return null;
	}

	protected List<SelectItem> getSelectItems(UIComponent component) {

		List<SelectItem> items = new ArrayList<SelectItem>();

		int childCount = component.getChildCount();
	    if (childCount == 0)
	      return items;

	    List<UIComponent> children = component.getChildren();
		for (UIComponent child : children) {
			if (child instanceof UISelectItem) {
				this.addSelectItem((UISelectItem) child, items);
			} else if (child instanceof UISelectItems) {
				this.addSelectItems((UISelectItems) child, items);
			}
		}

		return items;
	}

	protected void addSelectItem(UISelectItem uiItem, List<SelectItem> items) {

		boolean isRendered = uiItem.isRendered();
		if (!isRendered) {
			items.add(null);
			return;
		}

		Object value = uiItem.getValue();
		SelectItem item;

		if (value instanceof SelectItem) {
			item = (SelectItem) value;
		} else {
			Object itemValue = uiItem.getItemValue();
			String itemLabel = uiItem.getItemLabel();
			// JSF throws a null pointer exception for null values and labels,
			// which is a serious problem at design-time.
			item = new SelectItem(itemValue == null ? "" : itemValue,
					itemLabel == null ? "" : itemLabel, uiItem
							.getItemDescription(), uiItem.isItemDisabled());
		}

		items.add(item);
	}

	@SuppressWarnings("unchecked")
	protected void addSelectItems(UISelectItems uiItems, List<SelectItem> items) {

		boolean isRendered = uiItems.isRendered();
		if (!isRendered) {
			items.add(null);
			return;
		}

		Object value = uiItems.getValue();
		if (value instanceof SelectItem) {
			items.add((SelectItem) value);
		} else if (value instanceof Object[]) {
			Object[] array = (Object[]) value;
			for (int i = 0; i < array.length; i++) {
				// TODO test - this section is untested
				if (array[i] instanceof SelectItemGroup) {
					resolveAndAddItems((SelectItemGroup) array[i], items);
				} else {
					items.add((SelectItem) array[i]);
				}
			}
		} else if (value instanceof Collection) {
			Iterator<SelectItem> iter = ((Collection<SelectItem>) value)
					.iterator();
			SelectItem item;
			while (iter.hasNext()) {
				item = iter.next();
				if (item instanceof SelectItemGroup) {
					resolveAndAddItems((SelectItemGroup) item, items);
				} else {
					items.add(item);
				}
			}
		} else if (value instanceof Map) {
			for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) value).entrySet()) {
				Object label = entry.getKey();
				SelectItem item = new SelectItem(entry.getValue(),
						label == null ? (String) null : label.toString());
				// TODO test - this section is untested
				if (item instanceof SelectItemGroup) {
					resolveAndAddItems((SelectItemGroup) item, items);
				} else {
					items.add(item);
				}
			}
		}
	}

	protected void resolveAndAddItems(SelectItemGroup group, List<SelectItem> items) {
		for (SelectItem item : group.getSelectItems()) {
			if (item instanceof SelectItemGroup) {
				resolveAndAddItems((SelectItemGroup) item, items);
			} else {
				items.add(item);
			}
		}
	}

}

Não há muito a se explicar sobre o converter acima, a “mágica” toda está nos métodos auxiliares (que por sinal maior parte do código foi retirado do Trinidad, porém enxugado para nossas necessidades).

Este converter diferentemente do SimpleEntityConverter não altera o estado do componente, evitando assim a utilização de mais memória, ele busca as entidades -que já existem- dentro do componente (SelectOneMenu?) como estado do mesmo.

Assim como os outros converters, este também possui algumas ressalvas. O que podemos citar é 1) Por ele se utilizar do index da lista de items talvez não seja possível trabalhar com o componente no lado cliente (javascript) se você depender dos valores (Id?) de cada item.

Concluindo

Dizer que não é possível simplificar a vida com JSF é mentir, a tecnologia te fornece recursos para abstrair a natureza stateless do HTTP, e é interessante que se aproveite destes recursos.

As soluções explanadas acima não são únicas, existem outras com toda certeza, eu tentei demonstrar algumas delas quando não se tem algum framework (Seam?) ou conjunto de componentes (Trinidad?) para nos auxiliar, ou seja, quando contamos apenas com a implementação do JSF, o que -acredito eu- na maioria dos casos é o que ocorre.

É 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.

Enfim, todas as soluções são plausíveis e funcionam para a maioria dos cenários, porém em determinados cenários cada uma se adequa melhor, resta a você analisar e ver qual se encaixa nas tuas necessidades. Além do mais, elas não são mutuamente excludentes.

Repitam comigo: Redirect não é forward

Saturday, July 12th, 2008

É engraçado o número de desenvolvedores que se utilizam da tag de navegação <redirect/> quando configuram suas regras de navegação no faces-config.xml sem entender o porquê de sua finalidade, na maioria das vezes a única coisa que eles tem conhecimento -e acreditam que esta é finalidade da tag- é que ao utiliza-la a url na barra de endereços do browser muda.

Depois disso eles não entendem porque os valores não existem mais no managed bean ou porque as mensagens de erro não são mais exibidas ao usuário ou mesmo porque uma nova instância do managed bean foi criada, enfim, eles não entendem porque afinal a aplicação parou de funcionar!

Tentarei com este post esclarer a diferença entre um redirect e um forward e como contornar o problema acima para que novos desenvolvedores não caiam em “maus lençóis”.

Redirect não é forward

Um servlet (controller) pode executar tanto uma operação de um redirect como de um forward no final do processamento de uma requisição, e isso influencia como as páginas no browser serão recarregadas. É importante que um desenvolvedor entenda os fundamentos por trás de uma aplicação web, e um deles é a diferença entre redirect e um forward.

Segue abaixo a diferença entre eles:

Forward

  • É executado internamente pelo servlet (controller);
  • O browser não sabe o que está ocorrendo durante o processamento no servidor, ou seja, não sabe por quais servlets ou páginas a requisição está passando;
  • No final do processamento da requisição a url da barra de endereços do browser não muda;
  • O reload da página resultante irá executar a requisição original;

Redirect

  • É um processo de dois passos, ao receber uma requisição a aplicação web “pede” ao browser para acessar uma segunda url, por isso a url muda;
  • O reload de página não repetirá a requisição original, mas sim a nova url (2ª requisição);
  • É um processo muito mais lento que um forward, pois são necessárias duas requisições, e não uma;
  • Objetos colocados no escopo do request original são perdidos durante o segundo request;

Resumindo, um redirect é uma nova requisição que o cliente (browser) faz a pedido da aplicação web, logo ele fica ciente sobre como está ocorrendo a navegação e para onde ele está sendo redirecionado, enquanto um forward pode executar várias requisições no lado servidor sem o conhecimento do cliente e no final retornar uma página qualquer. Atentem também que um forward mantém os atributos e parâmetros do request original, já um redirect não.

Como podem ver, de maneira sútil eles fazem a mesma coisa, mas são bem diferentes.

O problema

Vamos observar um caso clássico abaixo:

Imagine que temos um managed bean configurado em escopo de request e que temos uma regra de navegação no nosso faces-config.xml na qual explicitamos o uso de redirect através da tag <redirect/>:

<navigation-rule>
	<from-view-id>/pages/pageX.jsp</from-view-id>
	<navigation-case>
		<from-outcome>nova_pagina_com_redirect</from-outcome>
		<to-view-id>/pages/pageY.jsp</to-view-id>
		<redirect />
	</navigation-case>
</navigation-rule>

E em uma determinada página X o usuário clica em um botão que executa um método no managed bean e depois disso o usuário é enviado para uma outra página Y com uma mensagem de sucesso (ou erro) e alguns objetos populados no managed bean para serem exibidos, logo teríamos um método no managed bean semelhante a isso:

public String submit() {
	// processa algo
	BacalhauService.processaRequisicao();

	// popula atributo do managed bean
	this.att1 = "A ligeira raposa marrom saltou sobre o cão preguiçoso.";

	// adiciona mensagem de sucesso
	FacesContext ctx = FacesContext.getCurrentInstance();
	ctx.addMessage(null, new FacesMessage("Operação concluída com sucesso."));

	return "nova_pagina_com_redirect";
}

Imagine que quando o usuário executou a ação o processamento no método do managed bean ocorreu, a navageção para a página Y funcionou perfeitamente e a url na barra de endereços do browser mudou como previsto, mas espere um pouco! “What the hell is this?” A mensagem de sucesso e os dados contidos no managed bean não foram exibidos, onde elas foram parar afinal de contas?

No caso acima nós navegamos da página X para a página Y através de um redirect (lembram da tag <redirect/> na regra de navegação?), e já sabemos que quando há um redirect todos os dados no escopo de request são perdidos, logo o nosso managed bean e nossa mensagem de sucesso foram perdidos.

Agora que já entendemos o funcionamento do redirect não foi uma surpresa o managed bean ter sido perdido (reinstanciado) pois sabiamos que ele estava configurado com escopo de request, mas e a mensagem de sucesso? Bem, isso fica óbvio ao saber que as mensagens do JSF são adicionadas no escopo de request, mesmo que seu managed bean esteja noutro escopo.

A solução

Como resolver isso? Bem, existem algumas soluções, porém a mais simples sem dúvida é remover a tag <redirect/> da regra de navegação, assim a navegação entre páginas ocorrerá através de forward (que é o default do framework), e os dados do seu managed bean e mensagens serão exibidos ao usuário como esperado.

Na maioria das aplicações web um dos motivos para se utilizar um redirect é para evitar que no reload/refresh da página ocorra uma resubmissão do form, evitando-se assim resultados inesperados na aplicação, sendo, uma solução para isso seria a utilização do Post-Redirect-Get Pattern (PRG). Uma excelente solução é a implementação do padrão PRG através deste Phase Listener implementado pelo BalusC.

Não tenho certeza, mas acho (quase certeza) que o JBoss Seam já tem algo implemetado para solucionar o problema. Se alguém puder confirmar, eu ficaria grato :)

Felizmente hoje em dia as aplicações web estão caminhando para uma GUI mais rica e versátil, e ao abrir mão da “velha escola” conseguimos contornar os problemas citados acima facilmente, como também obter inúmeras outras vantagens.

Concluindo

Como podem ver, o problema não era do JavaServer Faces (JSF) ou mesmo de qualquer outro framework MVC, mas certamente da falta de fundamentos sobre a web. Espero sinceramente que os desenvolvedores busquem entender os fundamentos dos frameworks, padrões e da web antes de desenvolver uma aplicação web, pois utilizar algo nas cegas provavelmente trará vários problemas mais cedo ou mais tarde.

Enfim, este é um problema decorrente na lista de discussão do javasf, e sempre acabamos por explicar a diferença entre as duas operações, que no final resolve-se apenas seguindo o caminho mais simples. Fico aqui e espero que este post tenha servido de ajuda a muitos desenvolvedores.