/* Open Source Java Caching Service
* Copyright (C) 2002 Frank Karlstr�m
* This library 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 library 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 library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* The author can be contacted by email: fjankk@sourceforge.net
*/
package org.fjank.jcache;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.ReferenceQueue;
import java.net.InetAddress;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;
import javax.util.jcache.Attributes;
import javax.util.jcache.CacheAttributes;
import javax.util.jcache.CacheException;
import javax.util.jcache.CacheLogger;
import javax.util.jcache.CacheNotAvailableException;
import javax.util.jcache.DiskCacheException;
import javax.util.jcache.NullObjectNameException;
import javax.util.jcache.ObjectExistsException;
import javax.util.jcache.RegionNotFoundException;
import org.fjank.jcache.distribution.DistributionEngine;
import org.fjank.jcache.persistence.DiskCache;
/**
* Contains several usefull methods for configuring, administering and
* monitoring the Cache. Is final to avoid subclassing to further lock down
* the singleton pattern.
*
* @author Frank Karlstr�m
*
* @todo fix Singleton serialization, Multiple classloaders, and JVM
* destroy/reload.
*/
public final class CacheImpl implements javax.util.jcache.Cache {
private DistributionEngine distributionEngine;
/** this is the actuall cache instance in this JVM */
private static CacheImpl _singleton;
/** the disk cache implementation */
private DiskCache diskCache;
/** a boolean indication wether this cache is ready or not. */
private boolean ready;
/** the CacheAttributes for this cache */
private CacheAttributes attributes;
/** the version of this cache */
private float version;
/** the user defined regions. */
private final Map userRegions = new HashMap();
/**
* a ReferenceQueue. all objects which users either destroys, or quit
* using, ends up in this queue. (The GC sends them here)
*/
private final ReferenceQueue refQueue = new ReferenceQueue();
/**
* starts the service Threads wich sweeps the cache to remove invalid
* objects.
*/
//2004/09-FB
//private CacheSweeper sweeper;
/**
* A thread pool for running potential long-running background tasks.
*/
private final JCacheExecutorPool execPool = new JCacheExecutorPool();
/**
* private constructor to implement the singleton pattern.
*/
private CacheImpl() {
}
/**
* Gets an instance of the Cache class. Is synchronized to avoid several
* Threads to create multiple instances of this class wich MUST be a
* singleton.
*
* @param init a boolean inication wether to actually performe the
* initialization or not
*
* @return A Cache instance global for this JVM.
*
* @throws CacheNotAvailableException if the Cache is not available.
*/
public static synchronized CacheImpl getCache(final boolean init) {
if (_singleton == null) {
_singleton = new CacheImpl();
if (init) {
_singleton.open(null);
}
}
return _singleton;
}
static CacheImpl getCache() {
return _singleton;
}
/**
* Gets the default region in this cache.,
*
* @return the default region in this cache.
*/
public CacheRegion getRegion() {
return CacheRegion.getRegion();
}
/**
* Gets the named region in this cache.,
*
* @param name the name of the region to get.
*
* @return the named region.
*/
public CacheRegion getRegion(final Object name) {
return (CacheRegion) this.userRegions.get(name);
}
/**
* Adds the specified region.
*
* @param name the name of the region to add.
* @param attributes the attributes for the new region.
*
* @throws ObjectExistsException if the name already exists in the cache.
* @throws NullObjectNameException if the region is attempted initialized
* with <CODE>null</CODE> as name.
*/
void addRegion(final String name, final Attributes attributes) throws ObjectExistsException, NullObjectNameException {
if (name == null) {
throw new NullObjectNameException("A region cannot be created with null as its name.");
}
if ("".equals(name)) {
throw new NullObjectNameException("A region cannot be created with an empty string as its name.");
}
if (userRegions.containsKey(name)) {
throw new ObjectExistsException("The object " + name + " already exists in the cache.");
}
userRegions.put(name, new CacheRegion(name, new AttributesImpl(attributes)));
}
/**
* Will invalidate all objects in the named region and the named region.
*
* @param name the name of the region to destroy.
*
*
* @see #destroy()
*/
public void destroyRegion(final Object name) {
CacheRegion localRegion = (CacheRegion) userRegions.get(name);
userRegions.remove(localRegion.getName());
localRegion.destroy();
}
/**
* initializes the cache, allocates space for metadata and starts the
* service threads. The cache is a process wide service, so it can only be
* initialized once per process. Subsequent init calls are ignored.
*
* @param attributes2 contains configuration information to initialize the
* cache system.
*
* @throws CacheNotAvailableException if the cache is not available.
*/
public void init(final CacheAttributes attributes) throws CacheNotAvailableException{
synchronized (_singleton) {
if (!ready) {
this.attributes = attributes;
attributes.registerCache(this);
startServiceThreads();
if (attributes.getDiskPath() != null) {
try {
diskCache = new DiskCache(attributes);
} catch (DiskCacheException e) {
throw new CacheNotAvailableException(e);
}
}
if (attributes.isDistributed()) {
distributionEngine = DistributionEngine.getInstance(this);
}
ready = true;
}
}
}
/**
* starts the service Thread(s) which sweeps the cache to remove invalid
* objects.
*/
private void startServiceThreads() {
//2004/09-FB
CacheSweeper.getInstance().startSweeper();
}
/**
* stops the service Threads which sweeps the cache to remove invalid
* objects.
*/
private void stopServiceThreads() {
//2004/09-FB
CacheSweeper.getInstance().stopSweeper();
CacheSweeper.removeInstance();
}
/**
* will create a CacheAttributes object based on the values in a Java
* properties file, then call the method init. The properties file opened
* is called jcache.properties If this method is called, the init() method
* is not neccessary to call.
*
* @throws CacheNotAvailableException if the cache is not available.
*/
public void open() throws CacheNotAvailableException {
open(null);
}
/**
* tries to initialize a CacheLogger frorm the named class.
*
* @param logger the name of the class to initialize.
*
* @return a CacheLogger instance of the class with the specified name.
*
* @throws CacheException if exceptions occur during initialization.
*/
private CacheLogger parseLogger(final String logger) throws CacheException {
try {
return (CacheLogger) Class.forName(logger).newInstance();
} catch (ClassNotFoundException e) {
throw new CacheException("The CacheLogger " + logger + " could not be found.");
} catch (InstantiationException e) {
throw new CacheException("The CacheLogger " + logger + " could not be loaded.");
} catch (IllegalAccessException e) {
throw new CacheException("The CacheLogger " + logger + " is appearently not a CacheLogger.");
}
}
/**
* returns an int describing the CacheLoggerSeverity
*
* @param logSeverity a String representation of the log severity.
*
* @return an int describing the CacheLoggerSeverity
*/
private int parseLogSeverity(final String logSeverity) {
try {
java.lang.reflect.Field[] fields = CacheLogger.class.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
if (fields[i].getName().equals(logSeverity)) {
return fields[i].getInt(null);
}
}
} catch (IllegalAccessException e) {
;
}
return CacheLogger.DEFAULT;
}
/**
* parser the addresses into a List of URLs.
*
* @param addresses the lilst of addresses to parse.
*
* @return a List of URLS
*/
private java.util.List parseAddresses(final String addresses) {
java.util.ArrayList returnValue = new java.util.ArrayList();
java.util.StringTokenizer tokenizer = new java.util.StringTokenizer(addresses, ",");
while (tokenizer.hasMoreTokens()) {
try {
returnValue.add(new java.net.URL("http://" + tokenizer.nextToken()));
} catch (java.net.MalformedURLException e) {
e.printStackTrace();
}
}
return returnValue;
}
/**
* will create a CacheAttributes object based on the values in a Java
* properties file, then call the method init. If this method is called,
* the init() method is not neccessary to call.
*
* @param configFile the name of the configuration file.
*
* @throws CacheNotAvailableException if the cache is not available.
*/
public void open(final String configFile) {
InputStream in = null;
try {
synchronized (_singleton) {
if (!ready) {
Properties properties = new Properties();
if (configFile == null) {
in = getClass().getClassLoader().getResourceAsStream("./jcache.properties");
} else {
in = new FileInputStream(configFile);
}
if (in == null) {
properties = new Properties();
} else {
properties.load(in);
in.close();
}
//convert the properties to legal values, use default if no props available.
boolean distribute = Boolean.valueOf(properties.getProperty("distribute", "false")).booleanValue();
String logFileName = properties.getProperty("logFileName", "jcache.log");
int cleanInterval = 30;
try {
cleanInterval = Integer.parseInt(properties.getProperty("cleanInterval", "30"), 10);
} catch (NumberFormatException e) {
}
int diskSize = 10;
try {
diskSize = Integer.parseInt(properties.getProperty("diskSize", "10"), 10);
} catch (NumberFormatException e) {
}
String diskPath = properties.getProperty("diskPath");
float version = (float) 1.0;
try {
version = Float.parseFloat(properties.getProperty("version", "1.0"));
} catch (NumberFormatException e) {
}
int maxObjects = 5000;
try {
maxObjects = Integer.parseInt(properties.getProperty("maxObjects", "5000"), 10);
} catch (NumberFormatException e) {
}
int maxSize = -1;
try {
maxSize = Integer.parseInt(properties.getProperty("maxSize", "1"), 10);
} catch (NumberFormatException e) {
}
int logSeverity = parseLogSeverity(properties.getProperty("logSeverity", "DEFAULT"));
CacheLogger logger = new DefaultCacheLogger();
try {
logger = parseLogger(properties.getProperty("logger", "org.fjank.jcache.DefaultCacheLogger"));
logger.setSeverity(logSeverity);
} catch (CacheException e) {
//ugh.
e.printStackTrace();
}
List addresses = parseAddresses(properties.getProperty("discoveryAddress", "localhost:12345"));
//is now parsed, create and populate the CacheAttributes.
CacheAttributes attributes = CacheAttributes.getDefaultCacheAttributes();
if (!distribute) {
attributes.setLocal();
}
attributes.setDefaultLogFileName(logFileName);
attributes.setCleanInterval(cleanInterval);
attributes.setDiskCacheSize(diskSize);
attributes.setDiskPath(diskPath);
attributes.setMaxObjects(maxObjects);
attributes.setMemoryCacheSize(maxSize);
this.version = version;
attributes.setLogger(logger);
Iterator iter = addresses.iterator();
while (iter.hasNext()) {
URL url = (URL) iter.next();
attributes.addCacheAddr(InetAddress.getByName(url.getHost()), url.getPort());
}
init(attributes);
}
}
} catch (IOException e) {
throw new IllegalStateException("Error loading configuration from properties file. Caused by:" + e.getMessage());
} catch (CacheNotAvailableException e) {
throw new IllegalStateException("The cache was not available. "+e.getMessage());
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e1) {
throw new IllegalStateException("Failed to close stream to properties file. Caused by:" + e1.getMessage());
}
}
}
}
/**
* will mark the cache as "not ready" and shutdown the cache. Marking the
* cache as "not ready" will prevent any threads from accessing the Cache
* during shutdown. If the cache is distributed, close will unregister
* with the distributed caching system. The method should be called as a
* part of process termination.
*/
public void close() {
synchronized (this) {
this.ready = false;
stopServiceThreads();
if (diskCache != null) {
diskCache.close();
}
}
}
/**
* will mark all objects in the cache, both disk and memory, as invalid,
* forcing objects to be reloaded. All processes sharing the disk cache
* are notified when the cache is flushed.
*
* @throws CacheException if an error occurs.
*/
public void flush() throws CacheException {
flushMemory();
flushDisk();
}
/**
* will mark all objects in the cache as invalid, forcing objects to be
* reloaded. Flushing the memory cache will also invalidate memory objects
* spooled to disk. Objects that are only cached on disk will not be
* affected.
*
* @throws CacheException if an error occurs.
*/
public void flushMemory() throws CacheException {
Iterator iter = userRegions.keySet().iterator();
while (iter.hasNext()) {
Object name = iter.next();
CacheRegion reg = (CacheRegion) userRegions.get(name);
reg.invalidate();
}
CacheRegion.getRegion().invalidate();
}
/**
* wll mark all objects in the cache as invalid, forcing objects to be
* reloaded. Flushing the disk cache will also invalidate memory objects
* spooled to disk. All processes sharing the disk cache are notified when
* the cache is flushed.
*
* @throws CacheException if an error occurs.
*/
public void flushDisk() throws CacheException {
if (this.diskCache == null) {
return;
}
diskCache.removeAll();
}
/**
* returns the current version of the cache.
*
* @return the current version of the cache.
*/
public float getVersion() {
return version;
}
/**
* returns true if the cache has been initialized and not closed, false
* otherwise.
*
* @return true if the cache has been initialized and not closed, false
* otherwise.
*/
public boolean isReady() {
return this.ready;
}
/**
* returns true if the cache is currently in distributed mode, that it is
* distributing updates and invalidates within the site, false if all
* cache actions are local only.
*
* @return true if the cache is currently in distributed mode, that it is
* distributing updates and invalidates within the site, false if
* all cache actions are local only.
*/
public boolean isDistributed() {
return this.attributes.isDistributed();
}
/**
* will return an Enumeration of CacheObjectInfo objects describing the
* objects in all regions in the cache. CacheObjectInfo will include
* information such as the object name, the type, what group it is
* associated with, the reference count, the expiration time if any and
* object attributes.
*
* @return an Enumeration of CacheObjectInfo objects.
*
*@todo add feature to list objects in the disk cache.
*/
public Enumeration listCacheObjects() {
Vector temp = new Vector();
addNamedCacheObjects(temp, CacheRegion.getRegion());
Iterator iter = userRegions.keySet().iterator();
while (iter.hasNext()) {
addNamedCacheObjects(temp, (CacheRegion) userRegions.get(iter.next()));
}
return temp.elements();
}
/**
* adds all objects in the region to the vector.
* will also add from all the groups, if any.
* @param vector the vector to add all objects to.
* @param region the region to add from.
*/
private void addNamedCacheObjects(final Vector vector, final CacheRegion region) {
recurseObjects(vector, region);
}
private void recurseObjects(final Vector vector, final CacheGroup group) {
Map objects = group.weakReferenceObjects;
Iterator iter = objects.keySet().iterator();
while (iter.hasNext()) {
vector.add(new CacheObjectInfoImpl((CacheObject) objects.get(iter.next())));
}
Map groups = group.getGroups();
for (Iterator iterator = groups.keySet().iterator(); iterator.hasNext();) {
Object key = iterator.next();
CacheGroup tmp = (CacheGroup) groups.get(key);
recurseObjects(vector, tmp);
}
}
/**
* will return an Enumeration of CacheObjectInfo objects describing the
* objects in the specified in the cache. CacheObjectInfo will include
* information such as the object name, the type, what group it is
* associated with, the reference count, the expiration time if any and
* object attributes.
*
* @param region the region to get the Enumeration for.
*
* @return an Enumeration of CacheObjectInfo objects.
*
* @throws RegionNotFoundException if the named region os not present in
* the cache.
*
*@todo add feature to list objects in the disk cache.
*/
public Enumeration listCacheObjects(final String region) throws RegionNotFoundException {
Vector temp = new Vector();
if (region == null) {
throw new RegionNotFoundException("The regionName cannot be null.");
}
if (!userRegions.containsKey(region)) {
throw new RegionNotFoundException("The region " + region + " is not present in the cache.");
}
addNamedCacheObjects(temp, (CacheRegion) userRegions.get(region));
return temp.elements();
}
/**
* returns the current attributes of the cache including the cache version
* number, wether the cache is local or distributed, the maximum number of
* objects in the cache, the disk cache location, and the disk cache size.
*
* @return the current attributes of this cache.
*
* @throws CacheNotAvailableException if the cache is not ready.
*/
public CacheAttributes getAttributes(){
return this.attributes;
}
/**
* sets the log severity of the cache system. This determines wich messages
* the cache formats and logs into the log destination. Severity's are
* defined in the CacheLogger class.
*
* @param severity the severity level to set
*/
public void setLogSeverity(final int severity) {
this.attributes.getLogger().setSeverity(severity);
}
/**
* returns a String representation of this Cache.
*
* @return a String representation of this Cache.
*/
public String toString() {
return "Fjanks FKache version " + getVersion() + " running in " + (isDistributed() ? "distributed" : "local") + " mode is " + (isReady() ? "" : "not ")
+ "ready.";
}
/**
* gets the diskCache
*
* @return the diskCache
*/
DiskCache getDiskCache() {
return diskCache;
}
/**
* gets the ReferenceQueue.
*
* @return the ReferenceQueue.
*/
public ReferenceQueue getReferenceQueue() {
return refQueue;
}
/**
* @return
*/
public JCacheExecutorPool getExecPool() {
return execPool;
}
/**Returns an Iterator over the user defined regions.
* @return an Iterator over the user defined regions.
*/
Iterator userRegionNames() {
return userRegions.keySet().iterator();
}
public DistributionEngine getDistributionEngine() {
return distributionEngine;
}
}