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.io.Serializable;
019    import java.lang.ref.ReferenceQueue;
020    import java.lang.ref.SoftReference;
021    import java.util.HashMap;
022    import java.util.Iterator;
023    import java.util.Properties;
024    
025    import org.apache.ojb.broker.Identity;
026    import org.apache.ojb.broker.PBStateEvent;
027    import org.apache.ojb.broker.PBStateListener;
028    import org.apache.ojb.broker.PersistenceBroker;
029    import org.apache.ojb.broker.core.DelegatingPersistenceBroker;
030    import org.apache.ojb.broker.core.PersistenceBrokerImpl;
031    import org.apache.ojb.broker.core.proxy.ProxyHelper;
032    import org.apache.ojb.broker.metadata.ClassDescriptor;
033    import org.apache.ojb.broker.metadata.FieldDescriptor;
034    import org.apache.ojb.broker.metadata.MetadataException;
035    import org.apache.ojb.broker.util.ClassHelper;
036    import org.apache.ojb.broker.util.logging.Logger;
037    import org.apache.ojb.broker.util.logging.LoggerFactory;
038    import org.apache.commons.lang.builder.ToStringBuilder;
039    
040    /**
041     * A two-level {@link ObjectCache} implementation with a session- and an application cache. The application
042     * cache could be specified by the property <code>applicationCache</code>.
043     * <p/>
044     * The first level is a transactional session
045     * cache which cache objects till {@link org.apache.ojb.broker.PersistenceBroker#close()} or if
046     * a PB-tx is running till {@link org.apache.ojb.broker.PersistenceBroker#abortTransaction()} or
047     * {@link org.apache.ojb.broker.PersistenceBroker#commitTransaction()}. On commit all objects written to
048     * database will be pushed to the application cache.
049     * </p>
050     * <p/>
051     * The session cache act as a temporary storage for all read/store operations of persistent objects
052     * and only on commit or close of the used PB instance the buffered objects of type
053     * {@link #TYPE_WRITE} will be written to the application cache. Except objects of type
054     * {@link #TYPE_NEW_MATERIALIZED} these objects will be immediatly pushed to application cache.
055     * </p>
056     * <p/>
057     * <p/>
058     * </p>
059     * <p/>
060     * The application cache
061     * </p>
062     * <p/>
063     * <table cellspacing="2" cellpadding="2" border="3" frame="box">
064     * <tr>
065     * <td><strong>Property Key</strong></td>
066     * <td><strong>Property Values</strong></td>
067     * </tr>
068     * <p/>
069     * <tr>
070     * <td>applicationCache</td>
071     * <td>
072     * Specifies the {@link ObjectCache} implementation used as application cache (second level cache).
073     * By default {@link ObjectCacheDefaultImpl} was used. It's recommended to use a shared cache implementation
074     * (all used PB instances should access the same pool of objects - e.g. by using a static Map in cache
075     * implementation).
076     * </td>
077     * </tr>
078     * <p/>
079     * <tr>
080     * <td>copyStrategy</td>
081     * <td>
082     * Specifies the implementation class of the {@link ObjectCacheTwoLevelImpl.CopyStrategy}
083     * interface, which was used to copy objects on read and write to application cache. If not
084     * specified a default implementation based was used ({@link ObjectCacheTwoLevelImpl.CopyStrategyImpl}
085     * make field-descriptor based copies of the cached objects).
086     * </td>
087     * </tr>
088     * <p/>
089     * <tr>
090     * <td>forceProxies</td>
091     * <td>
092     * If <em>true</em> on materialization of cached objects, all referenced objects will
093     * be represented by proxy objects (independent from the proxy settings in reference- or
094     * collection-descriptor).
095     * <br/>
096     * <strong>Note:</strong> To use this feature all persistence capable objects have to be
097     * interface based <strong>or</strong> the <em>ProxyFactory</em> and
098     * <em>IndirectionHandler</em> implementation classes supporting dynamic proxy enhancement
099     * for all classes (see OJB.properties file).
100     * </td>
101     * </tr>
102     * </table>
103     * <p/>
104     *
105     * @version $Id: ObjectCacheTwoLevelImpl.java,v 1.1 2007-08-24 22:17:29 ewestfal Exp $
106     */
107    public class ObjectCacheTwoLevelImpl implements ObjectCacheInternal, PBStateListener
108    {
109        private Logger log = LoggerFactory.getLogger(ObjectCacheTwoLevelImpl.class);
110    
111        public static final String APPLICATION_CACHE_PROP = "applicationCache";
112        public static final String COPY_STRATEGY_PROP = "copyStrategy";
113        public static final String FORCE_PROXIES = "forceProxies";
114        private static final String DEF_COPY_STRATEGY = ObjectCacheTwoLevelImpl.CopyStrategyImpl.class.getName();
115        private static final String DEF_APP_CACHE = ObjectCacheDefaultImpl.class.getName();
116    
117        private HashMap sessionCache;
118        // private boolean enabledReadCache;
119        private int invokeCounter;
120        private ReferenceQueue queue = new ReferenceQueue();
121        private ObjectCacheInternal applicationCache;
122        private CopyStrategy copyStrategy;
123        private PersistenceBrokerImpl broker;
124        private boolean forceProxies = false;
125    
126        public ObjectCacheTwoLevelImpl(final PersistenceBroker broker, Properties prop)
127        {
128            // TODO: Fix cast. Cast is needed to get access to ReferenceBroker class in PBImpl, see method #lookup
129            if(broker instanceof PersistenceBrokerImpl)
130            {
131                this.broker = (PersistenceBrokerImpl) broker;
132            }
133            else if(broker instanceof DelegatingPersistenceBroker)
134            {
135                this.broker = (PersistenceBrokerImpl) ((DelegatingPersistenceBroker) broker).getInnermostDelegate();
136            }
137            else
138            {
139                throw new RuntimeCacheException("Can't initialize two level cache, expect instance of"
140                        + PersistenceBrokerImpl.class + " or of " + DelegatingPersistenceBroker.class
141                        + " to setup application cache, but was " + broker);
142            }
143            this.sessionCache = new HashMap(100);
144            // this.enabledReadCache = false;
145            setupApplicationCache(this.broker, prop);
146            // we add this instance as a permanent PBStateListener
147            broker.addListener(this, true);
148        }
149    
150        /**
151         * Returns the {@link org.apache.ojb.broker.PersistenceBroker} instance associated with
152         * this cache instance.
153         */
154        public PersistenceBrokerImpl getBroker()
155        {
156            return broker;
157        }
158    
159        private void setupApplicationCache(PersistenceBrokerImpl broker, Properties prop)
160        {
161            if(log.isDebugEnabled()) log.debug("Start setup application cache for broker " + broker);
162            if(prop == null)
163            {
164                prop = new Properties();
165            }
166            String copyStrategyName = prop.getProperty(COPY_STRATEGY_PROP, DEF_COPY_STRATEGY).trim();
167            if(copyStrategyName.length() == 0)
168            {
169                copyStrategyName = DEF_COPY_STRATEGY;
170            }
171            String applicationCacheName = prop.getProperty(APPLICATION_CACHE_PROP, DEF_APP_CACHE).trim();
172            if(applicationCacheName.length() == 0)
173            {
174                applicationCacheName = DEF_APP_CACHE;
175            }
176            
177            String forceProxyValue = prop.getProperty(FORCE_PROXIES, "false").trim();
178            forceProxies = Boolean.valueOf(forceProxyValue).booleanValue();
179            
180            if (forceProxies && broker.getProxyFactory().interfaceRequiredForProxyGeneration()){
181                log.warn("'" + FORCE_PROXIES + "' is set to true, however a ProxyFactory implementation " +
182                        "[" + broker.getProxyFactory().getClass().getName() +"] " +
183                        " that requires persistent objects to implement an inteface is being used. Please ensure " +
184                        "that all persistent objects implement an interface, or change the ProxyFactory setting to a dynamic " +
185                        "proxy generator (like ProxyFactoryCGLIBImpl).");
186            }
187            
188            Class[] type = new Class[]{PersistenceBroker.class, Properties.class};
189            Object[] objects = new Object[]{broker, prop};
190            try
191            {
192                this.copyStrategy = (CopyStrategy) ClassHelper.newInstance(copyStrategyName);
193                Class target = ClassHelper.getClass(applicationCacheName);
194                if(target.equals(ObjectCacheDefaultImpl.class))
195                {
196                    // this property doesn't make sense in context of two-level cache
197                    prop.setProperty(ObjectCacheDefaultImpl.AUTOSYNC_PROP, "false");
198                }
199                ObjectCache temp = (ObjectCache) ClassHelper.newInstance(target, type, objects);
200                if(!(temp instanceof ObjectCacheInternal))
201                {
202                    log.warn("Specified application cache class doesn't implement '" + ObjectCacheInternal.class.getName()
203                        + "'. For best interaction only specify caches implementing the internal object cache interface.");
204                    temp = new CacheDistributor.ObjectCacheInternalWrapper(temp);
205                }
206                this.applicationCache = (ObjectCacheInternal) temp;
207            }
208            catch(Exception e)
209            {
210                throw new MetadataException("Can't setup application cache. Specified application cache was '"
211                        + applicationCacheName + "', copy strategy was '" + copyStrategyName + "'", e);
212            }
213            if(log.isEnabledFor(Logger.INFO))
214            {
215                ToStringBuilder buf = new ToStringBuilder(this);
216                buf.append("copyStrategy", copyStrategyName)
217                        .append("applicationCache", applicationCacheName);
218                log.info("Setup cache: " + buf.toString());
219            }
220        }
221    
222        /**
223         * Returns the application cache that this 2-level cache uses.
224         * 
225         * @return The application cache
226         */
227        public ObjectCacheInternal getApplicationCache()
228        {
229            return applicationCache;
230        }
231    
232        private Object lookupFromApplicationCache(Identity oid)
233        {
234            Object result = null;
235            Object obj = getApplicationCache().lookup(oid);
236            if(obj != null)
237            {
238                result = copyStrategy.read(broker, obj);
239            }
240            return result;
241        }
242    
243        private boolean putToApplicationCache(Identity oid, Object obj, boolean cacheIfNew)
244        {
245            /*
246            we allow to reuse cached objects, so lookup the old cache object
247            and forward it to the CopyStrategy
248            */
249            Object oldTarget = null;
250            if(!cacheIfNew)
251            {
252                oldTarget = getApplicationCache().lookup(oid);
253            }
254            Object target = copyStrategy.write(broker, obj, oldTarget);
255            if(cacheIfNew)
256            {
257                return getApplicationCache().cacheIfNew(oid, target);
258            }
259            else
260            {
261                getApplicationCache().cache(oid, target);
262                return false;
263            }
264        }
265    
266        /**
267         * Discard all session cached objects and reset the state of
268         * this class for further usage.
269         */
270        public void resetSessionCache()
271        {
272            sessionCache.clear();
273            invokeCounter = 0;
274        }
275    
276        /**
277         * Push all cached objects of the specified type, e.g. like {@link #TYPE_WRITE} to
278         * the application cache and reset type to the specified one.
279         */
280        private void pushToApplicationCache(int typeToProcess, int typeAfterProcess)
281        {
282            for(Iterator iter = sessionCache.values().iterator(); iter.hasNext();)
283            {
284                CacheEntry entry = (CacheEntry) iter.next();
285                // if the cached object was garbage collected, nothing to do
286                Object result = entry.get();
287                if(result == null)
288                {
289                    if(log.isDebugEnabled())
290                        log.debug("Object in session cache was gc, nothing to push to application cache");
291                }
292                else
293                {
294                    // push all objects of the specified type to application cache
295                    if(entry.type == typeToProcess)
296                    {
297                        if(log.isDebugEnabled())
298                        {
299                            log.debug("Move obj from session cache --> application cache : " + entry.oid);
300                        }
301                        /*
302                        arminw:
303                        only cache non-proxy or real subject of materialized proxy objects
304                        */
305                        if(ProxyHelper.isMaterialized(result))
306                        {
307                            putToApplicationCache(entry.oid, ProxyHelper.getRealObject(result), false);
308                            // set the new type after the object was pushed to application cache
309                            entry.type = typeAfterProcess;
310                        }
311                    }
312                }
313            }
314        }
315    
316        /**
317         * Cache the given object. Creates a
318         * {@link org.apache.ojb.broker.cache.ObjectCacheTwoLevelImpl.CacheEntry} and put it
319         * to session cache. If the specified object to cache is of type {@link #TYPE_NEW_MATERIALIZED}
320         * it will be immediately pushed to the application cache.
321         */
322        public void doInternalCache(Identity oid, Object obj, int type)
323        {
324            processQueue();
325            // pass new materialized objects immediately to application cache
326            if(type == TYPE_NEW_MATERIALIZED)
327            {
328                boolean result = putToApplicationCache(oid, obj, true);
329                CacheEntry entry = new CacheEntry(oid, obj, TYPE_CACHED_READ, queue);
330                if(result)
331                {
332                    // as current session says this object is new, put it
333                    // in session cache
334                    putToSessionCache(oid, entry, false);
335                }
336                else
337                {
338                    // object is not new, but if not in session cache
339                    // put it in
340                    putToSessionCache(oid, entry, true);
341                    if(log.isDebugEnabled())
342                    {
343                        log.debug("The 'new' materialized object was already in cache," +
344                                " will not push it to application cache: " + oid);
345                    }
346                }
347            }
348            else
349            {
350                // other types of cached objects will only be put to the session
351                // cache.
352                CacheEntry entry = new CacheEntry(oid, obj, type, queue);
353                putToSessionCache(oid, entry, false);
354            }
355        }
356    
357        /**
358         * Lookup corresponding object from session cache or if not found from
359         * the underlying real {@link ObjectCache} - Return <em>null</em> if no
360         * object was found.
361         */
362        public Object lookup(Identity oid)
363        {
364            Object result = null;
365            // 1. lookup an instance in session cache
366            CacheEntry entry = (CacheEntry) sessionCache.get(oid);
367            if(entry != null)
368            {
369                result = entry.get();
370            }
371            if(result == null)
372            {
373                result = lookupFromApplicationCache(oid);
374                // 4. if we have a match
375                // put object in session cache
376                if(result != null)
377                {
378                    doInternalCache(oid, result, TYPE_CACHED_READ);
379                    materializeFullObject(result);
380                    if(log.isDebugEnabled()) log.debug("Materialized object from second level cache: " + oid);
381                }
382            }
383            if(result != null && log.isDebugEnabled())
384            {
385                log.debug("Match for: " + oid);
386            }
387            return result;
388        }
389    
390        /**
391         * This cache implementation cache only "flat" objects (persistent objects without any
392         * references), so when {@link #lookup(org.apache.ojb.broker.Identity)} a cache object
393         * it needs full materialization (assign all referenced objects) before the cache returns
394         * the object. The materialization of the referenced objects based on the auto-XXX settings
395         * specified in the metadata mapping.
396         * <br/>
397         * Override this method if needed in conjunction with a user-defined
398         * {@link org.apache.ojb.broker.cache.ObjectCacheTwoLevelImpl.CopyStrategy}.
399         *
400         * @param target The "flat" object for full materialization
401         */
402        public void materializeFullObject(Object target)
403        {
404            ClassDescriptor cld = broker.getClassDescriptor(target.getClass());
405            // don't force, let OJB use the user settings
406            final boolean forced = false;
407            if (forceProxies){
408                broker.getReferenceBroker().retrieveProxyReferences(target, cld, forced);
409                broker.getReferenceBroker().retrieveProxyCollections(target, cld, forced);
410            }else{
411                broker.getReferenceBroker().retrieveReferences(target, cld, forced);
412                broker.getReferenceBroker().retrieveCollections(target, cld, forced);
413            }    
414        }
415    
416        /**
417         * Remove the corresponding object from session AND application cache.
418         */
419        public void remove(Identity oid)
420        {
421            if(log.isDebugEnabled()) log.debug("Remove object " + oid);
422            sessionCache.remove(oid);
423            getApplicationCache().remove(oid);
424        }
425    
426        /**
427         * Clear session cache and application cache.
428         */
429        public void clear()
430        {
431            sessionCache.clear();
432            getApplicationCache().clear();
433        }
434    
435        /**
436         * Put the specified object to session cache.
437         */
438        public void cache(Identity oid, Object obj)
439        {
440            doInternalCache(oid, obj, TYPE_UNKNOWN);
441        }
442    
443        public boolean cacheIfNew(Identity oid, Object obj)
444        {
445            boolean result = putToApplicationCache(oid, obj, true);
446            if(result)
447            {
448                CacheEntry entry = new CacheEntry(oid, obj, TYPE_CACHED_READ, queue);
449                putToSessionCache(oid, entry, true);
450            }
451            return result;
452        }
453    
454        /**
455         * Put object to session cache.
456         *
457         * @param oid The {@link org.apache.ojb.broker.Identity} of the object to cache
458         * @param entry The {@link org.apache.ojb.broker.cache.ObjectCacheTwoLevelImpl.CacheEntry} of the object
459         * @param onlyIfNew Flag, if set <em>true</em> only new objects (not already in session cache) be cached.
460         */
461        private void putToSessionCache(Identity oid, CacheEntry entry, boolean onlyIfNew)
462        {
463            if(onlyIfNew)
464            {
465                // no synchronization needed, because session cache was used per broker instance
466                if(!sessionCache.containsKey(oid)) sessionCache.put(oid, entry);
467            }
468            else
469            {
470                sessionCache.put(oid, entry);
471            }
472        }
473    
474        /**
475         * Make sure that the Identity objects of garbage collected cached
476         * objects are removed too.
477         */
478        private void processQueue()
479        {
480            CacheEntry sv;
481            while((sv = (CacheEntry) queue.poll()) != null)
482            {
483                sessionCache.remove(sv.oid);
484            }
485        }
486    
487        //------------------------------------------------------------
488        // PBStateListener methods
489        //------------------------------------------------------------
490        /**
491         * After committing the transaction push the object
492         * from session cache ( 1st level cache) to the application cache
493         * (2d level cache). Finally, clear the session cache.
494         */
495        public void afterCommit(PBStateEvent event)
496        {
497            if(log.isDebugEnabled()) log.debug("afterCommit() call, push objects to application cache");
498            if(invokeCounter != 0)
499            {
500                log.error("** Please check method calls of ObjectCacheTwoLevelImpl#enableMaterialization and" +
501                        " ObjectCacheTwoLevelImpl#disableMaterialization, number of calls have to be equals **");
502            }
503            try
504            {
505                // we only push "really modified objects" to the application cache
506                pushToApplicationCache(TYPE_WRITE, TYPE_CACHED_READ);
507            }
508            finally
509            {
510                resetSessionCache();
511            }
512        }
513    
514        /**
515         * Before closing the PersistenceBroker ensure that the session
516         * cache is cleared
517         */
518        public void beforeClose(PBStateEvent event)
519        {
520            /*
521            arminw:
522            this is a workaround for use in managed environments. When a PB instance is used
523            within a container a PB.close call is done when leave the container method. This close
524            the PB handle (but the real instance is still in use) and the PB listener are notified.
525            But the JTA tx was not committed at
526            this point in time and the session cache should not be cleared, because the updated/new
527            objects will be pushed to the real cache on commit call (if we clear, nothing to push).
528            So we check if the real broker is in a local tx (in this case we are in a JTA tx and the handle
529            is closed), if true we don't reset the session cache.
530            */
531            if(!broker.isInTransaction())
532            {
533                if(log.isDebugEnabled()) log.debug("Clearing the session cache");
534                resetSessionCache();
535            }
536        }
537    
538        /**
539         * Before rollbacking clear the session cache (first level cache)
540         */
541        public void beforeRollback(PBStateEvent event)
542        {
543            if(log.isDebugEnabled()) log.debug("beforeRollback()");
544            resetSessionCache();
545        }
546    
547        public void afterOpen(PBStateEvent event)
548        {
549        }
550    
551        public void beforeBegin(PBStateEvent event)
552        {
553        }
554    
555        public void afterBegin(PBStateEvent event)
556        {
557        }
558    
559        public void beforeCommit(PBStateEvent event)
560        {
561        }
562    
563        public void afterRollback(PBStateEvent event)
564        {
565        }
566        //------------------------------------------------------------
567    
568        //-----------------------------------------------------------
569        // inner class
570        //-----------------------------------------------------------
571    
572        /**
573         * Helper class to wrap cached objects using {@link java.lang.ref.SoftReference}, which
574         * allows to release objects when they no longer referenced within the PB session.
575         */
576        static final class CacheEntry extends SoftReference implements Serializable
577        {
578            private int type;
579            private Identity oid;
580    
581            public CacheEntry(Identity oid, Object obj, int type, final ReferenceQueue q)
582            {
583                super(obj, q);
584                this.oid = oid;
585                this.type = type;
586            }
587        }
588    
589    
590        public interface CopyStrategy
591        {
592            /**
593             * Called when an object is read from the application cache (second level cache)
594             * before the object is full materialized, see {@link ObjectCacheTwoLevelImpl#materializeFullObject(Object)}.
595             *
596             * @param broker The current used {@link org.apache.ojb.broker.PersistenceBroker} instance.
597             * @param obj The object read from the application cache.
598             * @return A copy of the object.
599             */
600            public Object read(PersistenceBroker broker, Object obj);
601    
602            /**
603             * Called before an object is written to the application cache (second level cache).
604             *
605             * @param broker The current used {@link org.apache.ojb.broker.PersistenceBroker} instance.
606             * @param obj The object to cache in application cache.
607             * @param oldObject The old cache object or <em>null</em>
608             * @return A copy of the object to write to application cache.
609             */
610            public Object write(PersistenceBroker broker, Object obj, Object oldObject);
611        }
612    
613        public static class CopyStrategyImpl implements CopyStrategy
614        {
615            static final String CLASS_NAME_STR = "ojbClassName11";
616    
617            public CopyStrategyImpl()
618            {
619            }
620    
621            public Object read(PersistenceBroker broker, Object obj)
622            {
623                HashMap source = (HashMap) obj;
624                String className = (String) source.get(CLASS_NAME_STR);
625                ClassDescriptor cld = broker.getDescriptorRepository().getDescriptorFor(className);
626                Object target = ClassHelper.buildNewObjectInstance(cld);
627                // perform main object values
628                FieldDescriptor[] flds = cld.getFieldDescriptor(true);
629                FieldDescriptor fld;
630                int length = flds.length;
631                for(int i = 0; i < length; i++)
632                {
633                    fld = flds[i];
634                    // read the field value
635                    Object value = source.get(fld.getPersistentField().getName());
636                    // copy the field value
637                    if(value != null) value = fld.getJdbcType().getFieldType().copy(value);
638                    // now make a field-conversion to java-type, because we only
639                    // the sql type of the field
640                    value = fld.getFieldConversion().sqlToJava(value);
641                    // set the copied field value in new object
642                    fld.getPersistentField().set(target, value);
643                }
644                return target;
645            }
646    
647            public Object write(PersistenceBroker broker, Object obj, Object oldObject)
648            {
649                ClassDescriptor cld = broker.getClassDescriptor(obj.getClass());
650                // we store field values by name in a Map
651                HashMap target = oldObject != null ? (HashMap) oldObject : new HashMap();
652                // perform main object values
653                FieldDescriptor[] flds = cld.getFieldDescriptor(true);
654                FieldDescriptor fld;
655                int length = flds.length;
656                for(int i = 0; i < length; i++)
657                {
658                    fld = flds[i];
659                    // get the value
660                    Object value = fld.getPersistentField().get(obj);
661                    // convert value to a supported sql type
662                    value = fld.getFieldConversion().javaToSql(value);
663                    // copy the sql type
664                    value = fld.getJdbcType().getFieldType().copy(value);
665                    target.put(fld.getPersistentField().getName(), value);
666                }
667                target.put(CLASS_NAME_STR, obj.getClass().getName());
668                return target;
669            }
670        }
671    }