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

Jens Vagelpohl jens at dataflake.org
Sun Nov 12 17:13:59 EST 2006


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__':



More information about the CMF-checkins mailing list