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.

Deixe um comentário