/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.deltaspike.jpa.impl.transaction.context;
import javax.enterprise.context.spi.Contextual;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <p>This class stores information about
* @{@link org.apache.deltaspike.jpa.api.transaction.TransactionScoped}
* contextual instances, their {@link javax.enterprise.context.spi.CreationalContext} etc.</p>
*
* <p>We use a RequestScoped bean because this way we don't need to take
* care about cleaning up any ThreadLocals ourselves. This also makes sure that
* we subsequently destroy any left over TransactionScoped beans (which should not happen,
* but who knows). We also don't need to do any fancy synchronization stuff since
* we are sure that we are always in the same Thread.</p>
*/
public class TransactionBeanStorage
{
private static final Logger LOGGER = Logger.getLogger(TransactionBeanStorage.class.getName());
private static ThreadLocal<TransactionBeanStorage> transactionBeanStorage =
new ThreadLocal<TransactionBeanStorage>();
private static class TransactionContextInfo
{
/**
* This is the actual bean storage.
* The structure is:
* <ol>
* <li>transactionKey identifies the 'database qualifier'</li>
* <li>transactionKey -> Stack: we need the Stack because of REQUIRES_NEW, etc</li>
* <li>top Element in the Stack -> Context beans for the transactionKey</li>
* </ol>
*
*/
private Map<Contextual, TransactionBeanEntry> contextualInstances =
new HashMap<Contextual, TransactionBeanEntry>();
private Set<EntityManagerEntry> ems = new HashSet<EntityManagerEntry>();
/**
* counts the 'depth' of the interceptor invocation.
*/
private AtomicInteger refCounter = new AtomicInteger(0);
}
/**
* If we hit a layer with REQUIRES_NEW, then create a new TransactionContextInfo
* and push the old one on top of this stack.
*/
private Stack<TransactionContextInfo> oldTci = new Stack<TransactionContextInfo>();
/**
* The TransactionContextInfo which is on top of the stack.
*/
private TransactionContextInfo currentTci = null;
private TransactionBeanStorage()
{
}
public static TransactionBeanStorage getInstance()
{
TransactionBeanStorage result = transactionBeanStorage.get();
if (result == null)
{
result = new TransactionBeanStorage();
transactionBeanStorage.set(result);
}
return result;
}
public static void close()
{
TransactionBeanStorage currentStorage = transactionBeanStorage.get();
if (currentStorage != null)
{
currentStorage.endAllTransactionScopes();
transactionBeanStorage.set(null);
transactionBeanStorage.remove();
}
}
public static boolean isOpen()
{
return transactionBeanStorage.get() != null;
}
/**
* Increment the ref counter and return the old value.
* Must only be called if the bean storage is not {@link #isEmpty()}.
*
* @return the the previous values of the refCounters. If 0 then we are 'outermost'
*/
public int incrementRefCounter()
{
return currentTci.refCounter.incrementAndGet() - 1;
}
/**
* Decrement the reference counter and return the layer.
*
* @return the layer number. 0 represents the outermost interceptor for the qualifier
*/
public int decrementRefCounter()
{
if (currentTci == null)
{
return 0;
}
return currentTci.refCounter.decrementAndGet();
}
/**
* @return <code>true</code> if we are the outermost interceptor over all qualifiers
* and the TransactionBeanStorage is yet empty.
*/
public boolean isEmpty()
{
return currentTci == null;
}
/**
* Start a new TransactionScope
*/
public void startTransactionScope()
{
// first store away any previous TransactionContextInfo
if (currentTci != null)
{
oldTci.push(currentTci);
}
currentTci = new TransactionContextInfo();
if (LOGGER.isLoggable(Level.FINER))
{
LOGGER.finer( "starting TransactionScope");
}
}
/**
* End the TransactionScope with the given qualifier.
* This will subsequently destroy all beans which are stored
* in the context.
*
* This method only gets used if we leave a transaction with REQUIRES_NEW.
*/
public void endTransactionScope()
{
if (LOGGER.isLoggable(Level.FINER))
{
LOGGER.finer("ending TransactionScope");
}
destroyBeans(currentTci.contextualInstances);
if (!oldTci.isEmpty())
{
currentTci = oldTci.pop();
endTransactionScope();
}
else
{
currentTci = null;
}
}
public void storeUsedEntityManager(EntityManagerEntry entityManagerEntry)
{
currentTci.ems.add(entityManagerEntry);
}
public Set<EntityManagerEntry> getUsedEntityManagerEntries()
{
return currentTci.ems;
}
public void cleanUsedEntityManagers()
{
currentTci.ems.clear();
}
/**
* @return the Map which represents the currently active Context content.
*/
public Map<Contextual, TransactionBeanEntry> getActiveTransactionContext()
{
if (currentTci == null)
{
return null;
}
return currentTci.contextualInstances;
}
private void endAllTransactionScopes()
{
while (!isEmpty())
{
endTransactionScope();
}
}
/**
* Properly destroy all the given beans.
* @param activeBeans to destroy
*/
private void destroyBeans(Map<Contextual, TransactionBeanEntry> activeBeans)
{
for (TransactionBeanEntry beanEntry : activeBeans.values())
{
beanEntry.getBean().destroy(beanEntry.getContextualInstance(), beanEntry.getCreationalContext());
}
}
}