Internacionalização (i18N) com JBOSS SEAM e RichFaces

Por mais que esse seja um assunto já bastante explorado em outros blogs e na documentação de vários componentes, o que vou mostrar aqui auxilia na utilização do Hibernate Validations para validação da entrada de dados em aplicações JSF usando RichFaces.

Se você também usa o RichFaces com Hibernate Validations deve estar recebendo as mensagens em inglês ao invés de português. Bom depois de muita pesquisa vendo “MessagesInterpolator”, depurando, código e etc eu tentei definir a internacionalização tanto pelo JBoss Seam quanto pelo JSF (faces-config.xml) e para minha surpreza as mensagens passaram a ser exibidas em português.

A solução começa por configurar a inicialização do JBOSS definindo “Português-Brasil” como o idioma e local padrão. Isso é feito adicionando após a linha 77 no arquivo “JBOSS_HOME\bin\run.bat” a linha mostrada abaixo (linha destacada):

set JAVA_OPTS=%JAVA_OPTS% -Dsun.rmi.dgc.client.gcInterval=3600000 -Dsun.rmi.dgc.server.gcInterval=3600000
set JAVA_OPTS=%JAVA_OPTS% -Duser.language=pt -Duser.country=BR

Apenas isso já resolve o “problema” de exibir mensagens em inglês onde deveriam estar aparecendo em português entretanto, algumas vezes temos que dar suporte a outros idiomas como inglês e espanhol e apenas com esse ajuste de configuração a aplicação vai continuar mostrando as mensagens em português quando deveria estar mostrando em inglês ou espanhol.

Para evitar esse problema e recuperar o idioma que deve ser usado de acordo com o navegador utilizado pelo cliente, temos agora que ajustar a configuração do arquivo “components.xml” do conforme mostrado abaixo:

<components xmlns="http://jboss.com/products/seam/components"
	xmlns:core="http://jboss.com/products/seam/core" xmlns:drools="http://jboss.com/products/seam/drools"
	xmlns:mail="http://jboss.com/products/seam/mail" xmlns:persistence="http://jboss.com/products/seam/persistence"
	xmlns:security="http://jboss.com/products/seam/security" xmlns:web="http://jboss.com/products/seam/web"
	xmlns:i18n="http://jboss.com/products/seam/international"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://jboss.com/products/seam/core http://jboss.com/products/seam/core-2.2.xsd
	                  http://jboss.com/products/seam/persistence
	                  http://jboss.com/products/seam/persistence-2.2.xsd
	                  http://jboss.com/products/seam/drools http://jboss.com/products/seam/drools-2.2.xsd
	                  http://jboss.com/products/seam/bpm http://jboss.com/products/seam/bpm-2.2.xsd
	                  http://jboss.com/products/seam/security http://jboss.com/products/seam/security-2.2.xsd
	                  http://jboss.com/products/seam/mail http://jboss.com/products/seam/mail-2.2.xsd
	                  http://jboss.com/products/seam/web http://jboss.com/products/seam/web-2.2.xsd
	                  http://jboss.com/products/seam/i18n http://jboss.com/products/seam/international-2.0.xsd
	                  http://jboss.com/products/seam/components http://jboss.com/products/seam/components-2.2.xsd">

	<!-- As linhas abaixo devem ser adicionadas no seu components.xml -->

	<web:character-encoding-filter encoding="UTF-8" override-client="true" url-pattern="*.seam" />

	<i18n:locale-config default-locale="br" supported-locales="br en es"/>

</components>

Agora que o SEAM já está preparado para tratar a localização do usuário temos que adicionar o suporte a essas linguagens no arquivo “faces-config.xml” da aplicação como abaixo:

<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xi="http://www.w3.org/2001/XInclude" 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_1_2.xsd">
	
	<!-- A tag abaixo deve ser alterada. -->
	
	<application>
		<view-handler>com.sun.facelets.FaceletViewHandler</view-handler>
		<locale-config>
			<default-locale>br</default-locale>
			<supported-locale>br</supported-locale>
			<supported-locale>en</supported-locale>
			<supported-locale>es</supported-locale>
		</locale-config>
	</application>
</faces-config>

Concluídas essas configurações a aplicação se comportará como esperado em relação a internacionalização e, principalmente, as mensagens do Hibernate Validations serão apresentadas no idioma esperado.

Por último, mas não menos importante, essas são as versões do que usei para elaborar esta publicação:

  • RichFaces 3.3.3
  • JBOSS SEAM 2.2.1
  • JBOSS Application Server 4.2.3 GA e 5.1.0 GA

Qualquer dúvida, por favor, deixe um comentário.

Uma solução para saber quem está editando com JBoss Seam

Semana passada eu tive que implementar o seguinte requisito:

Um registro não pode ser visualizado por usuários distintos ao mesmo tempo.

Explicando melhor, isso quer dizer que dois usuários não podem visualizar o detalhe de um determinado registro ao mesmo tempo.

A solução mais simples é criar um componente com escopo “APPLICATION” e colocar nele o código que será responsável por esse gerenciamento. E, é exatamente o código que elaborei para fazer esse trabalho que vou mostrar aqui.

