package com.joe.utilities.core.lookup;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.joe.utilities.core.configuration.Globals;
import com.joe.utilities.core.util.Utils;
import com.opensymphony.oscache.base.events.CacheEntryEventListener;
import com.opensymphony.oscache.extra.CacheEntryEventListenerImpl;
import com.opensymphony.oscache.general.GeneralCacheAdministrator;
/**
* CacheManager. Manages cached data using OS (open symphony) cache implementation
*
* @author Dave Ousey
* @version 1.0
*
* Creation date: 07/13/2005 2:177 PM
* Copyright (c) 2005 MEDecision, Inc. All rights reserved.
*/
public class CacheManager
{
private static Log log = LogFactory.getLog(CacheManager.class.getName());
/** OSCache adminstrator */
private GeneralCacheAdministrator cacheAdministrator = null;
/** Records JVM times per MCO when caches should be flushed */
private Map<String, Date> flushTimesByMCO = new HashMap<String, Date> (4);
private static CacheEntryEventListenerImpl cacheEntryEventListener = null;
/** Reference to flush thread */
private FlushThread flusher = null;
private static CacheManager singleton;
static
{
initialize();
}
private CacheManager()
{
}
/**
* Gets the single instance of CacheManager.
*
* @return single instance of CacheManager
*/
public static CacheManager getInstance()
{
return singleton;
}
/**
* Method initialize. Creates new OS cache administrator objects for each MCO defined in the service configuration factory.
*/
public static void initialize()
{
singleton = new CacheManager();
// Lookup configuration properties
Properties cacheProperties = null;
String osCachePropertiesFileName = System.getProperty("oscache.configuration");
if (osCachePropertiesFileName != null && osCachePropertiesFileName.length() > 0)
{
log.info("Reading OSCache property settings from " + osCachePropertiesFileName+"...");
InputStream fileStream = null;
try
{
cacheProperties = new Properties();
fileStream = new BufferedInputStream(new FileInputStream(osCachePropertiesFileName));
cacheProperties.load(fileStream);
}
catch (IOException e)
{
cacheProperties = null;
log.error("Error loading OSCache properties file from " +osCachePropertiesFileName +'.');
}
finally
{
if (fileStream != null) try { fileStream.close(); } catch (IOException e) {}
}
}
// Use the following default setting if configuration file is unavailable
if (cacheProperties == null)
{
log.info("Using default OSCache property settings...");
cacheProperties = new Properties();
cacheProperties.put("cache.capacity", "50000");
}
// Initialize the cache administrator with the specified properties
singleton.cacheAdministrator = new GeneralCacheAdministrator(cacheProperties);
initializeMCOFlushTimes();
}
/**
* Method initializeMCOFlusher.
*/
public static void initializeMCOFlushTimes()
{
// Build a list of all MCO IDs configured for the application
Set<String> mcos = getMcoSet();
String [] mcoList = Globals.getStringArray("com.med.config.mco.list");
if (mcoList == null || mcoList.length == 0)
mcos.add("MDL");
else
mcos.addAll(Arrays.asList(mcoList));
// Lookup flush times per MCO. Set initialized flag to true to avoid cache-related errors in OP cache lookup.
Iterator<String> iter = mcos.iterator();
while (iter.hasNext())
{
String mcoID = iter.next();
try
{
setNextFlushTime(mcoID);
} catch (Throwable t) {
log.warn("Unable to set next flush time for MCO: '" + mcoID + "'", t);
//Ignore exceptions for MCOs that may not be available during initialization
continue;
}
}
// Start up flush timer
singleton.flusher = new FlushThread();
singleton.flusher.start();
}
/**
* getMcoSet: Returns set of MCOs configured in the globals properties file. If setting is not defined,
* assume single, default MCO environment
* @return Set<String>
*/
private static Set<String> getMcoSet()
{
// Get Set of all MCOs defined in services and resource helpers configuration since
// some MCOs may use Services but no Resource Helpers and some MCOs may use Resource
// Helpers but no Services.
Set<String> mcos = new HashSet<String>(16);
String [] mcoList = Globals.getStringArray("com.med.config.mco.list");
if (mcoList == null || mcoList.length == 0)
mcos.add("MDL");
else
mcos.addAll(Arrays.asList(mcoList));
return mcos;
}
/**
* Method setNextFlushTime. Determine s and records in a map the next time (JVM time) when the given mco's cache
* items should be flushed.
* @param mcoID
*/
private static void setNextFlushTime(String mcoID)
{
// Get time value from operation parameter (e.g. "2 AM")
String jvmFlushTimeConfiguration = Globals.getString("com.med.config.mco."+mcoID+".cache.flushTime");
if (jvmFlushTimeConfiguration == null)
jvmFlushTimeConfiguration = "12:00 AM";
// Check if current JVM time has passed the flush time on the current date. If not, use that time, else use
// same time tomorrow.
Date midnight = Utils.extractDate(new Date());
Date mcoFlushDateTime = new Date(midnight.getTime()+Utils.timeToMillis(Utils.parseTime(jvmFlushTimeConfiguration)));
if (mcoFlushDateTime.before(new Date()))
mcoFlushDateTime = Utils.dateAdd(mcoFlushDateTime, 1, false);
log.info("Next flush time calculated as " + mcoFlushDateTime + " for MCO = " + mcoID);
// Put in map
singleton.flushTimesByMCO.put(mcoID, mcoFlushDateTime);
}
/**
* Method reset. Clears current cache and reinitialized CacheManager - possibly after changes made to SOA configuration.
* @throws SOAException
*/
public static void reset()
{
flushAll();
singleton.flusher.kill = true;
singleton.flusher = null;
initialize();
}
/**
* Method putInCache. Places an object into given MCO's cache
* @param mcoID
* @param key
* @param o
*/
public static void putInCache(String mcoID, String key, Object o)
{
putInCache(mcoID, key, o, (String []) null);
}
/**
* Method putInCache. Places an object into given MCO's cache with associated group name.
* @param mcoID
* @param key
* @param o
* @param groupName
*/
public static void putInCache(String mcoID, String key, Object o, String groupName)
{
putInCache(mcoID, key, o, new String[] {groupName});
}
/**
* Method putInCache. Places an object into given MCO's cache with associated group names
* @param mcoID
* @param key
* @param o
* @param groupName
*/
public static void putInCache(String mcoID, String key, Object o, String [] groupNamesArray)
{
if (log.isTraceEnabled())
log.trace("CacheManager - putInCache: mcoID = " +mcoID + ", key = "+key +", object = "+o+", group(s) = "+Arrays.toString(groupNamesArray));
singleton.validate(mcoID);
singleton.validateNotNull(key, "Key must be valued.");
singleton.validateNotNull(o, "Object to cache must be valued.");
// Setup list of groups to include. Always include MCO group to tag this entry as belonging to this MCO ID
List<String> groupList = new ArrayList<String>(4);
groupList.add("mco."+mcoID);
if (groupNamesArray != null)
{
// Add mco ID to each group name
for (int i = 0; i < groupNamesArray.length; i++)
{
groupList.add(mcoID+"."+groupNamesArray[i]);
}
}
// Add to cache with list of groups associated with entry
try
{
singleton.cacheAdministrator.putInCache(mcoID+'.'+key, o, groupList.toArray(new String[groupList.size()]));
}
catch (Throwable t)
{
log.warn("Could not update cache for key = '"+key+"'.", t);
singleton.cacheAdministrator.cancelUpdate(key);
}
}
/**
* Method getFromCache. Retrieve an object from the cache with the given key
* @param mcoID
* @param key
* @return Cachable
*/
public static Object getFromCache(String mcoID, String key)
{
singleton.validate(mcoID);
singleton.validateNotNull(key, "Key must be valued");
try
{
// Fetch object from cache
Object o = singleton.cacheAdministrator.getFromCache(mcoID+'.'+key);
if (log.isTraceEnabled() && o != null)
log.trace("CacheManager - getFromCache: mcoID = " +mcoID + ", key = "+key +", object retrieved = "+o);
return o;
}
catch (Throwable t1)
{
if (log.isTraceEnabled())
log.trace("CacheManager - getFromCache:cacheAdministrator.getFromCache: mcoID = " +mcoID + ", key = "+key +": " + t1.getLocalizedMessage());
// OSCache will wait for us to refresh missing item and place into cache by blocking the current thread.
// We don't want it to work this way because it involves invasive code or callbacks from cache processing to layer above.
// Therefore, cancel this "update".
try
{
singleton.cacheAdministrator.cancelUpdate(mcoID+'.'+key);
}
catch (Throwable t2)
{
if (log.isTraceEnabled())
log.trace("CacheManager - getFromCache:cacheAdministrator.cancelUpdate: mcoID = " +mcoID + ", key = "+key +": " + t2.getLocalizedMessage());
}
// Return null instead of passing back silly exception
return null;
}
}
/**
* Method setMCOFlushTime. Sets time when MCO cache will be flushed. This method does not usually need to be called
* because CacheManager.initialize will automatically set these values automatically for all MCOs defined in the SOA
* service configuration
* @param mcoID
* @param mcoTimeToFlush
*/
public static void setMCOFlushTime(String mcoID, Date mcoTimeToFlush)
{
singleton.validate(mcoID);
singleton.validateNotNull(mcoTimeToFlush, "Organization time to flush must be valued.");
singleton.flushTimesByMCO.put(mcoID, mcoTimeToFlush);
}
/**
* Method flushAll. Flushes all caches across all MCOs
*/
public static void flushAll()
{
log.info("flushAll called. Removing all elements from CacheManager.");
try
{
singleton.cacheAdministrator.flushAll();
}
catch (Throwable t)
{
log.error("Error in calling to OSCache - flushAll.", t);
}
}
/**
* Method flushMCO.
* @param mcoID
*/
public static void flushMCO(String mcoID)
{
singleton.validate(mcoID);
log.info("flushMCO called for MCO = '"+mcoID+"'...");
try
{
singleton.cacheAdministrator.flushGroup("mco."+mcoID);
}
catch (Throwable t)
{
log.info("Error in calling to OSCache - flushMCO for MCO = '"+mcoID+"'. If NullPointerException, this is safe to ignore since error is thrown when the group is not already in the cache.", t);
}
}
/**
* Method flushGroup.
* @param mcoID
* @param groupName
*/
public static void flushGroup(String mcoID, String groupName)
{
singleton.validate(mcoID);
singleton.validateNotNull(groupName, "Group name must be valued.");
log.info("flushGroup called for MCO = '"+mcoID+"' and group = '"+groupName+"'...");
try
{
singleton.cacheAdministrator.flushGroup(mcoID+'.'+groupName);
}
catch (Throwable t)
{
// OSCache stupidly returns NullPointerException if group does not exist in the cache
log.info("Error in calling to OSCache - flushGroup for MCO = '"+mcoID+"' and group = '"+groupName+"'. If NullPointerException, this is safe to ignore since error is thrown when the group is not already in the cache.", t);
}
}
/**
* Method flushItem.
* @param mcoID
* @param key
*/
public static void flushItem(String mcoID, String key)
{
singleton.validate(mcoID);
singleton.validateNotNull(key, "Key must be valued.");
log.trace("flushItem called for MCO = '"+mcoID+"' and key = '"+key+"'...");
try
{
singleton.cacheAdministrator.flushEntry(mcoID+'.'+key);
}
catch (Throwable t)
{
log.error("Error in calling to OSCache - flushEntry.", t);
}
}
/**
* Method validate. Common validation when calling CacheManager function
* @param mcoID
*/
private void validate(String mcoID)
{
if (mcoID == null || mcoID.length() == 0)
throw new IllegalArgumentException("MCO ID must be valued when accessing the CacheManager API.");
}
/**
* Method validateNotNull.
* @param o
* @param validationMessage
*/
private void validateNotNull(Object o, String validationMessage)
{
if (o == null)
throw new IllegalArgumentException(validationMessage);
}
/**
* Method getEntryAddedCount. Returns # of entries added since cache listener was started.
* @return int
*/
public static int getEntryAddedCount()
{
if (cacheEntryEventListener == null)
throw new RuntimeException("Must call CacheManager - startCacheListener before calling this method.");
startCacheListener();
return cacheEntryEventListener.getEntryAddedCount();
}
/**
* Method getEntryFlushedCount. Returns # of entries flushed since cache listener was started.
* @return int
*/
public static int getEntryFlushedCount()
{
if (cacheEntryEventListener == null)
throw new RuntimeException("Must call CacheManager - startCacheListener before calling this method.");
startCacheListener();
return cacheEntryEventListener.getEntryFlushedCount();
}
/**
* Method getEntryRemovedCount. Returns # of entries removed since cache listener was started.
* @return int
*/
public static int getEntryRemovedCount()
{
if (cacheEntryEventListener == null)
throw new RuntimeException("Must call CacheManager - startCacheListener before calling this method.");
startCacheListener();
return cacheEntryEventListener.getEntryRemovedCount();
}
/**
* Method getEntryUpdatedCount. Returns # of entries updated since cache listener was started.
* @return int
*/
public static int getEntryUpdatedCount()
{
if (cacheEntryEventListener == null)
throw new RuntimeException("Must call CacheManager - startCacheListener before calling this method.");
startCacheListener();
return cacheEntryEventListener.getEntryUpdatedCount();
}
/**
* Method startCacheListener. This should be used primarily for debug/analysis. This will activate a cache listener
* that will listen for events in the underlying OSCache caching mechanism.
*
* Returns void
*/
public static void startCacheListener()
{
if (cacheEntryEventListener == null)
{
cacheEntryEventListener = new CacheEntryEventListenerImpl();
singleton.cacheAdministrator.getCache().addCacheEventListener(cacheEntryEventListener, CacheEntryEventListener.class);
}
}
/**
* Method stopCacheListener. Deactivate the cache listener
*
* Returns void
*/
public static void stopCacheListener()
{
if (cacheEntryEventListener != null)
{
singleton.cacheAdministrator.getCache().removeCacheEventListener(cacheEntryEventListener, CacheEntryEventListener.class);
cacheEntryEventListener = null;
}
}
/**
* Private class to flush MCO caches
**/
private static class FlushThread extends Thread
{
protected boolean kill = false;
/**
* @see java.lang.Runnable#run()
*/
public void run()
{
// Build a list of all MCO IDs configured for the application
Set<String> mcos = getMcoSet();
log.info("Flush thread started...");
while (!kill)
{
try
{
// Check every minute
sleep(60000);
}
catch (InterruptedException e)
{}
Iterator<String> iter = mcos.iterator();
while (iter.hasNext())
{
String mcoID = iter.next();
Date currentJVMTime = new Date();
Date flushTime = (Date) singleton.flushTimesByMCO.get(mcoID);
if (flushTime == null)
{
setNextFlushTime(mcoID);
}
else if (currentJVMTime.after(flushTime))
{
// Flush the MCO
flushMCO(mcoID);
log.info("Cached items for MCO ID '"+mcoID+"' flushed. JVM time = "+currentJVMTime);
// Set next flush time
setNextFlushTime(mcoID);
}
}
}
}
}
}