package er.extensions.eof;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
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.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSNotification;
import com.webobjects.foundation.NSNotificationCenter;
import com.webobjects.foundation.NSSelector;
import er.extensions.foundation.ERXSelectorUtilities;
/**
* Caches objects of one entity by a given key. Listens to
* EOEditingContextDidSaveChanges notifications to track changes.
* Typically you'd fetch values by:<pre><code>
* ERXEnterpriseObjectArrayCache<HelpText> helpTextCache = new ERXEnterpriseObjectArrayCache<HelpText>("HelpText") {
* protected void handleUnsuccessfullQueryForKey(Object key) {
* NSArray helpTexts = ... fetch from somewhere
* setObjectsForKey(helpTexts, key);
* }
* };
* ...
* NSArray<HelpText> helpTexts = helpTextCache.objectsForKey(ec, "AllTexts");
* ...
* </code></pre>
* You can supply a timeout after which the cache is to get cleared and all the objects refetched. Note
* that this implementation only caches the global IDs, not the actual data.
* @author ak
* @param <T>
*/
public class ERXEnterpriseObjectArrayCache<T extends EOEnterpriseObject> {
private String _entityName;
private Map<Object, NSArray<EOGlobalID>> _cache;
private long _timeout;
private long _fetchTime;
public static class NotFoundArray extends NSArray {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
}
protected static final NSArray NOT_FOUND_MARKER= new NotFoundArray();
/**
* Creates the cache for the given entity name and the given keypath. No
* timeout value is used.
* @param entityName
*/
public ERXEnterpriseObjectArrayCache(String entityName) {
this(entityName, 0L);
}
/**
* Creates the cache for the given entity name and the given keypath. No
* timeout value is used.
*/
public ERXEnterpriseObjectArrayCache(Class c) {
this(entityNameForClass(c));
}
protected static String entityNameForClass(Class c) {
EOEditingContext ec = ERXEC.newEditingContext();
ec.lock();
try {
EOEntity entity = EOUtilities.entityForClass(ec, c);
if(entity != null) {
return entity.name();
}
return null;
} finally {
ec.unlock();
}
}
/**
* Creates the cache for the given entity, keypath and timeout value in milliseconds.
* @param entityName
* @param timeout
*/
public ERXEnterpriseObjectArrayCache(String entityName, long timeout) {
_entityName = entityName;
_timeout = timeout;
registerForNotifications();
}
protected void registerForNotifications() {
NSSelector selector = ERXSelectorUtilities.notificationSelector("editingContextDidSaveChanges");
NSNotificationCenter.defaultCenter().addObserver(this, selector,
EOEditingContext.EditingContextDidSaveChangesNotification, null);
selector = ERXSelectorUtilities.notificationSelector("clearCaches");
NSNotificationCenter.defaultCenter().addObserver(this, selector,
ERXEnterpriseObjectCache.ClearCachesNotification, null);
}
/**
* Helper to check if an array of EOs contains the handled entity.
* @param eos
*/
private boolean hadRelevantChanges(NSDictionary dict, String key) {
NSArray<EOEnterpriseObject> eos = (NSArray<EOEnterpriseObject>) dict.objectForKey(key);
for (Enumeration<EOEnterpriseObject> enumeration = eos.objectEnumerator(); enumeration.hasMoreElements();) {
EOEnterpriseObject eo = enumeration.nextElement();
if(eo.entityName().equals(entityName())) {
return true;
}
}
return false;
}
/**
* Handler for the editingContextDidSaveChanges notification. Calls reset if
* and object of the given entity were changed.
* @param n
*/
public void editingContextDidSaveChanges(NSNotification n) {
EOEditingContext ec = (EOEditingContext) n.object();
if(ec.parentObjectStore() instanceof EOObjectStoreCoordinator) {
if(!hadRelevantChanges(n.userInfo(), EOEditingContext.InsertedKey)) {
if(!hadRelevantChanges(n.userInfo(), EOEditingContext.UpdatedKey)) {
if(!hadRelevantChanges(n.userInfo(), EOEditingContext.DeletedKey)) {
return;
}
}
}
reset();
}
}
/**
* Handler for the clearCaches notification. Calls reset if
* n.object is the entity name.
* @param n
*/
public void clearCaches(NSNotification n) {
if(n.object() == null || entityName().equals(n.object())) {
reset();
}
}
protected String entityName() {
return _entityName;
}
/**
* Returns the backing cache. If the cache is to old, it is cleared first.
*/
private synchronized Map cache() {
long now = System.currentTimeMillis();
if(_timeout > 0L && (now - _timeout) > _fetchTime) {
reset();
}
if(_cache == null) {
_cache = Collections.synchronizedMap(new HashMap());
_fetchTime = System.currentTimeMillis();
}
return _cache;
}
/**
* Add a list of objects to the cache with the given key. The object
* can be null.
* @param bugs array of objects
*/
public void setObjectsForKey(NSArray<? extends EOEnterpriseObject> bugs, Object key) {
NSArray<EOGlobalID> gids = NOT_FOUND_MARKER;
if(bugs != null) {
gids = ERXEOControlUtilities.globalIDsForObjects(bugs);
}
setCachedArrayForKey(gids, key);
}
protected void setCachedArrayForKey(NSArray<EOGlobalID> gids, Object key) {
cache().put(key, gids);
}
protected NSArray<EOGlobalID> cachedArrayForKey(Object key) {
return (NSArray<EOGlobalID>) cache().get(key);
}
/**
* Retrieves a list of EOs that matches the given key or null if no match
* is in the cache.
* @param ec editing context to get the objects into
* @param key key value under which the objects are registered
*/
public NSArray<T> objectsForKey(EOEditingContext ec, Object key) {
synchronized (this) {
NSArray<EOGlobalID> gids = cachedArrayForKey(key);
if(isNotFound(gids)) {
return null;
} else if(gids == null) {
handleUnsuccessfullQueryForKey(key);
gids = cachedArrayForKey(key);
if(isNotFound(gids)) {
return null;
} else if(gids == null) {
return null;
}
}
NSArray<T> eos = ERXEOControlUtilities.faultsForGlobalIDs(ec, gids);
return eos;
}
}
protected boolean isNotFound(NSArray<EOGlobalID> gids) {
return gids != null ? NotFoundArray.class == gids.getClass() : false;
}
/**
* 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. You could override this
* method to create an EO with sensible default values and
* call {@link #setObjectsForKey(NSArray, Object)} on it.
* @param key
*/
protected void handleUnsuccessfullQueryForKey(Object key) {
setCachedArrayForKey(NOT_FOUND_MARKER, key);
}
/**
* Resets the cache by clearing the internal map. When the next value
* is accessed, the objects are refetched.
*/
public synchronized void reset() {
_cache = null;
}
protected long timeout() {
return _timeout;
}
protected long fetchTime() {
return _fetchTime;
}
}