Nessa solução eu usei um EJB Statefull, que pode ser adaptada para um JavaBean, eis o código:

Da interface:

package br.com.herberson.core;

import javax.ejb.Local;

import br.com.herberson.entity.User;
import br.com.herberson.entity.concept.BaseConcept;

@Local
public interface ConceptOnUpdate {
	void initialize();
	Boolean onUpdate(BaseConcept concept);
	String identifyUpdater(BaseConcept concept);
	Boolean onUpdateByUser(BaseConcept concept, User userUpdating);
	void releaseConceptsFromUser(User userUpdating);
	void releaseConcept(BaseConcept concept, User userUpdating);
	void putOnUpdate(BaseConcept concept, User userUpdating);
	String toString();
	void destroy();
}

Da Implementação:

package br.com.herberson.core;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import javax.ejb.Remove;
import javax.ejb.Stateful;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.AutoCreate;
import org.jboss.seam.annotations.Create;
import org.jboss.seam.annotations.Destroy;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.annotations.Synchronized;
import org.jboss.seam.log.Log;

import br.com.herberson.entity.User;
import br.com.herberson.entity.concept.BaseConcept;
import br.com.herberson.entity.concept.CustomerConcept;
import br.com.herberson.entity.concept.MaterialConcept;
import br.com.herberson.entity.concept.ProductConcept;
import br.com.herberson.entity.concept.VendorConcept;
import br.com.herberson.util.Bean;

@Name("conceptOnUpdate")
@Synchronized(timeout=3000)
@Scope(ScopeType.APPLICATION)
@Startup(depends="entityManager")
@AutoCreate
@Stateful
public class ConceptOnUpdateImpl implements ConceptOnUpdate {
	private static final long serialVersionUID = 1L;

	/**
	 * Map com a lista de materiais em atualização, ou seja, travados.
	 */
	private Map<Long, String[]> materialOnUpdate;

	/**
	 * Map com a lista de produtos em atualização, ou seja, travados.
	 */
	private Map<Long, String[]> productOnUpdate;

	/**
	 * Map com a lista de clientes em atualização, ou seja, travados.
	 */
	private Map<Long, String[]> customerOnUpdate;

	/**
	 * Map com a lista de fornecedores em atualização, ou seja, travados.
	 */
	private Map<Long, String[]> vendorOnUpdate;

	/**
	 * Índice no array de values correspondente à posição do nome do usuário.
	 */
	private int USER_NAME = 0;

	/**
	 * Índice no array de values correspondente à posição ao ID da sessão do usuário.
	 */
	private int SESSION_ID = 1;

	@Logger // NOPMD by herberson on 07/07/10 11:25
	private Log log; // NOPMD by herberson on 07/07/10 11:26

	/**
	 * Initializa as tabelas de objetos em atualização.
	 */
	@Create
	public void initialize() {
		if (materialOnUpdate == null) {
			log.info("Method initialize():void - materialOnUpdate");
			materialOnUpdate = new HashMap<Long, String[]>();
		} //end if
		if (productOnUpdate == null) {
			log.info("Method initialize():void - productOnUpdate");
			productOnUpdate = new HashMap<Long, String[]>();
		} //end if
		if (customerOnUpdate == null) {
			log.info("Method initialize():void - customerOnUpdate");
			customerOnUpdate = new HashMap<Long, String[]>();
		} //end if
		if (vendorOnUpdate == null) {
			log.info("Method initialize():void - vendorOnUpdate");
			vendorOnUpdate = new HashMap<Long, String[]>();
		} //end if
	} //end method

	/**
	 * Verifica se o objeto está sendo atualizado.
	 *
	 * @param concept
	 * @return
	 */
	public Boolean onUpdate(BaseConcept concept) {
		log.info("Method onUpdate(concept):Boolean");
		initialize();
		return Bean.isNotNull(identifyUpdater(concept));
	} //end method

	/**
	 * Identifica o usuário que está atualizando o registro.
	 *
	 * @param concept
	 * @return
	 */
	public String identifyUpdater(BaseConcept concept) {
		log.info("Method identifyUpdater(concept):String");
		initialize();
		return getValue(concept)[USER_NAME];
	} //end method

	/**
	 * Verifica se o usuário informado no argumento <code>userUpdating</code> é o
	 * mesmo que está atualizando o registro.
	 *
	 * @param concept
	 * @param userUpdating
	 * @return
	 */
	public Boolean onUpdateByUser(BaseConcept concept, User userUpdating) {
		log.info("Method onUpdateByUser(concept, userUpdating):Boolean");
		initialize();
		boolean onUpdate;
		String[] value;

		value = getValue(concept);

		if (value[USER_NAME].equals(userUpdating.getUserName())
				&& value[SESSION_ID].equals(userUpdating.getHttpSessionId())) {
			onUpdate = true;
		} else {
			onUpdate = false;
		} //end if

		return onUpdate;
	} //end method

