package org.apache.commons.jcs.admin;

/*
 * 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 org.apache.commons.jcs.access.exception.CacheException;
import org.apache.commons.jcs.auxiliary.remote.server.RemoteCacheServer;
import org.apache.commons.jcs.auxiliary.remote.server.RemoteCacheServerFactory;
import org.apache.commons.jcs.engine.CacheElementSerialized;
import org.apache.commons.jcs.engine.behavior.ICacheElement;
import org.apache.commons.jcs.engine.behavior.IElementAttributes;
import org.apache.commons.jcs.engine.control.CompositeCache;
import org.apache.commons.jcs.engine.control.CompositeCacheManager;
import org.apache.commons.jcs.engine.memory.behavior.IMemoryCache;

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.text.DateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedList;
import java.util.Set;

/**
 * A servlet which provides HTTP access to JCS. Allows a summary of regions to be viewed, and
 * removeAll to be run on individual regions or all regions. Also provides the ability to remove
 * items (any number of key arguments can be provided with action 'remove'). Should be initialized
 * with a properties file that provides at least a classpath resource loader.
 */
public class JCSAdminBean implements JCSJMXBean
{
    /** The cache manager. */
    private final CompositeCacheManager cacheHub;

    /**
     * Default constructor
     */
    public JCSAdminBean()
    {
        super();
        try
        {
            this.cacheHub = CompositeCacheManager.getInstance();
        }
        catch (CacheException e)
        {
            throw new RuntimeException("Could not retrieve cache manager instance", e);
        }
    }

    /**
     * Parameterized constructor
     *
	 * @param cacheHub the cache manager instance
	 */
	public JCSAdminBean(CompositeCacheManager cacheHub)
	{
		super();
		this.cacheHub = cacheHub;
	}

	/**
     * Builds up info about each element in a region.
     * <p>
     * @param cacheName
     * @return Array of CacheElementInfo objects
     * @throws Exception
     */
    @Override
    public CacheElementInfo[] buildElementInfo( String cacheName )
        throws Exception
    {
        CompositeCache<Serializable, Serializable> cache = cacheHub.getCache( cacheName );

        Serializable[] keys = cache.getMemoryCache().getKeySet().toArray(new Serializable[0]);

        // Attempt to sort keys according to their natural ordering. If that
        // fails, get the key array again and continue unsorted.
        try
        {
            Arrays.sort( keys );
        }
        catch ( Exception e )
        {
            keys = cache.getMemoryCache().getKeySet().toArray(new Serializable[0]);
        }

        LinkedList<CacheElementInfo> records = new LinkedList<CacheElementInfo>();

        ICacheElement<Serializable, Serializable> element;
        IElementAttributes attributes;
        CacheElementInfo elementInfo;

        DateFormat format = DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT );

        long now = System.currentTimeMillis();

        for (Serializable key : keys)
        {
            element = cache.getMemoryCache().getQuiet( key );

            attributes = element.getElementAttributes();

            elementInfo = new CacheElementInfo(
            		String.valueOf( key ),
            		attributes.getIsEternal(),
            		format.format(new Date(attributes.getCreateTime())),
            		attributes.getMaxLife(),
            		(now - attributes.getCreateTime() - attributes.getMaxLife() * 1000 ) / -1000);

            records.add( elementInfo );
        }

