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 }