# 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