	/**
	 * Libera todos os registros travados para o usuário.
	 *
	 * @param userUpdating
	 */
	public void releaseConceptsFromUser(User userUpdating) {
		log.info("Method releaseConceptsFromUser(userUpdating):void - Start...");
		initialize();
		ArrayList<Long> customerToRemove = new ArrayList<Long>();
		ArrayList<Long> materialToRemove = new ArrayList<Long>();
		ArrayList<Long> productToRemove = new ArrayList<Long>();
		ArrayList<Long> vendorToRemove = new ArrayList<Long>();

		for (Map.Entry<Long, String[]> entry : customerOnUpdate.entrySet()) {
			if (entry.getValue()[USER_NAME].equals(userUpdating.getUserName())) {
				customerToRemove.add(entry.getKey());
			} //end method
		} //end for

		for (Map.Entry<Long, String[]> entry : materialOnUpdate.entrySet()) {
			if (entry.getValue()[USER_NAME].equals(userUpdating.getUserName())) {
				materialToRemove.add(entry.getKey());
			} //end method
		} //end for

		for (Map.Entry<Long, String[]> entry : productOnUpdate.entrySet()) {
			if (entry.getValue()[USER_NAME].equals(userUpdating.getUserName())) {
				productToRemove.add(entry.getKey());
			} //end method
		} //end for

		for (Map.Entry<Long, String[]> entry : vendorOnUpdate.entrySet()) {
			if (entry.getValue()[USER_NAME].equals(userUpdating.getUserName())) {
				vendorToRemove.add(entry.getKey());
			} //end method
		} //end for

		for (Long keyToRemove : customerToRemove) {
			customerOnUpdate.remove(keyToRemove);
		} //end for

		for (Long keyToRemove : materialToRemove) {
			materialOnUpdate.remove(keyToRemove);
		} //end for

		for (Long keyToRemove : productToRemove) {
			productOnUpdate.remove(keyToRemove);
		} //end for

		for (Long keyToRemove : vendorToRemove) {
			vendorOnUpdate.remove(keyToRemove);
		} //end for
		log.info("Method releaseConceptsFromUser(userUpdating):void - End...");
	} //end method

	/**
	 * Libera um determinado registro que estava travado para o usuário.
	 *
	 * @param concept
	 */
	public void releaseConcept(BaseConcept concept, User userUpdating) {
		log.info("Method releaseConcept(concept, userUpdating):void - Start...");
		initialize();
		if (onUpdateByUser(concept, userUpdating)) {
			if (concept instanceof MaterialConcept) {
				materialOnUpdate.remove(getKey(concept));
			} else if (concept instanceof ProductConcept) {
				productOnUpdate.remove(getKey(concept));
			} else if (concept instanceof VendorConcept) {
				vendorOnUpdate.remove(getKey(concept));
			} else if (concept instanceof CustomerConcept) {
				customerOnUpdate.remove(getKey(concept));
			} //end if
		} //end if
		log.info("Method releaseConcept(concept, userUpdating):void - End...");
	} //end method

	/**
	 * Trava o registro para atualização.
	 *
	 * @param concept
	 * @param userUpdating
	 */
	public void putOnUpdate(BaseConcept concept, User userUpdating) {
		log.info("Method putOnUpdate(concept, userUpdating):void - Start...");
		initialize();
		String[] value;
		Long key;

		if (Bean.isNotNull(userUpdating.getHttpSessionId())) {
			value = new String[]{userUpdating.getUserName(), userUpdating.getHttpSessionId()};
		} else {
			value = new String[]{userUpdating.getUserName(), ""};
		} //end if

		key = getKey(concept);

		if (concept instanceof MaterialConcept) {
			materialOnUpdate.put(key, value);
		} else if (concept instanceof ProductConcept) {
			productOnUpdate.put(key, value);
		} else if (concept instanceof VendorConcept) {
			vendorOnUpdate.put(key, value);
		} else if (concept instanceof CustomerConcept) {
			customerOnUpdate.put(key, value);
		} //end if
		log.info("Method putOnUpdate(concept, userUpdating):void - End...");
	} //end method

	/**
	 * Monta a chave para o Map de objetos travados.
	 *
	 * @param concept
	 * @return
	 */
	private Long getKey(BaseConcept concept) {
		initialize();
		Long key;

		if (concept instanceof MaterialConcept) {
			key = ((MaterialConcept)concept).getMaterialId();
		} else if (concept instanceof ProductConcept) {
			key = ((ProductConcept)concept).getProductId();
		} else if (concept instanceof CustomerConcept) {
			key = ((CustomerConcept)concept).getCustomerId();
		} else if (concept instanceof VendorConcept) {
			key = ((VendorConcept)concept).getVendorId();
		} else {
			key = Long.MIN_VALUE;
		} //end if

		return key;
	} //end method

	/**
	 * Recupera o valor para o Map de objetos travados.
	 * @param concept
	 * @return
	 */
	private String[] getValue(BaseConcept concept) {
		initialize();
		String[] value;

		if (concept instanceof MaterialConcept) {
			value = materialOnUpdate.get(getKey(concept));
		} else if (concept instanceof ProductConcept) {
			value = productOnUpdate.get(getKey(concept));
		} else if (concept instanceof CustomerConcept) {
			value = customerOnUpdate.get(getKey(concept));
		} else if (concept instanceof VendorConcept) {
			value = vendorOnUpdate.get(getKey(concept));
		} else {
			value = materialOnUpdate.get(getKey(concept));
		} //end if

		if (value == null) {
			value = new String[]{"", ""};
		} //end if

		return value;
	} //end method

