package org.apache.commons.jcs.engine.control;

/*
 * 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.
 */

import java.io.IOException;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.apache.commons.jcs.access.exception.CacheException;
import org.apache.commons.jcs.admin.JCSAdminBean;
import org.apache.commons.jcs.auxiliary.AuxiliaryCache;
import org.apache.commons.jcs.auxiliary.AuxiliaryCacheAttributes;
import org.apache.commons.jcs.auxiliary.AuxiliaryCacheFactory;
import org.apache.commons.jcs.auxiliary.remote.behavior.IRemoteCacheConstants;
import org.apache.commons.jcs.engine.CacheConstants;
import org.apache.commons.jcs.engine.CompositeCacheAttributes;
import org.apache.commons.jcs.engine.ElementAttributes;
import org.apache.commons.jcs.engine.behavior.ICache;
import org.apache.commons.jcs.engine.behavior.ICacheType.CacheType;
import org.apache.commons.jcs.engine.behavior.ICompositeCacheAttributes;
import org.apache.commons.jcs.engine.behavior.ICompositeCacheManager;
import org.apache.commons.jcs.engine.behavior.IElementAttributes;
import org.apache.commons.jcs.engine.behavior.IProvideScheduler;
import org.apache.commons.jcs.engine.behavior.IShutdownObserver;
import org.apache.commons.jcs.engine.control.event.ElementEventQueue;
import org.apache.commons.jcs.engine.control.event.behavior.IElementEventQueue;
import org.apache.commons.jcs.engine.stats.CacheStats;
import org.apache.commons.jcs.engine.stats.behavior.ICacheStats;
import org.apache.commons.jcs.utils.config.OptionConverter;
import org.apache.commons.jcs.utils.threadpool.DaemonThreadFactory;
import org.apache.commons.jcs.utils.threadpool.ThreadPoolManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Manages a composite cache. This provides access to caches and is the primary way to shutdown the
 * caching system as a whole.
 * <p>
 * The composite cache manager is responsible for creating / configuring cache regions. It serves as
 * a factory for the ComositeCache class. The CompositeCache is the core of JCS, the hub for various
 * auxiliaries.
 */
