001    package org.apache.ojb.broker.cache;
002    
003    /* Copyright 2004-2005 The Apache Software Foundation
004     *
005     * Licensed under the Apache License, Version 2.0 (the "License");
006     * you may not use this file except in compliance with the License.
007     * You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    import java.lang.ref.ReferenceQueue;
019    import java.lang.ref.SoftReference;
020    import java.util.ArrayList;
021    import java.util.Hashtable;
022    import java.util.Iterator;
023    import java.util.List;
024    import java.util.Map;
025    import java.util.Properties;
026    
027    import org.apache.commons.lang.builder.ToStringBuilder;
028    import org.apache.commons.lang.builder.ToStringStyle;
029    import org.apache.ojb.broker.Identity;
030    import org.apache.ojb.broker.OJBRuntimeException;
031    import org.apache.ojb.broker.PBStateEvent;
032    import org.apache.ojb.broker.PBStateListener;
033    import org.apache.ojb.broker.PersistenceBroker;
034    import org.apache.ojb.broker.util.logging.Logger;
035    import org.apache.ojb.broker.util.logging.LoggerFactory;
036    
037    /**
038     * This global ObjectCache stores all Objects loaded by the <code>PersistenceBroker</code>
039     * from a DB using a static {@link java.util.Map}. This means each {@link ObjectCache}
040     * instance associated with all {@link PersistenceBroker} instances use the same
041     * <code>Map</code> to cache objects. This could lead in "dirty-reads" (similar to read-uncommitted
042     * mode in DB) when a concurrent thread look up same object modified by another thread.
043     * <br/>
044     * When the PersistenceBroker tries to get an Object by its {@link Identity}.
045     * It first lookups the cache if the object has been already loaded and cached.
046     * <p/>
047     * NOTE: By default objects cached via {@link SoftReference} which allows
048     * objects (softly) referenced by the cache to be reclaimed by the Java Garbage Collector when
049     * they are not longer referenced elsewhere, so lifetime of cached object is limited by
050     * <br/> - the lifetime of the cache object - see property <code>timeout</code>.
051     * <br/> - the garabage collector used memory settings - see property <code>useSoftReferences</code>.
052     * <br/> - the maximum capacity of the cache - see property <code>maxEntry</code>.
053     * </p>
054     * <p/>
055     * Implementation configuration properties:
056     * </p>
057     * <p/>
058     * <p/>
059     * <table cellspacing="2" cellpadding="2" border="3" frame="box">
060     * <tr>
061     * <td><strong>Property Key</strong></td>
062     * <td><strong>Property Values</strong></td>
063     * </tr>
064     * <p/>
065     * <tr>
066     * <td>timeout</td>
067     * <td>
068     * Lifetime of the cached objects in seconds.
069     * If expired the cached object was not returned
070     * on lookup call (and removed from cache). Default timeout
071     * value is 900 seconds. When set to <tt>-1</tt> the lifetime of
072     * the cached object depends only on GC and do never get timed out.
073     * </td>
074     * </tr>
075     * <p/>
076     * <tr>
077     * <td>autoSync</td>
078     * <td>
079     * If set <tt>true</tt> all cached/looked up objects within a PB-transaction are traced.
080     * If the the PB-transaction was aborted all traced objects will be removed from
081     * cache. Default is <tt>false</tt>.
082     * <p/>
083     * NOTE: This does not prevent "dirty-reads" (more info see above).
084     * </p>
085     * <p/>
086     * It's not a smart solution for keeping cache in sync with DB but should do the job
087     * in most cases.
088     * <br/>
089     * E.g. if you lookup 1000 objects within a transaction and modify one object and then abort the
090     * transaction, 1000 objects will be passed to cache, 1000 objects will be traced and
091     * all 1000 objects will be removed from cache. If you read these objects without tx or
092     * in a former tx and then modify one object in a tx and abort the tx, only one object was
093     * traced/removed.
094     * </p>
095     * </td>
096     * </tr>
097     * <p/>
098     * <tr>
099     * <td>cachingKeyType</td>
100     * <td>
101     * Determines how the key was build for the cached objects:
102     * <br/>
103     * 0 - Identity object was used as key, this was the <em>default</em> setting.
104     * <br/>
105     * 1 - Idenity + jcdAlias name was used as key. Useful when the same object metadata model
106     * (DescriptorRepository instance) are used for different databases (JdbcConnectionDescriptor)
107     * <br/>
108     * 2 - Identity + model (DescriptorRepository) was used as key. Useful when different metadata
109     * model (DescriptorRepository instance) are used for the same database. Keep in mind that there
110     * was no synchronization between cached objects with same Identity but different metadata model.
111     * <br/>
112     * 3 - all together (1+2)
113     * </td>
114     * </tr>
115     * <p/>
116     * <tr>
117     * <td>useSoftReferences</td>
118     * <td>
119     * If set <em>true</em> this class use {@link java.lang.ref.SoftReference} to cache
120     * objects. Default value is <em>true</em>.
121     * </td>
122     * </tr>
123     * </table>
124     * <p/>
125     *
126     * @author <a href="mailto:thma@apache.org">Thomas Mahler<a>
127     * @version $Id: ObjectCacheDefaultImpl.java,v 1.1 2007-08-24 22:17:29 ewestfal Exp $
128     */
129    public class ObjectCacheDefaultImpl implements ObjectCacheInternal, PBStateListener
130    {
131        private Logger log = LoggerFactory.getLogger(ObjectCacheDefaultImpl.class);
132    
133        public static final String TIMEOUT_PROP = "timeout";
134        public static final String AUTOSYNC_PROP = "autoSync";
135        public static final String CACHING_KEY_TYPE_PROP = "cachingKeyType";
136        public static final String SOFT_REFERENCES_PROP = "useSoftReferences";
137        /**
138         * static Map held all cached objects
139         */
140        protected static final Map objectTable = new Hashtable();
141        private static final ReferenceQueue queue = new ReferenceQueue();
142    
143        private static long hitCount = 0;
144        private static long failCount = 0;
145        private static long gcCount = 0;
146    
147        protected PersistenceBroker broker;
148        private List identitiesInWork;
149        /**
150         * Timeout of the cached objects. Default was 900 seconds.
151         */
152        private long timeout = 1000 * 60 * 15;
153        private boolean useAutoSync = false;
154        /**
155         * Determines how the key was build for the cached objects:
156         * <br/>
157         * 0 - Identity object was used as key
158         * 1 - Idenity + jcdAlias name was used as key
159         * 2 - Identity + model (DescriptorRepository) was used as key
160         * 3 - all together (1+2)
161         */
162        private int cachingKeyType;
163        private boolean useSoftReferences = true;
164    
165        public ObjectCacheDefaultImpl(PersistenceBroker broker, Properties prop)
166        {
167            this.broker = broker;
168            timeout = prop == null ? timeout : (Long.parseLong(prop.getProperty(TIMEOUT_PROP, "" + (60 * 15))) * 1000);
169            useSoftReferences = prop != null && (Boolean.valueOf((prop.getProperty(SOFT_REFERENCES_PROP, "true")).trim())).booleanValue();
170            cachingKeyType = prop == null ? 0 : (Integer.parseInt(prop.getProperty(CACHING_KEY_TYPE_PROP, "0")));
171            useAutoSync = prop != null && (Boolean.valueOf((prop.getProperty(AUTOSYNC_PROP, "false")).trim())).booleanValue();
172            if(useAutoSync)
173            {
174                if(broker != null)
175                {
176                    // we add this instance as a permanent PBStateListener
177                    broker.addListener(this, true);
178                }
179                else
180                {
181                    log.info("Can't enable property '" + AUTOSYNC_PROP + "', because given PB instance is null");
182                }
183            }
184            identitiesInWork = new ArrayList();
185            if(log.isEnabledFor(Logger.INFO))
186            {
187                ToStringBuilder buf = new ToStringBuilder(this);
188                buf.append("timeout", timeout)
189                        .append("useSoftReferences", useSoftReferences)
190                        .append("cachingKeyType", cachingKeyType)
191                        .append("useAutoSync", useAutoSync);
192                log.info("Setup cache: " + buf.toString());
193            }
194        }
195    
196        /**
197         * Clear ObjectCache. I.e. remove all entries for classes and objects.
198         */
199        public void clear()
200        {
201            //processQueue();
202            objectTable.clear();
203            identitiesInWork.clear();
204        }
205    
206        public void doInternalCache(Identity oid, Object obj, int type)
207        {
208            //processQueue();
209            if((obj != null))
210            {
211                traceIdentity(oid);
212                synchronized(objectTable)
213                {
214                    if(log.isDebugEnabled()) log.debug("Cache object " + oid);
215                    objectTable.put(buildKey(oid), buildEntry(obj, oid));
216                }
217            }
218        }
219    
220        /**
221         * Makes object persistent to the Objectcache.
222         * I'm using soft-references to allow gc reclaim unused objects
223         * even if they are still cached.
224         */
225        public void cache(Identity oid, Object obj)
226        {
227            doInternalCache(oid, obj, ObjectCacheInternal.TYPE_UNKNOWN);
228        }
229    
230        public boolean cacheIfNew(Identity oid, Object obj)
231        {
232            //processQueue();
233            boolean result = false;
234            Object key = buildKey(oid);
235            if((obj != null))
236            {
237                synchronized(objectTable)
238                {
239                    if(!objectTable.containsKey(key))
240                    {
241                        objectTable.put(key, buildEntry(obj, oid));
242                        result = true;
243                    }
244                }
245                if(result) traceIdentity(oid);
246            }
247            return result;
248        }
249    
250        /**
251         * Lookup object with Identity oid in objectTable.
252         * Returns null if no matching id is found
253         */
254        public Object lookup(Identity oid)
255        {
256            processQueue();
257            hitCount++;
258            Object result = null;
259    
260            CacheEntry entry = (CacheEntry) objectTable.get(buildKey(oid));
261            if(entry != null)
262            {
263                result = entry.get();
264                if(result == null || entry.getLifetime() < System.currentTimeMillis())
265                {
266                    /*
267                    cached object was removed by gc or lifetime was exhausted
268                    remove CacheEntry from map
269                    */
270                    gcCount++;
271                    remove(oid);
272                    // make sure that we return null
273                    result = null;
274                }
275                else
276                {
277                    /*
278                    TODO: Not sure if this makes sense, could help to avoid corrupted objects
279                    when changed in tx but not stored.
280                    */
281                    traceIdentity(oid);
282                    if(log.isDebugEnabled()) log.debug("Object match " + oid);
283                }
284            }
285            else
286            {
287                failCount++;
288            }
289            return result;
290        }
291    
292        /**
293         * Removes an Object from the cache.
294         */
295        public void remove(Identity oid)
296        {
297            //processQueue();
298            if(oid != null)
299            {
300                removeTracedIdentity(oid);
301                objectTable.remove(buildKey(oid));
302                if(log.isDebugEnabled()) log.debug("Remove object " + oid);
303            }
304        }
305    
306        public String toString()
307        {
308            ToStringBuilder buf = new ToStringBuilder(this, ToStringStyle.DEFAULT_STYLE);
309            buf.append("Count of cached objects", objectTable.keySet().size());
310            buf.append("Lookup hits", hitCount);
311            buf.append("Failures", failCount);
312            buf.append("Reclaimed", gcCount);
313            return buf.toString();
314        }
315    
316        private void traceIdentity(Identity oid)
317        {
318            if(useAutoSync && (broker != null) && broker.isInTransaction())
319            {
320                identitiesInWork.add(oid);
321            }
322        }
323    
324        private void removeTracedIdentity(Identity oid)
325        {
326            identitiesInWork.remove(oid);
327        }
328    
329        private void synchronizeWithTracedObjects()
330        {
331            Identity oid;
332            log.info("tx was aborted," +
333                    " remove " + identitiesInWork.size() + " traced (potentially modified) objects from cache");
334            for(Iterator iterator = identitiesInWork.iterator(); iterator.hasNext();)
335            {
336                oid = (Identity) iterator.next();
337                objectTable.remove(buildKey(oid));
338            }
339        }
340    
341        public void beforeRollback(PBStateEvent event)
342        {
343            synchronizeWithTracedObjects();
344            identitiesInWork.clear();
345        }
346    
347        public void beforeCommit(PBStateEvent event)
348        {
349            // identitiesInWork.clear();
350        }
351    
352        public void beforeClose(PBStateEvent event)
353        {
354            /*
355            arminw: In managed environments listener method "beforeClose" is called twice
356            (when the PB handle is closed and when the real PB instance is closed/returned to pool).
357            We are only interested in the real close call when all work is done.
358            */
359            if(!broker.isInTransaction())
360            {
361                identitiesInWork.clear();
362            }
363        }
364    
365        public void afterRollback(PBStateEvent event)
366        {
367        }
368    
369        public void afterCommit(PBStateEvent event)
370        {
371            identitiesInWork.clear();
372        }
373    
374        public void afterBegin(PBStateEvent event)
375        {
376        }
377    
378        public void beforeBegin(PBStateEvent event)
379        {
380        }
381    
382        public void afterOpen(PBStateEvent event)
383        {
384        }
385    
386        private CacheEntry buildEntry(Object obj, Identity oid)
387        {
388            if(useSoftReferences)
389            {
390                return new CacheEntrySoft(obj, oid, queue, timeout);
391            }
392            else
393            {
394                return new CacheEntryHard(obj, oid, timeout);
395            }
396        }
397    
398        private void processQueue()
399        {
400            CacheEntry sv;
401            while((sv = (CacheEntry) queue.poll()) != null)
402            {
403                removeTracedIdentity(sv.getOid());
404                objectTable.remove(buildKey(sv.getOid()));
405            }
406        }
407    
408        private Object buildKey(Identity oid)
409        {
410            Object key;
411            switch(cachingKeyType)
412            {
413                case 0:
414                    key = oid;
415                    break;
416                case 1:
417                    key = new OrderedTuple(oid, broker.getPBKey().getAlias());
418                    break;
419                case 2:
420                    /*
421                    this ObjectCache implementation only works in single JVM, so the hashCode
422                    of the DescriptorRepository class is unique
423                    TODO: problem when different versions of same DR are used
424                    */
425                    key = new OrderedTuple(oid,
426                            new Integer(broker.getDescriptorRepository().hashCode()));
427                    break;
428                case 3:
429                    key = new OrderedTuple(oid, broker.getPBKey().getAlias(),
430                            new Integer(broker.getDescriptorRepository().hashCode()));
431                    break;
432                default:
433                    throw new OJBRuntimeException("Unexpected error, 'cacheType =" + cachingKeyType + "' was not supported");
434            }
435            return key;
436        }
437    
438    
439        //-----------------------------------------------------------
440        // inner class to build unique key for cached objects
441        //-----------------------------------------------------------
442        /**
443         * Implements equals() and hashCode() for an ordered tuple of constant(!)
444         * objects
445         *
446         * @author Gerhard Grosse
447         * @since Oct 12, 2004
448         */
449        static final class OrderedTuple
450        {
451            private static int[] multipliers =
452                    new int[]{13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 51};
453    
454            private Object[] elements;
455            private int hashCode;
456    
457            public OrderedTuple(Object element)
458            {
459                elements = new Object[1];
460                elements[0] = element;
461                hashCode = calcHashCode();
462            }
463    
464            public OrderedTuple(Object element1, Object element2)
465            {
466                elements = new Object[2];
467                elements[0] = element1;
468                elements[1] = element2;
469                hashCode = calcHashCode();
470            }
471    
472            public OrderedTuple(Object element1, Object element2, Object element3)
473            {
474                elements = new Object[3];
475                elements[0] = element1;
476                elements[1] = element2;
477                elements[2] = element3;
478                hashCode = calcHashCode();
479            }
480    
481            public OrderedTuple(Object[] elements)
482            {
483                this.elements = elements;
484                this.hashCode = calcHashCode();
485            }
486    
487            private int calcHashCode()
488            {
489                int code = 7;
490                for(int i = 0; i < elements.length; i++)
491                {
492                    int m = i % multipliers.length;
493                    code += elements[i].hashCode() * multipliers[m];
494                }
495                return code;
496            }
497    
498            public boolean equals(Object obj)
499            {
500                if(!(obj instanceof OrderedTuple))
501                {
502                    return false;
503                }
504                else
505                {
506                    OrderedTuple other = (OrderedTuple) obj;
507                    if(this.hashCode != other.hashCode)
508                    {
509                        return false;
510                    }
511                    else if(this.elements.length != other.elements.length)
512                    {
513                        return false;
514                    }
515                    else
516                    {
517                        for(int i = 0; i < elements.length; i++)
518                        {
519                            if(!this.elements[i].equals(other.elements[i]))
520                            {
521                                return false;
522                            }
523                        }
524                        return true;
525                    }
526                }
527            }
528    
529            public int hashCode()
530            {
531                return hashCode;
532            }
533    
534            public String toString()
535            {
536                StringBuffer s = new StringBuffer();
537                s.append('{');
538                for(int i = 0; i < elements.length; i++)
539                {
540                    s.append(elements[i]).append('#').append(elements[i].hashCode()).append(',');
541                }
542                s.setCharAt(s.length() - 1, '}');
543                s.append("#").append(hashCode);
544                return s.toString();
545            }
546        }
547    
548        //-----------------------------------------------------------
549        // inner classes to wrap cached objects
550        //-----------------------------------------------------------
551        interface CacheEntry
552        {
553            Object get();
554            Identity getOid();
555            long getLifetime();
556        }
557    
558        final static class CacheEntrySoft extends SoftReference implements CacheEntry
559        {
560            private final long lifetime;
561            private final Identity oid;
562    
563            CacheEntrySoft(Object object, final Identity k, final ReferenceQueue q, long timeout)
564            {
565                super(object, q);
566                oid = k;
567                // if timeout is negative, lifetime of object never expire
568                if(timeout < 0)
569                {
570                    lifetime = Long.MAX_VALUE;
571                }
572                else
573                {
574                    lifetime = System.currentTimeMillis() + timeout;
575                }
576            }
577    
578            public Identity getOid()
579            {
580                return oid;
581            }
582    
583            public long getLifetime()
584            {
585                return lifetime;
586            }
587        }
588    
589        final static class CacheEntryHard implements CacheEntry
590        {
591            private final long lifetime;
592            private final Identity oid;
593            private Object obj;
594    
595            CacheEntryHard(Object object, final Identity k, long timeout)
596            {
597                obj = object;
598                oid = k;
599                // if timeout is negative, lifetime of object never expire
600                if(timeout < 0)
601                {
602                    lifetime = Long.MAX_VALUE;
603                }
604                else
605                {
606                    lifetime = System.currentTimeMillis() + timeout;
607                }
608            }
609    
610            public Object get()
611            {
612                return obj;
613            }
614    
615            public Identity getOid()
616            {
617                return oid;
618            }
619    
620            public long getLifetime()
621            {
622                return lifetime;
623            }
624        }
625    }