From 9b7f05cb4d9dd700141f68687dc7aa71d44042a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Aur=C3=A9lien=20Calonne?= <aurel@nexedi.com>
Date: Thu, 6 Sep 2007 13:53:47 +0000
Subject: [PATCH] initial import of HBTreeFolder product

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@16118 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/HBTreeFolder2/CHANGES.txt             |   4 +
 product/HBTreeFolder2/CMFHBTreeFolder.py      | 101 +++
 product/HBTreeFolder2/HBTreeFolder2.py        | 605 ++++++++++++++++++
 product/HBTreeFolder2/README.txt              |   2 +
 product/HBTreeFolder2/__init__.py             |  48 ++
 product/HBTreeFolder2/btreefolder2.gif        | Bin 0 -> 179 bytes
 product/HBTreeFolder2/contents.dtml           | 164 +++++
 product/HBTreeFolder2/folderAdd.dtml          |  67 ++
 product/HBTreeFolder2/tests/__init__.py       |   1 +
 .../HBTreeFolder2/tests/testHBTreeFolder2.py  | 219 +++++++
 product/HBTreeFolder2/version.txt             |   1 +
 11 files changed, 1212 insertions(+)
 create mode 100755 product/HBTreeFolder2/CHANGES.txt
 create mode 100755 product/HBTreeFolder2/CMFHBTreeFolder.py
 create mode 100755 product/HBTreeFolder2/HBTreeFolder2.py
 create mode 100755 product/HBTreeFolder2/README.txt
 create mode 100755 product/HBTreeFolder2/__init__.py
 create mode 100755 product/HBTreeFolder2/btreefolder2.gif
 create mode 100755 product/HBTreeFolder2/contents.dtml
 create mode 100755 product/HBTreeFolder2/folderAdd.dtml
 create mode 100755 product/HBTreeFolder2/tests/__init__.py
 create mode 100755 product/HBTreeFolder2/tests/testHBTreeFolder2.py
 create mode 100755 product/HBTreeFolder2/version.txt

