/* * JBoss, Home of Professional Open Source. * Copyright 2008, Red Hat Middleware LLC, and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.ejb; import org.jboss.logging.Logger; import org.jboss.tm.TransactionLocal; import javax.ejb.EJBException; import javax.transaction.RollbackException; import javax.transaction.Status; import javax.transaction.Synchronization; import javax.transaction.SystemException; import javax.transaction.Transaction; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; /** * This class provides a way to find out what entities are contained in * what transaction. It is used, to find which entities to call ejbStore() * on when a ejbFind() method is called within a transaction. EJB 2.0- 9.6.4 * also, it is used to synchronize on a remove. * Used in EntitySynchronizationInterceptor, EntityContainer * * Entities are stored in an ArrayList to ensure specific ordering. * * @author <a href="bill@burkecentral.com">Bill Burke</a> * @author <a href="alex@jboss.org">Alexey Loubyansky</a> * @version $Revision: 100259 $ */ public class GlobalTxEntityMap { private static final Logger log = Logger.getLogger(GlobalTxEntityMap.class); private final TransactionLocal txSynch = new TransactionLocal(); /** * An instance can be in one of the three states: * <ul> * <li>not associated with the tx and, hence, does not need to be synchronized</li> * <li>associated with the tx and needs to be synchronized</li> * <li>associated with the tx but does not need to be synchronized</li> * </ul> * Implementations of TxAssociation implement these states. */ public static interface TxAssociation { /** * Schedules the instance for synchronization. The instance might or might not be associated with the tx. * * @param tx the transaction the instance should be associated with if not yet associated * @param instance the instance to be scheduled for synchronization * @throws SystemException * @throws RollbackException */ void scheduleSync(Transaction tx, EntityEnterpriseContext instance) throws SystemException, RollbackException; /** * Synchronizes the instance if it is needed. * @param thread current thread * @param tx current transaction * @param instance the instance to be synchronized * @throws Exception thrown if synchronization failed */ void synchronize(Thread thread, Transaction tx, EntityEnterpriseContext instance) throws Exception; /** * Invokes ejbStore if needed * @param thread current thread * @param instance the instance to be synchronized * @throws Exception thrown if synchronization failed */ void invokeEjbStore(Thread thread, EntityEnterpriseContext instance) throws Exception; } public static final TxAssociation NONE = new TxAssociation() { public void scheduleSync(Transaction tx, EntityEnterpriseContext instance) throws SystemException, RollbackException { EntityContainer.getGlobalTxEntityMap().associate(tx, instance); instance.setTxAssociation(SYNC_SCHEDULED); } public void synchronize(Thread thread, Transaction tx, EntityEnterpriseContext instance) { throw new UnsupportedOperationException(); } public void invokeEjbStore(Thread thread, EntityEnterpriseContext instance) { throw new UnsupportedOperationException(); } }; public static final TxAssociation SYNC_SCHEDULED = new TxAssociation() { public void scheduleSync(Transaction tx, EntityEnterpriseContext instance) { } public void invokeEjbStore(Thread thread, EntityEnterpriseContext instance) throws Exception { if(instance.getId() != null) { EntityContainer container = (EntityContainer) instance.getContainer(); // set the context class loader before calling the store method SecurityActions.setContextClassLoader(thread, container.getClassLoader()); container.pushENC(); try { // store it container.invokeEjbStore(instance); } finally { container.popENC(); } } } public void synchronize(Thread thread, Transaction tx, EntityEnterpriseContext instance) throws Exception { // only synchronize if the id is not null. A null id means // that the entity has been removed. if(instance.getId() != null) { EntityContainer container = (EntityContainer) instance.getContainer(); // set the context class loader before calling the store method SecurityActions.setContextClassLoader(thread, container.getClassLoader()); container.pushENC(); try { // store it container.storeEntity(instance); instance.setTxAssociation(SYNCHRONIZED); } finally { container.popENC(); } } } }; public static final TxAssociation SYNCHRONIZED = new TxAssociation() { public void scheduleSync(Transaction tx, EntityEnterpriseContext instance) { instance.setTxAssociation(SYNC_SCHEDULED); } public void invokeEjbStore(Thread thread, EntityEnterpriseContext instance) { } public void synchronize(Thread thread, Transaction tx, EntityEnterpriseContext instance) { } }; public static final TxAssociation PREVENT_SYNC = new TxAssociation() { public void scheduleSync(Transaction tx, EntityEnterpriseContext instance) { } public void synchronize(Thread thread, Transaction tx, EntityEnterpriseContext instance) throws Exception { EntityContainer container = (EntityContainer)instance.getContainer(); if(container.getPersistenceManager().isStoreRequired(instance)) { throw new EJBException("The instance of " + container.getBeanMetaData().getEjbName() + " with pk=" + instance.getId() + " was not stored to prevent potential inconsistency of data in the database:" + " the instance was evicted from the cache during the transaction" + " and the database was possibly updated by another process."); } } public void invokeEjbStore(Thread thread, EntityEnterpriseContext instance) throws Exception { GlobalTxEntityMap.SYNC_SCHEDULED.invokeEjbStore(thread, instance); } }; /** * Used for instances in the create phase, * i.e. before the ejbCreate and until after the ejbPostCreate returns */ public static final TxAssociation NOT_READY = new TxAssociation() { public void scheduleSync(Transaction tx, EntityEnterpriseContext instance) { } public void synchronize(Thread thread, Transaction tx, EntityEnterpriseContext instance) throws Exception { } public void invokeEjbStore(Thread thread, EntityEnterpriseContext instance) throws Exception { } }; /** * sync all EntityEnterpriseContext that are involved (and changed) * within a transaction. */ public void synchronizeEntities(Transaction tx) { GlobalTxSynchronization globalSync = (GlobalTxSynchronization) txSynch.get(tx); if(globalSync != null) { globalSync.synchronize(); } } public GlobalTxSynchronization getGlobalSynchronization(Transaction tx) throws RollbackException, SystemException { GlobalTxSynchronization globalSync = (GlobalTxSynchronization) txSynch.get(tx); if(globalSync == null) { globalSync = new GlobalTxSynchronization(tx); txSynch.set(tx, globalSync); tx.registerSynchronization(globalSync); } return globalSync; } public Transaction getTransaction() { return txSynch.getTransaction(); } /** * associate instance with transaction */ private void associate(Transaction tx, EntityEnterpriseContext entity) throws RollbackException, SystemException { GlobalTxSynchronization globalSync = getGlobalSynchronization(tx); //There should be only one thread associated with this tx at a time. //Therefore we should not need to synchronize on entityFifoList to ensure exclusive //access. entityFifoList is correct since it was obtained in a synch block. globalSync.associate(entity); } // Inner /** * A list of instances associated with the transaction. */ public static class GlobalTxSynchronization implements Synchronization { private Transaction tx; private List instances = new ArrayList(); private boolean synchronizing; private List<Synchronization> otherSync = Collections.emptyList(); private Map<Object, Object> txLocals = Collections.emptyMap(); public GlobalTxSynchronization(Transaction tx) { this.tx = tx; } public void addSynchronization(Synchronization sync) { if(otherSync.isEmpty()) otherSync = Collections.singletonList(sync); else { if(otherSync.size() == 1) otherSync = new ArrayList<Synchronization>(otherSync); otherSync.add(sync); } } public void putTxLocal(Object key, Object value) { if(txLocals.isEmpty()) txLocals = Collections.singletonMap(key, value); else { if(txLocals.size() == 1) txLocals = new HashMap<Object, Object>(txLocals); txLocals.put(key, value); } } public Object getTxLocal(Object key) { return txLocals.get(key); } public void associate(EntityEnterpriseContext ctx) { instances.add(ctx); } public void synchronize() { if(synchronizing || instances.isEmpty()) { return; } synchronizing = true; // This is an independent point of entry. We need to make sure the // thread is associated with the right context class loader Thread currentThread = Thread.currentThread(); ClassLoader oldCl = SecurityActions.getContextClassLoader(); EntityEnterpriseContext instance = null; try { for(int i = 0; i < instances.size(); i++) { // any one can mark the tx rollback at any time so check // before continuing to the next store if(tx.getStatus() == Status.STATUS_MARKED_ROLLBACK) { return; } instance = (EntityEnterpriseContext) instances.get(i); instance.getTxAssociation().invokeEjbStore(currentThread, instance); } for(int i = 0; i < instances.size(); i++) { // any one can mark the tx rollback at any time so check // before continuing to the next store if(tx.getStatus() == Status.STATUS_MARKED_ROLLBACK) { return; } // read-only instances will never get into this list. instance = (EntityEnterpriseContext) instances.get(i); instance.getTxAssociation().synchronize(currentThread, tx, instance); } } catch(Exception causeByException) { // EJB 1.1 section 12.3.2 and EJB 2 section 18.3.3 // exception during store must log exception, mark tx for // rollback and throw a TransactionRolledback[Local]Exception // if using caller's transaction. All of this is handled by // the AbstractTxInterceptor and LogInterceptor. // // All we need to do here is mark the transaction for rollback // and rethrow the causeByException. The caller will handle logging // and wraping with TransactionRolledback[Local]Exception. try { tx.setRollbackOnly(); } catch(Exception e) { log.warn("Exception while trying to rollback tx: " + tx, e); } // Rethrow cause by exception if(causeByException instanceof EJBException) { throw (EJBException) causeByException; } throw new EJBException("Exception in store of entity:" + ((instance == null || instance.getId() == null) ? "<null>" : instance.getId().toString()), causeByException); } finally { SecurityActions.setContextClassLoader(oldCl); synchronizing = false; } } // Synchronization implementation ----------------------------- public void beforeCompletion() { if(log.isTraceEnabled()) { log.trace("beforeCompletion called for tx " + tx); } // let the runtime exceptions fall out, so the committer can determine // the root cause of a rollback synchronize(); for(Synchronization sync : otherSync) { sync.beforeCompletion(); } } public void afterCompletion(int status) { for(Synchronization sync : otherSync) { sync.afterCompletion(status); } } } }