From a305ed41a543d40ee8f4eb1bc2d5df6c24f66977 Mon Sep 17 00:00:00 2001
From: Vincent Pelletier <vincent@nexedi.com>
Date: Wed, 10 Jan 2007 09:52:45 +0000
Subject: [PATCH] Add a cache on portal skins: No object update needed, cache
 filled up completely at first use and when skin selections are changed, then
 updates are transparently handled.

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@11987 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5Type/ZopePatch.py                |   3 +
 product/ERP5Type/patches/CMFCoreSkinnable.py | 152 +++++++++++++++++++
 product/ERP5Type/patches/CMFCoreSkinsTool.py |  57 +++++++
 product/ERP5Type/patches/OFSFolder.py        |  48 ++++++
 4 files changed, 260 insertions(+)
 create mode 100644 product/ERP5Type/patches/CMFCoreSkinnable.py
 create mode 100644 product/ERP5Type/patches/CMFCoreSkinsTool.py
 create mode 100644 product/ERP5Type/patches/OFSFolder.py

diff --git a/product/ERP5Type/ZopePatch.py b/product/ERP5Type/ZopePatch.py
index 8bd8456e8c..d10af77c59 100644
--- a/product/ERP5Type/ZopePatch.py
+++ b/product/ERP5Type/ZopePatch.py
@@ -43,6 +43,9 @@ from Products.ERP5Type.patches import Localizer
 from Products.ERP5Type.patches import CMFMailIn
 from Products.ERP5Type.patches import CMFCoreUtils
 from Products.ERP5Type.patches import PropertySheets
+from Products.ERP5Type.patches import CMFCoreSkinnable
+from Products.ERP5Type.patches import CMFCoreSkinsTool
+from Products.ERP5Type.patches import OFSFolder
 
 # These symbols are required for backward compatibility
 from Products.ERP5Type.patches.PropertyManager import ERP5PropertyManager