diff --git a/product/HBTreeFolder2/CHANGES.txt b/product/HBTreeFolder2/CHANGES.txt
new file mode 100755
index 0000000000..baf1329bf2
--- /dev/null
+++ b/product/HBTreeFolder2/CHANGES.txt
@@ -0,0 +1,4 @@
+Version 1.0.0
+
+  - Initial version
+
diff --git a/product/HBTreeFolder2/CMFHBTreeFolder.py b/product/HBTreeFolder2/CMFHBTreeFolder.py
new file mode 100755
index 0000000000..7b6b5b351e
--- /dev/null
+++ b/product/HBTreeFolder2/CMFHBTreeFolder.py
@@ -0,0 +1,101 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation 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 AccessControl.SecurityInfo import ClassSecurityInfo
+from Globals import InitializeClass
+from Products.HBTreeFolder2.HBTreeFolder2 import HBTreeFolder2Base
+
+try:
+    from Products.CMFCore.PortalFolder import PortalFolderBase as PortalFolder
+except ImportError:
+    from Products.CMFCore.PortalFolder import PortalFolder
+
+from Products.CMFCore.PortalFolder import factory_type_information as PortalFolder_FTI
+from Products.CMFCore.utils import getToolByName
+
+_actions = PortalFolder_FTI[0]['actions']
+
+factory_type_information = ( { 'id'             : 'CMF HBTree Folder',
+                               'meta_type'      : 'CMF HBTree Folder',
+                               'description'    : """\
+CMF folder designed to hold a lot of objects.""",
+                               'icon'           : 'folder_icon.gif',
+                               'product'        : 'CMFCore',
+                               'factory'        : 'manage_addCMFHBTreeFolder',
+                               'filter_content_types' : 0,
+                               'immediate_view' : 'folder_edit_form',
+                               'actions'        : _actions,
+                               },
+                           )
+
+
+def manage_addCMFHBTreeFolder(dispatcher, id, title='', REQUEST=None):
+    """Adds a new HBTreeFolder object with id *id*.
+    """
+    id = str(id)
+    ob = CMFHBTreeFolder(id)
+    ob.title = str(title)
+    dispatcher._setObject(id, ob)
+    ob = dispatcher._getOb(id)
+    if REQUEST is not None:
+        REQUEST['RESPONSE'].redirect(ob.absolute_url() + '/manage_main' )
+
+
+class CMFHBTreeFolder(HBTreeFolder2Base, PortalFolder):
+    """HBTree folder for CMF sites.
+    """
+    meta_type = 'CMF HBTree Folder'
+    security = ClassSecurityInfo()
+
+    def __init__(self, id, title=''):
+        PortalFolder.__init__(self, id, title)
+        HBTreeFolder2Base.__init__(self, id)
+
+    def _checkId(self, id, allow_dup=0):
+        PortalFolder._checkId(self, id, allow_dup)
+        HBTreeFolder2Base._checkId(self, id, allow_dup)
+
+
+    def allowedContentTypes(self):
+      """
+      List type info objects for types which can be added in
+      this folder.
+      """
+      result = []
+      portal_types = getToolByName(self, 'portal_types')
+      myType = portal_types.getTypeInfo(self)
+
+      if myType is not None:
+        allowed_types_to_check = []
+        if myType.filter_content_types:
+          for portal_type in myType.allowed_content_types:
+            contentType = portal_types.getTypeInfo(portal_type)
+            if contentType is None:
+              raise AttributeError, "Portal type '%s' does not exist " \
+                                    "and should not be allowed in '%s'" % \
+                                    (portal_type, self.getPortalType())
+            result.append(contentType)
+        else:
+          for contentType in portal_types.listTypeInfo(self):
+            if myType.allowType(contentType.getId()):
+              result.append(contentType)
+      else:
+          result = portal_types.listTypeInfo()
+
+      return filter(
+          lambda typ, container=self: typ.isConstructionAllowed(container),
+          result)
+
+
+InitializeClass(CMFHBTreeFolder)
diff --git a/product/HBTreeFolder2/HBTreeFolder2.py b/product/HBTreeFolder2/HBTreeFolder2.py
new file mode 100755
index 0000000000..2eb52fa269
--- /dev/null
+++ b/product/HBTreeFolder2/HBTreeFolder2.py
@@ -0,0 +1,605 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation 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
+#
+##############################################################################
+
+import sys
+from cgi import escape
+from urllib import quote
+from random import randint
+from types import StringType
+
+import Globals
+from Globals import DTMLFile
+from Globals import Persistent, PersistentMapping
+from Acquisition import aq_base
+from BTrees.OOBTree import OOBTree
+from BTrees.OIBTree import OIBTree, union
+from BTrees.Length import Length
+from ZODB.POSException import ConflictError
+from OFS.ObjectManager import BadRequestException, BeforeDeleteException
+from OFS.Folder import Folder
+from AccessControl import getSecurityManager, ClassSecurityInfo
+from AccessControl.Permissions import access_contents_information, \
+     view_management_screens
+from zLOG import LOG, INFO, ERROR, WARNING
+from Products.ZCatalog.Lazy import LazyMap, LazyFilter, LazyCat
+
+
+manage_addHBTreeFolder2Form = DTMLFile('folderAdd', globals())
+
+def manage_addHBTreeFolder2(dispatcher, id, title='', REQUEST=None):
+    """Adds a new HBTreeFolder object with id *id*.
+    """
+    id = str(id)
+    ob = HBTreeFolder2(id)
+    ob.title = str(title)
+    dispatcher._setObject(id, ob)
+    ob = dispatcher._getOb(id)
+    if REQUEST is not None:
+        return dispatcher.manage_main(dispatcher, REQUEST, update_menu=1)
+
+
+listtext0 = '''<select name="ids:list" multiple="multiple" size="%s">
+'''
+listtext1 = '''<option value="%s">%s</option>
+'''
+listtext2 = '''</select>
+'''
+
+
+_marker = []  # Create a new marker object.
+
+MAX_UNIQUEID_ATTEMPTS = 1000
+MAX_OBJECT_PER_LEVEL = 1000
+H_SEPARATOR = '-'
+
+class ExhaustedUniqueIdsError (Exception):
+    pass
+
+
+class HBTreeFolder2Base (Persistent):
+    """Base for BTree-based folders.
+    """
+
+    security = ClassSecurityInfo()
+
+    manage_options=(
+        ({'label':'Contents', 'action':'manage_main',},
+         ) + Folder.manage_options[1:]
+        )
+
+    security.declareProtected(view_management_screens,
+                              'manage_main')
+    manage_main = DTMLFile('contents', globals())
+
+    _htree = None      # OOBTree: { id -> object }
+    _count = None     # A BTrees.Length
+    _v_nextid = 0     # The integer component of the next generated ID
+    title = ''
+    _tree_list = None
+
+
+    def __init__(self, id=None):
+        if id is not None:
+            self.id = id
+        self._initBTrees()
+
+    def _initBTrees(self):
+        self._htree = OOBTree()
+        self._count = Length()
+        self._tree_list = PersistentMapping()
+
+    def initBTrees(self):
+        """ """
+        return self._initBTrees()
+
+    def _populateFromFolder(self, source):
+        """Fill this folder with the contents of another folder.
+        """
+        for name in source.objectIds():
+            value = source._getOb(name, None)
+            if value is not None:
+                self._setOb(name, aq_base(value))
+
+
+    security.declareProtected(view_management_screens, 'manage_fixCount')
+    def manage_fixCount(self):
+        """Calls self._fixCount() and reports the result as text.
+        """
+        old, new = self._fixCount()
+        path = '/'.join(self.getPhysicalPath())
+        if old == new:
+            return "No count mismatch detected in HBTreeFolder2 at %s." % path
+        else:
+            return ("Fixed count mismatch in HBTreeFolder2 at %s. "
+                    "Count was %d; corrected to %d" % (path, old, new))
+
+
+    def _fixCount(self):
+        """Checks if the value of self._count disagrees with
+        len(self.objectIds()). If so, corrects self._count. Returns the
+        old and new count values. If old==new, no correction was
+        performed.
+        """
+        old = self._count()
+        new = len(self.objectIds())
+        if old != new:
+            self._count.set(new)
+        return old, new
+
+
+    security.declareProtected(view_management_screens, 'manage_cleanup')
+    def manage_cleanup(self):
+        """Calls self._cleanup() and reports the result as text.
+        """
+        v = self._cleanup()
+        path = '/'.join(self.getPhysicalPath())
+        if v:
+            return "No damage detected in HBTreeFolder2 at %s." % path
+        else:
+            return ("Fixed HBTreeFolder2 at %s.  "
+                    "See the log for more details." % path)
+
+
+    def _cleanup(self):
+        """Cleans up errors in the BTrees.
+
+        Certain ZODB bugs have caused BTrees to become slightly insane.
+        Fortunately, there is a way to clean up damaged BTrees that
+        always seems to work: make a new BTree containing the items()
+        of the old one.
+
+        Returns 1 if no damage was detected, or 0 if damage was
+        detected and fixed.
+        """
+        def hCheck(htree):
+          """
+              Recursively check the btree
+          """
+          check(htree)
+          for key in htree.keys():
+              if not htree.has_key(key):
+                  raise AssertionError(
+                      "Missing value for key: %s" % repr(key))
+              else:
+                ob = htree[key]
+                if isinstance(ob, OOBTree):
+                  hCheck(ob)
+          return 1
+        
+        from BTrees.check import check
+        path = '/'.join(self.getPhysicalPath())
+        try:
+            return hCheck(self._htree)
+        except AssertionError:            
+            LOG('HBTreeFolder2', WARNING,
+                'Detected damage to %s. Fixing now.' % path,
+                error=sys.exc_info())
+            try:
+                self._htree = OOBTree(self._htree) # XXX hFix needed
+            except:
+                LOG('HBTreeFolder2', ERROR, 'Failed to fix %s.' % path,
+                    error=sys.exc_info())
+                raise
+            else:
+                LOG('HBTreeFolder2', INFO, 'Fixed %s.' % path)
+            return 0
+
+    def hashId(self, id):
+        """Return a tuple of ids
+        """
+        id_list = str(id).split(H_SEPARATOR)     # We use '-' as the separator by default
+        if len(id_list) > 1:
+          return tuple(id_list)
+        else:
+          return [id,]
+    
+#         try:                             # We then try int hashing
+#           id_int = int(id)
+#         except ValueError:
+#           return id_list
+#         result = []
+#         while id_int:
+#           result.append(id_int % MAX_OBJECT_PER_LEVEL)
+#           id_int = id_int / MAX_OBJECT_PER_LEVEL
+#         result.reverse()
+#         return tuple(result)
+
+    def _getOb(self, id, default=_marker):
+        """
+            Return the named object from the folder.
+        """
+        htree = self._htree
+        ob = htree
+        id_list = self.hashId(id)
+        for sub_id in id_list[0:-1]:
+          if default is _marker:
+            ob = ob[sub_id]
+          else:
+            ob = ob.get(sub_id, _marker)
+            if ob is _marker:
+              return default
+        if default is _marker:
+          ob = ob[id]
+        else:
+          ob = ob.get(id, _marker)
+          if ob is _marker:
+            return default
+        return ob.__of__(self)
+
+    def _setOb(self, id, object):
+        """Store the named object in the folder.
+        """
+        htree = self._htree
+        id_list = self.hashId(id)
+        for idx in xrange(len(id_list[0:-1])):
+          sub_id = id_list[idx]
+          if not htree.has_key(sub_id):
+            # Create a new index and index it
+            htree[sub_id] = OOBTree()
+            if isinstance(sub_id, int) or isinstance(sub_id, long):
+              tree_id = 0
+              for id in id_list[:idx+1]:
+                  tree_id = tree_id + id * MAX_OBJECT_PER_LEVEL
+            else:
+              tree_id = H_SEPARATOR.join(id_list[:idx+1])
+            self._tree_list[tree_id] = None
+            
+          htree = htree[sub_id]
+        # set object in subtree
+        ob_id = id_list[-1]
+        if htree.has_key(id):
+            raise KeyError('There is already an item named "%s".' % id)
+        htree[id] = object
+        self._count.change(1)
+
+    def _delOb(self, id):
+        """Remove the named object from the folder.
+        """
+        htree = self._htree
+        id_list = self.hashId(id)
+        for sub_id in id_list[0:-1]:
+          htree = htree[sub_id]
+        del htree[id]
+        self._count.change(-1)
+
+    security.declareProtected(view_management_screens, 'getBatchObjectListing')
+    def getBatchObjectListing(self, REQUEST=None):
+        """Return a structure for a page template to show the list of objects.
+        """
+        if REQUEST is None:
+            REQUEST = {}
+        pref_rows = int(REQUEST.get('dtpref_rows', 20))
+        b_start = int(REQUEST.get('b_start', 1))
+        b_count = int(REQUEST.get('b_count', 1000))
+        b_end = b_start + b_count - 1
+        url = self.absolute_url() + '/manage_main'
+        idlist = self.objectIds()  # Pre-sorted.
+        count = self.objectCount()
+
+        if b_end < count:
+            next_url = url + '?b_start=%d' % (b_start + b_count)
+        else:
+            b_end = count
+            next_url = ''
+
+        if b_start > 1:
+            prev_url = url + '?b_start=%d' % max(b_start - b_count, 1)
+        else:
+            prev_url = ''
+
+        formatted = []
+        formatted.append(listtext0 % pref_rows)
+        for i in range(b_start - 1, b_end):
+            optID = escape(idlist[i])
+            formatted.append(listtext1 % (escape(optID, quote=1), optID))
+        formatted.append(listtext2)
+        return {'b_start': b_start, 'b_end': b_end,
+                'prev_batch_url': prev_url,
+                'next_batch_url': next_url,
+                'formatted_list': ''.join(formatted)}
+
+
+    security.declareProtected(view_management_screens,
+                              'manage_object_workspace')
+    def manage_object_workspace(self, ids=(), REQUEST=None):
+        '''Redirects to the workspace of the first object in
+        the list.'''
+        if ids and REQUEST is not None:
+            REQUEST.RESPONSE.redirect(
+                '%s/%s/manage_workspace' % (
+                self.absolute_url(), quote(ids[0])))
+        else:
+            return self.manage_main(self, REQUEST)
+
+
+    security.declareProtected(access_contents_information,
+                              'tpValues')
+    def tpValues(self):
+        """Ensures the items don't show up in the left pane.
+        """
+        return ()
+
+
+    security.declareProtected(access_contents_information,
+                              'objectCount')
+    def objectCount(self):
+        """Returns the number of items in the folder."""
+        return self._count()
+
+
+    security.declareProtected(access_contents_information, 'has_key')
+    def has_key(self, id):
+        """Indicates whether the folder has an item by ID.
+        """
+        htree = self._htree
+        id_list = self.hashId(id)
+        for sub_id in id_list[0:-1]:
+          if not isinstance(htree, OOBTree):
+            return 0
+          if not htree.has_key(sub_id):
+            return 0
+          htree = htree[sub_id]
+        if not htree.has_key(id):
+          return 0
+        return 1
+
+
+    security.declareProtected(access_contents_information,
+                              'treeIds')
+    def treeIds(self, base_id=None):
+        """ Return a list of subtree ids
+        """
+        tree = self._getTree(base_id=base_id)
+        return [x for x in self._htree.keys() if isinstance(self._htree[x], OOBTree)]
+
+
+    def _getTree(self, base_id):
+        """ Return the tree wich has the base_id
+        """
+        htree = self._htree
+        id_list = self.hashId(base_id)
+        for sub_id in id_list:            
+          if not isinstance(htree, OOBTree):
+            return None
+          if not htree.has_key(sub_id):
+            raise IndexError, base_id
+          htree = htree[sub_id]
+        return htree
+
+    def _getTreeIdList(self, htree=None):
+        """ recursively build a list of btree ids
+        """
+        if htree is None:
+          htree = self._htree
+          btree_list = [None,]
+        else:
+            btree_list = []
+        for obj_id in htree.keys():
+          obj = htree[obj_id]
+          if isinstance(obj, OOBTree):
+            btree_list.extend(["%s-%s"%(obj_id, x) for x in self._getTreeIdList(htree=obj)])
+            btree_list.append(obj_id)
+
+        return btree_list 
+
+    security.declareProtected(access_contents_information,
+                              'getTreeIdList')
+    def getTreeIdList(self, htree=None):
+        """ Return list of all tree ids
+        """
+        if self._tree_list is None or len(self._tree_list.keys()) == 0:
+            tree_list = self._getTreeIdList(htree=htree)
+            self._tree_list = PersistentMapping()
+            for tree in tree_list:                
+                self._tree_list[tree] = None
+        return sorted(self._tree_list.keys())
+
+
+    def _treeObjectValues(self, base_id=None):
+        """ return object values for a given btree
+        """
+        if base_id is not None:
+            return LazyFilter(self._isNotBTree, self._getTree("%s" %base_id).values())
+        else:
+            return LazyFilter(self._isNotBTree, self._htree.values())
+
+    def _treeObjectIds(self, base_id=None):
+        """ return object ids for a given btree
+        """
+        if base_id is not None:
+          return LazyFilter(self._checkObjectId, self._getTree("%s" %base_id).keys())
+        else:
+          return LazyFilter(self._checkObjectId, self._htree.keys())
+
+    def _isNotBTree(self, obj):
+        """ test object is not a btree
+        """
+        if isinstance(obj, OOBTree):
+            return False
+        else:
+            return True
+
+    def _checkObjectId(self, id):
+        """ test id is not in btree id list
+        """
+        return not self._tree_list.has_key(id)
+        
+    security.declareProtected(access_contents_information,
+                              'ObjectValues')
+    def objectValues(self, base_id=None):
+        return LazyMap(self._getOb, self.objectIds(base_id))
+
+
+    security.declareProtected(access_contents_information,
+                              'objectIds')
+    def objectIds(self, base_id=None):
+        if base_id is None:
+            return LazyCat(LazyMap(self._treeObjectIds, self.getTreeIdList()))
+        else:
+            return self._treeObjectIds(base_id=base_id)
+
+    
+    security.declareProtected(access_contents_information,
+                              'objectItems')
+    def objectItems(self, spec=None):
+        # Returns a list of (id, subobject) tuples of the current object.
+        # If 'spec' is specified, returns only objects whose meta_type match
+        # 'spec'
+        return LazyMap(lambda id, _getOb=self._getOb: (id, _getOb(id)),
+                       self.objectIds(spec))
+
+
+    security.declareProtected(access_contents_information,
+                              'objectMap')
+    def objectMap(self):
+        # Returns a tuple of mappings containing subobject meta-data.
+        return LazyMap(lambda (k, v):
+                       {'id': k, 'meta_type': getattr(v, 'meta_type', None)},
+                       self._htree.items(), self._count())
+
+    # superValues() looks for the _objects attribute, but the implementation
+    # would be inefficient, so superValues() support is disabled.
+    _objects = ()
+
+
+    security.declareProtected(access_contents_information,
+                              'objectIds_d')
+    def objectIds_d(self, t=None):
+        ids = self.objectIds(t)
+        res = {}
+        for id in ids:
+            res[id] = 1
+        return res
+
+
+    security.declareProtected(access_contents_information,
+                              'objectMap_d')
+    def objectMap_d(self, t=None):
+        return self.objectMap()
+
+
+    def _checkId(self, id, allow_dup=0):
+        if not allow_dup and self.has_key(id):
+            raise BadRequestException, ('The id "%s" is invalid--'
+                                        'it is already in use.' % id)
+
+
+    def _setObject(self, id, object, roles=None, user=None, set_owner=1):
+        v=self._checkId(id)
+        if v is not None: id=v
+
+        # If an object by the given id already exists, remove it.
+        if self.has_key(id):
+            self._delObject(id)
+
+        self._setOb(id, object)
+        object = self._getOb(id)
+
+        if set_owner:
+            object.manage_fixupOwnershipAfterAdd()
+
+            # Try to give user the local role "Owner", but only if
+            # no local roles have been set on the object yet.
+            if hasattr(object, '__ac_local_roles__'):
+                if object.__ac_local_roles__ is None:
+                    user=getSecurityManager().getUser()
+                    if user is not None:
+                        userid=user.getId()
+                        if userid is not None:
+                            object.manage_setLocalRoles(userid, ['Owner'])
+
+        object.manage_afterAdd(object, self)
+        return id
+
+
+    def _delObject(self, id, dp=1):
+        object = self._getOb(id)
+        try:
+            object.manage_beforeDelete(object, self)
+        except BeforeDeleteException, ob:
+            raise
+        except ConflictError:
+            raise
+        except:
+            LOG('Zope', ERROR, 'manage_beforeDelete() threw',
+                error=sys.exc_info())
+        self._delOb(id)
+
+
+    # Aliases for mapping-like access.
+    __len__ = objectCount
+    keys = objectIds
+    values = objectValues
+    items = objectItems
+
+    # backward compatibility
+    hasObject = has_key
+
+    security.declareProtected(access_contents_information, 'get')
+    def get(self, name, default=None):
+        return self._getOb(name, default)
+
+
+    # Utility for generating unique IDs.
+
+    security.declareProtected(access_contents_information, 'generateId')
+    def generateId(self, prefix='item', suffix='', rand_ceiling=999999999):
+        """Returns an ID not used yet by this folder.
+
+        The ID is unlikely to collide with other threads and clients.
+        The IDs are sequential to optimize access to objects
+        that are likely to have some relation.
+        """
+        tree = self._htree
+        n = self._v_nextid
+        attempt = 0
+        while 1:
+            if n % 4000 != 0 and n <= rand_ceiling:
+                id = '%s%d%s' % (prefix, n, suffix)
+                if not tree.has_key(id):
+                    break
+            n = randint(1, rand_ceiling)
+            attempt = attempt + 1
+            if attempt > MAX_UNIQUEID_ATTEMPTS:
+                # Prevent denial of service
+                raise ExhaustedUniqueIdsError
+        self._v_nextid = n + 1
+        return id
+
+    def __getattr__(self, name):
+        # Boo hoo hoo!  Zope 2 prefers implicit acquisition over traversal
+        # to subitems, and __bobo_traverse__ hooks don't work with
+        # restrictedTraverse() unless __getattr__() is also present.
+        # Oh well.
+        res = self._htree.get(name)
+        if res is None:
+            raise AttributeError, name
+        return res
+
+
+Globals.InitializeClass(HBTreeFolder2Base)
+
+
+class HBTreeFolder2 (HBTreeFolder2Base, Folder):
+    """BTreeFolder2 based on OFS.Folder.
+    """
+    meta_type = 'HBTreeFolder2'
+
+    def _checkId(self, id, allow_dup=0):
+        Folder._checkId(self, id, allow_dup)
+        HBTreeFolder2Base._checkId(self, id, allow_dup)
+    
+
+Globals.InitializeClass(HBTreeFolder2)
+
diff --git a/product/HBTreeFolder2/README.txt b/product/HBTreeFolder2/README.txt
new file mode 100755
index 0000000000..ccbf8103d0
--- /dev/null
+++ b/product/HBTreeFolder2/README.txt
@@ -0,0 +1,2 @@
+TODO
+
diff --git a/product/HBTreeFolder2/__init__.py b/product/HBTreeFolder2/__init__.py
new file mode 100755
index 0000000000..64f81d5ef5
--- /dev/null
+++ b/product/HBTreeFolder2/__init__.py
@@ -0,0 +1,48 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation 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
+#
+##############################################################################
+
+import HBTreeFolder2
+
+def initialize(context):
+
+    context.registerClass(
+        HBTreeFolder2.HBTreeFolder2,
+        constructors=(HBTreeFolder2.manage_addHBTreeFolder2Form,
+                      HBTreeFolder2.manage_addHBTreeFolder2),
+        icon='btreefolder2.gif',
+        )
+
+    #context.registerHelp()
+    #context.registerHelpTitle('Zope Help')
+
+    context.registerBaseClass(HBTreeFolder2.HBTreeFolder2)
+
+    try:
+        from Products.CMFCore import utils
+    except ImportError:
+        # CMF not installed
+        pass
+    else:
+        # CMF installed; make available a special folder type.
+        import CMFHBTreeFolder
+        ADD_FOLDERS_PERMISSION = 'Add portal folders'
+
+        utils.ContentInit(
+            'CMF HBTree Folder',
+            content_types=(CMFHBTreeFolder.CMFHBTreeFolder,),
+            permission=ADD_FOLDERS_PERMISSION,
+            extra_constructors=(CMFHBTreeFolder.manage_addCMFHBTreeFolder,),
+            fti=CMFHBTreeFolder.factory_type_information
+            ).initialize(context)
+
diff --git a/product/HBTreeFolder2/btreefolder2.gif b/product/HBTreeFolder2/btreefolder2.gif
new file mode 100755
index 0000000000000000000000000000000000000000..725e514aedf421be0b267a31f332aa31568ea53b
GIT binary patch
literal 179
zcmZ?wbhEHb6krfwc+9}CWy_X~jEwj18M?X{7A;z|dpARGZ*N0GLqI^lfddErg8%~%
zfEge{@h1x-0|P&U4oDPa1_MjNhm)S#t`ax9E`7T7tt5WtT%k=GIj=XJSy9szc*CLc
zNXH||o`eY>Zl1JbbyJ)uQ_-_mUO9lnfXDS-k0uY-yeLhxg8`}wZZBKQGtIM=<56Vv
XryHd<M-tD!f1fNA+vLPA$Y2csedbUg

