package er.extensions.eof; import java.util.Enumeration; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOUtilities; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.eocontrol.EOGlobalID; import com.webobjects.eocontrol.EOObjectStoreCoordinator; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.eocontrol.EOTemporaryGlobalID; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; import com.webobjects.foundation.NSSelector; import er.extensions.ERXExtensions; import er.extensions.foundation.ERXExpiringCache; import er.extensions.foundation.ERXSelectorUtilities; /** * Caches instances of one entity by a given key(path). Typically you'd have an "identifier" property * and you'd fetch values by:<pre><code> * ERXEnterpriseObjectCache<HelpText> helpTextCache = new ERXEnterpriseObjectCache<HelpText>("HelpText", "pageConfiguration"); * ... * HelpText helpText = helpTextCache.objectForKey(ec, "ListHelpText"); * </code></pre> * * You can supply a timeout after which individual objects (or all objects if fetchInitialValues * is <code>true</code>) get cleared and re-fetched. This implementation can cache either only the global IDs, * or the global ID and a copy of the actual object. Caching the actual object ensures that the snapshot stays around * and thus prevent additional trips to the database. * * Listens to EOEditingContextDidSaveChanges notifications to track changes to objects in the cache and ClearCachesNotification * for messages to purge the cache. * @author ak inspired by a class from Dominik Westner * @param <T> the type of EOEnterpriseObject in this cache */ public class ERXEnterpriseObjectCache<T extends EOEnterpriseObject> { /** Other code can send this notification if it needs to have this cache discard all of the * objects that it has. The object in the notification is the name of the EOEntity to discard cache for. */ public static String ClearCachesNotification = "ERXEnterpriseObjectCache.ClearCaches"; protected static final EOGlobalID NO_GID_MARKER = new EOTemporaryGlobalID(); /** Name of the EOEntity to cache. */ private String _entityName; /** Key path to data uniquely identifying an instance of this entity. */ private String _keyPath; /** EOQualifier restricting which instances are stored in this cache */ private EOQualifier _qualifier; /** Actual cache implementation. */ private ERXExpiringCache<Object, EORecord<T>> _cache; /** Time to live in milliseconds for an object in this cache. */ private long _timeout; /** The time when the objects in this cache were fetched. Only used if _fetchInitialValues is <code>true</code>. */ private long _fetchTime; /** <code>true</code> if this cache should be populated when created, <code>false</code> for lazy population. */ private boolean _fetchInitialValues; /** If <code>true</code>, just a single editing context instance is used for this cache instance. */ private boolean _reuseEditingContext; /** The single editing context instance is used for this cache instance if <code>_reuseEditingContext</code> is <code>true</code>. */ private ERXEC _editingContext; /** If <code>true</code>, this cache retains an instance of each object so that the snapshot does not expire. */ private boolean _retainObjects; /** If <code>true</code>, the entire cache contents are discarded when any object in it changes. Probably not what you want. * @see #editingContextDidSaveChanges(NSNotification) */ private boolean _resetOnChange; /** If <code>true</code>, object that have not been saved yet are found by the cache. */ private boolean _returnUnsavedObjects; /** If <code>false</code>, the cache is not allowed to fetch values as migrations may not have been processed yet. * @see ERXExtensions#didFinishInitialization() * @see #setApplicationDidFinishInitialization(boolean) */ private static boolean _applicationDidFinishInitialization; /** * Creates the cache for the given entity name and the given keypath. No * timeout value is used. * @param entityName name of the EOEntity to cache * @param keyPath key path to data uniquely identifying an instance of this entity */ public ERXEnterpriseObjectCache(String entityName, String keyPath) { this(entityName, keyPath, 0L); } /** * Creates the cache for the entity implemented by the passed class and the given keypath. No * timeout value is used. * @param c Class used to identify which EOEntity this cache is for * @param keyPath key path to data uniquely identifying an instance of this entity */ public ERXEnterpriseObjectCache(Class c, String keyPath) { this(entityNameForClass(c), keyPath); } private static String entityNameForClass(Class c) { ERXEC ec = (ERXEC)ERXEC.newEditingContext(); ec.setCoalesceAutoLocks(false); ec.lock(); try { EOEntity entity = EOUtilities.entityForClass(ec, c); if(entity != null) { return entity.name(); } return null; } finally { ec.unlock(); ec.dispose(); } } /** * Creates the cache for the given entity, keypath and timeout value in milliseconds. * @param entityName name of the EOEntity to cache * @param keyPath key path to data uniquely identifying an instance of this entity * @param timeout time to live in milliseconds for an object in this cache */ public ERXEnterpriseObjectCache(String entityName, String keyPath, long timeout) { this(entityName, keyPath, null, timeout); } /** * Creates the cache for the given entity, keypath and timeout value in milliseconds. Only objects * that match qualifier are stored in the cache. Note that _resetOnChange (and _fetchInitialValues) are * both <code>true</code> after this constructor. You will almost certainly want to call * <code>setResetOnChange(false);</code>. * * @see #setResetOnChange(boolean) * @see #setFetchInitialValues(boolean) * * @param entityName name of the EOEntity to cache * @param keyPath key path to data uniquely identifying an instance of this entity * @param qualifier EOQualifier restricting which instances are stored in this cache * @param timeout time to live in milliseconds for an object in this cache */ public ERXEnterpriseObjectCache(String entityName, String keyPath, EOQualifier qualifier, long timeout) { _entityName = entityName; _keyPath = keyPath; _timeout = timeout; _qualifier = qualifier; _resetOnChange = true; // MS: for backwards compatibility _fetchInitialValues = true; // MS: for backwards compatibility start(); } /** * Creates the cache for the given entity, keypath and timeout value in milliseconds. Only objects * that match qualifier are stored in the cache. * * @see #setResetOnChange(boolean) * @see #setFetchInitialValues(boolean) * @see #setRetainObjects(boolean) * * @param entityName name of the EOEntity to cache * @param keyPath key path to data uniquely identifying an instance of this entity * @param qualifier EOQualifier restricting which instances are stored in this cache * @param timeout time to live in milliseconds for an object in this cache * @param shouldRetainObjects true if this cache should retain the cached objects, false to keep only the GID * @param shouldFetchInitialValues true if the cache should be fully populated on first access */ public ERXEnterpriseObjectCache(String entityName, String keyPath, EOQualifier qualifier, long timeout, boolean shouldRetainObjects, boolean shouldFetchInitialValues) { this(entityName, keyPath, qualifier, timeout, shouldRetainObjects, shouldFetchInitialValues, false); } /** * Creates the cache for the given entity, keypath and timeout value in milliseconds. Only objects * that match qualifier are stored in the cache. * * @see #setResetOnChange(boolean) * @see #setFetchInitialValues(boolean) * @see #setRetainObjects(boolean) * * @param entityName name of the EOEntity to cache * @param keyPath key path to data uniquely identifying an instance of this entity * @param qualifier EOQualifier restricting which instances are stored in this cache * @param timeout time to live in milliseconds for an object in this cache * @param shouldRetainObjects true if this cache should retain the cached objects, false to keep only the GID * @param shouldFetchInitialValues true if the cache should be fully populated on first access * @param shouldReturnUnsavedObjects true if unsaved matching objects should be returned, see {@link #unsavedMatchingObject(EOEditingContext, Object)} */ public ERXEnterpriseObjectCache(String entityName, String keyPath, EOQualifier qualifier, long timeout, boolean shouldRetainObjects, boolean shouldFetchInitialValues, boolean shouldReturnUnsavedObjects) { _entityName = entityName; _keyPath = keyPath; _timeout = timeout; _qualifier = qualifier; _returnUnsavedObjects = shouldReturnUnsavedObjects; setRetainObjects(shouldRetainObjects); setResetOnChange(false); setFetchInitialValues(shouldFetchInitialValues); start(); } /** * Call this to re-start cache updating after stop() is called. This is automatically called from the * constructor so unless you call stop(), there is no need to ever call this method. * @see #stop() */ public void start() { // Catch this to update the cache when an object is changed NSSelector selector = ERXSelectorUtilities.notificationSelector("editingContextDidSaveChanges"); NSNotificationCenter.defaultCenter().addObserver(this, selector, EOEditingContext.EditingContextDidSaveChangesNotification, null); // Catch this for custom notifications that the cache should be discarded selector = ERXSelectorUtilities.notificationSelector("clearCaches"); NSNotificationCenter.defaultCenter().addObserver(this, selector, ERXEnterpriseObjectCache.ClearCachesNotification, null); if (_timeout > 0 && _cache != null) { _cache.startBackgroundExpiration(); } } /** * Call this to stop cache updating. * @see #start() */ public void stop() { NSNotificationCenter.defaultCenter().removeObserver(this, EOEditingContext.EditingContextDidSaveChangesNotification, null); NSNotificationCenter.defaultCenter().removeObserver(this, ERXEnterpriseObjectCache.ClearCachesNotification, null); _cache.stopBackgroundExpiration(); } /** * Called from {@link ERXExtensions#finishInitialization()} to enable fetches. This is to ensure that * migrations have run prior first fetch from this class. * * @param didFinish indicator if application did finish initialization phase */ public static void setApplicationDidFinishInitialization(boolean didFinish) { _applicationDidFinishInitialization = didFinish; } /** * Returns the editing context that holds object that are in this cache. If _reuseEditingContext is false, * a new editing context instance is returned each time. The returned editing context is autolocking. * * @return the editing context that holds object that are in this cache */ protected ERXEC editingContext() { ERXEC editingContext; if (_reuseEditingContext) { synchronized (this) { if (_editingContext == null) { _editingContext = (ERXEC)ERXEC.newEditingContext(); _editingContext.setCoalesceAutoLocks(false); } } editingContext = _editingContext; } else { editingContext = (ERXEC)ERXEC.newEditingContext(); editingContext.setCoalesceAutoLocks(false); } return editingContext; } /** * Helper to check a dictionary of objects from an EOF notification and return any that are for the * entity that we are caching. * * @param dict dictionary of key to {@literal NSArray<EOEnterpriseObject>} * @param key key into dict indicating which list to process * @return objects from the list that are of the entity we are caching, or an empty array if there are no matches */ private NSArray<T> relevantChanges(NSDictionary dict, String key) { NSMutableArray<T> releventEOs = null; NSArray<EOEnterpriseObject> eos = (NSArray<EOEnterpriseObject>) dict.objectForKey(key); for (Enumeration enumeration = eos.objectEnumerator(); enumeration.hasMoreElements();) { EOEnterpriseObject eo = (EOEnterpriseObject) enumeration.nextElement(); if(eo.entityName().equals(entityName())) { if (releventEOs == null) { releventEOs = new NSMutableArray(); } releventEOs.addObject((T)eo); } } return releventEOs != null ? releventEOs : NSArray.EmptyArray; } /** * Handler for the editingContextDidSaveChanges notification. If <code>_resetOnChange</code> is <code>true</code>, this * calls reset() to discard the entire cache contents if an object of the given entity has been changed. * If <code>_resetOnChange</code> is <code>false</code>, this updates the cache to reflect the added/changed/removed * objects. * * @see EOEditingContext#ObjectsChangedInEditingContextNotification * @see #reset() * * @param n NSNotification with EOEditingContext as the object and a dictionary of changes in the userInfo */ public void editingContextDidSaveChanges(NSNotification n) { EOEditingContext ec = (EOEditingContext) n.object(); if(_applicationDidFinishInitialization && ec.parentObjectStore() instanceof EOObjectStoreCoordinator) { NSArray<T> releventsInsertedEOs = relevantChanges(n.userInfo(), EOEditingContext.InsertedKey); NSArray<T> releventsUpdatedEOs = relevantChanges(n.userInfo(), EOEditingContext.UpdatedKey); NSArray<T> releventsDeletedEOs = relevantChanges(n.userInfo(), EOEditingContext.DeletedKey); if (_resetOnChange) { if (releventsInsertedEOs.count() > 0 || releventsUpdatedEOs.count() > 0 || releventsDeletedEOs.count() > 0) { reset(); } } else { ERXExpiringCache<Object, EORecord<T>> cache = cache(); synchronized (cache) { for (T eo : releventsInsertedEOs) { addObject(eo); } for (T eo : releventsUpdatedEOs) { updateObject(eo); } for (T eo : releventsDeletedEOs) { removeObject(eo); } } } } } /** * Handler for the clearCaches notification. Calls reset if n.object is the name of the entity we are caching. * Other code can send this notification if it needs to have this cache discard all of the objects. * * @see #ClearCachesNotification * * @param n NSNotification with an entity name */ public void clearCaches(NSNotification n) { if(n.object() == null || entityName().equals(n.object())) { reset(); } } /** * @return the name of the EOEntity this cache is for */ protected String entityName() { return _entityName; } /** * @return Key path to data uniquely identifying an instance of the entity in this cache */ protected String keyPath() { return _keyPath; } /** * Returns the backing cache. If the cache is to old, it is cleared first. The cache is created if needed, * and the contents populated if <code>_fetchInitialValues</code>. * @return the backing cache */ protected synchronized ERXExpiringCache<Object, EORecord<T>> cache() { if(_cache == null) { if (_fetchInitialValues) { _cache = new ERXExpiringCache<Object, EORecord<T>>(ERXExpiringCache.NO_TIMEOUT); } else { _cache = new ERXExpiringCache<Object, EORecord<T>>(_timeout); if (_timeout > 0) { _cache.startBackgroundExpiration(); } } preLoadCacheIfNeeded(); } // If initial values are fetched, the entire cache expires at the same time long now = System.currentTimeMillis(); if(_fetchInitialValues && _timeout > 0L && (now - _timeout) > _fetchTime) { reset(); } return _cache; } /** * Created an EORecord instance representing eo using its EOGlobalID. If <code>_retainObjects</code>, * this will also include an instance of the EO to ensure that the snapshot is retained. * * @param gid EOGlobalID of eo * @param eo the EO to make an EORecord for * @return EORecord instance representing eo */ protected EORecord<T> createRecord(EOGlobalID gid, T eo) { EORecord<T> record; if (_retainObjects) { EOEditingContext editingContext = editingContext(); T localEO = ERXEOControlUtilities.localInstanceOfObject(editingContext, eo); if (localEO != null && localEO.isFault()) { localEO.willRead(); } record = new EORecord<T>(gid, localEO); } else { record = new EORecord<T>(gid, null); } return record; } /** * Loads all relevant objects into the cache if set to fetch initial values. */ protected void preLoadCacheIfNeeded() { if (_fetchInitialValues) { _fetchTime = System.currentTimeMillis(); ERXEC ec = editingContext(); ec.setCoalesceAutoLocks(false); // The other methods are synchronized on cache and then lock the EC. If we do it backwards, // we can get a deadly embrace. synchronized (cache()) { ec.lock(); // Prevents lock churn try { ERXFetchSpecification fetchSpec = new ERXFetchSpecification(entityName(), qualifier(), null); fetchSpec.setRefreshesRefetchedObjects(true); fetchSpec.setIsDeep(true); NSArray objects = ec.objectsWithFetchSpecification(fetchSpec); for (Enumeration enumeration = objects.objectEnumerator(); enumeration.hasMoreElements();) { T eo = (T) enumeration.nextElement(); addObject(eo); } } finally { ec.unlock(); if ( ! _reuseEditingContext) { ec.dispose(); } } } } } /** * Add an object to the cache using <code>eo.valueForKeyPath(keyPath())</code> as the key. * @see #addObjectForKey(EOEnterpriseObject, Object) * @param eo the object to add to the cache */ public void addObject(T eo) { Object key = eo.valueForKeyPath(keyPath()); if (key == null) { key = NSKeyValueCoding.NullValue; } addObjectForKey(eo, key); } /** * Add an object to the cache with the given key if it matches the qualifier, or * if there is no qualifier. The object can be null, in which case a place holder * is added. * @param eo eo the object to add to the cache * @param key the key to add the object under */ public void addObjectForKey(T eo, Object key) { if (qualifier() == null || qualifier().evaluateWithObject(eo)) { EOGlobalID gid = NO_GID_MARKER; if(eo != null) { gid = eo.editingContext().globalIDForObject(eo); } cache().setObjectForKey(createRecord(gid, eo), key); } } /** * Removes an object from the cache using <code>eo.valueForKeyPath(keyPath())</code> as the key. * @see #removeObjectForKey(EOEnterpriseObject, Object) * @param eo the object to remove from the cache */ public void removeObject(T eo) { Object key = eo.valueForKeyPath(keyPath()); if (key == null) { key = NSKeyValueCoding.NullValue; } removeObjectForKey(eo, key); } /** * Removes the object associated with key from the cache. * * @param eo eo the object to remove from the cache (ignored) * @param key the key to remove the object for */ public void removeObjectForKey(T eo, Object key) { cache().setObjectForKey(createRecord(NO_GID_MARKER, null), key); } /** * Updates an object in the cache (adding if not present) using * <code>eo.valueForKeyPath(keyPath())</code> as the key. * @see #updateObjectForKey(EOEnterpriseObject, Object) * @param eo the object to update in the cache */ public void updateObject(T eo) { Object key = eo.valueForKeyPath(keyPath()); if (key == null) { key = NSKeyValueCoding.NullValue; } updateObjectForKey(eo, key); } /** * Updates an object in the cache (adding if not present) with the given key if it * matches the qualifier, or if there is no qualifier. The object can be null, in which * case is it removed from the cache. If <code>qualifier()</code> is not null, the object * is removed from the cache if it does not match the qualifier. * @param eo eo the object to update in the cache * @param key the key of the object to update */ public void updateObjectForKey(T eo, Object key) { EOGlobalID gid = NO_GID_MARKER; if(eo != null) { gid = eo.editingContext().globalIDForObject(eo); } ERXExpiringCache<Object, EORecord<T>> cache = cache(); synchronized (cache) { Object previousKey = key; T previousObject = objectForKey(editingContext(), key, false); // If the object does not exist under key, or a different object exists under that key, // the key value may have been changed. Search the entire cache for the object by GID if (previousObject == null || ! previousObject.editingContext().globalIDForObject(previousObject).equals(gid)) { previousKey = null; for (Object entryKey : cache.allKeys()) { EORecord<T> entryValue = cache.objectForKey(entryKey); if (entryValue != null && entryValue.gid.equals(gid)) { previousKey = entryKey; break; } } } if (previousKey != null) { if (!previousKey.equals(key)) { removeObjectForKey(eo, previousKey); addObjectForKey(eo, key); } else if (qualifier() != null && !qualifier().evaluateWithObject(eo)) { removeObjectForKey(eo, previousKey); } else { // leave it alone, the key value has not changed and EOF will take care of the rest } } else { addObjectForKey(eo, key); } } } /** * Retrieves an EO that matches the given key. If there is no match in the * cache, it attempts to fetch the missing objects. Null is returned if no matching * object can be fetched. * @param ec editing context to get the object into * @param key key value under which the object is registered * @return the matching object */ public T objectForKey(EOEditingContext ec, Object key) { return objectForKey(ec, key, true); } /** * Retrieves an EO that matches the given key. If there is no match in the * cache, and <code>_returnUnsavedObjects</code> is <code>true</code>, * it attempts to find and return an unsaved object. If there is still no match * and <code>handleUnsuccessfulQueryForKey</code> is <code>true</code>, * it attempts to fetch the missing objects. Null is returned if * <code>handleUnsuccessfulQueryForKey</code> is <code>false</code> or no matching * object can be fetched. * @param ec editing context to get the object into * @param key key value under which the object is registered * @param handleUnsuccessfulQueryForKey if false, a cache miss returns null rather than fetching * @return the matching object */ public T objectForKey(EOEditingContext ec, Object key, boolean handleUnsuccessfulQueryForKey) { ERXExpiringCache<Object, EORecord<T>> cache = cache(); EORecord<T> record = cache.objectForKey(key); if (record == null) { if (handleUnsuccessfulQueryForKey) { if (_returnUnsavedObjects) { T unsavedMatchingObject = unsavedMatchingObject(ec, key); if (unsavedMatchingObject != null) { return unsavedMatchingObject; } } handleUnsuccessfullQueryForKey(key); record = cache.objectForKey(key); if (record == null) { return null; } else if (record.gid == NO_GID_MARKER) { return null; } } else { return null; } } else if (record.gid == NO_GID_MARKER) { return null; } T eo = record.eo; if (eo == null) { eo = (T) ERXEOGlobalIDUtilities.fetchObjectWithGlobalID(ec, record.gid); } else { eo = ERXEOControlUtilities.localInstanceOfObject(ec, eo); } return eo; } /** * Looks in ec for an newly inserted (unsaved) EO that matches the given key. ONLY ec is examined. * Null is returned if no matching * * @param ec editing context to search for unsaved, matching objects * @param key key value to identify the unsaved object * @return the matching object or null if not found */ public T unsavedMatchingObject(EOEditingContext ec, Object key) { NSArray matchingObjects = EOQualifier.filteredArrayWithQualifier(ec.insertedObjects(), ERXQ.equals("entityName", _entityName)); matchingObjects = EOQualifier.filteredArrayWithQualifier(matchingObjects, fetchObjectsQualifier(key)); if (matchingObjects.count() > 1) { throw new EOUtilities.MoreThanOneException("There was more than one " + _entityName + " with the key '" + key + "'."); } return matchingObjects.count() == 1 ? (T)matchingObjects.lastObject() : null; } /** * Returns a list of all the objects currently in the cache and not yet expired. * @param ec editing context to get the objects into * @return all objects currently in the cache and unexpired */ public NSArray<T> allObjects(EOEditingContext ec) { return allObjects(ec, null); } /** * Returns a list of all the objects currently in the cache and not yet expired which match additionalQualifier. * @param ec editing context to get the objects into * @param additionalQualifier qualifier to restrict which objects are returned from the cache * @return all objects currently in the cache and unexpired */ public NSArray<T> allObjects(EOEditingContext ec, EOQualifier additionalQualifier) { additionalQualifier = ERXEOControlUtilities.localInstancesInQualifier(ec, additionalQualifier); ERXExpiringCache<Object, EORecord<T>> cache = cache(); NSArray allKeys = cache.allKeys(); NSMutableArray<T> allObjects = new NSMutableArray<>(allKeys.count()); for (Object entryKey : allKeys) { T object = objectForKey(ec, entryKey, false); if (object != null && (additionalQualifier == null || additionalQualifier.evaluateWithObject(object))) { allObjects.addObject(object); } } return allObjects; } /** * Called when a query hasn't found an entry in the cache. This * implementation puts a not-found marker in the cache so * the next query will return null. If <code>_fetchInitialValues</code> * is <code>false</code>, it will attempt to fetch the missing object and * adds it to the cache if it is found. * call {@link #addObject(EOEnterpriseObject)} on it. * @param key the key of the object that was not found in the cache */ protected void handleUnsuccessfullQueryForKey(Object key) { if (!_fetchInitialValues) { ERXExpiringCache cache = cache(); // The other methods are synchronized on cache and then lock the EC. If we do it backwards, // we can get a deadly embrace. synchronized (cache) { ERXEC editingContext = editingContext(); editingContext.lock(); try { NSArray<T> objects = fetchObjectsForKey(editingContext, key); if (objects.count() == 0) { cache.setObjectForKey(createRecord(NO_GID_MARKER, null), key); } else if (objects.count() == 1) { T eo = objects.objectAtIndex(0); addObject(eo); } else { throw new EOUtilities.MoreThanOneException("There was more than one " + _entityName + " with the key '" + key + "'."); } } finally { editingContext.unlock(); if (!_reuseEditingContext) { editingContext.dispose(); } } } } else { cache().setObjectForKey(createRecord(NO_GID_MARKER, null), key); } } /** * Actually performs a fetch for the given key. Override this method to implement * custom fetch rules. * * @param editingContext the editing context to fetch in * @param key the key to fetch with * @return the fetch objects */ protected NSArray<T> fetchObjectsForKey(EOEditingContext editingContext, Object key) { EOQualifier qualifier = fetchObjectsQualifier(key); ERXFetchSpecification fetchSpec = new ERXFetchSpecification(_entityName, qualifier, null); fetchSpec.setRefreshesRefetchedObjects(true); fetchSpec.setIsDeep(true); NSArray<T> objects = editingContext.objectsWithFetchSpecification(fetchSpec); return objects; } /** * Returns the additional qualifier for this cache. * @return the additional qualifier for this cache */ public EOQualifier qualifier() { return _qualifier; } /** * Returns the qualifier to use during for fetching: the value for keyPath matches key * AND qualifier() (if not null). * @param key the key to fetch * @return the qualifier to use */ protected EOQualifier fetchObjectsQualifier(Object key) { EOQualifier qualifier; if (qualifier() == null) { qualifier = ERXQ.is(_keyPath, key); } else { qualifier = ERXQ.is(_keyPath, key).and(qualifier()); } return qualifier; } /** * Resets the cache by clearing the internal map. The values are refreshed right away if * <code>_fetchInitialValues</code> is <code>true</code>, otherwise they are re-loaded on demand. * * @see #preLoadCacheIfNeeded() */ public synchronized void reset() { if (_cache != null) { _cache.removeAllObjects(); preLoadCacheIfNeeded(); } } /** * Sets whether or not the initial values should be fetched into * this cache or whether it should lazy load. If turned off, resetOnChange * will also be turned off. * * @param fetchInitialValues if true, the initial values are fetched into the cache */ public void setFetchInitialValues(boolean fetchInitialValues) { _fetchInitialValues = fetchInitialValues; if (!fetchInitialValues) { setResetOnChange(false); } } /** * Sets whether or not the editing context for this cache is reused for multiple requests. * * @param reuseEditingContext whether or not the editing context for this cache is reused for multiple requests */ public void setReuseEditingContext(boolean reuseEditingContext) { if (_retainObjects && !reuseEditingContext) { throw new IllegalArgumentException("If retainObjects is true, reuseEditingContext cannot be false."); } _reuseEditingContext = reuseEditingContext; } /** * Sets whether or not the cached EO's themselves are retained versus just their GID's. If set, * this implicitly sets reuseEditingContext to true. * * @param retainObjects if true, the EO's are retained */ public void setRetainObjects(boolean retainObjects) { if (retainObjects && ! ERXEC.defaultAutomaticLockUnlock()) { throw new RuntimeException("ERXEnterpriseObjectCache requires automatic locking when objects are retained. " + "Set er.extensions.ERXEC.defaultAutomaticLockUnlock or " + "er.extensions.ERXEC.safeLocking in your Properties file"); } _retainObjects = retainObjects; setReuseEditingContext(retainObjects); } /** * Sets whether or not the cache is cleared when any change occurs. This requires fetching initial values (and will * be turned on if you set this) * * @param resetOnChange if true, the cache will clear on changes; if false, the cache will update on changes */ public void setResetOnChange(boolean resetOnChange) { _resetOnChange = resetOnChange; if (_resetOnChange) { setFetchInitialValues(true); } } private static class EORecord<T> { public EOGlobalID gid; public T eo; public EORecord(@SuppressWarnings("hiding") EOGlobalID gid, @SuppressWarnings("hiding") T eo) { this.gid = gid; this.eo = eo; } } }