package org.apache.ojb.broker.cache;

/* Copyright  2004 The Apache Software Foundation
 *
 * Licensed 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.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.ojb.broker.Identity;
import org.apache.ojb.broker.PBStateEvent;
import org.apache.ojb.broker.PBStateListener;
import org.apache.ojb.broker.PersistenceBroker;
import org.apache.ojb.broker.cache.ObjectCache;
import org.apache.ojb.broker.util.logging.LoggerFactory;

/**
 * This global ObjectCache stores all Objects loaded by the
<code>PersistenceBroker</code>
 * from a DB using a static [EMAIL PROTECTED] java.util.Map}. This means each [EMAIL 
PROTECTED]
ObjectCache}
 * instance associated with all [EMAIL PROTECTED] PersistenceBroker} instances use the
same
 * <code>Map</code> to cache objects. This could lead in "dirty-reads"
(similar to read-uncommitted
 * mode in DB) when a concurrent thread look up same object modified by
another thread.
 * <br/>
 * When the PersistenceBroker tries to get an Object by its [EMAIL PROTECTED]
Identity}.
 * It first lookups the cache if the object has been already loaded and
cached.
 * <br/>
 * The cache uses soft-references which allows objects (softly) referenced
by
 * the cache to be reclaimed by the Java Garbage Collector when they are not
 * longer referenced elsewhere.
 *
 * <p>
 * Implementation configuration properties:
 * </p>
 *
 * <table cellspacing="2" cellpadding="2" border="3" frame="box">
 * <tr>
 *     <td><strong>Property Key</strong></td>
 *     <td><strong>Property Values</strong></td>
 * </tr>
 * <tr>
 *     <td>timeout</td>
 *     <td>
 *          Lifetime of the cached objects in seconds.
 *          If expired the cached object was not returned
 *          on lookup call (and removed from cache). Default timeout
 *          value is 900 seconds. When set to <tt>-1</tt> the lifetime of
 *          the cached object depends only on GC and do never get timed out.
 *          <p>
 *          NOTE: Objects internal cached via [EMAIL PROTECTED] SoftReference}, so
 *          lifetime of cached object is limited by the GC too.
 *          </p>
 *    </td>
 * </tr>
 * <tr>
 *     <td>autoSync</td>
 *     <td>
 *          If set <tt>true</tt> all cached/looked up objects within a
PB-transaction are traced.
 *          If the the PB-transaction was aborted all traced objects will be
removed from
 *          cache. Default is <tt>false</tt>.
 *          <p>
 *          NOTE: This does not prevent "dirty-reads" (more info see above).
 *          </p>
 *          <p>
 *          It's not a smart solution for keeping cache in sync with DB but
should do the job
 *          in most cases.
 *          <br/>
 *          E.g. if you lookup 1000 objects within a transaction and modify
one object and then abort the
 *          transaction, 1000 objects will be passed to cache, 1000 objects
will be traced and
 *          all 1000 objects will be removed from cache. If you read these
objects without tx or
 *          in a former tx and then modify one object in a tx and abort the
tx, only one object was
 *          traced/removed.
 *          </p>
 *    </td>
 * </tr>
 * </table>
 *
 * <br/>
 *
 * @author <a href="mailto:[EMAIL PROTECTED]">Thomas Mahler<a>
 * @version $Id: ObjectCacheDefaultImpl.java,v 1.21 2004/03/11 18:16:09
brianm Exp $
 */
public class ObjectCacheDefaultImpl implements ObjectCache, PBStateListener
{
    public static final String TIMEOUT_PROP = "timeout";
    public static final String AUTOSYNC_PROP = "autoSync";
    /**
     * static Map held all cached objects
     */
    protected static Map objectTable = new Hashtable();
    private static ReferenceQueue queue = new ReferenceQueue();

    private static long hitCount = 0;
    private static long failCount = 0;
    private static long gcCount = 0;

    protected PersistenceBroker broker;
    private List identitiesInWork;
    private boolean useAutoSync;
    /**
     * Timeout of the cached objects. Default was 900 seconds.
     */
    private long timeout = 1000 * 60 * 15;

