package org.apache.commons.jcs3.utils.access;

/*
 * 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.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.jcs3.JCS;
import org.apache.commons.jcs3.access.CacheAccess;
import org.apache.commons.jcs3.access.GroupCacheAccess;
import org.apache.commons.jcs3.access.exception.CacheException;
import org.apache.commons.jcs3.log.Log;
import org.apache.commons.jcs3.log.LogManager;

/**
 * Utility class to encapsulate doing a piece of work, and caching the results
 * in JCS. Simply construct this class with the region name for the Cache and
 * keep a static reference to it instead of the JCS itself. Then make a new
 * org.apache.commons.jcs3.utils.access.AbstractJCSWorkerHelper and implement Object
 * doWork() and do the work in there, returning the object to be cached. Then
 * call .getResult() with the key and the AbstractJCSWorkerHelper to get the
 * result of the work. If the object isn't already in the Cache,
 * AbstractJCSWorkerHelper.doWork() will get called, and the result will be put
 * into the cache. If the object is already in cache, the cached result will be
 * returned instead.
 * <p>
 * As an added bonus, multiple JCSWorkers with the same region, and key won't do
 * the work multiple times: The first JCSWorker to get started will do the work,
 * and all subsequent workers with the same region, group, and key will wait on
 * the first one and use his resulting work instead of doing the work
 * themselves.
 * <p>
 * This is ideal when the work being done is a query to the database where the
 * results may take time to be retrieved.
 * <p>
 * For example:
 *
 * <pre>
 *      public static JCSWorker cachingWorker = new JCSWorker(&quot;example region&quot;);
 *   		public Object getSomething(Serializable aKey){
 *        JCSWorkerHelper helper = new AbstractJCSWorkerHelper(){
 *          public Object doWork(){
 *            // Do some (DB?) work here which results in a list
 *            // This only happens if the cache doesn't have a item in this region for aKey
 *            // Note this is especially useful with Hibernate, which will cache individual
 *            // Objects, but not entire query result sets.
 *            List results = query.list();
 *            // Whatever we return here get's cached with aKey, and future calls to
 *            // getResult() on a CachedWorker with the same region and key will return that instead.
 *            return results;
 *        };
 *        List result = worker.getResult(aKey, helper);
 *      }
 * </pre>
 *
 * This is essentially the same as doing:
 *
 * <pre>
 * JCS jcs = JCS.getInstance( &quot;exampleregion&quot; );
 * List results = (List) jcs.get( aKey );
 * if ( results != null )
 * {
 *     //do the work here
 *     results = query.list();
 *     jcs.put( aKey, results );
 * }
 * </pre>
 *
 * <p>
 * But has the added benefit of the work-load sharing; under normal
 * circumstances if multiple threads all tried to do the same query at the same
 * time, the same query would happen multiple times on the database, and the
 * resulting object would get put into JCS multiple times.
 * </p>
 */
public class JCSWorker<K, V>
{
    /** The logger */
    private static final Log logger = LogManager.getLog( JCSWorker.class );

    /** The cache we are working with */
    private final CacheAccess<K, V> cache;

    /** The cache we are working with */
    private final GroupCacheAccess<K, V> groupCache;

    /**
     * Map to hold who's doing work presently.
     */
    private final ConcurrentMap<String, JCSWorkerHelper<V>> map = new ConcurrentHashMap<>();

    /**
     * Region for the JCS cache.
     */
    private final String region;

    /**
     * Constructor which takes a region for the JCS cache.
     * @param aRegion
     *            The Region to use for the JCS cache.
     */
    public JCSWorker( final String aRegion )
    {
        region = aRegion;
        try
        {
            cache = JCS.getInstance( aRegion );
            groupCache = JCS.getGroupCacheInstance( aRegion );
        }
        catch ( final CacheException e )
        {
            throw new RuntimeException( e.getMessage() );
        }
    }

    /**
     * Getter for the region of the JCS Cache.
     * @return The JCS region in which the result will be cached.
     */
    public String getRegion()
    {
        return region;
    }