literal 0
HcmV?d00001

diff --git a/product/HBTreeFolder2/contents.dtml b/product/HBTreeFolder2/contents.dtml
new file mode 100755
index 0000000000..4fadeb4360
--- /dev/null
+++ b/product/HBTreeFolder2/contents.dtml
@@ -0,0 +1,164 @@
+<dtml-let form_title="'Contents'">
+<dtml-if manage_page_header>
+  <dtml-var manage_page_header>
+<dtml-else>
+  <html><head><title>&dtml-form_title;</title></head>
+  <body bgcolor="#ffffff">
+</dtml-if>
+</dtml-let>
+<dtml-var manage_tabs>
+
+<script type="text/javascript">
+<!-- 
+
+isSelected = false;
+
+function toggleSelect() {
+  elem = document.objectItems.elements['ids:list'];
+  if (isSelected == false) {
+    for (i = 0; i < elem.options.length; i++) {
+      elem.options[i].selected = true;
+    }
+    isSelected = true;
+    document.objectItems.selectButton.value = "Deselect All";
+    return isSelected;
+  }
+  else {
+    for (i = 0; i < elem.options.length; i++) {
+      elem.options[i].selected = false;
+    }
+    isSelected = false;
+    document.objectItems.selectButton.value = "Select All";
+    return isSelected;
+  }
+}
+
+//-->
+</script>
+
+<dtml-unless skey><dtml-call expr="REQUEST.set('skey', 'id')"></dtml-unless>
+<dtml-unless rkey><dtml-call expr="REQUEST.set('rkey', '')"></dtml-unless>
+
+<!-- Add object widget -->
+<br />
+<dtml-if filtered_meta_types>
+  <table width="100%" cellspacing="0" cellpadding="0" border="0">
+  <tr>
+  <td align="left" valign="top">&nbsp;</td>
+  <td align="right" valign="top">
+  <div class="form-element">
+  <form action="&dtml-URL1;/" method="get">
+  <dtml-if "_.len(filtered_meta_types) > 1">
+    <select class="form-element" name=":action" 
+     onChange="location.href='&dtml-URL1;/'+this.options[this.selectedIndex].value">
+    <option value="manage_workspace" disabled>Select type to add...</option>
+    <dtml-in filtered_meta_types mapping sort=name>
+    <option value="&dtml.url_quote-action;">&dtml-name;</option>
+    </dtml-in>
+    </select>
+    <input class="form-element" type="submit" name="submit" value=" Add " />
+  <dtml-else>
+    <dtml-in filtered_meta_types mapping sort=name>
+    <input type="hidden" name=":method" value="&dtml.url_quote-action;" />
+    <input class="form-element" type="submit" name="submit" value=" Add &dtml-name;" />
+    </dtml-in>
+  </dtml-if>
+  </form>
+  </div>
+  </td>
+  </tr>
+  </table>
+</dtml-if>
+
+<form action="&dtml-URL1;/" name="objectItems" method="post">
+<dtml-if objectCount>
+<dtml-with expr="getBatchObjectListing(REQUEST)" mapping>
+
+<p>
+<dtml-if prev_batch_url><a href="&dtml-prev_batch_url;">&lt;&lt;</a></dtml-if>
+<em>Items <dtml-var b_start> through <dtml-var b_end> of <dtml-var objectCount></em>
+<dtml-if next_batch_url><a href="&dtml-next_batch_url;">&gt;&gt;</a></dtml-if>
+</p>
+
+<dtml-var formatted_list>
+
+<table cellspacing="0" cellpadding="2" border="0">
+<tr>
+  <td align="left" valign="top" width="16"></td>
+  <td align="left" valign="top">
+  <div class="form-element">
+  <input class="form-element" type="submit"
+   name="manage_object_workspace:method" value="Edit" />
+  <dtml-unless dontAllowCopyAndPaste>
+  <input class="form-element" type="submit" name="manage_renameForm:method" 
+   value="Rename" />
+  <input class="form-element" type="submit" name="manage_cutObjects:method" 
+   value="Cut" /> 
+  <input class="form-element" type="submit" name="manage_copyObjects:method" 
+   value="Copy" />
+  <dtml-if cb_dataValid>
+  <input class="form-element" type="submit" name="manage_pasteObjects:method" 
+   value="Paste" />
+  </dtml-if>
+  </dtml-unless>
+  <dtml-if "_.SecurityCheckPermission('Delete objects',this())">
+  <input class="form-element" type="submit" name="manage_delObjects:method" 
+   value="Delete" />
+  </dtml-if>
+  <dtml-if "_.SecurityCheckPermission('Import/Export objects', this())">
+  <input class="form-element" type="submit" 
+   name="manage_importExportForm:method" 
+   value="Import/Export" />
+  </dtml-if>
+<script type="text/javascript">
+<!-- 
+if (document.forms[0]) {
+  document.write('<input class="form-element" type="submit" name="selectButton" value="Select All" onClick="toggleSelect(); return false">')
+  }
+//-->
+</script>
+  </div>
+  </td>
+</tr>
+</table>
+
+</dtml-with>
+<dtml-else>
+<table cellspacing="0" cellpadding="2" border="0">
+<tr>
+<td>
+<div class="std-text">
+There are currently no items in <em>&dtml-title_or_id;</em>
+<br /><br />
+</div>
+<dtml-unless dontAllowCopyAndPaste>
+<dtml-if cb_dataValid>
+<div class="form-element">
+<input class="form-element" type="submit" name="manage_pasteObjects:method" 
+ value="Paste" />
+</div>
+</dtml-if>
+</dtml-unless>
+<dtml-if "_.SecurityCheckPermission('Import/Export objects', this())">
+<input class="form-element" type="submit"
+  name="manage_importExportForm:method" value="Import/Export" />
+</dtml-if>
+</td>
+</tr>
+</table>
+</dtml-if>
+</form>
+
+<dtml-if update_menu>
+<script type="text/javascript">
+<!--
+window.parent.update_menu();
+//-->
+</script>
+</dtml-if>
+
+<dtml-if manage_page_footer>
+  <dtml-var manage_page_footer>
+<dtml-else>
+  </body></html>
+</dtml-if>
diff --git a/product/HBTreeFolder2/folderAdd.dtml b/product/HBTreeFolder2/folderAdd.dtml
new file mode 100755
index 0000000000..1a8c3f66d2
--- /dev/null
+++ b/product/HBTreeFolder2/folderAdd.dtml
@@ -0,0 +1,67 @@
+<dtml-let form_title="'Add HBTreeFolder2'">
+<dtml-if manage_page_header>
+  <dtml-var manage_page_header>
+  <dtml-var manage_form_title>
+<dtml-else>
+  <html><head><title>&dtml-form_title;</title></head>
+  <body bgcolor="#ffffff">
+  <h2>&dtml-form_title;</h2>
+</dtml-if>
+</dtml-let>
+
+<p class="form-help">
+A Folder contains other objects. Use Folders to organize your
+web objects in to logical groups.
+</p>
+
+<p class="form-help">
+A HBTreeFolder2 may be able to handle a larger number
+of objects than a standard BTreeFolder because it use btree of
+btree to store objects.
+It is more efficient than the original BTreeFolder2 product,
+but you must provide well constructed id according to hashId method
+</p>
+
+<FORM ACTION="manage_addHBTreeFolder2" METHOD="POST">
+
+<table cellspacing="0" cellpadding="2" border="0">
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-label">
+    Id
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="id" size="40" />
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    <div class="form-optional">
+    Title
+    </div>
+    </td>
+    <td align="left" valign="top">
+    <input type="text" name="title" size="40" />
+    </td>
+  </tr>
+
+  <tr>
+    <td align="left" valign="top">
+    </td>
+    <td align="left" valign="top">
+    <div class="form-element">
+    <input class="form-element" type="submit" name="submit" 
+     value="Add" />
+    </div>
+    </td>
+  </tr>
+</table>
+</form>
+
+<dtml-if manage_page_footer>
+  <dtml-var manage_page_footer>
+<dtml-else>
+  </body></html>
+</dtml-if>
diff --git a/product/HBTreeFolder2/tests/__init__.py b/product/HBTreeFolder2/tests/__init__.py
new file mode 100755
index 0000000000..d2f3ead134
--- /dev/null
+++ b/product/HBTreeFolder2/tests/__init__.py
@@ -0,0 +1 @@
+"""Python package."""
diff --git a/product/HBTreeFolder2/tests/testHBTreeFolder2.py b/product/HBTreeFolder2/tests/testHBTreeFolder2.py
new file mode 100755
index 0000000000..a32a5a1eac
--- /dev/null
+++ b/product/HBTreeFolder2/tests/testHBTreeFolder2.py
@@ -0,0 +1,219 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Corporation 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
+#
+##############################################################################
+
+import unittest
+import ZODB
+import Testing
+import Zope
+from Products.HBTreeFolder2.HBTreeFolder2 \
+     import HBTreeFolder2, ExhaustedUniqueIdsError
+from OFS.ObjectManager import BadRequestException
+from OFS.Folder import Folder
+from Acquisition import aq_base
+
+
+class HBTreeFolder2Tests(unittest.TestCase):
+
+    def getBase(self, ob):
+        # This is overridden in subclasses.
+        return aq_base(ob)
+
+    def setUp(self):
+        self.f = HBTreeFolder2('root')
+        ff = HBTreeFolder2('item')
+        self.f._setOb(ff.id, ff)
+        self.ff = self.f._getOb(ff.id)
+
+    def testAdded(self):
+        self.assertEqual(self.ff.id, 'item')
+
+    def testCount(self):
+        self.assertEqual(self.f.objectCount(), 1)
+        self.assertEqual(self.ff.objectCount(), 0)
+        self.assertEqual(len(self.f), 1)
+        self.assertEqual(len(self.ff), 0)
+
+    def testObjectIds(self):
+        self.assertEqual(list(self.f.objectIds()), ['item'])
+        self.assertEqual(list(self.f.keys()), ['item'])
+        self.assertEqual(list(self.ff.objectIds()), [])
+        f3 = HBTreeFolder2('item3')
+        self.f._setOb(f3.id, f3)
+        lst = list(self.f.objectIds())
+        lst.sort()
+        self.assertEqual(lst, ['item', 'item3'])
+
+    def testObjectValues(self):
+        values = self.f.objectValues()
+        self.assertEqual(len(values), 1)
+        self.assertEqual(values[0].id, 'item')
+        # Make sure the object is wrapped.
+        self.assert_(values[0] is not self.getBase(values[0]))
+
+    def testObjectItems(self):
+        items = self.f.objectItems()
+        self.assertEqual(len(items), 1)
+        id, val = items[0]
+        self.assertEqual(id, 'item')
+        self.assertEqual(val.id, 'item')
+        # Make sure the object is wrapped.
+        self.assert_(val is not self.getBase(val))
+
+    def testHasKey(self):
+        self.assert_(self.f.hasObject('item'))  # Old spelling
+        self.assert_(self.f.has_key('item'))  # New spelling
+
+    def testDelete(self):
+        self.f._delOb('item')
+        self.assertEqual(list(self.f.objectIds()), [])
+        self.assertEqual(self.f.objectCount(), 0)
+
+    def testObjectMap(self):
+        map = self.f.objectMap()
+        self.assertEqual(list(map), [{'id': 'item', 'meta_type':
+                                      self.ff.meta_type}])
+        # I'm not sure why objectMap_d() exists, since it appears to be
+        # the same as objectMap(), but it's implemented by Folder.
+        self.assertEqual(list(self.f.objectMap_d()), list(self.f.objectMap()))
+
+    def testObjectIds_d(self):
+        self.assertEqual(self.f.objectIds_d(), {'item': 1})
+
+    def testCheckId(self):
+        self.assertEqual(self.f._checkId('xyz'), None)
+        self.assertRaises(BadRequestException, self.f._checkId, 'item')
+        self.assertRaises(BadRequestException, self.f._checkId, 'REQUEST')
+
+    def testSetObject(self):
+        f2 = HBTreeFolder2('item2')
+        self.f._setObject(f2.id, f2)
+        self.assert_(self.f.has_key('item2'))
+        self.assertEqual(self.f.objectCount(), 2)
+
+    def testWrapped(self):
+        # Verify that the folder returns wrapped versions of objects.
+        base = self.getBase(self.f._getOb('item'))
+        self.assert_(self.f._getOb('item') is not base)
+        self.assert_(self.f['item'] is not base)
+        self.assert_(self.f.get('item') is not base)
+        self.assert_(self.getBase(self.f._getOb('item')) is base)
+
+    def testGenerateId(self):
+        ids = {}
+        for n in range(10):
+            ids[self.f.generateId()] = 1
+        self.assertEqual(len(ids), 10)  # All unique
+        for id in ids.keys():
+            self.f._checkId(id)  # Must all be valid
+
+    def testGenerateIdDenialOfServicePrevention(self):
+        for n in range(10):
+            item = Folder()
+            item.id = 'item%d' % n
+            self.f._setOb(item.id, item)
+        self.f.generateId('item', rand_ceiling=20)  # Shouldn't be a problem
+        self.assertRaises(ExhaustedUniqueIdsError,
+                          self.f.generateId, 'item', rand_ceiling=9)
+
+    def testReplace(self):
+        old_f = Folder()
+        old_f.id = 'item'
+        inner_f = HBTreeFolder2('inner')
+        old_f._setObject(inner_f.id, inner_f)
+        self.ff._populateFromFolder(old_f)
+        self.assertEqual(self.ff.objectCount(), 1)
+        self.assert_(self.ff.has_key('inner'))
+        self.assertEqual(self.getBase(self.ff._getOb('inner')), inner_f)
+
+    def testObjectListing(self):
+        f2 = HBTreeFolder2('somefolder')
+        self.f._setObject(f2.id, f2)
+        # Hack in an absolute_url() method that works without context.
+        self.f.absolute_url = lambda: ''
+        info = self.f.getBatchObjectListing()
+        self.assertEqual(info['b_start'], 1)
+        self.assertEqual(info['b_end'], 2)
+        self.assertEqual(info['prev_batch_url'], '')
+        self.assertEqual(info['next_batch_url'], '')
+        self.assert_(info['formatted_list'].find('</select>') > 0)
+        self.assert_(info['formatted_list'].find('item') > 0)
+        self.assert_(info['formatted_list'].find('somefolder') > 0)
+
+        # Ensure batching is working.
+        info = self.f.getBatchObjectListing({'b_count': 1})
+        self.assertEqual(info['b_start'], 1)
+        self.assertEqual(info['b_end'], 1)
+        self.assertEqual(info['prev_batch_url'], '')
+        self.assert_(info['next_batch_url'] != '')
+        self.assert_(info['formatted_list'].find('item') > 0)
+        self.assert_(info['formatted_list'].find('somefolder') < 0)
+
+        info = self.f.getBatchObjectListing({'b_start': 2})
+        self.assertEqual(info['b_start'], 2)
+        self.assertEqual(info['b_end'], 2)
+        self.assert_(info['prev_batch_url'] != '')
+        self.assertEqual(info['next_batch_url'], '')
+        self.assert_(info['formatted_list'].find('item') < 0)
+        self.assert_(info['formatted_list'].find('somefolder') > 0)
+
+    def testObjectListingWithSpaces(self):
+        # The option list must use value attributes to preserve spaces.
+        name = " some folder "
+        f2 = HBTreeFolder2(name)
+        self.f._setObject(f2.id, f2)
+        self.f.absolute_url = lambda: ''
+        info = self.f.getBatchObjectListing()
+        expect = '<option value="%s">%s</option>' % (name, name)
+        self.assert_(info['formatted_list'].find(expect) > 0)
+
+    def testCleanup(self):
+        self.assert_(self.f._cleanup())
+        key = TrojanKey('a')
+        self.f._htree[key] = 'b'
+        self.assert_(self.f._cleanup())
+        key.value = 'z'
+
+        # With a key in the wrong place, there should now be damage.
+        self.assert_(not self.f._cleanup())
+        # Now it's fixed.
+        self.assert_(self.f._cleanup())
+        # Verify the management interface also works,
+        # but don't test return values.
+        self.f.manage_cleanup()
+        key.value = 'a'
+        self.f.manage_cleanup()
+
+
+class TrojanKey:
+    """Pretends to be a consistent, immutable, humble citizen...
+
+    then sweeps the rug out from under the HBTree.
+    """
+    def __init__(self, value):
+        self.value = value
+
+    def __cmp__(self, other):
+        return cmp(self.value, other)
+
+    def __hash__(self):
+        return hash(self.value)
+
+
+def test_suite():
+    return unittest.TestSuite((
+        unittest.makeSuite(HBTreeFolder2Tests),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
diff --git a/product/HBTreeFolder2/version.txt b/product/HBTreeFolder2/version.txt
new file mode 100755
index 0000000000..0b69fa3114
--- /dev/null
+++ b/product/HBTreeFolder2/version.txt
@@ -0,0 +1 @@
+HBTreeFolder2-1.0.0
-- 
2.30.9