Commit 193c3d7a authored by Arnaud Fontaine's avatar Arnaud Fontaine

ZODB Components not following ID=(document|extension|test).VERSION.REFERENCE...

ZODB Components not following ID=(document|extension|test).VERSION.REFERENCE convention are not importable anymore.

Import mechanism now relies on this convention to find the document in
portal_components.

This is necessary to get rid of _registry_dict (ZODB Components equivalent of
document_class_registry), along with several locks. This used to be a mapping
between (reference, version) to ID stored outside of Zope Transactions as a
dict on global erp5.component.XXX modules.

This did not work for long transactions such as installing many bt5s:
  1. Transaction1: Install a bt5 with ZODB Component "Foo".
     => Trigger a reset and _registry_dict is cleared.
  2. Transaction2: TimerServer kicks in.
     => Generate _registry_dict without "Foo".
  3. Transaction1: Install another bt5 using "Foo".
     => "Foo" not in _registry_dict and thus considered not present.
parent 9f6e6094
......@@ -4234,7 +4234,7 @@ class DocumentTemplateItem(FilesystemToZodbTemplateItem):
@staticmethod
def _getZodbObjectId(id):
return DocumentComponent._getIdPrefix() + '.' + id
return DocumentComponent.getIdPrefix() + '.' + id
@staticmethod
def _getFilesystemPath(class_id):
......@@ -4342,7 +4342,7 @@ class ExtensionTemplateItem(DocumentTemplateItem):
@staticmethod
def _getZodbObjectId(id):
return ExtensionComponent._getIdPrefix() + '.' + id
return ExtensionComponent.getIdPrefix() + '.' + id
def getTemplateIdList(self):
return self.getTemplateExtensionIdList()
......@@ -4363,7 +4363,7 @@ class TestTemplateItem(DocumentTemplateItem):
@staticmethod
def _getZodbObjectId(id):
return TestComponent._getIdPrefix() + '.' + id
return TestComponent.getIdPrefix() + '.' + id
def getTemplateIdList(self):
return self.getTemplateTestIdList()
......
......@@ -7349,7 +7349,7 @@ class TestBusinessTemplate(BusinessTemplateMixin):
Folder._setOb(self, id, object)
""")
component_id_prefix = DocumentComponent._getIdPrefix()
component_id_prefix = DocumentComponent.getIdPrefix()
component_portal_type = DocumentComponent.portal_type
tool_type = 'My Tool'
tool_class = 'MyTool'
......@@ -7859,7 +7859,7 @@ class TestDocumentTemplateItem(BusinessTemplateMixin):
sequence_list.addSequenceString(sequence_string)
sequence_list.play(self)
component_id_prefix = DocumentComponent._getIdPrefix()
component_id_prefix = DocumentComponent.getIdPrefix()
component_portal_type = DocumentComponent.portal_type
def stepCreateZodbDocument(self, sequence=None, **kw):
......@@ -8345,7 +8345,7 @@ class TestExtensionTemplateItem(TestDocumentTemplateItem):
template_property = 'template_extension_id_list'
# Specific to ZODB Extension Component
component_id_prefix = ExtensionComponent._getIdPrefix()
component_id_prefix = ExtensionComponent.getIdPrefix()
component_portal_type = ExtensionComponent.portal_type
from Products.ERP5Type.Core.TestComponent import TestComponent
......@@ -8364,7 +8364,7 @@ class TestTestTemplateItem(TestDocumentTemplateItem):
template_property = 'template_test_id_list'
# Specific to ZODB Extension Component
component_id_prefix = TestComponent._getIdPrefix()
component_id_prefix = TestComponent.getIdPrefix()
component_portal_type = TestComponent.portal_type
def stepAddTestToBusinessTemplate(self, sequence=None, **kw):
......
......@@ -60,5 +60,5 @@ class DocumentComponent(ComponentMixin, TextContentHistoryMixin):
return 'erp5.component.document'
@staticmethod
def _getIdPrefix():
def getIdPrefix():
return 'document'
......@@ -59,5 +59,5 @@ class ExtensionComponent(ComponentMixin, TextContentHistoryMixin):
return 'erp5.component.extension'
@staticmethod
def _getIdPrefix():
def getIdPrefix():
return 'extension'
......@@ -60,5 +60,5 @@ class TestComponent(ComponentMixin, TextContentHistoryMixin):
return 'erp5.component.test'
@staticmethod
def _getIdPrefix():
def getIdPrefix():
return 'test'
......@@ -210,7 +210,23 @@ class TypesTool(TypeProvider):
document_type_set = set(document_class_registry)
import erp5.component.document
document_type_set.update(erp5.component.document._registry_dict)
portal = self.getPortalObject()
version_priority_set = set(portal.getVersionPriorityNameList())
# objectValues should not be used for a large number of objects, but
# this is only done upon reset, moreover using the Catalog is too risky
# as it lags behind and depends upon objects being reindexed
for component in portal.portal_components.objectValues(portal_type='Document Component'):
# Only consider modified or validated states as state transition will
# be handled by component_validation_workflow which will take care of
# updating the registry
validation_state_tuple = component.getValidationState()
if validation_state_tuple in ('modified', 'validated'):
version = component.getVersion(validated_only=True)
# The versions should have always been set on ERP5Site property
# beforehand
if version in version_priority_set:
document_type_set.add(component.getReference(validated_only=True))
return sorted(document_type_set)
......
......@@ -39,6 +39,7 @@ from Products.ERP5Type.Globals import get_request
from . import aq_method_lock
from types import ModuleType
from zLOG import LOG, BLATHER, WARNING
from Acquisition import aq_base
class ComponentVersionPackage(ModuleType):
"""
......@@ -73,9 +74,9 @@ class ComponentDynamicPackage(ModuleType):
self._namespace = namespace
self._namespace_prefix = namespace + '.'
self._id_prefix = namespace.rsplit('.', 1)[1]
self._portal_type = portal_type
self.__version_suffix_len = len('_version')
self.__registry_dict = collections.defaultdict(dict)
self.__fullname_source_code_dict = {}
# Add this module to sys.path for future imports
......@@ -84,48 +85,6 @@ class ComponentDynamicPackage(ModuleType):
# Add the import hook
sys.meta_path.append(self)
@property
def _registry_dict(self):
"""
Create the component registry, this is very similar to
Products.ERP5Type.document_class_registry and avoids checking whether a
Component exists at each call at the expense of being slower when being
re-generated after a reset. Moreover, it allows to handle reference
easily.
"""
if not self.__registry_dict:
portal = getSite()
try:
component_tool = portal.portal_components
# When installing ERP5 site, erp5_core_components has not been installed
# yet, thus this will obviously failed...
#
# XXX-arnau: Is this needed as it is now done in synchronizeDynamicModules?
except AttributeError:
return {}
version_priority_set = set(portal.getVersionPriorityNameList())
# objectValues should not be used for a large number of objects, but
# this is only done upon reset, moreover using the Catalog is too risky
# as it lags behind and depends upon objects being reindexed
for component in component_tool.objectValues(portal_type=self._portal_type):
# Only consider modified or validated states as state transition will
# be handled by component_validation_workflow which will take care of
# updating the registry
validation_state_tuple = component.getValidationState()
if validation_state_tuple in ('modified', 'validated'):
version = component.getVersion(validated_only=True)
# The versions should have always been set on ERP5Site property
# beforehand
if version in version_priority_set:
reference = component.getReference(validated_only=True)
self.__registry_dict[reference][version] = (component.getId(),
component._p_oid)
return self.__registry_dict
def get_source(self, fullname):
"""
PEP-302 function to get the source code, used mainly by linecache for
......@@ -160,12 +119,6 @@ class ComponentDynamicPackage(ModuleType):
except RuntimeError:
import_lock_held = False
# The import lock has been released, but as _registry_dict may be
# initialized or cleared, no other Components should access this critical
# region
#
# TODO-arnau: Too coarse-grain?
aq_method_lock.acquire()
try:
site = getSite()
......@@ -173,6 +126,7 @@ class ComponentDynamicPackage(ModuleType):
# erp5.component.XXX.YYY.ZZZ where erp5.component.XXX.YYY is the current
# Component where an import is done
name = fullname[len(self._namespace_prefix):]
# name=VERSION_version.REFERENCE
if '.' in name:
try:
version, name = name.split('.')
......@@ -180,23 +134,38 @@ class ComponentDynamicPackage(ModuleType):
except ValueError:
return None
try:
self._registry_dict[name][version]
except KeyError:
id_ = "%s.%s.%s" % (self._id_prefix, version, name)
# aq_base() because this should not go up to ERP5Site and trigger
# side-effects, after all this only check for existence...
component = getattr(aq_base(site.portal_components), id_, None)
if component is None or component.getValidationState() not in ('modified',
'validated'):
return None
# Skip unavailable components, otherwise Products for example could be
# wrongly considered as importable and thus the actual filesystem class
# ignored
elif (name not in self._registry_dict and
name[:-self.__version_suffix_len] not in site.getVersionPriorityNameList()):
return None
#
# name=VERSION_version
elif name.endswith('_version'):
if name[:-self.__version_suffix_len] not in site.getVersionPriorityNameList():
return None
# name=REFERENCE
else:
component_tool = aq_base(site.portal_components)
for version in site.getVersionPriorityNameList():
id_ = "%s.%s.%s" % (self._id_prefix, version, name)
component = getattr(component_tool, id_, None)
if component is not None and component.getValidationState() in ('modified',
'validated'):
break
else:
return None
return self
finally:
aq_method_lock.release()
# Internal release of import lock at the end of import machinery will
# fail if the hook is not acquired
if import_lock_held:
......@@ -258,25 +227,17 @@ class ComponentDynamicPackage(ModuleType):
raise ImportError("%s: should be %s.VERSION.COMPONENT_REFERENCE (%s)" % \
(fullname, self._namespace, error))
try:
component_id = self._registry_dict[name][version][0]
except KeyError:
raise ImportError("%s: version %s of Component %s could not be found" % \
(fullname, version, name))
component_id = "%s.%s.%s" % (self._id_prefix, version, name)
# Otherwise, find the Component with the highest version priority
else:
try:
component_version_dict = self._registry_dict[name]
except KeyError:
raise ImportError("%s: Component %s could not be found" % (fullname,
name))
component_tool = aq_base(site.portal_components)
# Version priority name list is ordered in descending order
for version in site.getVersionPriorityNameList():
component_id_uid_tuple = component_version_dict.get(version)
if component_id_uid_tuple is not None:
component_id = component_id_uid_tuple[0]
component_id = "%s.%s.%s" % (self._id_prefix, version, name)
component = getattr(component_tool, component_id, None)
if component is not None and component.getValidationState() in ('modified',
'validated'):
break
else:
raise ImportError("%s: no version of Component %s in Site priority" % \
......@@ -433,8 +394,7 @@ class ComponentDynamicPackage(ModuleType):
if sub_package:
package = sub_package
else:
# Clear the Component registry and source code dict only once
self.__registry_dict.clear()
# Clear the source code dict only once
self.__fullname_source_code_dict.clear()
package = self
......
......@@ -63,7 +63,7 @@ class IComponent(Interface):
Return the module name where Component module are loaded into
"""
def _getIdPrefix():
def getIdPrefix():
"""
Return the ID prefix for Component objects
"""
......
......@@ -180,14 +180,14 @@ class ComponentMixin(PropertyRecordableMixin, Base):
default='')
}
_message_invalid_id = "ID is invalid, should be '${id_prefix}.VERSION.REFERENCE'"
_message_reference_not_set = "Reference must be set"
_message_invalid_reference = "Reference cannot end with '_version' or "\
"start with '_' or be equal to find_module, load_module or reset"
_message_version_not_set = "Version must be set"
_message_invalid_version = "Version cannot start with '_'"
_message_duplicated_version_reference = "${id} is validated has the same "\
"Reference and Version"
_message_text_content_not_set = "No source code"
_message_text_content_error = "Error in Source Code: ${error_message}"
......@@ -216,10 +216,27 @@ class ComponentMixin(PropertyRecordableMixin, Base):
"""
error_list = super(ComponentMixin, self).checkConsistency(*args, **kw)
object_relative_url = self.getRelativeUrl()
is_id_invalid = False
try:
prefix, version, reference = self.getId().split('.')
except ValueError:
is_id_invalid = True
else:
if (prefix != self.getIdPrefix() or
version != self.getVersion() or
reference != self.getReference()):
is_id_invalid = True
if is_id_invalid:
error_list.append(
ConsistencyMessage(self,
object_relative_url,
message=self._message_invalid_id,
mapping={'id_prefix': self.getIdPrefix()}))
reference = self.getReference()
reference_has_error = False
if not reference:
reference_has_error = True
error_list.append(
ConsistencyMessage(self,
object_relative_url,
......@@ -229,7 +246,6 @@ class ComponentMixin(PropertyRecordableMixin, Base):
elif (reference.endswith('_version') or
reference[0] == '_' or
reference in ('find_module', 'load_module', 'reset')):
reference_has_error = True
error_list.append(
ConsistencyMessage(self,
object_relative_url,
......@@ -247,26 +263,6 @@ class ComponentMixin(PropertyRecordableMixin, Base):
object_relative_url,
message=self._message_invalid_version,
mapping={}))
else:
package = __import__(self._getDynamicModuleNamespace(), globals(),
fromlist=[self._getDynamicModuleNamespace()], level=0)
component_id = None
component_uid = None
from Products.ERP5Type.dynamic import aq_method_lock
with aq_method_lock:
component_id_uid_tuple = package._registry_dict.get(
self.getReference(), {}).get(self.getVersion(), None)
if component_id_uid_tuple:
component_id, component_uid = component_id_uid_tuple
if (component_id is not None and component_uid is not None and
not reference_has_error and
component_uid != self._p_oid and component_id != self.getId()):
error_list.append(
ConsistencyMessage(self,
object_relative_url,
message=self._message_duplicated_version_reference,
mapping=dict(id=component_id)))
text_content = self.getTextContent()
if not text_content:
......@@ -360,7 +356,7 @@ class ComponentMixin(PropertyRecordableMixin, Base):
# needed when importing from filesystem, moreover errors may occur
# if in the same transaction a Component is created and another
# one depending upon the former...
object_id = '%s.%s.%s' % (cls._getIdPrefix(), version, reference)
object_id = '%s.%s.%s' % (cls.getIdPrefix(), version, reference)
new_component = context.newContent(id=object_id,
reference=reference,
version=version,
......
......@@ -1316,7 +1316,7 @@ class _TestZodbComponent(SecurityTestCase):
def afterSetUp(self):
self._component_tool = self.portal.portal_components
self._module = __import__(self._getComponentModuleName(),
self._module = __import__(self._document_class._getDynamicModuleNamespace(),
fromlist=['erp5.component'])
self._component_tool.reset(force=True,
reset_portal_type_at_transaction_boundary=True)
......@@ -1325,28 +1325,18 @@ class _TestZodbComponent(SecurityTestCase):
"""
Create new Component
"""
full_id = '%s.%s.%s' % (self._getComponentModuleName(),
version + '_version',
reference)
if id_ is not None:
full_id += '.%s' % id_
if id_ is None:
id_ = '%s.%s.%s' % (self._document_class.getIdPrefix(), version, reference)
return self._component_tool.newContent(
id=full_id,
id=id_,
version=version,
reference=reference,
text_content=text_content,
portal_type=self._component_portal_type)
@abc.abstractmethod
def _getComponentModuleName(self):
"""
Abstract method defining ZODB Component top-level package name
"""
portal_type=self._portal_type)
def _getComponentFullModuleName(self, module_name):
return self._getComponentModuleName() + '.' + module_name
return self._document_class._getDynamicModuleNamespace() + '.' + module_name
def failIfModuleImportable(self, module_name):
"""
......@@ -1380,7 +1370,7 @@ class _TestZodbComponent(SecurityTestCase):
self._getComponentFullModuleName(module_name))
if expected_default_version is not None:
top_module_name = self._getComponentModuleName()
top_module_name = self._document_class._getDynamicModuleNamespace()
top_module = __import__(top_module_name, level=0, fromlist=[top_module_name])
# The module must be available in its default version
......@@ -1410,7 +1400,7 @@ class _TestZodbComponent(SecurityTestCase):
def _importModule(self, module_name):
return __import__(self._getComponentFullModuleName(module_name),
fromlist=[self._getComponentModuleName()],
fromlist=[self._document_class._getDynamicModuleNamespace()],
level=0)
def testValidateInvalidateDelete(self):
......@@ -1492,6 +1482,37 @@ class _TestZodbComponent(SecurityTestCase):
if o.getReference() == 'TestValidateInvalidateComponent'],
[])
def testInvalidId(self):
"""
Check whether checkConsistency has been properly implemented for checking
Component ID which should follow the format 'getIdPrefix().VERSION.REFERENCE'
"""
id_prefix = self._document_class.getIdPrefix()
version = "erp5"
reference = "TestWithInvalidId"
valid_id = "%s.%s.%s" % (id_prefix, version, reference)
component = self._newComponent(reference,
'def foobar():\n return 42',
version,
valid_id)
self.tic()
self.assertEqual(component.checkConsistency(), [])
for invalid_id in ("INVALID_PREFIX.%s.%s" % (version, reference),
"%s.INVALID_VERSION.%s" % (id_prefix, reference),
"%s.%s.INVALID_REFERENCE" % (id_prefix, version)):
component.setId(invalid_id)
self.tic()
self.assertEqual(
[m.getMessage().translate() for m in component.checkConsistency()],
[self.portal.Base_translateString(ComponentMixin._message_invalid_id,
mapping={'id_prefix': id_prefix})])
component.setId(valid_id)
self.tic()
self.assertEqual(component.checkConsistency(), [])
def testReferenceWithReservedKeywords(self):
"""
Check whether checkConsistency has been properly implemented for checking
......@@ -1537,6 +1558,10 @@ class _TestZodbComponent(SecurityTestCase):
'find_module': ComponentMixin._message_invalid_reference,
'load_module': ComponentMixin._message_invalid_reference}
invalid_id_error_message = self.portal.Base_translateString(
ComponentMixin._message_invalid_id,
mapping={'id_prefix': self._document_class.getIdPrefix()})
for invalid_reference, error_message in invalid_reference_dict.iteritems():
# Reset should not be performed
ComponentTool.reset = assertResetNotCalled
......@@ -1550,7 +1575,8 @@ class _TestZodbComponent(SecurityTestCase):
self.assertEqual(component.getValidationState(), 'modified')
self.assertEqual([m.getMessage().translate()
for m in component.checkConsistency()],
[error_message])
[invalid_id_error_message,
error_message])
self.assertEqual(component.getTextContentErrorMessageList(), [])
self.assertEqual(component.getTextContentWarningMessageList(), [])
self.assertEqual(component.getReference(), invalid_reference)
......@@ -1620,6 +1646,10 @@ class _TestZodbComponent(SecurityTestCase):
# make sense to have reference starting with '_'
'_TestVersionWithReservedKeywords': ComponentMixin._message_invalid_version}
invalid_id_error_message = self.portal.Base_translateString(
ComponentMixin._message_invalid_id,
mapping={'id_prefix': self._document_class.getIdPrefix()})
for invalid_version, error_message in invalid_version_dict.iteritems():
# Reset should not be performed
ComponentTool.reset = assertResetNotCalled
......@@ -1633,7 +1663,8 @@ class _TestZodbComponent(SecurityTestCase):
self.assertEqual(component.getValidationState(), 'modified')
self.assertEqual([m.getMessage().translate()
for m in component.checkConsistency()],
[error_message])
[invalid_id_error_message,
error_message])
self.assertEqual(component.getTextContentErrorMessageList(), [])
self.assertEqual(component.getTextContentWarningMessageList(), [])
self.assertEqual(component.getVersion(), invalid_version)
......@@ -1797,7 +1828,7 @@ class _TestZodbComponent(SecurityTestCase):
component.validate()
self.tic()
top_module_name = self._getComponentModuleName()
top_module_name = self._document_class._getDynamicModuleNamespace()
# Create a new Component which uses a specific version of the previously
# created Component
......@@ -1877,7 +1908,7 @@ def bar(*args, **kwargs):
# added to ERP5Site version priorities
self.failIfModuleImportable('foo_version.TestVersionPriority')
top_module_name = self._getComponentModuleName()
top_module_name = self._document_class._getDynamicModuleNamespace()
top_module = __import__(top_module_name, level=0,
fromlist=[top_module_name])
......@@ -1963,45 +1994,6 @@ def bar(*args, **kwargs):
self.assertUserCanModifyDocument(user_id, component)
self.assertUserCanDeleteDocument(user_id, component)
def testValidateComponentWithSameReferenceVersionAlreadyValidated(self):
reference = 'ValidateComponentWithSameReferenceVersionAlreadyValidated'
component = self._newComponent(reference, 'def foo():\n print "ok"')
component.validate()
self.tic()
component_dup = self._newComponent(reference, 'def foo():\n print "ok"',
id_='duplicated')
self.tic()
from Products.DCWorkflow.DCWorkflow import ValidationFailed
self.assertRaises(ValidationFailed,
self.portal.portal_workflow.doActionFor,
component_dup, 'validate_action')
self.assertEqual(component_dup.getValidationState(), 'draft')
component_dup.setReference(reference + '_copy')
component_dup.validate()
self.tic()
component_dup.setReference(reference)
self.tic()
self.assertEqual(component_dup.getValidationState(), 'modified')
self.assertEqual(component_dup.getReference(), reference)
self.assertEqual(component_dup.getReference(validated_only=True),
reference + '_copy')
component_dup.invalidate()
self.tic()
component_dup.setReference(reference)
self.assertRaises(ValidationFailed,
self.portal.portal_workflow.doActionFor,
component_dup, 'validate_action')
self.assertEqual(component_dup.getValidationState(), 'invalidated')
from Products.ERP5Type.Core.ExtensionComponent import ExtensionComponent
class TestZodbExtensionComponent(_TestZodbComponent):
......@@ -2009,10 +2001,8 @@ class TestZodbExtensionComponent(_TestZodbComponent):
Tests specific to ZODB Extension Component (previously defined in bt5 and
installed on the filesystem in $INSTANCE_HOME/Extensions)
"""
_component_portal_type = 'Extension Component'
def _getComponentModuleName(self):
return ExtensionComponent._getDynamicModuleNamespace()
_portal_type = 'Extension Component'
_document_class = ExtensionComponent
def testExternalMethod(self):
"""
......@@ -2109,10 +2099,8 @@ class TestZodbDocumentComponent(_TestZodbComponent):
previously defined in bt5 and installed on the filesystem in
$INSTANCE_HOME/Document. Later on, Product Documents will also be migrated
"""
_component_portal_type = 'Document Component'
def _getComponentModuleName(self):
return DocumentComponent._getDynamicModuleNamespace()
_portal_type = 'Document Component'
_document_class = DocumentComponent
def testAssignToPortalTypeClass(self):
"""
......@@ -2240,10 +2228,8 @@ class TestZodbTestComponent(_TestZodbComponent):
Tests specific to ZODB Test Component (known as Live Tests, and previously
defined in bt5 and installed in $INSTANCE_HOME/test)
"""
_component_portal_type = 'Test Component'
def _getComponentModuleName(self):
return TestComponent._getDynamicModuleNamespace()
_portal_type = 'Test Component'
_document_class = TestComponent
def testRunLiveTest(self):
"""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment