SVN: CMF/trunk/C - CMFCore.CachingPolicyManager: Implemented the old OFS.Cache.CacheManager

Jens Vagelpohl <[email protected]>
Newsgroups gmane.comp.web.zope.cmf.cvs
Message-ID <20061112221359.79816203251__14223.8879031085$1163369644$gmane$org@mail.zope.org>
Log message for revision 71114:
  - CMFCore.CachingPolicyManager: Implemented the old OFS.Cache.CacheManager
    API. Now objects other than CMF content or CMF templates can have their
    caching headers set by the caching policy manager with the same
    fine-grained control.
    (http://www.zope.org/Collectors/CMF/408)
  

Changed:
  U   CMF/trunk/CHANGES.txt
  U   CMF/trunk/CMFCore/CachingPolicyManager.py
  U   CMF/trunk/CMFCore/event.zcml
  U   CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py

-=-
Modified: CMF/trunk/CHANGES.txt
===================================================================
--- CMF/trunk/CHANGES.txt	2006-11-12 21:41:20 UTC (rev 71113)
+++ CMF/trunk/CHANGES.txt	2006-11-12 22:13:58 UTC (rev 71114)
@@ -2,6 +2,12 @@
 
   New Features
 
+    - CMFCore.CachingPolicyManager: Implemented the old OFS.Cache.CacheManager
+      API. Now objects other than CMF content or CMF templates can have their
+      caching headers set by the caching policy manager with the same 
+      fine-grained control.
+      (http://www.zope.org/Collectors/CMF/408)
+
     - testing: Added test layers for setting up ZCML.
 
     - CMFDefault formlib: Added zope.formlib support.

Modified: CMF/trunk/CMFCore/CachingPolicyManager.py
===================================================================
--- CMF/trunk/CMFCore/CachingPolicyManager.py	2006-11-12 21:41:20 UTC (rev 71113)
+++ CMF/trunk/CMFCore/CachingPolicyManager.py	2006-11-12 22:13:58 UTC (rev 71114)
@@ -21,9 +21,15 @@
 from Globals import DTMLFile
 from Globals import InitializeClass
 from Globals import PersistentMapping
+from OFS.Cache import Cache
+from OFS.Cache import CacheManager
+from OFS.Cache import getVerifiedManagerIds
+from OFS.Cache import ZCM_MANAGERS
+from OFS.interfaces import IObjectWillBeMovedEvent
 from OFS.SimpleItem import SimpleItem
 from Products.PageTemplates.Expressions import getEngine
 from Products.PageTemplates.Expressions import SecureModuleImporter
+from zope.app.container.interfaces import IObjectMovedEvent
 from zope.interface import implements
 
 from permissions import ManagePortal
@@ -34,9 +40,22 @@
 from interfaces.CachingPolicyManager \
         import CachingPolicyManager as z2ICachingPolicyManager
 from utils import _dtmldir
+from utils import _setCacheHeaders
+from utils import _ViewEmulator
 from utils import getToolByName
 
+# This is lame :(
+# This listing is used to decide whether to wrap an object inside a "fake view"
+# for the OFS.Cache caching. If it is a view type, no fake view wrap is needed.
+VIEW_METATYPES = (
+    'Page Template',
+    'DTML Method',
+    'DTML Document',
+    'Filesystem DTML Method',
+    'Filesystem Page Template',
+    )
 
+
 def createCPContext( content, view_method, keywords, time=None ):
     """
         Construct an expression context for TALES expressions,
@@ -66,7 +85,42 @@
 
     return getEngine().getContext( data )
 
+class CPMCache(Cache):
+    """ Simple OFS.Cache-implementation
+    """
+    security = ClassSecurityInfo()
 
+    security.declarePrivate('ZCache_invalidate')
+    def ZCache_invalidate(self, ob):
+        """ An object is forced out of the cache
+
+        This implementation stores nothing and does not attempt to 
+        communicate with cache servers, so this is a no-op.
+        """
+        pass
+
+    security.declarePrivate('ZCache_get')
+    def ZCache_get(self, ob, view_name, keywords, mtime_func, default):
+        """ An object is retrieved from the cache
+
+        This implementation stores nothing - a no-op.
+        """
+        pass
+
+    security.declarePrivate('ZCache_set')
+    def ZCache_set(self, ob, data, view_name, keywords, mtime_func):
+        """ An object is pushed into the cache
+
+        Even though this cache implementation does not cache anything per se,
+        this method is used as a suitable hook to activate the real heavy
+        lifting done by the CachePolicyManager.
+        """
+        if ob.meta_type not in VIEW_METATYPES:
+            ob = _ViewEmulator().__of__(ob)
+
+        return _setCacheHeaders(ob, extra_context={})
+
+
 class CachingPolicy:
     """
         Represent a single class of cachable objects:
@@ -398,7 +452,7 @@
 
 
 
-class CachingPolicyManager( SimpleItem ):
+class CachingPolicyManager( SimpleItem, CacheManager ):
     """
         Manage the set of CachingPolicy objects for the site;  dispatch
         to them from skin methods.
@@ -409,6 +463,7 @@
 
     id = 'caching_policy_manager'
     meta_type = 'CMF Caching Policy Manager'
+    _isCacheManager = 1 # Dead chicken. Yum.
 
     security = ClassSecurityInfo()
 
@@ -425,6 +480,7 @@
                          }
                        ,
                        )
+                     + CacheManager.manage_options
                      + SimpleItem.manage_options
                      )
 
@@ -803,10 +859,47 @@
             
         return None
 
+    #
+    # OFS.CacheManager API
+    #
+    security.declarePrivate('ZCacheManager_getCache')
+    def ZCacheManager_getCache(self):
+        """ Retrieve a cache object
+        """
+        cache = getattr(self, '_cache', None)
 
+        if cache is None:
+            self._cache = CPMCache()
+            cache = self._cache
+
+        return cache
+
+
 InitializeClass( CachingPolicyManager )
 
 
+def handleCachingPolicyManagerEvent(ob, event):
+    """ Event subscriber for (un)registering a CPM as CacheManager
+    """
+    if not ICachingPolicyManager.providedBy(ob):
+        return
+
+    if IObjectMovedEvent.providedBy(event):
+        if event.newParent is not None:
+            ids = getVerifiedManagerIds(event.newParent)
+            id = ob.getId()
+            if id not in ids:
+                setattr(event.newParent, ZCM_MANAGERS, ids + (id,))
+
+    elif IObjectWillBeMovedEvent.providedBy(event):
+        if event.oldParent is not None:
+            ids = list(getVerifiedManagerIds(event.oldParent))
+            id = ob.getId()
+            if id in ids:
+                ids.remove(id)
+                setattr(event.oldParent, ZCM_MANAGERS, tuple(ids))
+
+
 def manage_addCachingPolicyManager( self, REQUEST=None ):
     """
         Add a CPM to self.

Modified: CMF/trunk/CMFCore/event.zcml
===================================================================
--- CMF/trunk/CMFCore/event.zcml	2006-11-12 21:41:20 UTC (rev 71113)
+++ CMF/trunk/CMFCore/event.zcml	2006-11-12 22:13:58 UTC (rev 71114)
@@ -2,6 +2,12 @@
     xmlns="http://namespaces.zope.org/zope">
 
   <subscriber
+      for=".interfaces.ICachingPolicyManager
+           zope.component.interfaces.IObjectEvent"
+      handler=".CachingPolicyManager.handleCachingPolicyManagerEvent"
+      />
+
+  <subscriber
       for=".interfaces.ICookieCrumbler
            zope.component.interfaces.IObjectEvent"
       handler=".CookieCrumbler.handleCookieCrumblerEvent"

Modified: CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py
===================================================================
--- CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py	2006-11-12 21:41:20 UTC (rev 71113)
+++ CMF/trunk/CMFCore/tests/test_CachingPolicyManager.py	2006-11-12 22:13:58 UTC (rev 71114)
@@ -23,8 +23,10 @@
 from os.path import join as path_join
 
 from AccessControl.SecurityManagement import newSecurityManager
+from Acquisition import Implicit
 from App.Common import rfc1123_date
 from DateTime.DateTime import DateTime
+from OFS.Cache import Cacheable
 
 from Products.CMFCore.FSPageTemplate import FSPageTemplate
 from Products.CMFCore.FSDTMLMethod import FSDTMLMethod
@@ -54,7 +56,39 @@
     def modified( self ):
         return self.modified
 
+class CacheableDummyContent(Implicit, Cacheable):
 
+    __allow_access_to_unprotected_subobjects__ = 1
+
+    def __init__(self, id):
+        self.id = id
+        self.modified = DateTime()
+
+    def getId(self):
+        """ """
+        return self.id
+
+    def modified( self ):
+        return self.modified
+
+    def __call__(self):
+        """ """
+        if self.ZCacheable_isCachingEnabled():
+            result = self.ZCacheable_get(default=None)
+            if result is not None:
+                # We will always get None from RAMCacheManager and HTTP
+                # Accelerated Cache Manager but we will get
+                # something implementing the IStreamIterator interface
+                # from a "FileCacheManager"
+                return result
+
+        self.ZCacheable_set(None) 
+
+class DummyView(CacheableDummyContent):
+
+    meta_type = 'DTML Method'
+    
+
 class CachingPolicyTests(unittest.TestCase):
 
     layer = TraversingZCMLLayer
@@ -1173,12 +1207,122 @@
                          )
         
 
+class OFSCacheTests(RequestTest):
+
+    layer = FunctionalZCMLLayer
+
+    def setUp(self):
+        from Products.CMFCore import CachingPolicyManager
+
+        RequestTest.setUp(self)
+
+        # Create a fake portal and the tools we need
+        self.portal = DummySite(id='portal').__of__(self.root)
+        self.portal._setObject('doc1', CacheableDummyContent('doc1'))
+        self.portal._setObject('doc2', CacheableDummyContent('doc2'))
+        CachingPolicyManager.manage_addCachingPolicyManager(self.portal)
+        cpm = self.portal.caching_policy_manager
+
+        # This policy only applies to doc1. It will not emit any ETag header
+        # but it enables If-modified-since handling.
+        cpm.addPolicy(policy_id = 'policy_1',
+                      predicate = 'python:object.getId()=="doc1"',
+                      mtime_func = '',
+                      max_age_secs = 100,
+                      no_cache = 0,
+                      no_store = 0,
+                      must_revalidate = 0,
+                      vary = 'doc1',
+                      etag_func = '',
+                      enable_304s = 0)
+
+    def test_empty(self):
+        
+        from Products.CMFCore.CachingPolicyManager import CPMCache
+
+        cpm = self.portal.caching_policy_manager
+        doc1 = self.portal.doc1
+        self.failUnless(cpm._isCacheManager)
+        self.failUnless(isinstance(cpm.ZCacheManager_getCache(), CPMCache))
+        self.assertEquals( doc1.ZCacheable_getManagerIds()
+                         , ({'id':cpm.getId(), 'title':''},)
+                         )
+
+    def test_no_association(self):
+        # Render an item that would match the CPM policy, but don't 
+        # associate it with the CPM.
+        self.portal.doc1()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+
+    def test_unsuitable_association(self):
+        # Render an item that is associated with the CPM, but that does not
+        # match any policy.
+        cpm = self.portal.caching_policy_manager
+        doc2 = self.portal.doc2
+        doc2.ZCacheable_setManagerId(cpm.getId())
+
+        doc2()
+
+        # no headers should be added by the CPM if all is well
+        headers = [x.lower() for x in self.RESPONSE.headers.keys()]
+        self.failIf('x-cache-headers-set-by' in headers)
+        self.failIf('vary' in headers)
+
+    def test_suitable_association(self):
+        # Render a content item that will trigger the CPM
+        cpm = self.portal.caching_policy_manager
+        doc1 = self.portal.doc1
+        doc1.ZCacheable_setManagerId(cpm.getId())
+
+        doc1()
+
+        # Policy "policy_1" should have triggered
+        # Just to be sure, change headers so they are definitely all 
+        # lower-cased
+        headers = {}
+        header_info = self.RESPONSE.headers.items()
+        [headers.__setitem__(x[0].lower(), x[1]) for x in header_info]
+
+        self.failUnless(headers.get('x-cache-headers-set-by'))
+        self.assertEquals(headers.get('vary'), 'doc1')
+        self.assertEquals( headers.get('cache-control')
+                         , 'max-age=100'
+                         )
+
+    def test_with_view(self):
+        # Render a view for a content item that will trigger the CPM
+        cpm = self.portal.caching_policy_manager
+        self.portal._setObject('a_view', DummyView(id='a_view'))
+        self.portal.a_view.ZCacheable_setManagerId(cpm.getId())
+        doc1 = self.portal.doc1
+
+        doc1.a_view()
+
+        # Policy "policy_1" should have triggered
+        # Just to be sure, change headers so they are definitely all 
+        # lower-cased
+        headers = {}
+        header_info = self.RESPONSE.headers.items()
+        [headers.__setitem__(x[0].lower(), x[1]) for x in header_info]
+
+        self.failUnless(headers.get('x-cache-headers-set-by'))
+        self.assertEquals(headers.get('vary'), 'doc1')
+        self.assertEquals( headers.get('cache-control')
+                         , 'max-age=100'
+                         )
+
+
 def test_suite():
     return unittest.TestSuite((
         unittest.makeSuite(CachingPolicyTests),
         unittest.makeSuite(CachingPolicyManagerTests),
         unittest.makeSuite(CachingPolicyManager304Tests),
         unittest.makeSuite(NestedTemplateTests),
+        unittest.makeSuite(OFSCacheTests),
         ))
 
 if __name__ == '__main__':
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.