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 }