	@Override
	public String toString() {
		initialize();
		StringBuffer string = new StringBuffer();

		string.append("MATERIAL: ");
		string.append(materialOnUpdate.toString());
		string.append(System.getProperty("line.separator"));

		string.append("PRODUCT: ");
		string.append(productOnUpdate.toString());
		string.append(System.getProperty("line.separator"));

		string.append("VENDOR: ");
		string.append(vendorOnUpdate.toString());
		string.append(System.getProperty("line.separator"));

		string.append("CUSTOMER: ");
		string.append(customerOnUpdate.toString());
		string.append(System.getProperty("line.separator"));

		return string.toString();
	} //end method

	@Destroy
	@Remove
    public void destroy() {
		log.info("Method destroy():void");
	} //end method

} //end class

Agora a explicação sobre o componente

Como as informações do componente devem estar disponíveis para todos os usuários autenticados na aplicação o escopo escolhido foi “APPLICATION”, o uso da anotação “@AutoCreate” foi para forçar a criação do componente na inicialização (deploy) da aplicação, o uso da anotação “@Startup” foi para evitarmos que o componente esteja indisponível sem uma conexão de banco de dados ativa e por último a anotação “@Synchronized” foi para evitar a exceção “java.util.ConcurrentModificationException” ao manipularmos os maps com os registros “travados”.

Para evitar identificadores iguais eu crieu um map (java.util.Map) para cada conceito controlado e dessa forma, apesar de mais código escrito, a utilização de memória fica um pouco mais otimizada.

Dos métodos do componente, os mais importantes são:

  • public Boolean onUpdateByUser(BaseConcept concept, User userUpdating) – Verifica se um conceito está sendo editado por um determinado usuário.
  • public void releaseConceptsFromUser(User userUpdating) – Libera dos os conceitos em edição pelo usuário.
  • public void putOnUpdate(BaseConcept concept, User userUpdating) – “Trava” o conceito para edição por um determinado usuário.

Como vocês já devem ter percebido essa solução é intrusiva, ou seja, temos que codificar a chamada aos métodos pertinentes, na sequência apropriada para que tenhamos o comportamento desejado.

Portanto, a forma de utilização que adotei foi a seguinte:

  • Executo o método “releaseConceptsFromUser(User userUpdating)”:
    1. quando o usuário se autentica.
    2. quando o usuário acessa qualquer item de menu.
  • Quando o usuário solicita a edição de um registro:
    1. verifico se o conceito não está sendo editado usando o método “onUpdate(BaseConcept concept)”.
    2. caso o conceito esteja sendo editado por algum usuário eu devolvo uma mensagem de erro identificando o usuário que está fazendo a edição usando o método “identifyUpdater(BaseConcept concept)”.
    3. caso o conceito não esteja sendo editado eu carrego as informações, travo o conceito usando o método “putOnUpdate(BaseConcept concept, User userUpdating)” e encaminho o usuário para a página de edição dos dados.

Abaixo temos um exemplo de como usar o componente antes de carrgar a tela de alteração de um conceito:

public String loadUpdate(Material materialEdit) {
	material = entityManager.find(Material.class, materialEdit.getMaterialId());

	if (conceptOnUpdate.onUpdate(material)) {
		FacesMessages.instance().add(Severity.ERROR, "#{messages['concept.on.update.already.editing']}", conceptOnUpdate.identifyUpdater(material));
		return FW_LIST;
	} else {
		entityManager.refresh(material);
		conceptOnUpdate.putOnUpdate(material, userAuthenticated);
		return FW_EDIT;
	} //end if
} //end method

O ponto-forte dessa solução é que você escolhe como e quando usá-la o que é justamente o seu ponto-fraco, pois não é transparente a todos os implementadores e depende de uma orientação clara da forma como deve ser usada e quando deve ser usada. Mas, como disse, é uma solução que prima pela simplicidade de código e utilização.

Qualquer dúvida, por favor, deixe um comentário.

Paginação no banco de dados com RichFaces e Seam – Continuação

No post anterior eu esqueci de mostrar como usar a solução de paginação, portanto, este vai ser dedicado exclusivamente a mostrar como usá-la.

A utilização dessa solução começa pela implementação do EJB onde você deve declarar um atributo cuja classe será aquela mostrada anteriormente, como mostrado abaixo:

@Out(required=false)
private EntityDataModel materialsDataModel;

Esse será o atributo referenciado na tag “<rich:dataTable />” e a forma de uso da tag será semelhante ao que já estamos acostumados a fazer quando usamos a tag “@DataModel”, veja o exemplo abaixo:

