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)”:
- quando o usuário se autentica.
- quando o usuário acessa qualquer item de menu.
- Quando o usuário solicita a edição de um registro:
- verifico se o conceito não está sendo editado usando o método “onUpdate(BaseConcept concept)”.
- 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)”.
- 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.