# # Copyright (c) 2013 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # """ Implementation of the utilities needed for the relations package. """ from logging import getLogger from BTrees.IOBTree import IOBTree from persistent import Persistent from persistent.interfaces import IPersistent from zope import component from zope.component import adapts from zope.interface import Interface, Attribute, implements from zope.app.catalog.catalog import Catalog, ResultSet from zope.app.catalog.field import FieldIndex from zope.intid.interfaces import IIntIds from zope.location.interfaces import ILocation from zope.event import notify from zope.component.interfaces import ObjectEvent from zope.security.proxy import removeSecurityProxy from zope.traversing.api import getName, getParent from interfaces import IRelationRegistry, IRelationInvalidatedEvent, IRelation logger = getLogger('cybertools.relation.registry') class DummyRelationRegistry(object): """ Dummy implementation for demonstration and test purposes. """ implements(IRelationRegistry) def __init__(self): self.relations = [] self.objects = [] def register(self, relation): if relation not in self.relations: self.relations.append(relation) if relation not in self.objects: self.objects.append(relation) for attr in ('first', 'second', 'third',): value = getattr(relation, attr, None) if value is not None: intids = component.queryUtility(IIntIds) if intids is not None: intids.register(value) elif value not in self.objects: self.objects.append(value) def unregister(self, relation): if relation in self.relations: self.relations.remove(relation) notify(RelationInvalidatedEvent(relation)) def getUniqueIdForObject(self, obj): if obj == '*': # wild card return '*' intids = component.queryUtility(IIntIds) if intids is not None: return intids.register(obj) if obj not in self.objects: self.objects.append(obj) return self.objects.index(obj) def query(self, example=None, **kw): result = [] criteria = {} if example is not None: for attr in ('first', 'second', 'third',): value = getattr(example, attr, None) if value is not None: criteria[attr] = value criteria['relationship'] = example criteria.update(kw) for r in self.relations: if r is None: continue hit = True for k in criteria: crit = criteria[k] if k == 'relationship': critpn = crit.getPredicateName() if critpn.endswith('*'): if not r.getPredicateName().startswith(critpn[:-1]): hit = False; break elif r.getPredicateName() != critpn: hit = False; break else: if not hasattr(r, k) or getattr(r, k) != crit: hit = False; break if hit: result.append(r) return result class RelationRegistry(Catalog): """ Local utility for registering (cataloguing) and searching relations. """ implements(IRelationRegistry) relations = None def __init__(self, *args, **kw): super(RelationRegistry, self).__init__(*args, **kw) self.relations = IOBTree() def setupIndexes(self): for idx in ('relationship', 'first', 'second', 'third'): if idx not in self: self[idx] = FieldIndex(idx, IIndexableRelation) def register(self, relation): if getattr(relation, '__parent__', None) is None: # Allow the IntIds utility to get a DB connection: relation.__parent__ = self uid = component.getUtility(IIntIds).register(relation) self.index_doc(uid, relation) if self.relations is not None: self.relations[uid] = relation #logger.info('added relation with uid %i.' % uid) def unregister(self, relation): uid = component.getUtility(IIntIds).getId(relation) self.unindex_doc(uid) if self.relations is not None and uid in self.relations: del self.relations[uid] #logger.info('removed relation with uid %i.' % uid) notify(RelationInvalidatedEvent(relation)) def cleanupRelations(self): logger = getLogger('cybertools.relation.registry.cleanup') intids = component.getUtility(IIntIds) if self.relations is not None: logger.info('%i relations currently stored.' % len(self.relations)) self.relations = IOBTree() result = self.apply(dict(relationship='*')) logger.info('%i relations found.' % len(result)) for idx, uid in enumerate(result): relation = intids.getObject(uid) self.relations[uid] = relation pass def getUniqueIdForObject(self, obj): if obj == '*': # wild card return '*' return component.getUtility(IIntIds).queryId(obj) def apply(self, criteria): for k in criteria: # set min, max value = criteria[k] if k == 'relationship' and value.endswith('*'): criteria[k] = (value[:-1], value[:-1] + '\x7f') else: criteria[k] = (value, value) return super(RelationRegistry, self).apply(criteria) def query(self, example=None, **kw): intids = component.getUtility(IIntIds) criteria = {} if example is not None: for attr in ('first', 'second', 'third',): value = getattr(example, attr, None) if value is not None: criteria[attr] = intids.getId(value) pn = example.getPredicateName() if pn is not None: criteria['relationship'] = pn for k in kw: # overwrite example fields with explicit values if k == 'relationship': criteria[k] = kw[k].getPredicateName() else: criteria[k] = intids.getId(kw[k]) results = self.apply(criteria) return ResultSet(results, intids) class IIndexableRelation(Interface): """ Provides the attributes needed for indexing relation objects in a catalog-based registry. """ class IndexableRelationAdapter(object): """ Adapter for providing the attributes needed for indexing relation objects. """ implements(IIndexableRelation) adapts(IRelation) def __init__(self, context): self.context = context def getRelationship(self): return self.context.getPredicateName() relationship = property(getRelationship) def __getattr__(self, attr): value = getattr(self.context, attr) if IPersistent.providedBy(value): return component.getUtility(IIntIds).getId(value) else: return value # convenience functions: def getRelations(first=None, second=None, third=None, relationships=None): """ Return a sequence of relations matching the query specified by the parameters. The relationships parameter expects a sequence of relationships (relation classes or predicate objects). """ registry = component.getUtility(IRelationRegistry) query = {} if first is not None: query['first'] = first if second is not None: query['second'] = second if third is not None: query['third'] = third if not relationships: return registry.query(**query) else: predicates = [] for r in relationships: if hasattr(r, 'predicate'): predicates.append(r.predicate) r.predicate = None else: predicates.append(r.getPredicateName()) result = registry.query(**query) if predicates: return [r for r in result if r.ident in predicates or r.fallback in predicates] return result def getRelationSingle(obj=None, relationship=None, forSecond=True): """ Returns the one and only relation for first having relationship or None if there is none. Raise an error if there is more than one hit. """ if forSecond: rels = getRelations(second=obj, relationships=[relationship]) else: rels = getRelations(first=obj, relationships=[relationship]) if len(rels) == 0: return None if len(rels) > 1: raise ValueError('Multiple hits when only one relation expected: ' '%s, relationship: %s' % (getName(obj), relationship.getPredicateName())) return list(rels)[0] def setRelationSingle(relation, forSecond=True): """ Register the relation given, unregistering already existing relations for first and relationship. After this operation there will be only one relation for first with the relationship given. """ first = relation.first second = relation.second registry = component.getUtility(IRelationRegistry) if forSecond: rels = list(registry.query(second=second, relationship=relation)) else: rels = list(registry.query(first=first, relationship=relation)) for oldRel in rels: registry.unregister(oldRel) registry.register(relation) # events and handlers class RelationInvalidatedEvent(ObjectEvent): implements(IRelationInvalidatedEvent) def invalidateRelations(context, event): """ Handles IObjectRemoved event: unregisters all relations the object to be removed is involved in. """ # TODO: check marker interface of object: # if not IRelatable.providedBy(event.object): # return relations = [] registries = component.getAllUtilitiesRegisteredFor(IRelationRegistry) for registry in registries: for attr in ('first', 'second', 'third'): try: relations = registry.query(**{attr: context}) for relation in relations: registry.unregister(relation) except KeyError: pass def removeRelation(context, event): """ Handles IRelationInvalidatedEvent by removing the relation (that should be already unregistered from the relation registry) from its container (if appropriate) and the IntIds utility. """ if ILocation.providedBy(context): parent = getParent(context) if parent is not None: del parent[context] intids = component.getUtility(IIntIds) intids.unregister(context) def setupIndexes(context, event): """ Handles IObjectAdded event for the RelationRegistry utility and creates the indexes needed. """ if isinstance(context, RelationRegistry): context.setupIndexes()