<rich:dataTable value="#{materialsDataModel}" var="_material" cellpadding="0" cellspacing="0" rows="2">
    <rich:column>
        <f:facet name="header">#{messages['material.label.material_number']}</f:facet>
        <h:outputText value="#{_material.materialNumber}"/>
    </rich:column>
    <rich:column>
        <f:facet name="header">#{messages['material.label.cntr_company']}</f:facet>
        <h:outputText value="#{_material.cntrCompany.name}"/>
    </rich:column>
    <rich:column>
        <f:facet name="header">#{messages['material.label.cntr_system']}</f:facet>
        <h:outputText value="#{_material.cntrSystem.name}"/>
    </rich:column>
    <rich:column>
        <f:facet name="header">#{messages['material.label.short_description']}</f:facet>
        <h:outputText value="#{_material.shortDescription}"/>
    </rich:column>
    <rich:column width="14" style="text-align:center;">
        <h:commandLink action="#{materialManager.loadUpdate(_material)}">
            <h:graphicImage value="/img/edit.gif" title="#{messages['button.edit']}"/>
        </h:commandLink>
    </rich:column>
    <f:facet name="footer">
        <rich:datascroller renderIfSinglePage="false" immediate="true"/>
    </f:facet>
</rich:dataTable>

Agora que passamos da tela, vamos ao EJB pois é nele que aplicamos os filtros na consulta e inicializamos o “EntityDataModel”. Essa parte pode ser feita como mostrado no trecho de código abaixo:

public String search() {
    log.info("Method search():String");
    StringBuffer query;
    HashMap<String, Object> parameters;
    boolean and = false;

    parameters = new HashMap<String, Object>();
    query = new StringBuffer();

    query.append("select m from Material m where");

    if (Bean.isNotNull(materialCriteria.getCntrCompany())) {
        query.append((and ? " and " : ""));
        query.append(" m.cntrCompany.companyId = :companyId");
        and = true;
        parameters.put("companyId", materialCriteria.getCntrCompany().getCompanyId());
    } else {
        userAuthenticated = entityManager.find(User.class, userAuthenticated.getUserId());
        boolean f = true;
        query.append((and ? " and " : ""));
        query.append(" m.cntrCompany.companyId in (");
        for (Company comp : userAuthenticated.getCompanies()) {
            if (f) {
                query.append(String.format("%d", comp.getCompanyId()));
            } else {
                query.append(String.format(", %d", comp.getCompanyId()));
            }
            f = false;
        }
        query.append(" )");
        and = true;
    } //end if

    if (Bean.isNotNull(materialCriteria.getCntrSystem())) {
        query.append((and ? " and " : ""));
        query.append(" m.cntrSystem.systemId = :systemId");
        and = true;
        parameters.put("systemId", materialCriteria.getCntrSystem().getSystemId());
    } //end if

    if (Bean.isNotNull(materialCriteria.getMaterialNumber())) {
        query.append((and ? " and " : ""));
        query.append(" m.materialNumber like :materialNumber");
        and = true;
        parameters.put("materialNumber",  String.format("%%%s%%", materialCriteria.getMaterialNumber()));
    } //end if

    materialsDataModel = new EntityDataModel(Material.class, query, parameters);
    renderMaterialList = true;

    return FW_LIST;
} //end method

Entre as linhas 07 e 47 do código acima é montado um StringBuffer com a query em EjbQL (ou, se preferir, HQL) e um HashMap com os valores a serem aplicados nos filtros da query e, por fim, na linha 48 o EntityDataModel é inicializado. A partir do momento que a execução do método é completada o RichFaces assume o controle do DataModel para realizar a navegação nas “páginas” em que o ResultSet da query pode ser dividido.

Eis alguns questionamentos que me fiz em relação a essa solução:

  1. Essa é a solução definitiva para o “problema paginação”? R.: Não.
  2. Pode ser melhorada? R.: Sim.
  3. Atende a necessidade de consultas paginadas no banco de dados? R.: Sim.

Se depois de ler este post você alguma parte do código, por favor, me envie sua atualização para que eu poste uma nova versão do EntityDataModel e assim poderemos chegar cada vez mais próximo ao ponto de equilíbrio entre “uma solução“, “a melhor solução” e “a mais otimizada“.

Qualquer dúvida, por favor deixe,um comentário.

Paginação no banco de dados com RichFaces e Seam

Paginação de dados é uma funcionalidade presente em quase todos os sistemas implementados, a maioria dos frameworks de implementação (RichFaces, Ice Faces, MyFaces, Deplhi, .Net, etc.) tem suporte a paginação dos dados para facilitar a navegação do usuário pela lista de registros.

O porém dessas implementações é que a grande maioria delas faz a paginação em memória, ou seja, recupera todos os registros do banco de dados coloca-os em memória e faz a divisão e navegação de páginas na memória.

Essa abordagem funciona muito bem para conjuntos pequenos de dados, mas quando começamos a falar de grandes quantidades de registros retornados pelas consultas os problemas começam a aparecer.

Em aplicações Java JEE pré-JSF para realizar a paginação eu utilizava o DisplayTag e conseguia uma paginação em memória descente e com alguma especialização do biblioteca também conseguia uma boa paginação no banco de dados, mas quando passei autilizar o JSF a necessidade de paginação no banco de dados voltou a ser um problema.