    /**
     * Gets the cached result for this region/key OR does the work and caches
     * the result, returning the result. If the result has not been cached yet,
     * this calls doWork() on the JCSWorkerHelper to do the work and cache the
     * result. This is also an opportunity to do any post processing of the
     * result in your CachedWorker implementation.
     * @param aKey
     *            The key to get/put with on the Cache.
     * @param aWorker
     *            The JCSWorkerHelper implementing Object doWork(). This gets
     *            called if the cache get misses, and the result is put into
     *            cache.
     * @return The result of doing the work, or the cached result.
     * @throws Exception
     *             Throws an exception if anything goes wrong while doing the
     *             work.
     */
    public V getResult( final K aKey, final JCSWorkerHelper<V> aWorker )
        throws Exception
    {
        return run( aKey, null, aWorker );
    }

    /**
     * Gets the cached result for this region/key OR does the work and caches
     * the result, returning the result. If the result has not been cached yet,
     * this calls doWork() on the JCSWorkerHelper to do the work and cache the
     * result. This is also an opportunity to do any post processing of the
     * result in your CachedWorker implementation.
     * @param aKey
     *            The key to get/put with on the Cache.
     * @param aGroup
     *            The cache group to put the result in.
     * @param aWorker
     *            The JCSWorkerHelper implementing Object doWork(). This gets
     *            called if the cache get misses, and the result is put into
     *            cache.
     * @return The result of doing the work, or the cached result.
     * @throws Exception
     *             Throws an exception if anything goes wrong while doing the
     *             work.
     */
    public V getResult( final K aKey, final String aGroup, final JCSWorkerHelper<V> aWorker )
        throws Exception
    {
        return run( aKey, aGroup, aWorker );
    }

    /**
     * Try and get the object from the cache, and if it's not there, do the work
     * and cache it. This also ensures that only one CachedWorker is doing the
     * work and subsequent calls to a CachedWorker with identical
     * region/key/group will wait on the results of this call. It will call the
     * JCSWorkerHelper.doWork() if the cache misses, and will put the result.
     * @param aKey
     * @param aGroup
     * @param aHelper
     * @return Either the result of doing the work, or the cached result.
     * @throws Exception
     *             If something goes wrong while doing the work, throw an
     *             exception.
     */
    private V run( final K aKey, final String aGroup, final JCSWorkerHelper<V> aHelper )
        throws Exception
    {
        V result = null;
        // long start = 0;
        // long dbTime = 0;
        final JCSWorkerHelper<V> helper = map.putIfAbsent(getRegion() + aKey, aHelper);

        if ( helper != null )
        {
            synchronized ( helper )
            {
                logger.debug( "Found a worker already doing this work ({0}:{1}).",
                        this::getRegion, () -> aKey );
                while ( !helper.isFinished() )
                {
                    try
                    {
                        helper.wait();
                    }
                    catch (final InterruptedException e)
                    {
                        // expected
                    }
                }
                logger.debug( "Another thread finished our work for us. Using "
                        + "those results instead. ({0}:{1}).",
                        this::getRegion, () -> aKey );
            }
        }
        // Do the work
        try
        {
            logger.debug( "{0} is doing the work.", this::getRegion);

            // Try to get the item from the cache
            if ( aGroup != null )
            {
                result = groupCache.getFromGroup( aKey, aGroup );
            }
            else
            {
                result = cache.get( aKey );
            }
            // If the cache doesn't have it, do the work.
            if ( result == null )
            {
                result = aHelper.doWork();
                logger.debug( "Work Done, caching: key:{0}, group:{1}, result:{2}.",
                        aKey, aGroup, result );
                // Stick the result of the work in the cache.
                if ( aGroup != null )
                {
                    groupCache.putInGroup( aKey, aGroup, result );
                }
                else
                {
                    cache.put( aKey, result );
                }
            }
            // return the result
            return result;
        }
        finally
        {
            logger.debug( "{0}:{1} entered finally.", this::getRegion,
                    () -> aKey );

            // Remove ourselves as the worker.
            if ( helper == null )
            {
                map.remove( getRegion() + aKey );
            }
            synchronized ( aHelper )
            {
                aHelper.setFinished( true );
                // Wake everyone waiting on us
                aHelper.notifyAll();
            }
        }
    }
}