public class CompositeCacheManager
    implements IRemoteCacheConstants, ICompositeCacheManager, IProvideScheduler
{
    /** The logger */
    private static final Log log = LogFactory.getLog( CompositeCacheManager.class );

    /** JMX object name */
    public static final String JMX_OBJECT_NAME = "org.apache.commons.jcs:type=JCSAdminBean";

    /** default region prefix */
    private static final String DEFAULT_REGION = "jcs.default";

    /** Caches managed by this cache manager */
    private final ConcurrentMap<String, ICache<?, ?>> caches =
        new ConcurrentHashMap<String, ICache<?, ?>>();

    /** Lock for initialization of caches */
    private final ReentrantLock cacheLock = new ReentrantLock();

    /** Number of clients accessing this cache manager */
    private final AtomicInteger clients = new AtomicInteger(0);

    /** Default cache attributes for this cache manager */
    private ICompositeCacheAttributes defaultCacheAttr = new CompositeCacheAttributes();

    /** Default element attributes for this cache manager */
    private IElementAttributes defaultElementAttr = new ElementAttributes();

    /** Used to keep track of configured auxiliaries */
    private final ConcurrentMap<String, AuxiliaryCacheFactory> auxiliaryFactoryRegistry =
        new ConcurrentHashMap<String, AuxiliaryCacheFactory>( );

    /** Used to keep track of attributes for auxiliaries. */
    private final ConcurrentMap<String, AuxiliaryCacheAttributes> auxiliaryAttributeRegistry =
        new ConcurrentHashMap<String, AuxiliaryCacheAttributes>( );

    /** Used to keep track of configured auxiliaries */
    private final ConcurrentMap<String, AuxiliaryCache<?, ?>> auxiliaryCaches =
        new ConcurrentHashMap<String, AuxiliaryCache<?, ?>>( );

    /** Properties with which this manager was configured. This is exposed for other managers. */
    private Properties configurationProperties;

    /** The default auxiliary caches to be used if not preconfigured */
    private String defaultAuxValues;

    /** The Singleton Instance */
    private static CompositeCacheManager instance;

    /** Should we use system property substitutions. */
    private static final boolean DEFAULT_USE_SYSTEM_PROPERTIES = true;

    /** Once configured, you can force a reconfiguration of sorts. */
    private static final boolean DEFAULT_FORCE_RECONFIGURATION = false;

    /** Stack for those waiting for notification of a shutdown. */
    private final LinkedBlockingDeque<IShutdownObserver> shutdownObservers = new LinkedBlockingDeque<IShutdownObserver>();

    /** The central background scheduler. */
    private ScheduledExecutorService scheduledExecutor;

    /** The central event queue. */
    private IElementEventQueue elementEventQueue;

    /** Shutdown hook thread instance */
    private ShutdownHook shutdownHook;

    /** Indicates whether the instance has been initialized. */
    private boolean isInitialized = false;

    /** Indicates whether configure has been called. */
    private boolean isConfigured = false;

    /** Indicates whether JMX bean has been registered. */
    private boolean isJMXRegistered = false;

    private String jmxName = JMX_OBJECT_NAME;

    /**
     * Gets the CacheHub instance. For backward compatibility, if this creates the instance it will
     * attempt to configure it with the default configuration. If you want to configure from your
     * own source, use {@link #getUnconfiguredInstance}and then call {@link #configure}
     * <p>
     * @return CompositeCacheManager
     * @throws CacheException if the configuration cannot be loaded
     */
    public static synchronized CompositeCacheManager getInstance() throws CacheException
    {
        return getInstance( CacheConstants.DEFAULT_CONFIG );
    }

    /**
     * Initializes the cache manager using the props file for the given name.
     * <p>
     * @param propsFilename
     * @return CompositeCacheManager configured from the give propsFileName
     * @throws CacheException if the configuration cannot be loaded
     */
    public static synchronized CompositeCacheManager getInstance( String propsFilename ) throws CacheException
    {
        if ( instance == null )
        {
            if ( log.isInfoEnabled() )
            {
                log.info( "Instance is null, creating with config [" + propsFilename + "]" );
            }

            instance = createInstance();
        }

        if (!instance.isInitialized())
        {
            instance.initialize();
        }

        if (!instance.isConfigured())
        {
            instance.configure( propsFilename );
        }

        instance.clients.incrementAndGet();

        return instance;
    }

    /**
     * Get a CacheHub instance which is not configured. If an instance already exists, it will be
     * returned.
     *<p>
     * @return CompositeCacheManager
     */
    public static synchronized CompositeCacheManager getUnconfiguredInstance()
    {
        if ( instance == null )
        {
            if ( log.isInfoEnabled() )
            {
                log.info( "Instance is null, returning unconfigured instance" );
            }

            instance = createInstance();
        }

        if (!instance.isInitialized())
        {
            instance.initialize();
        }

        instance.clients.incrementAndGet();

        return instance;
    }

    /**
     * Simple factory method, must override in subclasses so getInstance creates / returns the
     * correct object.
     * <p>
     * @return CompositeCacheManager
     */
    protected static CompositeCacheManager createInstance()
    {
        return new CompositeCacheManager();
    }

    /**
     * Default constructor
     */
    protected CompositeCacheManager()
    {
        // empty
    }

    /** Creates a shutdown hook and starts the scheduler service */
    protected void initialize()
    {
        if (!isInitialized)
        {
            this.shutdownHook = new ShutdownHook();
            try
            {
                Runtime.getRuntime().addShutdownHook( shutdownHook );
            }
            catch ( AccessControlException e )
            {
                log.error( "Could not register shutdown hook.", e );
            }

            this.scheduledExecutor = Executors.newScheduledThreadPool(4,
                    new DaemonThreadFactory("JCS-Scheduler-", Thread.MIN_PRIORITY));

            // Register JMX bean
            if (!isJMXRegistered && jmxName != null)
            {
                MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
                JCSAdminBean adminBean = new JCSAdminBean(this);
                try
                {
                    ObjectName jmxObjectName = new ObjectName(jmxName);
                    mbs.registerMBean(adminBean, jmxObjectName);
                    isJMXRegistered = true;
                }
                catch (Exception e)
                {
                    log.warn( "Could not register JMX bean.", e );
                }
            }

            this.elementEventQueue = new ElementEventQueue();

            isInitialized = true;
        }
    }

    /**
     * Get the element event queue
     *
     * @return the elementEventQueue
     */
    public IElementEventQueue getElementEventQueue()
    {
        return elementEventQueue;
    }

    /**
     * Get the scheduler service
     *
     * @return the scheduledExecutor
     */
    @Override
    public ScheduledExecutorService getScheduledExecutorService()
    {
        return scheduledExecutor;
    }

    /**
     * Configure with default properties file
     * @throws CacheException if the configuration cannot be loaded
     */
    public void configure() throws CacheException
    {
        configure( CacheConstants.DEFAULT_CONFIG );
    }

    /**
     * Configure from specific properties file.
     * <p>
     * @param propFile Path <u>within classpath </u> to load configuration from
     * @throws CacheException if the configuration cannot be loaded
     */
    public void configure( String propFile ) throws CacheException
    {
        log.info( "Creating cache manager from config file: " + propFile );

        Properties props = new Properties();

        InputStream is = getClass().getResourceAsStream( propFile );

        if ( is != null )
        {
            try
            {
                props.load( is );

                if ( log.isDebugEnabled() )
                {
                    log.debug( "File [" + propFile + "] contained " + props.size() + " properties" );
                }
            }
            catch ( IOException ex )
            {
                throw new CacheException("Failed to load properties for name [" + propFile + "]", ex);
            }
            finally
            {
                try
                {
                    is.close();
                }
                catch ( IOException ignore )
                {
                    // Ignored
                }
            }
        }
        else
        {
            throw new CacheException( "Failed to read configuration file [" + propFile + "]" );
        }

        configure( props );
    }

    /**
     * Configure from properties object.
     * <p>
     * This method will call configure, instructing it to use system properties as a default.
     * @param props
     */
    public void configure( Properties props )
    {
        configure( props, DEFAULT_USE_SYSTEM_PROPERTIES );
    }

    /**
     * Configure from properties object, overriding with values from the system properties if
     * instructed.
     * <p>
     * You can override a specific value by passing in a system property:
     * <p>
     * For example, you could override this value in the cache.ccf file by starting up your program
     * with the argument: -Djcs.auxiliary.LTCP.attributes.TcpListenerPort=1111
     * <p>
     * @param props
     * @param useSystemProperties -- if true, values starting with jcs will be put into the props
     *            file prior to configuring the cache.
     */
    public void configure( Properties props, boolean useSystemProperties )
    {
        configure( props, useSystemProperties, DEFAULT_FORCE_RECONFIGURATION );
    }

    /**
     * Configure from properties object, overriding with values from the system properties if
     * instructed.
     * <p>
     * You can override a specific value by passing in a system property:
     * <p>
     * For example, you could override this value in the cache.ccf file by starting up your program
     * with the argument: -Djcs.auxiliary.LTCP.attributes.TcpListenerPort=1111
     * <p>
     * @param props
     * @param useSystemProperties -- if true, values starting with jcs will be put into the props
     *            file prior to configuring the cache.
     * @param forceReconfiguration - if the manager is already configured, we will try again. This
     *            may not work properly.
     */
    public synchronized void configure( Properties props, boolean useSystemProperties, boolean forceReconfiguration )
    {
        if ( props == null )
        {
            log.error( "No properties found.  Please configure the cache correctly." );
            return;
        }

        if ( isConfigured )
        {
            if ( !forceReconfiguration )
            {
                if ( log.isDebugEnabled() )
                {
                    log.debug( "Configure called after the manager has been configured.  "
                        + "Force reconfiguration is false.  Doing nothing" );
                }
                return;
            }
            else
            {
                if ( log.isInfoEnabled() )
                {
                    log.info( "Configure called after the manager has been configured.  "
                        + "Force reconfiguration is true.  Reconfiguring as best we can." );
                }
            }
        }
        if ( useSystemProperties )
        {
            CompositeCacheConfigurator.overrideWithSystemProperties( props );
        }
        doConfigure( props );
    }

    /**
     * Configure the cache using the supplied properties.
     * <p>
     * @param properties assumed not null
     */
    private void doConfigure( Properties properties )
    {
        // We will expose this for managers that need raw properties.
        this.configurationProperties = properties;

        // set the props value and then configure the ThreadPoolManager
        ThreadPoolManager.setProps( properties );
        ThreadPoolManager poolMgr = ThreadPoolManager.getInstance();
        if ( log.isDebugEnabled() )
        {
            log.debug( "ThreadPoolManager = " + poolMgr );
        }

        // configure the cache
        CompositeCacheConfigurator configurator = new CompositeCacheConfigurator();

        long start = System.currentTimeMillis();

        // set default value list
        this.defaultAuxValues = OptionConverter.findAndSubst( CompositeCacheManager.DEFAULT_REGION,
                properties );

        log.info( "Setting default auxiliaries to " + this.defaultAuxValues );

        // set default cache attr
        this.defaultCacheAttr = configurator.parseCompositeCacheAttributes( properties, "",
                new CompositeCacheAttributes(), DEFAULT_REGION );

        log.info( "setting defaultCompositeCacheAttributes to " + this.defaultCacheAttr );

        // set default element attr
        this.defaultElementAttr = configurator.parseElementAttributes( properties, "",
                new ElementAttributes(), DEFAULT_REGION );

        log.info( "setting defaultElementAttributes to " + this.defaultElementAttr );

        // set up system caches to be used by non system caches
        // need to make sure there is no circularity of reference
        configurator.parseSystemRegions( properties, this );

        // setup preconfigured caches
        configurator.parseRegions( properties, this );

        long end = System.currentTimeMillis();
        if ( log.isInfoEnabled() )
        {
            log.info( "Finished configuration in " + ( end - start ) + " ms." );
        }

        isConfigured = true;
    }

    /**
     * Gets the defaultCacheAttributes attribute of the CacheHub object
     * <p>
     * @return The defaultCacheAttributes value
     */
    public ICompositeCacheAttributes getDefaultCacheAttributes()
    {
        return this.defaultCacheAttr.clone();
    }

    /**
     * Gets the defaultElementAttributes attribute of the CacheHub object
     * <p>
     * @return The defaultElementAttributes value
     */
    public IElementAttributes getDefaultElementAttributes()
    {
        return this.defaultElementAttr.clone();
    }

    /**
     * Gets the cache attribute of the CacheHub object
     * <p>
     * @param cacheName
     * @return CompositeCache -- the cache region controller
     */
    @Override
    public <K, V> CompositeCache<K, V> getCache( String cacheName )
    {
        return getCache( cacheName, this.defaultCacheAttr.clone() );
    }

    /**
     * Gets the cache attribute of the CacheHub object
     * <p>
     * @param cacheName
     * @param cattr
     * @return CompositeCache
     */
    public <K, V> CompositeCache<K, V> getCache( String cacheName, ICompositeCacheAttributes cattr )
    {
        cattr.setCacheName( cacheName );
        return getCache( cattr, this.defaultElementAttr );
    }

    /**
     * Gets the cache attribute of the CacheHub object
     * <p>
     * @param cacheName
     * @param cattr
     * @param attr
     * @return CompositeCache
     */
    public <K, V> CompositeCache<K, V>  getCache( String cacheName, ICompositeCacheAttributes cattr, IElementAttributes attr )
    {
        cattr.setCacheName( cacheName );
        return getCache( cattr, attr );
    }

    /**
     * Gets the cache attribute of the CacheHub object
     * <p>
     * @param cattr
     * @return CompositeCache
     */
    public <K, V> CompositeCache<K, V>  getCache( ICompositeCacheAttributes cattr )
    {
        return getCache( cattr, this.defaultElementAttr );
    }

    /**
     * If the cache has already been created, then the CacheAttributes and the element Attributes
     * will be ignored. Currently there is no overriding the CacheAttributes once it is set up. You
     * can change the default ElementAttributes for a region later.
     * <p>
     * Overriding the default elemental attributes will require changing the way the attributes are
     * assigned to elements. Get cache creates a cache with defaults if none are specified. We might
     * want to create separate method for creating/getting. . .
     * <p>
     * @param cattr
     * @param attr
     * @return CompositeCache
     */
    @SuppressWarnings("unchecked") // Need to cast because of common map for all caches
    public <K, V> CompositeCache<K, V>  getCache( ICompositeCacheAttributes cattr, IElementAttributes attr )
    {
        CompositeCache<K, V> cache;

        if ( log.isDebugEnabled() )
        {
            log.debug( "attr = " + attr );
        }

        cache = (CompositeCache<K, V>) caches.get( cattr.getCacheName() );

        if (cache == null)
        {
            cacheLock.lock();

            try
            {
                // double check
                cache = (CompositeCache<K, V>) caches.get( cattr.getCacheName() );

                if ( cache == null )
                {
                    CompositeCacheConfigurator configurator = new CompositeCacheConfigurator();

                    cache = configurator.parseRegion( this.getConfigurationProperties(), this, cattr.getCacheName(),
                                                      this.defaultAuxValues, cattr );

                    caches.put( cattr.getCacheName(), cache );
                }
            }
            finally
            {
                cacheLock.unlock();
            }
        }

        return cache;
    }

    /**
     * @param name
     */
    public void freeCache( String name )
    {
        freeCache( name, false );
    }

    /**
     * @param name
     * @param fromRemote
     */
    public void freeCache( String name, boolean fromRemote )
    {
        CompositeCache<?, ?> cache = (CompositeCache<?, ?>) caches.remove( name );

        if ( cache != null )
        {
            cache.dispose( fromRemote );
        }
    }

    /**
     * Calls freeCache on all regions
     */
    public void shutDown()
    {
        synchronized (CompositeCacheManager.class)
        {
            // shutdown element event queue
            this.elementEventQueue.dispose();

            // shutdown all scheduled jobs
            this.scheduledExecutor.shutdownNow();

            // shutdown all thread pools
            ThreadPoolManager.dispose();

            // notify any observers
            IShutdownObserver observer = null;
            while ((observer = shutdownObservers.poll()) != null)
            {
                observer.shutdown();
            }

            // Unregister JMX bean
            if (isJMXRegistered)
            {
                MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
                try
                {
                    ObjectName jmxObjectName = new ObjectName(jmxName);
                    mbs.unregisterMBean(jmxObjectName);
                }
                catch (Exception e)
                {
                    log.warn( "Could not unregister JMX bean.", e );
                }

                isJMXRegistered = false;
            }

            // do the traditional shutdown of the regions.
            for (String name : getCacheNames())
            {
                freeCache( name );
            }

            // shut down factories
            for (AuxiliaryCacheFactory factory : auxiliaryFactoryRegistry.values())
            {
                factory.dispose();
            }

            auxiliaryAttributeRegistry.clear();
            auxiliaryFactoryRegistry.clear();

            if (shutdownHook != null)
            {
                try
                {
                    Runtime.getRuntime().removeShutdownHook(shutdownHook);
                }
                catch (IllegalStateException e)
                {
                    // May fail if the JVM is already shutting down
                }

                this.shutdownHook = null;
            }

            isConfigured = false;
            isInitialized = false;
        }
    }

    /** */
    public void release()
    {
        release( false );
    }

    /**
     * @param fromRemote
     */
    private void release( boolean fromRemote )
    {
        synchronized ( CompositeCacheManager.class )
        {
            // Wait until called by the last client
            if ( clients.decrementAndGet() > 0 )
            {
                if ( log.isDebugEnabled() )
                {
                    log.debug( "Release called, but " + clients + " remain" );
                    return;
                }
            }

            if ( log.isDebugEnabled() )
            {
                log.debug( "Last client called release. There are " + caches.size() + " caches which will be disposed" );
            }

            for (ICache<?, ?> c : caches.values() )
            {
                CompositeCache<?, ?> cache = (CompositeCache<?, ?>) c;

                if ( cache != null )
                {
                    cache.dispose( fromRemote );
                }
            }
        }
    }

    /**
     * Returns a list of the current cache names.
     * @return String[]
     */
    public String[] getCacheNames()
    {
        return caches.keySet().toArray(new String[caches.size()]);
    }

    /**
     * @return ICacheType.CACHE_HUB
     */
    public CacheType getCacheType()
    {
        return CacheType.CACHE_HUB;
    }

    /**
     * @param auxFac
     */
    public void registryFacPut( AuxiliaryCacheFactory auxFac )
    {
        auxiliaryFactoryRegistry.put( auxFac.getName(), auxFac );
    }

    /**
     * @param name
     * @return AuxiliaryCacheFactory
     */
    public AuxiliaryCacheFactory registryFacGet( String name )
    {
        return auxiliaryFactoryRegistry.get( name );
    }

    /**
     * @param auxAttr
     */
    public void registryAttrPut( AuxiliaryCacheAttributes auxAttr )
    {
        auxiliaryAttributeRegistry.put( auxAttr.getName(), auxAttr );
    }

    /**
     * @param name
     * @return AuxiliaryCacheAttributes
     */
    public AuxiliaryCacheAttributes registryAttrGet( String name )
    {
        return auxiliaryAttributeRegistry.get( name );
    }

    /**
     * Add a cache to the map of registered caches
     *
     * @param cacheName the region name
     * @param cache the cache instance
     */
    public void addCache(String cacheName, ICache<?, ?> cache)
    {
        caches.put(cacheName, cache);
    }

    /**
     * Add a cache to the map of registered auxiliary caches
     *
     * @param auxName the auxiliary name
     * @param cacheName the region name
     * @param cache the cache instance
     */
    public void addAuxiliaryCache(String auxName, String cacheName, AuxiliaryCache<?, ?> cache)
    {
        String key = String.format("aux.%s.region.%s", auxName, cacheName);
        auxiliaryCaches.put(key, cache);
    }

    /**
     * Get a cache from the map of registered auxiliary caches
     *
     * @param auxName the auxiliary name
     * @param cacheName the region name
     *
     * @return the cache instance
     */
    @Override
    @SuppressWarnings("unchecked") // because of common map for all auxiliary caches
    public <K, V> AuxiliaryCache<K, V> getAuxiliaryCache(String auxName, String cacheName)
    {
        String key = String.format("aux.%s.region.%s", auxName, cacheName);
        return (AuxiliaryCache<K, V>) auxiliaryCaches.get(key);
    }

    /**
     * Gets stats for debugging. This calls gets statistics and then puts all the results in a
     * string. This returns data for all regions.
     * <p>
     * @return String
     */
    @Override
    public String getStats()
    {
        ICacheStats[] stats = getStatistics();
        if ( stats == null )
        {
            return "NONE";
        }

        // force the array elements into a string.
        StringBuilder buf = new StringBuilder();
        int statsLen = stats.length;
        for ( int i = 0; i < statsLen; i++ )
        {
            buf.append( "\n---------------------------\n" );
            buf.append( stats[i] );
        }
        return buf.toString();
    }

    /**
     * This returns data gathered for all regions and all the auxiliaries they currently uses.
     * <p>
     * @return ICacheStats[]
     */
    public ICacheStats[] getStatistics()
    {
        ArrayList<ICacheStats> cacheStats = new ArrayList<ICacheStats>();
        for (ICache<?, ?> c :  caches.values())
        {
            CompositeCache<?, ?> cache = (CompositeCache<?, ?>) c;
            if ( cache != null )
            {
                cacheStats.add( cache.getStatistics() );
            }
        }
        ICacheStats[] stats = cacheStats.toArray( new CacheStats[0] );
        return stats;
    }

    /**
     * Perhaps the composite cache itself should be the observable object. It doesn't make much of a
     * difference. There are some problems with region by region shutdown. Some auxiliaries are
     * global. They will need to track when every region has shutdown before doing things like
     * closing the socket with a lateral.
     * <p>
     * @param observer
     */
    @Override
    public void registerShutdownObserver( IShutdownObserver observer )
    {
    	if (!shutdownObservers.contains(observer))
    	{
    		shutdownObservers.push( observer );
    	}
    	else
    	{
    		log.warn("Shutdown observer added twice " + observer);
    	}
    }

    /**
     * @param observer
     */
    @Override
    public void deregisterShutdownObserver( IShutdownObserver observer )
    {
        shutdownObservers.remove( observer );
    }

    /**
     * This is exposed so other manager can get access to the props.
     * <p>
     * @return the configurationProperties
     */
    @Override
    public Properties getConfigurationProperties()
    {
        return configurationProperties;
    }

    /**
     * @return the isInitialized
     */
    public boolean isInitialized()
    {
        return isInitialized;
    }

    /**
     * @return the isConfigured
     */
    public boolean isConfigured()
    {
        return isConfigured;
    }

    public void setJmxName(final String name)
    {
        if (isJMXRegistered)
        {
            throw new IllegalStateException("Too late, MBean registration is done");
        }
        jmxName = name;
    }

    /**
     * Called on shutdown. This gives use a chance to store the keys and to optimize even if the
     * cache manager's shutdown method was not called manually.
     */
    class ShutdownHook
        extends Thread
    {
        /**
         * This will persist the keys on shutdown.
         * <p>
         * @see java.lang.Thread#run()
         */
        @SuppressWarnings("synthetic-access")
        @Override
        public void run()
        {
            if ( isInitialized() )
            {
                log.info( "Shutdown hook activated.  Shutdown was not called.  Shutting down JCS." );
                shutDown();
            }
        }
    }
}
