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.