    /**
     *
     */
    public ObjectCacheDefaultImpl(PersistenceBroker broker, Properties prop)
    {
        this.broker = broker;
        timeout = prop == null ? timeout :
(Long.parseLong(prop.getProperty(TIMEOUT_PROP, "" + (60 * 15))) * 1000);

        useAutoSync = prop == null ?
                false : (Boolean.valueOf((prop.getProperty(AUTOSYNC_PROP,
"false")).trim())).booleanValue();
        if (useAutoSync && broker != null)
        {
            // we add this instance as a permanent PBStateListener
            broker.addListener(this, true);
        }
        if (useAutoSync && broker == null)
        {
            LoggerFactory.getDefaultLogger().info("[" +
ObjectCacheDefaultImpl.class.getName() +
                    "] Can't enable " + AUTOSYNC_PROP + ", because given PB
instance is null");
        }
        identitiesInWork = new ArrayList();
    }

    /**
     * Clear ObjectCache. I.e. remove all entries for classes and objects.
     */
    public void clear()
    {
        processQueue();
        objectTable.clear();
        identitiesInWork.clear();
    }

    /**
     * Makes object persistent to the Objectcache.
     * I'm using soft-references to allow gc reclaim unused objects
     * even if they are still cached.
     */
    public void cache(Identity oid, Object obj)
    {
        processQueue();
        if ((obj != null))
        {
            traceIdentity(oid);
            objectTable.put(oid, new CacheEntry(obj, oid, queue));
        }
    }

    /**
     * Lookup object with Identity oid in objectTable.
     * Returns null if no matching id is found
     */
    public Object lookup(Identity oid)
    {
        processQueue();
        hitCount++;
        Object result = null;

        CacheEntry entry = (CacheEntry) objectTable.get(oid);
        if (entry != null)
        {
            result = entry.get();
            if (result == null || entry.lifetime <
System.currentTimeMillis())
            {
                /*
                cached object was removed by gc or lifetime was exhausted
                remove CacheEntry from map
                */
                gcCount++;
                remove(oid);
                // make sure that we return null
                result = null;
            }
            else
            {
                /*
                TODO: Not sure if this makes sense, could help to avoid
corrupted objects
                when changed in tx but not stored.
                */
                traceIdentity(oid);
            }
        }
        else
        {
            failCount++;
        }
        return result;
    }

    /**
     * Removes an Object from the cache.
     */
    public void remove(Identity oid)
    {
        processQueue();
        if (oid != null)
        {
            removeTracedIdentity(oid);
            objectTable.remove(oid);
        }
    }

    public String toString()
    {
        ToStringBuilder buf = new ToStringBuilder(this,
ToStringStyle.DEFAULT_STYLE);
        buf.append("Count of cached objects", objectTable.keySet().size());
        buf.append("Lookup hits", hitCount);
        buf.append("Failures", failCount);
        buf.append("Reclaimed", gcCount);
        return buf.toString();
    }

    protected void finalize()
    {
        LoggerFactory.getDefaultLogger().debug("Finalize ObjectCache: " +
this.toString());
    }

    private void traceIdentity(Identity oid)
    {
        if (useAutoSync && (broker != null) && broker.isInTransaction())
        {
            identitiesInWork.add(oid);
        }
    }

    private void removeTracedIdentity(Identity oid)
    {
        identitiesInWork.remove(oid);
    }

    private void synchronizeWithTracedObjects()
    {
        Identity oid;
        LoggerFactory.getDefaultLogger().info("[" +
this.getClass().getName() + "] tx was aborted," +
                " remove " + identitiesInWork.size() + " traced (potentially
modified) objects from cache");
        for (Iterator iterator = identitiesInWork.iterator();
iterator.hasNext();)
        {
            oid = (Identity) iterator.next();
            objectTable.remove(oid);
        }
    }

    public void beforeRollback(PBStateEvent event)
    {
        synchronizeWithTracedObjects();
    }

    public void beforeCommit(PBStateEvent event)
    {
        identitiesInWork.clear();
    }

    public void beforeClose(PBStateEvent event)
    {
        identitiesInWork.clear();
    }

    public void afterRollback(PBStateEvent event)
    {
    }

    public void afterCommit(PBStateEvent event)
    {
    }

    public void afterBegin(PBStateEvent event)
    {
    }

    public void beforeBegin(PBStateEvent event)
    {
    }

    public void afterOpen(PBStateEvent event)
    {
    }

    private void processQueue() {
        CacheEntry sv;
        while ((sv = (CacheEntry) queue.poll()) != null) {
            removeTracedIdentity(sv.oid);
            objectTable.remove(sv.oid); // we can access private data!
        }
    }

    //-----------------------------------------------------------
    // inner class
    //-----------------------------------------------------------
    class CacheEntry extends SoftReference
    {
        private final long lifetime;
        private final Identity oid;

        public CacheEntry(Object object, final Identity k, final
ReferenceQueue q)
        {
                super(object, q);
                oid = k;
            // if timeout is negative, lifetime of object never expire
            if (timeout < 0)
            {
                lifetime = Long.MAX_VALUE;
            }
            else
            {
                lifetime = System.currentTimeMillis() + timeout;
            }
        }
    }
}


---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to