001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.ejb.plugins.cmp.jdbc;
023:
024: import org.jboss.ejb.EntityEnterpriseContext;
025: import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMPFieldBridge;
026: import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCCMRFieldBridge;
027: import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCFieldBridge;
028: import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCEntityBridge;
029: import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCReadAheadMetaData;
030: import org.jboss.logging.Logger;
031: import org.jboss.tm.TransactionLocal;
032:
033: import javax.transaction.Transaction;
034: import javax.transaction.SystemException;
035: import java.lang.ref.SoftReference;
036: import java.util.Collection;
037: import java.util.Collections;
038: import java.util.HashMap;
039: import java.util.HashSet;
040: import java.util.Iterator;
041: import java.util.LinkedList;
042: import java.util.List;
043: import java.util.Map;
044:
045: /**
046: * ReadAheadCache stores all of the data readahead for an entity.
047: * Data is stored in the JDBCStoreManager entity tx data map on a per entity
048: * basis. The read ahead data for each entity is stored with a soft reference.
049: *
050: * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
051: * @version $Revision: 57209 $
052: */
053: public final class ReadAheadCache {
054: /**
055: * To simplify null values handling in the preloaded data pool we use
056: * this value instead of 'null'
057: */
058: private static final Object NULL_VALUE = new Object();
059:
060: private final JDBCStoreManager manager;
061: private final Logger log;
062:
063: private final TransactionLocal listMapTxLocal = new TransactionLocal() {
064: protected Object initialValue() {
065: return new HashMap();
066: }
067:
068: public Transaction getTransaction() {
069: try {
070: return transactionManager.getTransaction();
071: } catch (SystemException e) {
072: throw new IllegalStateException(
073: "An error occured while getting the "
074: + "transaction associated with the current thread: "
075: + e);
076: }
077: }
078: };
079:
080: private ListCache listCache;
081: private int listCacheMax;
082:
083: public ReadAheadCache(JDBCStoreManager manager) {
084: this .manager = manager;
085:
086: // Create the Log
087: log = Logger.getLogger(this .getClass().getName() + "."
088: + manager.getMetaData().getName());
089: }
090:
091: public void create() {
092: // Create the list cache
093: listCacheMax = ((JDBCEntityBridge) manager.getEntityBridge())
094: .getListCacheMax();
095: listCache = new ListCache(listCacheMax);
096: }
097:
098: public void start() {
099: }
100:
101: public void stop() {
102: listCache.clear();
103: }
104:
105: public void destroy() {
106: listCache = null;
107: }
108:
109: public void addFinderResults(List results,
110: JDBCReadAheadMetaData readahead) {
111: if (listCacheMax == 0 || results.size() < 2) {
112: // nothing to see here... move along
113: return;
114: }
115:
116: Map listMap = getListMap();
117: if (listMap == null) {
118: // no active transaction
119: return;
120: }
121:
122: if (log.isTraceEnabled()) {
123: log
124: .trace("Add finder results:" + " entity="
125: + manager.getEntityBridge().getEntityName()
126: + " results=" + results + " readahead="
127: + readahead);
128: }
129:
130: // add the finder to the LRU list
131: if (!readahead.isNone()) {
132: listCache.add(results);
133: }
134:
135: //
136: // Create a map between the entity primary keys and the list.
137: // The primary key will point to the last list added that contained the
138: // primary key.
139: //
140: HashSet dereferencedResults = new HashSet();
141: Iterator iter = results.iterator();
142: for (int i = 0; iter.hasNext(); i++) {
143: Object pk = iter.next();
144:
145: // create the new entry object
146: EntityMapEntry entry;
147: if (readahead.isNone()) {
148: entry = new EntityMapEntry(0, Collections
149: .singletonList(pk), readahead);
150: } else {
151: entry = new EntityMapEntry(i, results, readahead);
152: }
153:
154: // Keep track of the results that have been dereferenced. Later we
155: // all results from the list cache that are no longer referenced.
156: EntityMapEntry oldInfo = (EntityMapEntry) listMap.put(pk,
157: entry);
158: if (oldInfo != null) {
159: dereferencedResults.add(oldInfo.results);
160: }
161: }
162:
163: //
164: // Now we remove all lists from the list cache that are no longer
165: // referenced in the list map.
166: //
167:
168: // if we don't have any dereferenced results at this point we are done
169: if (dereferencedResults.isEmpty()) {
170: return;
171: }
172:
173: //
174: // Go through the dereferenced results set and look at the PKs for each
175: // dereferenced list. If you find one key that references the
176: // dereferenced list, remove it from the dereferenced results set and
177: // move on to the next dereferenced results.
178: //
179: iter = dereferencedResults.iterator();
180: while (iter.hasNext()) {
181: List dereferencedList = (List) iter.next();
182:
183: boolean listHasReference = false;
184: Iterator iter2 = dereferencedList.iterator();
185: while (!listHasReference && iter2.hasNext()) {
186: EntityMapEntry entry = (EntityMapEntry) listMap
187: .get(iter2.next());
188: if (entry != null && entry.results == dereferencedList) {
189: listHasReference = true;
190: }
191: }
192:
193: if (listHasReference) {
194: // this list does not have any references
195: iter.remove();
196: }
197: }
198:
199: // if we don't have any dereferenced results at this point we are done
200: if (dereferencedResults.isEmpty()) {
201: return;
202: }
203:
204: // remove all results from the cache that are no longer referenced
205: iter = dereferencedResults.iterator();
206: while (iter.hasNext()) {
207: List list = (List) iter.next();
208: if (log.isTraceEnabled()) {
209: log.trace("Removing dereferenced results: " + list);
210: }
211: listCache.remove(list);
212: }
213: }
214:
215: private void removeFinderResult(List results) {
216: Map listMap = getListMap();
217: if (listMap == null) {
218: // no active transaction
219: return;
220: }
221:
222: // remove the list from the list cache
223: listCache.remove(results);
224:
225: // remove all primary keys from the listMap that reference this list
226: if (!listMap.isEmpty()) {
227: Iterator iter = listMap.values().iterator();
228: while (iter.hasNext()) {
229: EntityMapEntry entry = (EntityMapEntry) iter.next();
230:
231: // use == because only identity matters here
232: if (entry.results == results) {
233: iter.remove();
234: }
235: }
236: }
237: }
238:
239: public EntityReadAheadInfo getEntityReadAheadInfo(Object pk) {
240: Map listMap = getListMap();
241: if (listMap == null) {
242: // no active transaction
243: return new EntityReadAheadInfo(Collections
244: .singletonList(pk));
245: }
246:
247: EntityMapEntry entry = (EntityMapEntry) getListMap().get(pk);
248: if (entry != null) {
249: // we're using these results so promote it to the head of the
250: // LRU list
251: if (!entry.readahead.isNone()) {
252: listCache.promote(entry.results);
253: }
254:
255: // get the readahead metadata
256: JDBCReadAheadMetaData readahead = entry.readahead;
257: if (readahead == null) {
258: readahead = manager.getMetaData().getReadAhead();
259: }
260:
261: int from = entry.index;
262: int to = Math.min(entry.results.size(), entry.index
263: + readahead.getPageSize());
264: List loadKeys = entry.results.subList(from, to);
265:
266: return new EntityReadAheadInfo(loadKeys, readahead);
267: } else {
268: return new EntityReadAheadInfo(Collections
269: .singletonList(pk));
270: }
271: }
272:
273: /**
274: * Loads all of the preloaded data for the ctx into it.
275: * @param ctx the context that will be loaded
276: * @return true if at least one field was loaded.
277: */
278: public boolean load(EntityEnterpriseContext ctx) {
279: if (log.isTraceEnabled()) {
280: log.trace("load data:" + " entity="
281: + manager.getEntityBridge().getEntityName()
282: + " pk=" + ctx.getId());
283: }
284:
285: // get the preload data map
286: Map preloadDataMap = getPreloadDataMap(ctx.getId(), false);
287: if (preloadDataMap == null || preloadDataMap.isEmpty()) {
288: // no preloaded data for this entity
289: if (log.isTraceEnabled()) {
290: log.trace("No preload data found:" + " entity="
291: + manager.getEntityBridge().getEntityName()
292: + " pk=" + ctx.getId());
293: }
294: return false;
295: }
296:
297: boolean cleanReadAhead = manager.getMetaData()
298: .isCleanReadAheadOnLoad();
299:
300: boolean loaded = false;
301: JDBCCMRFieldBridge onlyOneSingleValuedCMR = null;
302:
303: // iterate over the keys in the preloaded map
304: Iterator iter = preloadDataMap.entrySet().iterator();
305: while (iter.hasNext()) {
306: Map.Entry entry = (Map.Entry) iter.next();
307: Object field = entry.getKey();
308:
309: // get the value that was preloaded for this field
310: Object value = entry.getValue();
311:
312: // if we didn't get a value something is seriously hosed
313: if (value == null) {
314: throw new IllegalStateException(
315: "Preloaded value not found");
316: }
317:
318: if (cleanReadAhead) {
319: // remove this value from the preload cache as it is about to be loaded
320: iter.remove();
321: }
322:
323: // check for null value standin
324: if (value == NULL_VALUE) {
325: value = null;
326: }
327:
328: if (field instanceof JDBCCMPFieldBridge) {
329: JDBCCMPFieldBridge cmpField = (JDBCCMPFieldBridge) field;
330:
331: if (!cmpField.isLoaded(ctx)) {
332: if (log.isTraceEnabled()) {
333: log.trace("Preloading data:"
334: + " entity="
335: + manager.getEntityBridge()
336: .getEntityName() + " pk="
337: + ctx.getId() + " cmpField="
338: + cmpField.getFieldName());
339: }
340:
341: // set the value
342: cmpField.setInstanceValue(ctx, value);
343:
344: // mark this field clean as it's value was just loaded
345: cmpField.setClean(ctx);
346:
347: loaded = true;
348: } else {
349: if (log.isTraceEnabled()) {
350: log.trace("CMPField already loaded:"
351: + " entity="
352: + manager.getEntityBridge()
353: .getEntityName() + " pk="
354: + ctx.getId() + " cmpField="
355: + cmpField.getFieldName());
356: }
357: }
358: } else if (field instanceof JDBCCMRFieldBridge) {
359: JDBCCMRFieldBridge cmrField = (JDBCCMRFieldBridge) field;
360:
361: if (!cmrField.isLoaded(ctx)) {
362: if (log.isTraceEnabled()) {
363: log.trace("Preloading data:"
364: + " entity="
365: + manager.getEntityBridge()
366: .getEntityName() + " pk="
367: + ctx.getId() + " cmrField="
368: + cmrField.getFieldName());
369: }
370:
371: // set the value
372: cmrField.load(ctx, (List) value);
373:
374: // add the loaded list to the related entity's readahead cache
375: JDBCStoreManager relatedManager = (JDBCStoreManager) cmrField
376: .getRelatedCMRField().getManager();
377: ReadAheadCache relatedReadAheadCache = relatedManager
378: .getReadAheadCache();
379: relatedReadAheadCache.addFinderResults(
380: (List) value, cmrField.getReadAhead());
381:
382: if (!loaded) {
383: // this is a hack to fix on-load read-ahead for 1:m relationships
384: if (cmrField.isSingleValued()
385: && onlyOneSingleValuedCMR == null) {
386: onlyOneSingleValuedCMR = cmrField;
387: } else {
388: loaded = true;
389: }
390: }
391: } else {
392: if (log.isTraceEnabled()) {
393: log.trace("CMRField already loaded:"
394: + " entity="
395: + manager.getEntityBridge()
396: .getEntityName() + " pk="
397: + ctx.getId() + " cmrField="
398: + cmrField.getFieldName());
399: }
400: }
401: }
402: }
403:
404: if (cleanReadAhead) {
405: // remove all preload data map as all of the data has been loaded
406: manager.removeEntityTxData(new PreloadKey(ctx.getId()));
407: }
408:
409: return loaded;
410: }
411:
412: /**
413: * Returns the cached value of a CMR field or null if nothing was cached for this field.
414: * @param pk primary key.
415: * @param cmrField the field to get the cached value for.
416: * @return cached value for the <code>cmrField</code> or null if no value cached.
417: */
418: public Collection getCachedCMRValue(Object pk,
419: JDBCCMRFieldBridge cmrField) {
420: Map preloadDataMap = getPreloadDataMap(pk, true);
421: return (Collection) preloadDataMap.get(cmrField);
422: }
423:
424: /**
425: * Add preloaded data for an entity within the scope of a transaction
426: */
427: public void addPreloadData(Object pk, JDBCFieldBridge field,
428: Object fieldValue) {
429: if (field instanceof JDBCCMRFieldBridge) {
430: if (fieldValue == null) {
431: fieldValue = Collections.EMPTY_LIST;
432: } else if (!(fieldValue instanceof Collection)) {
433: fieldValue = Collections.singletonList(fieldValue);
434: }
435: }
436:
437: if (log.isTraceEnabled()) {
438: log.trace("Add preload data:" + " entity="
439: + manager.getEntityBridge().getEntityName()
440: + " pk=" + pk + " field=" + field.getFieldName());
441: }
442:
443: // convert null values to a null value standing object
444: if (fieldValue == null) {
445: fieldValue = NULL_VALUE;
446: }
447:
448: // store the preloaded data
449: Map preloadDataMap = getPreloadDataMap(pk, true);
450: Object overriden = preloadDataMap.put(field, fieldValue);
451:
452: if (log.isTraceEnabled() && overriden != null) {
453: log.trace("Overriding cached value " + overriden + " with "
454: + (fieldValue == NULL_VALUE ? null : fieldValue)
455: + ". pk=" + pk + ", field=" + field.getFieldName());
456: }
457: }
458:
459: public void removeCachedData(Object primaryKey) {
460: if (log.isTraceEnabled()) {
461: log.trace("Removing cached data for " + primaryKey);
462: }
463:
464: Map listMap = getListMap();
465: if (listMap == null) {
466: // no active tx
467: return;
468: }
469:
470: // remove the preloaded data
471: manager.removeEntityTxData(new PreloadKey(primaryKey));
472:
473: // if the entity didn't have readahead entry, or it was read-ahead
474: // none; return
475: EntityMapEntry oldInfo = (EntityMapEntry) listMap
476: .remove(primaryKey);
477: if (oldInfo == null || oldInfo.readahead.isNone()) {
478: return;
479: }
480:
481: // check to see if the dereferenced finder result is still referenced
482: Iterator iter = listMap.values().iterator();
483: while (iter.hasNext()) {
484: EntityMapEntry entry = (EntityMapEntry) iter.next();
485:
486: // use == because only identity matters here
487: if (entry.results == oldInfo.results) {
488: // ok it is still referenced
489: return;
490: }
491: }
492:
493: // a reference to the old finder set was not found so remove it
494: if (log.isTraceEnabled()) {
495: log.trace("Removing dereferenced finder results: "
496: + oldInfo.results);
497: }
498: listCache.remove(oldInfo.results);
499: }
500:
501: /**
502: * Gets the map of preloaded data.
503: * @param entityPrimaryKey the primary key of the entity
504: * @param create should a new preload data map be created if one is not found
505: * @return the preload data map for null if one is not found
506: */
507: public Map getPreloadDataMap(Object entityPrimaryKey, boolean create) {
508: //
509: // Be careful in this code. A soft reference may be cleared at any time,
510: // so don't check if a reference has a value and then get that value.
511: // Instead get the value and then check if it is null.
512: //
513:
514: // create a preload key for the entity
515: PreloadKey preloadKey = new PreloadKey(entityPrimaryKey);
516:
517: // get the soft reference to the preload data map
518: SoftReference ref = (SoftReference) manager
519: .getEntityTxData(preloadKey);
520:
521: // did we get a reference
522: if (ref != null) {
523: // get the map from the reference
524: Map preloadDataMap = (Map) ref.get();
525:
526: // did we actually get a map? (will be null if it has been GC'd)
527: if (preloadDataMap != null) {
528: return preloadDataMap;
529: }
530: }
531:
532: //
533: // at this point we did not get an existing value
534: //
535: // if we got a dead reference remove it
536: if (ref != null) {
537: //log.info(manager.getMetaData().getName() + " was GC'd from read ahead");
538: manager.removeEntityTxData(preloadKey);
539: }
540:
541: // if we are not creating, we're done
542: if (!create) {
543: return null;
544: }
545:
546: // create the new preload data map
547: Map preloadDataMap = new HashMap();
548:
549: // create new soft reference
550: ref = new SoftReference(preloadDataMap);
551:
552: // store the reference
553: manager.putEntityTxData(preloadKey, ref);
554:
555: // return the new preload data map
556: return preloadDataMap;
557: }
558:
559: private Map getListMap() {
560: return (Map) listMapTxLocal.get();
561: }
562:
563: private final class ListCache {
564: private final TransactionLocal cacheTxLocal = new TransactionLocal() {
565: protected Object initialValue() {
566: return new LinkedList();
567: }
568:
569: public Transaction getTransaction() {
570: try {
571: return transactionManager.getTransaction();
572: } catch (SystemException e) {
573: throw new IllegalStateException(
574: "An error occured while getting the "
575: + "transaction associated with the current thread: "
576: + e);
577: }
578: }
579: };
580: private int max;
581:
582: public ListCache(int max) {
583: if (max < 0)
584: throw new IllegalArgumentException(
585: "list-cache-max is negative: " + max);
586: this .max = max;
587: }
588:
589: public void add(List list) {
590: if (max == 0) {
591: // we're not caching lists, so we're done
592: return;
593: }
594:
595: LinkedList cache = getCache();
596: if (cache == null)
597: return;
598: cache.addFirst(new IdentityObject(list));
599:
600: // shrink size to max
601: while (cache.size() > max) {
602: IdentityObject object = (IdentityObject) cache
603: .removeLast();
604: ageOut((List) object.getObject());
605: }
606: }
607:
608: public void promote(List list) {
609: if (max == 0) {
610: // we're not caching lists, so we're done
611: return;
612: }
613:
614: LinkedList cache = getCache();
615: if (cache == null)
616: return;
617:
618: IdentityObject object = new IdentityObject(list);
619: if (cache.remove(object)) {
620: // it was in the cache so add it to the front
621: cache.addFirst(object);
622: }
623: }
624:
625: public void remove(List list) {
626: if (max == 0) {
627: // we're not caching lists, so we're done
628: return;
629: }
630: LinkedList cache = getCache();
631: if (cache != null)
632: cache.remove(new IdentityObject(list));
633: }
634:
635: public void clear() {
636: if (max == 0) {
637: // we're not caching lists, so we're done
638: return;
639: }
640: }
641:
642: private void ageOut(List list) {
643: removeFinderResult(list);
644: }
645:
646: private LinkedList getCache() {
647: return (LinkedList) cacheTxLocal.get();
648: }
649: }
650:
651: /**
652: * Wraps an entity primary key, so it does not collide with other
653: * data stored in the entityTxDataMap.
654: */
655: private static final class PreloadKey {
656: private final Object entityPrimaryKey;
657:
658: public PreloadKey(Object entityPrimaryKey) {
659: if (entityPrimaryKey == null) {
660: throw new IllegalArgumentException(
661: "Entity primary key is null");
662: }
663: this .entityPrimaryKey = entityPrimaryKey;
664: }
665:
666: public boolean equals(Object object) {
667: if (object instanceof PreloadKey) {
668: PreloadKey preloadKey = (PreloadKey) object;
669: return preloadKey.entityPrimaryKey
670: .equals(entityPrimaryKey);
671: }
672: return false;
673: }
674:
675: public int hashCode() {
676: return entityPrimaryKey.hashCode();
677: }
678:
679: public String toString() {
680: return "PreloadKey: entityId=" + entityPrimaryKey;
681: }
682: }
683:
684: private static final class EntityMapEntry {
685: public final int index;
686: public final List results;
687: public final JDBCReadAheadMetaData readahead;
688:
689: private EntityMapEntry(int index, List results,
690: JDBCReadAheadMetaData readahead) {
691:
692: this .index = index;
693: this .results = results;
694: this .readahead = readahead;
695: }
696: }
697:
698: public final static class EntityReadAheadInfo {
699: private final List loadKeys;
700: private final JDBCReadAheadMetaData readahead;
701:
702: private EntityReadAheadInfo(List loadKeys) {
703: this (loadKeys, null);
704: }
705:
706: private EntityReadAheadInfo(List loadKeys,
707: JDBCReadAheadMetaData r) {
708: this .loadKeys = loadKeys;
709: this .readahead = r;
710: }
711:
712: public List getLoadKeys() {
713: return loadKeys;
714: }
715:
716: public JDBCReadAheadMetaData getReadAhead() {
717: return readahead;
718: }
719: }
720:
721: /**
722: * Wraps an Object and does equals/hashCode based on object identity.
723: */
724: private static final class IdentityObject {
725: private final Object object;
726:
727: public IdentityObject(Object object) {
728: if (object == null) {
729: throw new IllegalArgumentException("Object is null");
730: }
731: this .object = object;
732: }
733:
734: public Object getObject() {
735: return object;
736: }
737:
738: public boolean equals(Object object) {
739: return this .object == object;
740: }
741:
742: public int hashCode() {
743: return object.hashCode();
744: }
745:
746: public String toString() {
747: return object.toString();
748: }
749: }
750: }
|