Package er.extensions.eof

Source Code of er.extensions.eof.ERXEnterpriseObjectCache

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.appserver.ERXApplication;
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:<code><pre>
* ERXEnterpriseObjectCache&lt;HelpText&gt; helpTextCache = new ERXEnterpriseObjectCache&lt;HelpText&gt;("HelpText", "pageConfiguration");
* ...
* HelpText helpText = helpTextCache.objectForKey(ec, "ListHelpText");
* </pre></code>
*
* 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 ERXApplication#ApplicationDidFinishInitializationNotification
     */
    private 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
        _applicationDidFinishInitialization = false;
        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;
        _applicationDidFinishInitialization = false;
        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 disable caching before application did finish to start (and most importantly processed migrations)
    NSSelector selector = ERXSelectorUtilities.notificationSelector("enableFetchingOfInitialValues");
    NSNotificationCenter.defaultCenter().addObserver(this, selector,
            ERXApplication.ApplicationDidFinishInitializationNotification, null);
   
    // Catch this to update the cache when an object is changed
    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, ERXApplication.ApplicationDidFinishInitializationNotification, null);
    NSNotificationCenter.defaultCenter().removeObserver(this, EOEditingContext.EditingContextDidSaveChangesNotification, null);
    NSNotificationCenter.defaultCenter().removeObserver(this, ERXEnterpriseObjectCache.ClearCachesNotification, null);
      _cache.stopBackgroundExpiration();
  }
   
  /**
   * 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 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.
     * @param n
     */
    public void clearCaches(NSNotification n) {
      if(n.object() == null || entityName().equals(n.object())) {
        reset();
      }
    }
   
    /**
    * Handler for the ApplicationDidFinishInitializationNotification notification. Enables the fetching of initial
    * values and such ensure that any migrations have been processed before.
    * @param n notification that is fired in ERXApplication.finishInitialization
    */
    public void enableFetchingOfInitialValues(NSNotification n) {
        _applicationDidFinishInitialization = true;
        NSNotificationCenter.defaultCenter().removeObserver(this,
        ERXApplication.ApplicationDidFinishInitializationNotification, null);
    }
   
    /**
     * @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 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;
      }
    }
}
TOP

Related Classes of er.extensions.eof.ERXEnterpriseObjectCache

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.