        return records.toArray(new CacheElementInfo[0]);
    }

    /**
     * Builds up data on every region.
     * <p>
     * TODO we need a most light weight method that does not count bytes. The byte counting can
     *       really swamp a server.
     * @return list of CacheRegionInfo objects
     * @throws Exception
     */
    @Override
    public CacheRegionInfo[] buildCacheInfo()
        throws Exception
    {
        String[] cacheNames = cacheHub.getCacheNames();

        Arrays.sort( cacheNames );

        LinkedList<CacheRegionInfo> cacheInfo = new LinkedList<CacheRegionInfo>();

        CacheRegionInfo regionInfo;
        CompositeCache<?, ?> cache;

        for ( int i = 0; i < cacheNames.length; i++ )
        {
            cache = cacheHub.getCache( cacheNames[i] );

            regionInfo = new CacheRegionInfo(
                    cache.getCacheName(),
                    cache.getSize(),
                    cache.getStatus().toString(),
                    cache.getStats(),
                    cache.getHitCountRam(),
                    cache.getHitCountAux(),
                    cache.getMissCountNotFound(),
                    cache.getMissCountExpired(),
                    getByteCount( cache ));

            cacheInfo.add( regionInfo );
        }

        return cacheInfo.toArray(new CacheRegionInfo[0]);
    }


	/**
     * Tries to estimate how much data is in a region. This is expensive. If there are any non serializable objects in
     * the region or an error occurs, suppresses exceptions and returns 0.
     * <p>
     *
     * @return int The size of the region in bytes.
     */
	@Override
    public int getByteCount(String cacheName)
	{
		return getByteCount(cacheHub.getCache(cacheName));
	}

	/**
     * Tries to estimate how much data is in a region. This is expensive. If there are any non serializable objects in
     * the region or an error occurs, suppresses exceptions and returns 0.
     * <p>
     *
     * @return int The size of the region in bytes.
     */
    public <K, V> int getByteCount(CompositeCache<K, V> cache)
    {
        if (cache == null)
        {
            throw new IllegalArgumentException("The cache object specified was null.");
        }

        long size = 0;
        IMemoryCache<K, V> memCache = cache.getMemoryCache();

        for (K key : memCache.getKeySet())
        {
            ICacheElement<K, V> ice = null;
			try
			{
				ice = memCache.get(key);
			}
			catch (IOException e)
			{
                throw new RuntimeException("IOException while trying to get a cached element", e);
			}

			if (ice == null)
			{
				continue;
			}

			if (ice instanceof CacheElementSerialized)
            {
                size = size + ((CacheElementSerialized<K, V>) ice).getSerializedValue().length;
            }
            else
            {
                Object element = ice.getVal();

                //CountingOnlyOutputStream: Keeps track of the number of bytes written to it, but doesn't write them anywhere.
                CountingOnlyOutputStream counter = new CountingOnlyOutputStream();
                ObjectOutputStream out = null;
                try
                {
                    out = new ObjectOutputStream(counter);
                    out.writeObject(element);
                }
                catch (IOException e)
                {
                    throw new RuntimeException("IOException while trying to measure the size of the cached element", e);
                }
                finally
                {
                	try
                	{
                		if (out != null)
                		{
                			out.close();
                		}
					}
                	catch (IOException e)
                	{
                		// ignore
					}
                	try
                	{
						counter.close();
					}
                	catch (IOException e)
                	{
                		// ignore
					}
                }

                // 4 bytes lost for the serialization header
                size = size + counter.getCount() - 4;
            }
        }

        if (size > Integer.MAX_VALUE)
        {
            throw new IllegalStateException("The size of cache " + cache.getCacheName() + " (" + size + " bytes) is too large to be represented as an integer.");
        }

        return (int) size;
    }

    /**
     * Clears all regions in the cache.
     * <p>
     * If this class is running within a remote cache server, clears all regions via the <code>RemoteCacheServer</code>
     * API, so that removes will be broadcast to client machines. Otherwise clears all regions in the cache directly via
     * the usual cache API.
     */
    @Override
    public void clearAllRegions() throws IOException
    {
        if (RemoteCacheServerFactory.getRemoteCacheServer() == null)
        {
            // Not running in a remote cache server.
            // Remove objects from the cache directly, as no need to broadcast removes to client machines...

            String[] names = cacheHub.getCacheNames();

            for (int i = 0; i < names.length; i++)
            {
                cacheHub.getCache(names[i]).removeAll();
            }
        }
        else
        {
            // Running in a remote cache server.
            // Remove objects via the RemoteCacheServer API, so that removes will be broadcast to client machines...
            try
            {
                String[] cacheNames = cacheHub.getCacheNames();

                // Call remoteCacheServer.removeAll(String) for each cacheName...
                RemoteCacheServer<?, ?> remoteCacheServer = RemoteCacheServerFactory.getRemoteCacheServer();
                for (int i = 0; i < cacheNames.length; i++)
                {
                    String cacheName = cacheNames[i];
                    remoteCacheServer.removeAll(cacheName);
                }
            }
            catch (IOException e)
            {
                throw new IllegalStateException("Failed to remove all elements from all cache regions: " + e, e);
            }
        }
    }

    /**
     * Clears a particular cache region.
     * <p>
     * If this class is running within a remote cache server, clears the region via the <code>RemoteCacheServer</code>
     * API, so that removes will be broadcast to client machines. Otherwise clears the region directly via the usual
     * cache API.
     */
    @Override
    public void clearRegion(String cacheName) throws IOException
    {
        if (cacheName == null)
        {
            throw new IllegalArgumentException("The cache name specified was null.");
        }
        if (RemoteCacheServerFactory.getRemoteCacheServer() == null)
        {
            // Not running in a remote cache server.
            // Remove objects from the cache directly, as no need to broadcast removes to client machines...
            cacheHub.getCache(cacheName).removeAll();
        }
        else
        {
            // Running in a remote cache server.
            // Remove objects via the RemoteCacheServer API, so that removes will be broadcast to client machines...
            try
            {
                // Call remoteCacheServer.removeAll(String)...
                RemoteCacheServer<?, ?> remoteCacheServer = RemoteCacheServerFactory.getRemoteCacheServer();
                remoteCacheServer.removeAll(cacheName);
            }
            catch (IOException e)
            {
                throw new IllegalStateException("Failed to remove all elements from cache region [" + cacheName + "]: " + e, e);
            }
        }
    }

    /**
     * Removes a particular item from a particular region.
     * <p>
     * If this class is running within a remote cache server, removes the item via the <code>RemoteCacheServer</code>
     * API, so that removes will be broadcast to client machines. Otherwise clears the region directly via the usual
     * cache API.
     *
     * @param cacheName
     * @param key
     *
     * @throws IOException
     */
    @Override
    public void removeItem(String cacheName, String key) throws IOException
    {
        if (cacheName == null)
        {
            throw new IllegalArgumentException("The cache name specified was null.");
        }
        if (key == null)
        {
            throw new IllegalArgumentException("The key specified was null.");
        }
        if (RemoteCacheServerFactory.getRemoteCacheServer() == null)
        {
            // Not running in a remote cache server.
            // Remove objects from the cache directly, as no need to broadcast removes to client machines...
            cacheHub.getCache(cacheName).remove(key);
        }
        else
        {
            // Running in a remote cache server.
            // Remove objects via the RemoteCacheServer API, so that removes will be broadcast to client machines...
            try
            {
                Object keyToRemove = null;
                CompositeCache<?, ?> cache = CompositeCacheManager.getInstance().getCache(cacheName);

                // A String key was supplied, but to remove elements via the RemoteCacheServer API, we need the
                // actual key object as stored in the cache (i.e. a Serializable object). To find the key in this form,
                // we iterate through all keys stored in the memory cache until we find one whose toString matches
                // the string supplied...
                Set<?> allKeysInCache = cache.getMemoryCache().getKeySet();
                for (Object keyInCache : allKeysInCache)
                {
                    if (keyInCache.toString().equals(key))
                    {
                        if (keyToRemove == null)
                        {
                            keyToRemove = keyInCache;
                        }
                        else
                        {
                            // A key matching the one specified was already found...
                            throw new IllegalStateException("Unexpectedly found duplicate keys in the cache region matching the key specified.");
                        }
                    }
                }
                if (keyToRemove == null)
                {
                    throw new IllegalStateException("No match for this key could be found in the set of keys retrieved from the memory cache.");
                }
                // At this point, we have retrieved the matching K key.

                // Call remoteCacheServer.remove(String, Serializable)...
                RemoteCacheServer<Serializable, Serializable> remoteCacheServer = RemoteCacheServerFactory.getRemoteCacheServer();
                remoteCacheServer.remove(cacheName, key);
            }
            catch (Exception e)
            {
                throw new IllegalStateException("Failed to remove element with key [" + key + ", " + key.getClass() + "] from cache region [" + cacheName + "]: " + e, e);
            }
        }
    }
}
