# Adapted from Cache.py,v 1.10 2002/08/14 21:42:56 mj Exp # Portions Copyright (c) 2001 Zope Corporation and Contributors. # All Rights Reserved. __doc__ = """Cacheable object and cache management base classes. $Id$""" __version__ = "$Revision$"[11:-2] import time, weakref from AccessControl import ClassSecurityInfo, getSecurityManager, Unauthorized from AccessControl.Role import _isBeingUsedAsAMethod from AccessControl.ZopeGuards import guarded_getattr from Acquisition import aq_acquire, aq_base, aq_get, aq_inner, aq_parent from Globals import DTMLFile, InitializeClass from zLOG import LOG, WARNING ZCM_MANAGERS = "__ZCacheManager_ids__" CHANGE_PERM = "Change cache settings" MANAGE_PERM = "View management screens" # Anytime a CacheManager is added or removed, all _v_ZCacheable_cache # attributes must be invalidated; manager_timestamp is a way to do that. manager_timestamp = 0 def isCacheable(ob): return getattr(aq_base(ob), "_isCacheable", False) def filterCacheTab(ob): # Show "Cache" tab only when appropriate. if _isBeingUsedAsAMethod(ob): return isCacheable(aq_parent(aq_inner(ob))) else: return bool(aq_get(ob, ZCM_MANAGERS, None, 1)) def filterCacheManagers(orig, container, name, value, extra): # A filter method for aq_acquire to return only cache managers. return bool(hasattr(aq_base(container), ZCM_MANAGERS) and name in getattr(container, ZCM_MANAGERS)) def getVerifiedManagerIds(container): # Returns a list of cache managers in a container, verifying each one. return tuple([i for i in getattr(container, ZCM_MANAGERS, ()) \ if getattr(getattr(container, i, None), "_isCacheManager", False)]) def findCacheables(ob, manager_id, associated_only, recurse, meta_types, cacheables, path=()): # Recursively finds all Cacheable objects in a hierarchy. # Used by the CacheManager UI. try: objectValues = guarded_getattr(ob, "objectValues") except (Unauthorized, AttributeError): return sm = getSecurityManager() objects = objectValues(*(meta_types and (meta_types,) or ())) cacheables_append = cacheables.append for o in objects: if not isCacheable(o): continue associated = (o.ZCacheable_getManagerId() == manager_id) if associated_only and not associated or \ not sm.checkPermission(CHANGE_PERM, o): continue opath = path + (o.getId(),) o = aq_base(o) cacheables_append({ "sortkey":opath, "path":'/'.join(opath), "title":getattr(o, "title", ''), "icon":getattr(o, "icon", ''), "associated":associated, }) if recurse: for o in meta_types and objectValues() or objects: if hasattr(aq_base(o), "objectValues"): findCacheables(o, manager_id, associated_only, recurse, meta_types, cacheables, path + (o.getId(),)) class CacheException(Exception): """Exception type raised when a recoverable problem is encountered""" # Subclass and use me in your cache implementations! class Cacheable: """Mix-in for cacheable objects.""" security = ClassSecurityInfo() __manager_id = None __enabled = True _v_ZCacheable_cache = None _v_ZCacheable_manager_timestamp = 0 _isCacheable = True manage_options = ({ "label":"Cache", "action":"ZCacheable_manage", "filter":filterCacheTab, "help":("OFSP","Cacheable-properties.stx"), },) security.declarePrivate("ZCacheable_isCachingEnabled") def ZCacheable_isCachingEnabled(self): return self.__enabled and self.ZCacheable_getCache() security.declarePrivate("ZCacheable_getManager") def ZCacheable_getManager(self): # Returns the currently associated cache manager. manager_id = self.__manager_id if manager_id is None: return None try: return aq_acquire(self, manager_id, filter=filterCacheManagers, extra=None, default=None, containment=1) except AttributeError: return None security.declarePrivate("ZCacheable_getCache") def ZCacheable_getCache(self): # Return the cache associated with this object. if self.__manager_id is None: return None c = self._v_ZCacheable_cache if c is not None and \ self._v_ZCacheable_manager_timestamp == manager_timestamp: return aq_base(c) manager = self.ZCacheable_getManager() if manager is None: return None c = aq_base(manager.ZCacheManager_getCache()) self._v_ZCacheable_cache = c self._v_ZCacheable_manager_timestamp = manager_timestamp return c security.declarePrivate("ZCacheable_getModTime") def ZCacheable_getModTime(self, mtime_func=None): # Returns the most recent last modification time between mtime_func(), # self.mtime, and self.__class__.mtime. If applied to a ZClass # zclass_instance.mtime and zclass_instance.__class__.mtime are # also included in the possibilities. mtime = 0 if mtime_func: mtime = mtime_func() base = aq_base(self) mtime = max(getattr(base, "_p_mtime", mtime), mtime) klass = getattr(base, "__class__", None) if klass: mtime = max(getattr(klass, "_p_mtime", mtime), mtime) if self.ZCacheable_isAMethod(): base = aq_base(aq_parent(aq_inner(self))) mtime = max(getattr(base, "_p_mtime", mtime), mtime) klass = getattr(base, "__class__", None) if klass: mtime = max(getattr(klass, "_p_mtime", mtime), mtime) return mtime security.declarePrivate("ZCacheable_getObAndView") def ZCacheable_getObAndView(self, view_name): # If this object is a method of a ZClass and we're working with the # primary view, uses the ZClass instance as ob and our own ID as the # view_name. Otherwise returns self and view_name unchanged. ob = self if not view_name and self.ZCacheable_isAMethod(): ob = aq_parent(aq_inner(self)) if isCacheable(ob): view_name = self.getId() else: ob = self return ob, view_name security.declarePrivate("ZCacheable_get") def ZCacheable_get(self, view_name='', keywords=None, mtime_func=None, default=None): # Returns the cached view or default if the cached view isn't available # or is otherwise inappropriate. c = self.ZCacheable_getCache() if c is not None and self.__enabled: ob, view_name = self.ZCacheable_getObAndView(view_name) try: return c.ZCache_get(ob, view_name=view_name, keywords=keywords, mtime_func=mtime_func, default=default) except weakref.ReferenceError: self._v_ZCacheable_cache = None except CacheException, e: LOG("Cache", WARNING, e) return default security.declarePrivate("ZCacheable_set") def ZCacheable_set(self, data, view_name='', keywords=None, mtime_func=None): # Cacheable objects should call this method after generating cacheable # results. The data argument may be of any Python type. c = self.ZCacheable_getCache() if c is not None and self.__enabled: ob, view_name = self.ZCacheable_getObAndView(view_name) try: c.ZCache_set(ob, data, view_name=view_name, keywords=keywords, mtime_func=mtime_func) except weakref.ReferenceError: self._v_ZCacheable_cache = None except CacheException, e: LOG("Cache", WARNING, e) security.declareProtected(MANAGE_PERM, "ZCacheable_manage") ZCacheable_manage = DTMLFile("dtml/cacheable", globals()) security.declareProtected(MANAGE_PERM, "ZCacheable_isAMethod") def ZCacheable_isAMethod(self): """Returns true if this object is a ZClass method.""" return _isBeingUsedAsAMethod(self) security.declareProtected(MANAGE_PERM, "ZCacheable_enabled") def ZCacheable_enabled(self): """Returns true if caching is enabled for this object or method.""" return self.__enabled security.declareProtected(MANAGE_PERM, "ZCacheable_invalidate") def ZCacheable_invalidate(self, view_name='', REQUEST=None): """Removes all cache entries that apply to this object or method. Returns a status message. """ message = "This object is not associated with a cache manager." c = self.ZCacheable_getCache() if c is not None: ob, view_name = self.ZCacheable_getObAndView(view_name) try: message = c.ZCache_invalidate(ob) if not message: message = "Invalidated." except weakref.ReferenceError: message = "Invalidated." self._v_ZCacheable_cache = None except CacheException, e: message = "An error occurred: %s" % e LOG("Cache", WARNING, e) return REQUEST is None and message or \ self.ZCacheable_manage(self, REQUEST, management_view="Cache", manage_tabs_message=message) security.declareProtected(MANAGE_PERM, "ZCacheable_getManagerURL") def ZCacheable_getManagerURL(self): """Returns the URL of the current ZCacheManager.""" manager = self.ZCacheable_getManager() return manager is not None and manager.absolute_url() or None security.declareProtected(MANAGE_PERM, "ZCacheable_getManagerId") def ZCacheable_getManagerId(self): """Returns the id of the current ZCacheManager.""" return self.__manager_id security.declareProtected(MANAGE_PERM, "ZCacheable_getManagerIds") def ZCacheable_getManagerIds(self): """Returns a list of mappings containing the id and title of the available cache managers. """ managers = [] seen = {} ob = self while ob is not None: if hasattr(aq_base(ob), ZCM_MANAGERS): for mid in getattr(ob, ZCM_MANAGERS): manager = getattr(ob, mid, None) if manager is not None: mid = manager.getId() if not seen.has_key(mid): seen[mid] = getattr(aq_base(manager), "title", '') managers.append({"id":mid, "title":seen[mid]}) ob = aq_parent(aq_inner(ob)) return tuple(managers) security.declareProtected(CHANGE_PERM, "ZCacheable_setManagerId") def ZCacheable_setManagerId(self, manager_id, REQUEST=None): """Change the manager_id for this object or method. Returns a status message. """ self.ZCacheable_invalidate() self._v_ZCacheable_cache = None self.__manager_id = manager_id and str(manager_id) or None message = "Cache settings changed." return REQUEST is None and message or \ self.ZCacheable_manage(self, REQUEST, management_view="Cache", manage_tabs_message=message) security.declareProtected(CHANGE_PERM, "ZCacheable_setEnabled") def ZCacheable_setEnabled(self, enabled=0, REQUEST=None): """Change the enabled flag; generally only used with ZClass methods. Returns a status message. """ self.__enabled = bool(enabled) message = "Cache settings changed." return REQUEST is None and message or \ self.ZCacheable_manage(self, REQUEST, management_view="Cache", manage_tabs_message=message) security.declareProtected(MANAGE_PERM, "ZCacheable_configHTML") def ZCacheable_configHTML(self): """Cacheable objects which wish to provide per-object cache behavior may override this method to include additional markup in the results of the ZCacheable_manage method. """ return '' InitializeClass(Cacheable) class CacheManager: """Mix-in for cache managers.""" security = ClassSecurityInfo() _isCacheManager = True manage_options = ({ "label":"Associate", "action":"ZCacheManager_associate", "help":("OFSP","CacheManager-associate.stx"), },) security.declarePrivate("ZCacheManager_getCache") def ZCacheManager_getCache(self): raise NotImplementedError # Implement me in your cache manager! Should return the Cache-type # object (or a weakref ProxyType to one) that this manager represents. security.declarePrivate("manage_afterAdd") def manage_afterAdd(self, item, container): # Adds self to the list of cache managers in the container. if aq_base(self) is aq_base(item): ids = getVerifiedManagerIds(container) mid = self.getId() if mid not in ids: setattr(container, ZCM_MANAGERS, ids + (mid,)) global manager_timestamp manager_timestamp = time.time() security.declarePrivate("manage_beforeDelete") def manage_beforeDelete(self, item, container): # Removes self from the list of cache managers. if aq_base(self) is aq_base(item): ids = getVerifiedManagerIds(container) mid = self.getId() if mid in ids: setattr(container, ZCM_MANAGERS, filter(lambda i, mid=mid: i != mid, ids)) global manager_timestamp manager_timestamp = time.time() security.declareProtected(CHANGE_PERM, "ZCacheManager_associate") ZCacheManager_associate = DTMLFile("dtml/cmassoc", globals()) security.declareProtected(CHANGE_PERM, "ZCacheManager_locate") def ZCacheManager_locate(self, require_assoc, subfolders, meta_types=[], REQUEST=None): """Returns a list of cacheable objects.""" cacheables = [] if '' in meta_types: meta_types = [] # user selected "All" findCacheables(aq_parent(aq_inner(self)), self.getId(), require_assoc, subfolders, meta_types, cacheables) if REQUEST is None: return cacheables else: return self.ZCacheManager_associate(self, REQUEST, show_results=1, results=cacheables, management_view="Associate") security.declareProtected(CHANGE_PERM, "ZCacheManager_setAssociations") def ZCacheManager_setAssociations(self, props=None, REQUEST=None): """Associates and disassociates objects with this cache manager. Returns a status message. """ added = removed = 0 parent = aq_parent(aq_inner(self)) sm = getSecurityManager() mid = str(self.getId()) if props is None: props = REQUEST.form for key, associate in props.items(): if key.startswith("associate_"): ob = parent.restrictedTraverse(key[10:]) if not sm.checkPermission(CHANGE_PERM, ob): raise Unauthorized if isCacheable(ob): ours = (str(ob.ZCacheable_getManagerId()) == mid) if associate and not ours: ob.ZCacheable_setManagerId(mid) added += 1 elif not associate and ours: ob.ZCacheable_setManagerId(None) removed += 1 message = "%d association(s) made, %d removed." % (added, removed) return REQUEST is None and message or \ self.ZCacheManager_associate(self, REQUEST, management_view="Associate", manage_tabs_message=message) InitializeClass(CacheManager) class Cache: """A base class and interface description for caches. Note: Cache objects are not intended to be visible by restricted code. """ # When implementing the following methods of your cache, you may raise # exceptions based on CacheException to indicate a recoverable error # occured; the exception value will be logged as a problem, but execution # will continue. (For example ZCache_set() might raise a CacheException # if its unable to cache the type data provided, indicating in the # exception value that data of that type shouldn't be associated with the # cache's manager.) def ZCache_invalidate(self, ob): # any values pertaining to ob should be removed from the cache raise NotImplementedError def ZCache_get(self, ob, view_name, keywords, mtime_func, default): # return the data associated with ob if cached, otherwise return # default; see ZCache_set() below for a description of the other # paramaters raise NotImplementedError def ZCache_set(self, ob, data, view_name, keywords, mtime_func): # associate data with ob in the cache; view_name, keywords, and # mtime_func may be used to enhance cache behavior: # # view_name: If an object provides different views that would benefit # from caching, it will set view_name. Otherwise view_name will be # an empty string. # # keywords: Either None or a mapping containing keys that distinguish # this cache entry from others even though ob and view_name are the # same. (For example, DTMLMethods may use keywords derived from the # DTML namespace.) # # mtime_func: When (and if) the Cache calls ZCacheable_getModTime(), # it should pass this as an argument. It allow's cacheable objects # to influence calculation of the last modification time. raise NotImplementedError