Como trabalho com Seam Framework e RichFaces precisei encontar uma forma de paginação que se integrasse a implementação de JSF que escolhi, ou seja, que me permitisse continuar utilizando todas as características que o framework já dispõe.

Depois de muita pesquisa eu cheguei num post no fórum do Seam que mostra uma forma de atender essa necessidade (link para o post) entretanto ela me pareceu um pouco flexível em relação a definição dos filtros da query foi quando eu cheguei a implementação que estou disponibilizando neste post.

Não vou entrar em muitos detalhes sobre a implementação, mas eu ainda pretendo evoluir o método “public int getRowCount()” para que ele otimize a query antes de realizar a contagem.

Testei essa implementação com banco de dados HSQL e Oracle, utilizei o Seam 2.2.1 CR1 e RichFaces 3.3.3 Final e como servidor eu usei o JBoss 4.2.3 GA (por causa da velocidade de inicialização).

Se você usa Eclipse com JBoss Tools, não estranhe se a ferramenta apontar alguns warnings (avisos) para o arquivo xhtml onde você utilizar essa solução.

Agora deixando a falação de lado, eis o código (que realmente interessa):

package br.com.herberson.core;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.faces.context.FacesContext;
import javax.persistence.EntityManager;
import javax.persistence.Query;

import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.model.ExtendedDataModel;
import org.ajax4jsf.model.Range;
import org.ajax4jsf.model.SequenceRange;
import org.apache.commons.beanutils.PropertyUtils;
import org.jboss.seam.Component;

import br.com.herberson.util.Bean;

public class PaginatedDataModel extends ExtendedDataModel {

    private Serializable currentId;
    private Class<? extends Serializable> entityClass;
    private String query;
    private List<Serializable> wrappedKeys;
    private String entityManagerName;
    private HashMap<String, Object> parameters;
    private Map<Serializable, Serializable> wrappedData = new HashMap<Serializable, Serializable>();
    private Query _query;
    private String idAttribute;
    private Integer rowCount;

    public PaginatedDataModel(Class<? extends Serializable> entityClass, String entityManagerName, String query, HashMap<String, Object> parameters, String idAttribute) {
        super();
        load(entityClass, entityManagerName, query, parameters, idAttribute);
    }

    public PaginatedDataModel(Class<? extends Serializable> entityClass, StringBuffer query, HashMap<String, Object> parameters, String idAttribute) {
        super();
        load(entityClass, null, query.toString(), parameters, idAttribute);
    }

    public PaginatedDataModel(Class<? extends Serializable> entityClass, String query, HashMap<String, Object> parameters) {
        super();
        load(entityClass, null, query, parameters, null);
    }

    public PaginatedDataModel(Class<? extends Serializable> entityClass, StringBuffer query, HashMap<String, Object> parameters) {
        super();
        load(entityClass, null, query.toString(), parameters, null);
    }

    private void load(Class<? extends Serializable> entityClass, String entityManagerName, String query, HashMap<String, Object> parameters, String idAttribute) {
        this.entityClass = entityClass;
        if (Bean.isNotNull(entityManagerName)) {
            this.entityManagerName = entityManagerName;
        } else {
            this.entityManagerName = "entityManager";
        } //end if

        if (Bean.isNotNull(idAttribute)) {
            this.idAttribute = idAttribute;
        } else {
            this.idAttribute = entityClass.getSimpleName().toLowerCase() + "Id";
        }
        this.parameters = parameters;
        this.query = query;
        rowCount = null;
    }

    private EntityManager getEntityManager() {
        return (EntityManager) Component.getInstance(entityManagerName, true);
    }

    private Query buildQueryCount() {
        _query = null;
        return buildQuery();
    }

    private Query buildQuery() {
        if (_query == null) {
            _query = getEntityManager().createQuery(query.toString());
            for (String key : parameters.keySet()) {
                _query.setParameter(key, parameters.get(key));
            } //end for
        } //end method
        return _query;
    } //end method

    @Override
    public Object getRowKey() {
        return currentId;
    }

    /**
     * This method normally called by Visitor before request Data Row.
     */
    @Override
    public void setRowKey(Object key) {
        currentId = (Serializable) key;
    }

    @SuppressWarnings("unchecked")
    @Override
    public void walk(FacesContext fCtx, DataVisitor visitor, Range range,
            Object argument) throws IOException {
        int firstResult = ((SequenceRange) range).getFirstRow();
        int maxResults = ((SequenceRange) range).getRows();

        buildQuery().setFirstResult(firstResult);
        buildQuery().setMaxResults(maxResults);

        List<? extends Serializable> list = buildQuery().getResultList();

        wrappedKeys = new ArrayList<Serializable>();
        wrappedData = new HashMap<Serializable, Serializable>();

        for (Serializable row : list) {
            Serializable id = getId(row);
            wrappedKeys.add(id);
            wrappedData.put(id, row);
            visitor.process(fCtx, id, argument);
        }
    }

