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