diff --git a/product/ERP5Type/patches/CMFCoreSkinnable.py b/product/ERP5Type/patches/CMFCoreSkinnable.py
new file mode 100644
index 0000000000..141b5185df
--- /dev/null
+++ b/product/ERP5Type/patches/CMFCoreSkinnable.py
@@ -0,0 +1,152 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+# Copyright (c) 2006 Nexedi SARL and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+from Products.CMFCore import Skinnable
+from Products.CMFCore.Skinnable import SKINDATA, superGetAttr, SkinDataCleanup, SkinnableObjectManager
+from thread import get_ident
+from zLOG import LOG
+
+"""
+  This patch modifies the way CMF Portal Skins gets a skin by its name from
+  the right skin folder. This way, the access complexity is O(1), and not O(n)
+  (n was the number of skin folders in skin selection list) any more.
+
+  XXX: the resolve/ignore dicts used in
+  CMFCoreSkinnableSkinnableObjectManager___getattr__
+  implies that it's not possible to get skins from multiple skin selections
+  during the same request.
+"""
+
+def CMFCoreSkinnableSkinnableObjectManager_initializeCache(self):
+  '''
+    Initialize the cache on portal skins.
+  '''
+  portal_skins = getattr(self, 'portal_skins', None)
+  if portal_skins is None:
+    return
+  portal_skins = portal_skins.aq_base
+  skin_selection_mapping = {}
+  for selection_name, skin_folder_id_string in portal_skins._getSelections().iteritems():
+    skin_list = {}
+    skin_folder_id_list = skin_folder_id_string.split(',')
+    skin_folder_id_list.reverse()
+    for skin_folder_id in skin_folder_id_list:
+      skin_folder = getattr(portal_skins, skin_folder_id, None)
+      if skin_folder is not None:
+        for skin_id in skin_folder.objectIds():
+          skin_list[skin_id] = skin_folder_id
+      else:
+        LOG('__getattr__', 0, 'Skin folder %s is in selection list '\
+            'but does not exist.' % (skin_folder_id, ))
+    skin_selection_mapping[selection_name] = skin_list
+  portal_skins._v_skin_location_list = skin_selection_mapping
+
+Skinnable.SkinnableObjectManager.initializeCache = CMFCoreSkinnableSkinnableObjectManager_initializeCache
+
+def CMFCoreSkinnableSkinnableObjectManager___getattr__(self, name):
+  '''
+    Looks for the name in an object with wrappers that only reach
+    up to the root skins folder.
+    This should be fast, flexible, and predictable.
+  '''
+  if not name.startswith('_') and not name.startswith('aq_'):
+    skin_info = SKINDATA.get(get_ident())
+    if skin_info is not None:
+      skin_selection_name, ignore, resolve = skin_info
+      try:
+        return resolve[name]
+      except KeyError:
+        if not ignore.has_key(name):
+          portal_skins = self.portal_skins.aq_base
+          try:
+            skin_selection_mapping = portal_skins._v_skin_location_list
+          except AttributeError:
+            LOG('Skinnable Monkeypatch __getattr__', 0, 'Initial skin cache fill. This should not happen often. %s' % (get_ident(), ))
+            self.initializeCache()
+            skin_selection_mapping = portal_skins._v_skin_location_list
+          try:
+            skin_folder_id = skin_selection_mapping[skin_selection_name][name]
+          except KeyError:
+            pass
+          else:
+            object = getattr(getattr(portal_skins, skin_folder_id), name, None)
+            if object is not None:
+              resolve[name] = object.aq_base
+              return resolve[name]
+            else:
+              # We cannot find a document referenced in the cache.
+              # Try to find if there is any other candidate in another
+              # skin folder of lower priority.
+              selection_dict = portal_skins._getSelections()
+              candidate_folder_id_list = selection_dict[skin_selection_name].split(',')
+              previous_skin_folder_id = skin_selection_mapping[skin_selection_name][name]
+              del skin_selection_mapping[skin_selection_name][name]
+              if previous_skin_folder_id in candidate_folder_id_list:
+                previous_skin_index = candidate_folder_id_list.index(previous_skin_folder_id)
+                candidate_folder_id_list = candidate_folder_id_list[previous_skin_index + 1:]
+              for candidate_folder_id in candidate_folder_id_list:
+                candidate_folder = getattr(portal_skins, candidate_folder_id, None)
+                if candidate_folder is not None:
+                  object = getattr(candidate_folder, name, None)
+                  if object is not None:
+                    skin_selection_cache[name] = candidate_folder_id
+                    resolve[name] = object.aq_base
+                    return resolve[name]
+                else:
+                  LOG('__getattr__', 0, 'Skin folder %s is in selection list '\
+                      'but does not exist.' % (candidate_folder_id, ))
+          ignore[name] = None
+  if superGetAttr is None:
+    raise AttributeError, name
+  return superGetAttr(self, name)
+
+def CMFCoreSkinnableSkinnableObjectManager_changeSkin(self, skinname):
+  '''
+    Change the current skin.
+
+    Can be called manually, allowing the user to change
+    skins in the middle of a request.
+
+    Patched not to call getSkin.
+  '''
+  if skinname is None:
+    sfn = self.getSkinsFolderName()
+    if sfn is not None:
+      sf = getattr(self, sfn, None)
+      if sf is not None:
+        skinname = sf.getDefaultSkin()
+  tid = get_ident()
+  SKINDATA[tid] = (skinname, {}, {})
+  REQUEST = getattr(self, 'REQUEST', None)
+  if REQUEST is not None:
+    REQUEST._hold(SkinDataCleanup(tid))
+
+def CMFCoreSkinnableSkinnableObjectManager_getSkin(self, name=None):
+  """
+    Replacement for original getSkin which makes obvious possible remaining
+    calls.
+    FIXME: Which exception should be raised here ?
+  """
+  raise Exception, 'This method must not be called when new caching system is applied.'
+
+Skinnable.SkinnableObjectManager.__getattr__ = CMFCoreSkinnableSkinnableObjectManager___getattr__
+Skinnable.SkinnableObjectManager.changeSkin = CMFCoreSkinnableSkinnableObjectManager_changeSkin
+Skinnable.SkinnableObjectManager.getSkin = CMFCoreSkinnableSkinnableObjectManager_getSkin
+
+# Some original attributes from SkinnableObjectManager are explicitely set as
+# value on PortalObjectBase. They must be updated there too, otherwise
+# patching is incompletely available at ERP5Site class level.
+from Products.CMFCore.PortalObject import PortalObjectBase
+PortalObjectBase.__getattr__ = CMFCoreSkinnableSkinnableObjectManager___getattr__
+
diff --git a/product/ERP5Type/patches/CMFCoreSkinsTool.py b/product/ERP5Type/patches/CMFCoreSkinsTool.py
new file mode 100644
index 0000000000..252593afc8
--- /dev/null
+++ b/product/ERP5Type/patches/CMFCoreSkinsTool.py
@@ -0,0 +1,57 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+# Copyright (c) 2006 Nexedi SARL and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+from Products.CMFCore.SkinsTool import SkinsTool
+
+"""
+  This patch invalidates the skin cache when manage_skinLayers is called to
+  modify the skin selection.
+"""
+
+original_manage_skinLayers = SkinsTool.manage_skinLayers
+
+def CMFCoreSkinsTool_manage_skinLayers(self, *args, **kw):
+  """
+    Make sure cache is flushed when skin layers are modified.
+  """
+  if getattr(self, '_v_skin_location_list', None) is not None:
+    self._p_changed = 1
+    self._v_skin_location_list.clear()
+  return original_manage_skinLayers(self, *args, **kw)
+
+def CMFCoreSkinsTool__updateCacheEntry(self, container_id, object_id):
+  """
+    Update cache entry for object_id.
+    Container_id is used to determine quickly if the entry must be updated or
+    not by comparing its position with the current value if any.
+  """
+  skin_location_list = getattr(self, '_v_skin_location_list', None)
+  if skin_location_list is None:
+    self.initializeCache()
+    skin_location_list = getattr(self, '_v_skin_location_list')
+  for selection_name, skin_folder_id_string in self._getSelections().iteritems():
+    skin_folder_id_list = skin_folder_id_string.split(',')
+    if container_id in skin_folder_id_list:
+      skin_folder_id_list.reverse()
+      this_folder_index = skin_folder_id_list.index(container_id)
+      if skin_location_list.has_key(object_id):
+        existing_folder_index = skin_folder_id_list.index(skin_location_list[object_id])
+      else:
+        existing_folder_index = this_folder_index + 1
+      if existing_folder_index > this_folder_index:
+        skin_location_list[selection_name][object_id] = container_id
+
+SkinsTool.manage_skinLayers = CMFCoreSkinsTool_manage_skinLayers
+SkinsTool._updateCacheEntry = CMFCoreSkinsTool__updateCacheEntry
+
diff --git a/product/ERP5Type/patches/OFSFolder.py b/product/ERP5Type/patches/OFSFolder.py
new file mode 100644
index 0000000000..f6b423570b
--- /dev/null
+++ b/product/ERP5Type/patches/OFSFolder.py
@@ -0,0 +1,48 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+# Copyright (c) 2006 Nexedi SARL and Contributors. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+from OFS.Folder import Folder
+
+"""
+  This patch modifies OFS.Folder._setOb to update portal_skins cache when
+  needed.
+"""
+
+Folder_original__setOb = Folder._setOb
+
+def Folder_setOb(self, id, object):
+  """
+    Update portal_skins cache with the new files.
+
+    Checks must be done from the quickest to the slowest to avoid wasting
+    time when no cache must be updated.
+
+    Update must only be triggered if we (folder) are right below the skin
+    tool, not any deeper.
+  """
+  Folder_original__setOb(self, id, object)
+  aq_chain = getattr(self, 'aq_chain', None)
+  if aq_chain is None: # Not in acquisition context
+    return
+  if len(aq_chain) < 2: # Acquisition context is not deep enough for context to possibly be below portal skins.
+    return
+  portal_skins = aq_chain[-2]
+  if getattr(portal_skins, 'meta_type', '') != 'CMF Skins Tool' : # It is not a skin tool we're below.
+    return
+  _updateCacheEntry = getattr(portal_skins.aq_base, '_updateCacheEntry', None)
+  if _updateCacheEntry is None:
+    return
+  _updateCacheEntry(self.id, id)
+
+Folder._setOb = Folder_setOb
-- 
2.30.9