View Javadoc

1   package org.apache.ojb.broker.cache;
2   
3   /* Copyright 2004-2005 The Apache Software Foundation
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  import java.io.Serializable;
19  import java.lang.ref.ReferenceQueue;
20  import java.lang.ref.SoftReference;
21  import java.util.HashMap;
22  import java.util.Iterator;
23  import java.util.Properties;
24  
25  import org.apache.ojb.broker.Identity;
26  import org.apache.ojb.broker.PBStateEvent;
27  import org.apache.ojb.broker.PBStateListener;
28  import org.apache.ojb.broker.PersistenceBroker;
29  import org.apache.ojb.broker.core.DelegatingPersistenceBroker;
30  import org.apache.ojb.broker.core.PersistenceBrokerImpl;
31  import org.apache.ojb.broker.core.proxy.ProxyHelper;
32  import org.apache.ojb.broker.metadata.ClassDescriptor;
33  import org.apache.ojb.broker.metadata.FieldDescriptor;
34  import org.apache.ojb.broker.metadata.MetadataException;
35  import org.apache.ojb.broker.util.ClassHelper;
36  import org.apache.ojb.broker.util.logging.Logger;
37  import org.apache.ojb.broker.util.logging.LoggerFactory;
38  import org.apache.commons.lang.builder.ToStringBuilder;
39  
40  /**
41   * A two-level {@link ObjectCache} implementation with a session- and an application cache. The application
42   * cache could be specified by the property <code>applicationCache</code>.
43   * <p/>
44   * The first level is a transactional session
45   * cache which cache objects till {@link org.apache.ojb.broker.PersistenceBroker#close()} or if
46   * a PB-tx is running till {@link org.apache.ojb.broker.PersistenceBroker#abortTransaction()} or
47   * {@link org.apache.ojb.broker.PersistenceBroker#commitTransaction()}. On commit all objects written to
48   * database will be pushed to the application cache.
49   * </p>
50   * <p/>
51   * The session cache act as a temporary storage for all read/store operations of persistent objects
52   * and only on commit or close of the used PB instance the buffered objects of type
53   * {@link #TYPE_WRITE} will be written to the application cache. Except objects of type
54   * {@link #TYPE_NEW_MATERIALIZED} these objects will be immediatly pushed to application cache.
55   * </p>
56   * <p/>
57   * <p/>
58   * </p>
59   * <p/>
60   * The application cache
61   * </p>
62   * <p/>
63   * <table cellspacing="2" cellpadding="2" border="3" frame="box">
64   * <tr>
65   * <td><strong>Property Key</strong></td>
66   * <td><strong>Property Values</strong></td>
67   * </tr>
68   * <p/>
69   * <tr>
70   * <td>applicationCache</td>
71   * <td>
72   * Specifies the {@link ObjectCache} implementation used as application cache (second level cache).
73   * By default {@link ObjectCacheDefaultImpl} was used. It's recommended to use a shared cache implementation
74   * (all used PB instances should access the same pool of objects - e.g. by using a static Map in cache
75   * implementation).
76   * </td>
77   * </tr>
78   * <p/>
79   * <tr>
80   * <td>copyStrategy</td>
81   * <td>
82   * Specifies the implementation class of the {@link ObjectCacheTwoLevelImpl.CopyStrategy}
83   * interface, which was used to copy objects on read and write to application cache. If not
84   * specified a default implementation based was used ({@link ObjectCacheTwoLevelImpl.CopyStrategyImpl}
85   * make field-descriptor based copies of the cached objects).
86   * </td>
87   * </tr>
88   * <p/>
89   * <tr>
90   * <td>forceProxies</td>
91   * <td>
92   * If <em>true</em> on materialization of cached objects, all referenced objects will
93   * be represented by proxy objects (independent from the proxy settings in reference- or
94   * collection-descriptor).
95   * <br/>
96   * <strong>Note:</strong> To use this feature all persistence capable objects have to be
97   * interface based <strong>or</strong> the <em>ProxyFactory</em> and
98   * <em>IndirectionHandler</em> implementation classes supporting dynamic proxy enhancement
99   * 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 }