    @Override
    public int getRowCount() {
        if (Bean.isNull(rowCount, true)) {
            rowCount = buildQueryCount().getResultList().size();
        } //end if
        return rowCount;
    }

    @Override
    public Object getRowData() {
        if (currentId == null) {
            return null;
        } else {
            Serializable ret = wrappedData.get(currentId);
            if (ret == null) {
                ret = (Serializable) getEntityManager().find(entityClass, currentId);
                wrappedData.put(currentId, ret);
                return ret;
            } else {
                return ret;
            }
        }
    }

    @Override
    public int getRowIndex() {
        return 0;
    }

    /**
     * Unused rudiment from old JSF staff.
     */
    @Override
    public Object getWrappedData() {
        throw new UnsupportedOperationException();
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean isRowAvailable() {
        if (currentId == null) {
            return false;
        } else {
            for (Serializable row : (List<Serializable>) buildQuery().getResultList()) {
                Serializable rowId = getId(row);
                if (rowId.equals(currentId)) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * Unused rudiment from old JSF staff.
     */
    @Override
    public void setRowIndex(int newRowIndex) {
    }

    /**
     * Unused rudiment from old JSF staff.
     */
    @Override
    public void setWrappedData(Object data) {
        throw new UnsupportedOperationException();
    }

    private Serializable getId(Serializable row) {
        Serializable id;
        try {
            id = (Serializable) PropertyUtils.getProperty(row, idAttribute);
        } catch (Exception e) {
            throw new javax.faces.FacesException("Failed to obtain row id", e);
        }
        return id;
    }
}

Para fazer o download do arquivo com o código clique aqui.

Estou mostrando como utilizar essa solução neste post.

Qualquer dúvida por favor deixe um comentário.

Identificar e tratar o primeiro acesso do usuário

O que vou falar neste post já está na documentação do JBoss Seam e do JSF portanto, vou descrever o que fiz para forçar a alteração da senha no primeiro acesso do usuário ao sistema.

Nesse caso eu utilizei o JpaIdentityStore para identificar o usuário e recuperar seus perfis e para esse fim o componente é uma boa escolha, permitindo que as senhas possam ser criptografadas em MD5 e SHA além de permitir que você implemente seu próprio algoritimo para criptografar a senha.

Como um dos requisitos sistema era forçar o usuário a trocar a senha no primeiro acesso ao sistema eu precisei alterar as definições da classe “Usuario” adicionando o atributo “dataUltimoLogin”, criar uma exceção e mapeá-la no arquivo “pages.xml”, elaborar um componente SEAM para identificar o primeiro acesso e direcionar o usuário para a tela de troca da senha.

A classe “User” ficou da seguinte forma:

package com.jabutiinfo.changeuserpwd.entity;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import javax.persistence.Transient;
import javax.persistence.Version;

import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.security.management.UserFirstName;
import org.jboss.seam.annotations.security.management.UserLastName;
import org.jboss.seam.annotations.security.management.UserPassword;
import org.jboss.seam.annotations.security.management.UserPrincipal;
import org.jboss.seam.annotations.security.management.UserRoles;

@Name("user")
@Entity
@NamedQueries({
    @NamedQuery(name=User.FIND_BY_USER_NAME, query="select u from User u where u.userName = :userName")
    , @NamedQuery(name=User.LIST_ALL, query="select u from User u order by u.userName")
    , @NamedQuery(name=User.FIND_USERS_BY_ROLE, query="select u from User u, in (u.roles) r where r.roleName = :roleName order by u.userName")
})
public class User implements Serializable {
    // default serial version
    private static final long serialVersionUID = 1L;
    
    public static final String FIND_BY_USER_NAME = "findByUserName";
    
    public static final String LIST_ALL = "listAllUsers";
    
    public static final String FIND_USERS_BY_ROLE = "findUsersByRole";
    
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    private Integer userId;
    
    @UserPrincipal
    @Column(nullable = false, unique = true)
    private String userName;

    @UserFirstName
    @Column
    private String firstName;

    @UserLastName
    @Column
    private String lastName;

    @UserPassword(hash = "none") //default options: none, md5 and sha.
    @Column(nullable = false)
    private String password;

    @UserRoles
    @ManyToMany(targetEntity = Role.class)
    @JoinTable(name = "UserRoles", joinColumns = @JoinColumn(name = "UserId"), inverseJoinColumns = @JoinColumn(name = "RoleId"))
    private List<Role> roles;
    
    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private Date lastSuccessFullLogin;

    @Version
    private Integer version;
    
    @Transient
    private String pwdMatch;
}
/sourcecode]

A exceção criada foi a seguinte:


package com.jabutiinfo.changeuserpwd.exception;

public class ChangePasswordException extends RuntimeException {
    // default serial version
    private static final long serialVersionUID = 1L;

    public ChangePasswordException() {
        super("The user must change the password in first access.");
    }

    @SuppressWarnings("unused")
    private ChangePasswordException(String message, Throwable cause) {
        super(message, cause);
    }

    @SuppressWarnings("unused")
    private ChangePasswordException(String message) {
        super(message);
    }

    @SuppressWarnings("unused")
    private ChangePasswordException(Throwable cause) {
        super(cause);
    }
}

O próximo passo é alterar o arquivo “pages.xml” mapeando a exceção mostrada anteriormente para forçar o redirecionamento para a tela de alteração da senha. Vai ser adicionado o seguinte trecho de código no arquivo:

<exception class="com.jabutiinfo.changeuserpwd.exception.ChangePasswordException">
    <redirect view-id="/changepwd.xhtml">
        <message severity="warn">The user must change the password in first access.</message>
    </redirect>
</exception>

A página de alterar senha terá o seguinte código:

<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:s="http://jboss.com/products/seam/taglib"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:rich="http://richfaces.org/rich"
    template="layout/template.xhtml">

<ui:define name="body">

    <h:form id="changePwdForm">

        <rich:panel>
            <f:facet name="header">Change Password</f:facet>

            <p>Fist you must change your password to access the application.</p>

            <div class="dialog">
                <h:panelGrid columns="2" rowClasses="prop" columnClasses="name,value">
                    <h:outputLabel for="username">Username</h:outputLabel>
                    <h:inputText id="username" value="#{userAuthenticated.userName}" readonly="true"/>
                    <h:outputLabel for="password">New Password</h:outputLabel>
                    <h:inputSecret id="password" value="#{userAuthenticated.password}"/>
                    <h:outputLabel for="passwordMatch">Confirm Password</h:outputLabel>
                    <h:inputSecret id="passwordMatch" value="#{userAuthenticated.pwdMatch}"/>
                </h:panelGrid>
            </div>

        </rich:panel>

        <div class="actionButtons">
            <h:commandButton id="submit" value="Change" action="#{authenticator.changePwd}"/>
        </div>

    </h:form>

 </ui:define>
</ui:composition>

Abaixo está o código do arquivo “.pages.xml” correspondente:

<?xml version="1.0" encoding="UTF-8"?>
<page xmlns="http://jboss.com/products/seam/pages" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://jboss.com/products/seam/pages http://jboss.com/products/seam/pages-2.2.xsd">

    <action execute="#{authenticator.loadPwdChange}" if="#{firstAccess}"/>

</page>

O detalhe do arquivo é a tag “action” cuja restrição limita a execução do método “loadPwdChange” para a primeira vez que o usuário se autenticar no sistema e para limpar as mensagens no “FacesMessages” e adicionar a mensagem desejada.

/**
 * Clear and adjusts the messages in context.
 */
public void loadPwdChange() {
    log.info("Method loadPwdChange():void");
    if (firstAccess && identity.isLoggedIn()) {
        FacesMessages.instance().clear();
        FacesMessages.instance().add(Severity.WARN, "Your password must be changed in firts access.");
    }
}

Precisei de um método para recuperar as informações do usuário quando ele se autenticar, ele serviu para recuperar informações como e-mail, nome, endereço, lotação, etc.

/**
 * Load user data from database.
 * This method must be configurated in components.xml file
 * as action executed on tag of type "org.jboss.seam.security.loginSuccessful".
 */
@Transactional
public void loadUserInfo() {
    log.info("Method loadUserInfo():void");
    try {
        userAuthenticated = (User) entityManager.createNamedQuery(User.FIND_BY_USER_NAME).setParameter("userName", credentials.getUsername()).getSingleResult();
        
        if (userAuthenticated.getLastSuccessFullLogin() == null) {
            showMenu = false;
            firstAccess = true;
        } else {
            userAuthenticated.setLastSuccessFullLogin(new java.util.Date());
            entityManager.merge(userAuthenticated);
            entityManager.flush();
            showMenu = true;
            firstAccess = false;
        }
    } catch (NoResultException nre) {
        log.info("User not found");
        showMenu = false;
    }
}

Agora que o usuário já está identificado precisei de outro método para conferir se o usuário está acessando o sistema pela primeira vez e lançar a exceção que mostrei acima.

/**
 * Verify if is the first access of user.
 */
public void verifyFirstAccess() {
    log.info("Method verifyFirstAccess():void");
    if (firstAccess) {
        throw new ChangePasswordException();
    }
}

Por último precisei alterar o arquivo “components.xml”, modificando a lista de ações executadas para o evento “org.jboss.seam.security.loginSuccessful”.

<event type="org.jboss.seam.security.loginSuccessful">
    <action execute="#{authenticator.loadUserInfo}"/>
    <action execute="#{authenticator.verifyFirstAccess}"/>
    <action execute="#{redirect.returnToCapturedView}" />
</event>

A ordem dos métodos na tag “event” é importante pois vai fazer com que o framework carregue as informações do usuário e depois verifique se este é o primeiro acesso.

Para mais detalhes sobre como habilitar o LdapIdentityStore acesse a documentação do JBoss Seam no capítulo 15 nesse link http://docs.jboss.org/seam/2.2.1.CR1/reference/en-US/html/security.html.

Elaborei um projeto exemplo para Eclipse que roda no Tomcat 6.x e disponibilizei as seguintes opções de download:

Qualquer dúvida, por favor, deixe um comentário.