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