/*
* #%L
* Nazgul Project: nazgul-core-cache-impl-ehcache
* %%
* Copyright (C) 2010 - 2017 jGuru Europe AB
* %%
* Licensed under the jGuru Europe AB license (the "License"), based
* on Apache License, Version 2.0; you may not use this file except
* in compliance with the License.
*
* You may obtain a copy of the License at
*
* http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
*
* 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.
* #L%
*
*/
package se.jguru.nazgul.core.cache.impl.ehcache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import net.sf.ehcache.TransactionController;
import net.sf.ehcache.transaction.manager.TransactionManagerLookup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.jguru.nazgul.core.algorithms.api.Validate;
import se.jguru.nazgul.core.cache.api.Cache;
import se.jguru.nazgul.core.cache.api.CacheListener;
import se.jguru.nazgul.core.cache.api.ReadOnlyIterator;
import se.jguru.nazgul.core.cache.api.transaction.AbstractTransactedAction;
import se.jguru.nazgul.core.cache.api.transaction.TransactedAction;
import se.jguru.nazgul.core.clustering.api.AbstractClusterable;
import javax.transaction.SystemException;
import javax.transaction.TransactionManager;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Nazgul cache api implementation backed by a transacted EhCache with Strings as CacheKeys.
*
* @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB
*/
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public class NonDistributedEhCache extends AbstractClusterable implements Cache<String, Serializable> {
// Our log
private static final Logger log = LoggerFactory.getLogger(NonDistributedEhCache.class);
// Internal state
private net.sf.ehcache.Ehcache cacheInstance;
private CacheManager cacheManager;
private TransactionManager tx;
private TransactionController localTx;
private Map<String, EhCacheListenerAdapter> locallyRegisteredListeners = new TreeMap<String, EhCacheListenerAdapter>();
/**
* Creates an LocalEhCache instance using the provided classpath-relative configuration file.
*
* @param classpathRelativeConfigurationFile The LocalEhCache configuration file.
*/
public NonDistributedEhCache(final String classpathRelativeConfigurationFile) {
// Initialize our ID.
super(new EhCacheClusterIdGenerator(), false);
// Acquire the cacheManager
cacheManager = getCacheManager(classpathRelativeConfigurationFile);
((EhCacheClusterIdGenerator) idGenerator).setCacheManager(cacheManager);
// Perform common initialization.
initialize();
}
/**
* Creates an LocalEhCache instance from the provided CacheManager instance.
*
* @param manager The Manager used to create the LocalEhCache instance.
*/
public NonDistributedEhCache(@NotNull final CacheManager manager) {
// Initialize our ID.
super(new EhCacheClusterIdGenerator(), false);
// Check sanity
Validate.notNull(manager, "manager");
// Assign internal state
cacheManager = manager;
((EhCacheClusterIdGenerator) this.idGenerator).setCacheManager(cacheManager);
// Perform common initialization.
initialize();
}
/**
* Initializes this LocalEhCache. Override in subclasses to provide
* custom initialization - but <strong>do not forget to call super.initialize()</strong>.
*/
protected void initialize() {
// Create the local-instance cache.
if (cacheInstance == null) {
cacheManager.addCacheIfAbsent("nonDistributedCache");
cacheInstance = cacheManager.getCache("nonDistributedCache");
}
// Find the transactionLookup for the cacheInstance.
TransactionManagerLookup transactionLookup =
((net.sf.ehcache.Cache) cacheInstance).getTransactionManagerLookup();
if (transactionLookup != null) {
tx = transactionLookup.getTransactionManager();
}
if (tx == null) {
// We must revert to LocalTransactions
localTx = cacheManager.getTransactionController();
}
}
/**
* @return The wrapped EhCache instance.
*/
protected final Ehcache getCacheInstance() {
return cacheInstance;
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public Iterator<String> iterator() {
// This mode of implementation is required since EhCache is stupid
// enough to require that *all* operations in a transacted cache is
// executed within transaction boundaries.
final String errorMessage = "Could not retrieve cache keySet";
final List<String> keys = new ArrayList<String>();
AbstractElementReferenceTransactedAction getAction =
new AbstractElementReferenceTransactedAction(errorMessage) {
@Override
public void doInTransaction() throws RuntimeException {
for (Object current : cacheInstance.getKeys()) {
keys.add("" + current);
}
}
};
performTransactedAction(getAction);
// All done.
return new ReadOnlyIterator<String>(keys.iterator());
}
/**
* {@inheritDoc}
*/
@Override
public Serializable get(final String key) {
// This mode of implementation is required since EhCache is stupid
// enough to require that *all* operations in a transacted cache is
// executed within transaction boundaries.
final String errorMessage = "Could not acquire the Element for key [" + key + "]";
AbstractElementReferenceTransactedAction getAction =
new AbstractElementReferenceTransactedAction(errorMessage) {
@Override
public void doInTransaction() throws RuntimeException {
setAffectedElement(cacheInstance.get(key));
}
};
performTransactedAction(getAction);
Element toReturn = getAction.getAffectedElement();
if (toReturn == null) {
return toReturn;
}
return (Serializable) toReturn.getObjectValue();
}
/**
* {@inheritDoc}
*/
@Override
public Serializable put(final String key, final Serializable value) {
// This mode of implementation is required since EhCache is stupid
// enough to require that *all* operations in a transacted cache is
// executed within transaction boundaries.
final String errorMessage = "Could not assign [" + value + "] to key [" + key + "]";
AbstractElementReferenceTransactedAction putAction =
new AbstractElementReferenceTransactedAction(errorMessage) {
@Override
public void doInTransaction() throws RuntimeException {
setAffectedElement(cacheInstance.get(key));
cacheInstance.put(new Element(key, value));
}
};
performTransactedAction(putAction);
Element toReturn = putAction.getAffectedElement();
if (toReturn == null) {
return toReturn;
}
// All done.
return (Serializable) toReturn.getObjectValue();
}
/**
* {@inheritDoc}
*/
@Override
public Serializable remove(final String key) {
// This mode of implementation is required since EhCache is stupid
// enough to require that *all* operations in a transacted cache is
// executed within transaction boundaries.
final String errorMessage = "Could not remove Element for key [" + key + "]";
AbstractElementReferenceTransactedAction removeAction =
new AbstractElementReferenceTransactedAction(errorMessage) {
@Override
public void doInTransaction() throws RuntimeException {
setAffectedElement(cacheInstance.get(key));
cacheInstance.remove(key);
}
};
performTransactedAction(removeAction);
Element toReturn = removeAction.getAffectedElement();
if (toReturn == null) {
return toReturn;
}
// All done.
return (Serializable) toReturn.getObjectValue();
}
/**
* {@inheritDoc}
*/
@Override
public boolean addListener(final CacheListener<String, Serializable> listener) {
// Wrap the CacheListener inside an EhCacheListenerAdapter.
final EhCacheListenerAdapter toAdd = new EhCacheListenerAdapter(listener);
if (locallyRegisteredListeners.containsKey(toAdd.getId())) {
if (log.isWarnEnabled()) {
EhCacheListenerAdapter alreadyRegistered
= locallyRegisteredListeners.get(toAdd.getId());
log.warn("Already registered listener [" + alreadyRegistered.getId()
+ "] holding CacheListener of type ["
+ alreadyRegistered.getCacheListener().getClass().getName()
+ "]. Aborting registration.");
}
// We will not replace the current listener.
return false;
}
final String errMsg = "Could not add listener [" + listener.getClusterId() + "] of type ["
+ listener.getClass().getName() + "]";
performTransactedAction(
new AbstractTransactedAction(errMsg) {
@Override
public void doInTransaction() throws RuntimeException {
// Add to both the shared map and our internal state.
locallyRegisteredListeners.put(toAdd.getId(), toAdd);
cacheInstance.getCacheEventNotificationService().registerListener(toAdd);
}
});
// All done.
return true;
}
/**
* Acquires the list of all active Listeners of this Cache instance. Note that this does not include CacheListener
* instances wired to distributed objects, nor CacheListener instances wired to other members within a distributed
* cache.
*
* @return a List holding all IDs of the active Listeners onto this (local member) Cache. Note that this does not
* include CacheListener instances wired to distributed objects, nor nor CacheListener instances wired to
* other members within a distributed cache.
*/
@Override
public List<String> getListenerIds() {
return Collections.unmodifiableList(new ArrayList<String>(locallyRegisteredListeners.keySet()));
}
/**
* Removes the CacheListener with the given key. <strong>This operation may be an asynchronous operation
* depending on the underlying cache implementation.</strong>
*
* @param key The unique identifier for the given CacheListener to remove from operating on this Cache.
*/
@Override
public void removeListener(final String key) {
// Acquire the Cachelistener with the given key.
if (locallyRegisteredListeners.keySet().contains(key)) {
final EhCacheListenerAdapter listener = locallyRegisteredListeners.get(key);
final String errMsg = "Could not remove listener [" + listener.getId() + "] of type ["
+ listener.getClass().getName() + "]";
performTransactedAction(new AbstractTransactedAction(errMsg) {
@Override
public void doInTransaction() throws RuntimeException {
// Remove the CacheListenerAdapter and return its CacheListener.
EhCacheListenerAdapter removed = locallyRegisteredListeners.remove(key);
cacheInstance.getCacheEventNotificationService().unregisterListener(removed);
}
});
return;
}
// The cacheListener was not found.
if (log.isWarnEnabled()) {
log.warn("CacheListener with id [" + key + "] was not locally registered.");
}
}
/**
* Returns true if this cache contains a mapping for the specified key
*
* @param key The <code>key</code> whose presence in this map is to be tested.
* @return <code>true</code> if this map contains a mapping for the specified key.
*/
@Override
public boolean containsKey(final String key) {
// This mode of implementation is required since EhCache is stupid
// enough to require that *all* operations in a transacted cache is
// executed within transaction boundaries.
final String errorMessage = "Could not acquire the Element for key [" + key + "]";
AbstractElementReferenceTransactedAction containsKeyAction =
new AbstractElementReferenceTransactedAction(errorMessage) {
@Override
public void doInTransaction() throws RuntimeException {
setAffectedElement(cacheInstance.get(key));
}
};
performTransactedAction(containsKeyAction);
// All done.
Element toReturn = containsKeyAction.getAffectedElement();
return toReturn != null;
}
/**
* Acquires a Transactional context from this Cache, and Executes the
* TransactedAction::doInTransaction method within it.
*
* @param action The TransactedAction to be executed within a Cache Transactional context.
* @throws UnsupportedOperationException if the underlying Cache implementation does not
* support Transactions.
*/
@Override
public void performTransactedAction(final TransactedAction action) throws UnsupportedOperationException {
if (tx == null) {
// We are not participating in JTA or Appserver Transactions.
// Use a LocalTransaction instance.
try {
localTx.begin();
action.doInTransaction();
localTx.commit();
} catch (Exception ex) {
// Whoops.
log.error("(LocalTransaction) " + action.getRollbackErrorDescription(), ex);
try {
localTx.rollback();
} catch (Exception e) {
log.error("LocalTransaction Rollback failure", e);
}
}
} else {
try {
tx.begin();
action.doInTransaction();
tx.commit();
} catch (Exception ex) {
// Whoops.
log.error(action.getRollbackErrorDescription(), ex);
try {
tx.rollback();
} catch (SystemException e) {
log.error("Rollback failure", e);
}
}
}
}
/**
* Retrieves a CacheManager, using the configuration file located at the
* classpathRelativeConfigurationFile, read by the classloader of the
* EhCacheUtil class.
*
* @param classpathRelativeConfigurationFile an ehCache configuration file, i.e.
* read like "config/ehcache/someFile.xml"
* @return An instance CacheManager.
*/
public static CacheManager getCacheManager(final String classpathRelativeConfigurationFile) {
final URL config = NonDistributedEhCache.class.getClassLoader().getResource(classpathRelativeConfigurationFile);
return new CacheManager(config);
}
/**
* Kills the CacheManager of the provided LocalEhCache.
*
* @param toShutDown the LocalEhCache whose CacheManager should be shut down.
*/
public static void shutdownCache(final NonDistributedEhCache toShutDown) {
toShutDown.getCacheInstance().getCacheManager().shutdown();
}
/**
* Abstract TransactedAction skeleton implementation providing means to get/set an
* Element to be returned from the operation or assigned/used by the operation.
*/
abstract class AbstractElementReferenceTransactedAction extends AbstractTransactedAction {
// Hold the Element to be returned
private Element affectedElement;
/**
* Creates a new AbstractElementReferenceTransactedAction with the provided message
* to be logged on rollback / transaction failure.
*
* @param rollbackErrorMessage An exception message logged if the TransactedAction failed.
*/
protected AbstractElementReferenceTransactedAction(final String rollbackErrorMessage) {
super(rollbackErrorMessage);
}
/**
* @return The affected/returned element of the TransactedAction.
*/
public final Element getAffectedElement() {
return affectedElement;
}
/**
* Assigns the affected Element of this TransactedAction.
*
* @param affectedElement the affected Element of this TransactedAction.
*/
public void setAffectedElement(final Element affectedElement) {
this.affectedElement = affectedElement;
}
}
}