package org.apache.torque.manager;

/*
 * 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.ObjectInputStream;
import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.collections.FastArrayList;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.jcs.JCS;
import org.apache.jcs.access.GroupCacheAccess;
import org.apache.jcs.access.exception.CacheException;
import org.apache.torque.Torque;
import org.apache.torque.TorqueException;
import org.apache.torque.om.ObjectKey;
import org.apache.torque.om.Persistent;

/**
 * This class contains common functionality of a Manager for
 * instantiating OM's.
 *
 * @param <T> the class of the database object managed by this class.
 * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
 * @version $Id: AbstractBaseManager.java 1379317 2012-08-31 06:56:48Z tfischer $
 */
public abstract class AbstractBaseManager<T extends Persistent>
    implements Serializable
{
    /** serial version */
    private static final long serialVersionUID = 509798299473481305L;

    /** the log */
    protected static final Log log = LogFactory.getLog(AbstractBaseManager.class);

    /** used to cache the om objects. cache is set by the region property */
    protected transient GroupCacheAccess cache;

    /** method results cache */
    protected MethodResultCache mrCache;

    /** The OM class that the service will instantiate. */
    private Class<T> omClass;

    /** The name of the OM class that the service will instantiate. */
    private String className;

    /** The cache region used for JCS. */
    private String region;

    /** Whether the cache manager has already registered its cache Listeners. */
    private boolean isNew = true;

    /** The fields which are valid fields of interest for a listener. */
    protected Map<String, ?> validFields;

    /** The listeners for this manager. */
    protected Map<String, FastArrayList> listenersMap
        = new HashMap<String, FastArrayList>();

    /**
     * Get the Class instance
     *
     * @return the om class
     */
    protected Class<T> getOMClass()
    {
        return omClass;
    }

    /**
     * Set the Class that will be instantiated by this manager
     *
     * @param omClass the om class
     */
    protected void setOMClass(Class<T> omClass)
    {
        this.omClass = omClass;
    }

    /**
     * Get a fresh instance of an om
     *
     * @return an instance of the om class
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    protected T getOMInstance()
        throws TorqueException
    {
        try
        {
            return omClass.newInstance();
        }
        catch (InstantiationException e)
        {
            throw new TorqueException("Could not instantiate " + getClassName(), e);
        }
        catch (IllegalAccessException e)
        {
            throw new TorqueException("Could not access " + getClassName(), e);
        }
    }

    /**
     * Get the classname to instantiate for getInstance()
     *
     * @return value of className.
     */
    public String getClassName()
    {
        return className;
    }

    /**
     * Set the classname to instantiate for getInstance()
     * @param v  Value to assign to className.
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    @SuppressWarnings("unchecked")
    public void setClassName(String v)
        throws TorqueException
    {
        this.className = v;

        try
        {
            Class<?> clazz = Class.forName(getClassName());
            if (Persistent.class.isAssignableFrom(clazz))
            {
                setOMClass((Class<T>) clazz);
            }
            else
            {
                throw new TorqueException(
                        getClassName()
                        + " does not implement the interface Persistent");
            }
        }
        catch (ClassNotFoundException cnfe)
        {
            throw new TorqueException("Could not load " + getClassName(), cnfe);
        }
    }


    /**
     * Return an instance of an om based on the id
     *
     * @param id the primary key of the object
     * @return the object from persistent storage or from cache
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    protected T getOMInstance(ObjectKey id)
        throws TorqueException
    {
        return getOMInstance(id, true);
    }

    /**
     * Return an instance of an om based on the id
     *
     * @param key the primary key of the object
     * @param fromCache true if the object should be retrieved from cache
     * @return the object from persistent storage or from cache
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    protected T getOMInstance(ObjectKey key, boolean fromCache)
        throws TorqueException
    {
        T om = null;
        if (fromCache)
        {
            om = cacheGet(key);
        }

        if (om == null)
        {
            om = retrieveStoredOM(key);
            if (fromCache)
            {
                putInstanceImpl(om);
            }
        }

        return om;
    }

    /**
     * Get an object from cache
     *
     * @param key the primary key of the object
     * @return the object from cache
     */
    @SuppressWarnings("unchecked")
    protected T cacheGet(Serializable key)
    {
        T om = null;
        if (cache != null)
        {
            synchronized (this)
            {
                om = (T) cache.get(key);
            }
        }
        return om;
    }

    /**
     * Clears the cache
     *
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    protected void clearImpl()
        throws TorqueException
    {
        if (cache != null)
        {
            try
            {
                cache.clear();
            }
            catch (CacheException ce)
            {
                throw new TorqueException(
                        "Could not clear cache due to internal JCS error.", ce);
            }
        }
    }

    /**
     * Disposes of the cache. This triggers a shutdown of the connected cache
     * instances. This method should only be used during shutdown of Torque. The
     * manager instance will not cache anymore after this call.
     */
    public void dispose()
    {
        if (cache != null)
        {
            cache.dispose();
            cache = null;
        }
    }

    /**
     * Remove an object from the cache
     *
     * @param key the cache key for the object
     * @return the object one last time
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    @SuppressWarnings("unchecked")
    protected T removeInstanceImpl(Serializable key)
        throws TorqueException
    {
        T oldOm = null;
        if (cache != null)
        {
            try
            {
                synchronized (this)
                {
                    oldOm = (T) cache.get(key);
                    cache.remove(key);
                }
            }
            catch (CacheException ce)
            {
                throw new TorqueException
                    ("Could not remove from cache due to internal JCS error",
                     ce);
            }
        }
        return oldOm;
    }

    /**
     * Put an object into the cache
     *
     * @param om the object
     * @return if an object with the same key already is in the cache
     *         this object will be returned, else null
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    protected T putInstanceImpl(T om)
        throws TorqueException
    {
        ObjectKey key = om.getPrimaryKey();
        return putInstanceImpl(key, om);
    }

    /**
     * Put an object into the cache
     *
     * @param key the cache key for the object
     * @param om the object
     * @return if an object with this key already is in the cache
     *         this object will be returned, else null
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    @SuppressWarnings("unchecked")
    protected T putInstanceImpl(Serializable key, T om)
        throws TorqueException
    {
        if (getOMClass() != null && !getOMClass().isInstance(om))
        {
            throw new TorqueException(om + "; class=" + om.getClass().getName()
                + "; id=" + om.getPrimaryKey() + " cannot be cached with "
                + getOMClass().getName() + " objects");
        }

        T oldOm = null;
        if (cache != null)
        {
            try
            {
                synchronized (this)
                {
                    oldOm = (T) cache.get(key);
                    cache.put(key, om);
                }
            }
            catch (CacheException ce)
            {
                throw new TorqueException
                    ("Could not cache due to internal JCS error", ce);
            }
        }
        return oldOm;
    }

    /**
     * Retrieve an object from persistent storage
     *
     * @param id the primary key of the object
     * @return the object
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    protected abstract T retrieveStoredOM(ObjectKey id)
        throws TorqueException;

    /**
     * Gets a list of om's based on id's.
     *
     * @param ids a <code>ObjectKey[]</code> value
     * @return a <code>List</code> value
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    protected List<T> getOMs(ObjectKey[] ids)
        throws TorqueException
    {
        return getOMs(Arrays.asList(ids));
    }

    /**
     * Gets a list of om's based on id's.
     *
     * @param ids a <code>List</code> of <code>ObjectKey</code>'s
     * @return a <code>List</code> value
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    protected List<T> getOMs(List<? extends ObjectKey>  ids)
        throws TorqueException
    {
        return getOMs(ids, true);
    }

    /**
     * Gets a list of om's based on id's.
     *
     * @param ids a <code>List</code> of <code>ObjectKey</code>'s
     * @return a <code>List</code> value
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    protected List<T> getOMs(List<? extends ObjectKey> ids, boolean fromCache)
        throws TorqueException
    {
        List<T> oms = null;
        if (ids != null && ids.size() > 0)
        {
            // start a new list where we will replace the id's with om's
            Map<ObjectKey, T> omsMap = new HashMap<ObjectKey, T>();
            List<ObjectKey> newIds = new ArrayList<ObjectKey>(ids.size());
            for (ObjectKey key : ids)
            {
                T om = null;
                if (fromCache)
                {
                    om = cacheGet(key);
                }
                if (om == null)
                {
                    newIds.add(key);
                }
                else
                {
                    omsMap.put(key, om);
                }
            }

            if (newIds.size() > 0)
            {
                List<T> newOms = retrieveStoredOMs(newIds);
                for (T om : newOms)
                {
                    omsMap.put(om.getPrimaryKey(), om);
                    if (fromCache)
                    {
                        putInstanceImpl(om);
                    }
                }
            }

            oms = new ArrayList<T>(ids.size());
            for (ObjectKey key : ids)
            {
                oms.add(omsMap.get(key));
            }
        }
        return oms;
    }

    /**
     * Gets a list of om's based on id's.
     * This method must be implemented in the derived class
     *
     * @param ids a <code>List</code> of <code>ObjectKey</code>'s
     * @return a <code>List</code> value
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    protected abstract List<T> retrieveStoredOMs(List<? extends ObjectKey> ids)
        throws TorqueException;

    /**
     * Get the cache region used for JCS.
     *
     * @return the cache region used for JCS.
     */
    public String getRegion()
    {
        return region;
    }

    /**
     * Set the cache region used for JCS.
     *
     * @param v  Value to assign to region.
     * @throws TorqueException Any exceptions caught during processing will be
     *         rethrown wrapped into a TorqueException.
     */
    public void setRegion(String v)
        throws TorqueException
    {
        this.region = v;
        try
        {
            if (Torque.getConfiguration().getBoolean(Torque.CACHE_KEY, false))
            {
                cache = JCS.getInstance(getRegion());
                mrCache = new MethodResultCache(cache);
            }
            else
            {
                mrCache = new NoOpMethodResultCache(cache);
            }
        }
        catch (CacheException e)
        {
            throw new TorqueException("Cache could not be initialized", e);
        }

        if (cache == null)
        {
            log.info("Cache could not be initialized for region: " + v);
        }
    }

    /**
     * @return The cache instance.
     */
    public synchronized MethodResultCache getMethodResultCache()
    {
        if (isNew)
        {
            registerAsListener();
            isNew = false;
        }
        return mrCache;
    }

    /**
     * NoOp version.  Managers should override this method to notify other
     * managers that they are interested in CacheEvents.
     */
    protected void registerAsListener()
    {
        // empty
    }

    /**
     *
     * @param listener A new listener for cache events.
     */
    public void addCacheListenerImpl(CacheListener<? extends Persistent> listener)
    {
        List<String> keys = listener.getInterestedFields();
        for (String key : keys)
        {
            // Peer.column names are the fields
            if (validFields != null && validFields.containsKey(key))
            {
                FastArrayList listeners = listenersMap.get(key);
                if (listeners == null)
                {
                    listeners = createSubsetList(key);
                }

                boolean isListenerNew = true;
                Iterator<?> j = listeners.iterator();
                while (j.hasNext())
                {
                    Object listener2 =
                        ((WeakReference<?>) j.next()).get();
//                    if (listener2 == null)
//                    {
//                        // do a little cleanup while checking for dupes
//                        // not thread-safe, not likely to be many nulls
//                        // but should revisit
//                        //j.remove();
//                    }
//                    else
                    if (listener2 == listener)
                    {
                        isListenerNew = false;
                        break;
                    }
                }
                if (isListenerNew)
                {
                    listeners.add(new WeakReference<CacheListener<? extends Persistent>>(listener));
                }
            }
        }
    }

    /**
     *
     * @param key
     * @return A subset of the list identified by <code>key</code>.
     */
    private synchronized FastArrayList createSubsetList(String key)
    {
        FastArrayList list = null;
        if (listenersMap.containsKey(key))
        {
            list = listenersMap.get(key);
        }
        else
        {
            list = new FastArrayList();
            list.setFast(true);
            listenersMap.put(key, list);
        }
        return list;
    }

    /**
     *
     * @param listeners
     * @param oldOm
     * @param om
     */
    protected <TT extends Persistent> void notifyListeners(List<WeakReference<CacheListener<TT>>> listeners,
                                   TT oldOm, TT om)
    {
        if (listeners != null)
        {
            synchronized (listeners)
            {
                Iterator<WeakReference<CacheListener<TT>>> i = listeners.iterator();
                while (i.hasNext())
                {
                    CacheListener<TT> listener = i.next().get();
                    if (listener == null)
                    {
                        // remove reference as its object was cleared
                        i.remove();
                    }
                    else
                    {
                        if (oldOm == null)
                        {
                            // object was added
                            listener.addedObject(om);
                        }
                        else
                        {
                            // object was refreshed
                            listener.refreshedObject(om);
                        }
                    }
                }
            }
        }
    }


    /**
     * helper methods for the Serializable interface
     *
     * @param out
     * @throws IOException
     */
    private void writeObject(java.io.ObjectOutputStream out)
        throws IOException
    {
        out.defaultWriteObject();
    }

    /**
     * Helper methods for the <code>Serializable</code> interface.
     *
     * @param in The stream to read a <code>Serializable</code> from.
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private void readObject(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        in.defaultReadObject();
        // initialize the cache
        try
        {
            if (region != null)
            {
                setRegion(region);
            }
        }
        catch (Exception e)
        {
            log.error("Cache could not be initialized for region '"
                      + region + "' after deserialization");
        }
    }
}
