server/hooksmanager.py
author Sylvain Thénault <sylvain.thenault@logilab.fr>
Thu, 10 Sep 2009 12:23:16 +0200
branchstable
changeset 3164 b025592ff478
parent 2579 e69b2342bd8b
child 2809 04fa75d43af4
child 2918 452b4c9ee61d
permissions -rw-r--r--
nicer fix

"""Hooks management

Hooks are called before / after any individual update of entities / relations
in the repository.

Here is the prototype of the different hooks:

* filtered on the entity's type:

  before_add_entity    (session, entity)
  after_add_entity     (session, entity)
  before_update_entity (session, entity)
  after_update_entity  (session, entity)
  before_delete_entity (session, eid)
  after_delete_entity  (session, eid)

* filtered on the relation's type:

  before_add_relation    (session, fromeid, rtype, toeid)
  after_add_relation     (session, fromeid, rtype, toeid)
  before_delete_relation (session, fromeid, rtype, toeid)
  after_delete_relation  (session, fromeid, rtype, toeid)


:organization: Logilab
:copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"

ENTITIES_HOOKS = ('before_add_entity',    'after_add_entity',
                  'before_update_entity', 'after_update_entity',
                  'before_delete_entity', 'after_delete_entity')
RELATIONS_HOOKS = ('before_add_relation',   'after_add_relation' ,
                   'before_delete_relation','after_delete_relation')
SYSTEM_HOOKS = ('server_backup', 'server_restore',
                'server_startup', 'server_shutdown',
                'session_open', 'session_close')

ALL_HOOKS = frozenset(ENTITIES_HOOKS + RELATIONS_HOOKS + SYSTEM_HOOKS)

class HooksManager(object):
    """handle hooks registration and calls
    """
    verification_hooks_activated = True

    def __init__(self, schema):
        self.set_schema(schema)

    def set_schema(self, schema):
        self._hooks = {}
        self.schema = schema
        self._init_hooks(schema)

    def register_hooks(self, hooks):
        """register a dictionary of hooks :

             {'event': {'entity or relation type': [callbacks list]}}
        """
        for event, subevents in hooks.items():
            for subevent, callbacks in subevents.items():
                for callback in callbacks:
                    self.register_hook(callback, event, subevent)

    def register_hook(self, function, event, etype=''):
        """register a function to call when <event> occurs

        <etype> is an entity/relation type or an empty string.

        If etype is the empty string, the function will be called at each event,
        else the function will be called only when event occurs on an entity or
        relation of the given type.
        """
        assert event in ALL_HOOKS, '%r NOT IN %r' % (event, ALL_HOOKS)
        assert (not event in SYSTEM_HOOKS or not etype), (event, etype)
        etype = etype or ''
        try:
            self._hooks[event][etype].append(function)
            self.debug('registered hook %s on %s (%s)', event, etype or 'any',
                       function.func_name)

        except KeyError:
            self.error('can\'t register hook %s on %s (%s)',
                       event, etype or 'any', function.func_name)

    def unregister_hook(self, function_or_cls, event=None, etype=''):
        """unregister a function to call when <event> occurs, or a Hook subclass.
        In the later case, event/type information are extracted from the given
        class.
        """
        if isinstance(function_or_cls, type) and issubclass(function_or_cls, Hook):
            for event, ertype in function_or_cls.register_to():
                for hook in self._hooks[event][ertype]:
                    if getattr(hook, 'im_self', None).__class__ is function_or_cls:
                        self._hooks[event][ertype].remove(hook)
                        self.info('unregister hook %s on %s (%s)', event, etype,
                                  function_or_cls.__name__)
                        break
                else:
                    self.warning("can't unregister hook %s on %s (%s), not found",
                                 event, etype, function_or_cls.__name__)
        else:
            assert event in ALL_HOOKS, event
            etype = etype or ''
            self.info('unregister hook %s on %s (%s)', event, etype,
                      function_or_cls.func_name)
            self._hooks[event][etype].remove(function_or_cls)

    def call_hooks(self, __event, __type='', *args, **kwargs):
        """call hook matching event and optional type"""
        if __type:
            self.info('calling hooks for event %s (%s)', __event, __type)
        else:
            self.info('calling hooks for event %s', __event)
        # call generic hooks first
        for hook in self._hooks[__event]['']:
            #print '[generic]', hook.__name__
            hook(*args, **kwargs)
        if __type:
            for hook in self._hooks[__event][__type]:
                #print '[%s]'%__type, hook.__name__
                hook(*args, **kwargs)

    def _init_hooks(self, schema):
        """initialize the hooks map"""
        for hook_event in ENTITIES_HOOKS:
            self._hooks[hook_event] = {'': []}
            for etype in schema.entities():
                self._hooks[hook_event][etype] = []
        for hook_event in RELATIONS_HOOKS:
            self._hooks[hook_event] = {'': []}
            for r_type in schema.relations():
                self._hooks[hook_event][r_type] = []
        for hook_event in SYSTEM_HOOKS:
            self._hooks[hook_event] = {'': []}

    def register_system_hooks(self, config):
        """register system hooks according to the configuration"""
        self.info('register core hooks')
        from cubicweb.server.hooks import _register_metadata_hooks, _register_wf_hooks
        _register_metadata_hooks(self)
        self.info('register workflow hooks')
        _register_wf_hooks(self)
        if config.core_hooks:
            from cubicweb.server.hooks import _register_core_hooks
            _register_core_hooks(self)
        if config.schema_hooks:
            from cubicweb.server.schemahooks import _register_schema_hooks
            self.info('register schema hooks')
            _register_schema_hooks(self)
        if config.usergroup_hooks:
            from cubicweb.server.hooks import _register_usergroup_hooks
            from cubicweb.server.hooks import _register_eproperty_hooks
            self.info('register user/group hooks')
            _register_usergroup_hooks(self)
            _register_eproperty_hooks(self)
        if config.security_hooks:
            from cubicweb.server.securityhooks import register_security_hooks
            self.info('register security hooks')
            register_security_hooks(self)
        if not self.verification_hooks_activated:
            self.deactivate_verification_hooks()

    def deactivate_verification_hooks(self):
        from cubicweb.server.hooks import (cardinalitycheck_after_add_entity,
                                        cardinalitycheck_before_del_relation,
                                        cstrcheck_after_add_relation,
                                        uniquecstrcheck_before_modification)
        self.warning('deactivating verification hooks')
        self.verification_hooks_activated = False
        self.unregister_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
        self.unregister_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
        self.unregister_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
        self.unregister_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
        self.unregister_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
#         self.unregister_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
#         self.unregister_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')

    def reactivate_verification_hooks(self):
        from cubicweb.server.hooks import (cardinalitycheck_after_add_entity,
                                        cardinalitycheck_before_del_relation,
                                        cstrcheck_after_add_relation,
                                        uniquecstrcheck_before_modification)
        self.warning('reactivating verification hooks')
        self.verification_hooks_activated = True
        self.register_hook(cardinalitycheck_after_add_entity, 'after_add_entity', '')
        self.register_hook(cardinalitycheck_before_del_relation, 'before_delete_relation', '')
        self.register_hook(cstrcheck_after_add_relation, 'after_add_relation', '')
        self.register_hook(uniquecstrcheck_before_modification, 'before_add_entity', '')
        self.register_hook(uniquecstrcheck_before_modification, 'before_update_entity', '')
#         self.register_hook(tidy_html_fields('before_add_entity'), 'before_add_entity', '')
#         self.register_hook(tidy_html_fields('before_update_entity'), 'before_update_entity', '')

from cubicweb.selectors import yes
from cubicweb.appobject import AppObject

class autoid(type):
    """metaclass to create an unique 'id' attribute on the class using it"""
    # XXX is this metaclass really necessary ?
    def __new__(mcs, name, bases, classdict):
        cls = super(autoid, mcs).__new__(mcs, name, bases, classdict)
        cls.id = str(id(cls))
        return cls

class Hook(AppObject):
    __metaclass__ = autoid
    __registry__ = 'hooks'
    __select__ = yes()
    # set this in derivated classes
    events = None
    accepts = None
    enabled = True

    def __init__(self, event=None):
        super(Hook, self).__init__()
        self.event = event

    @classmethod
    def registered(cls, vreg):
        super(Hook, cls).registered(vreg)
        return cls()

    @classmethod
    def register_to(cls):
        if not cls.enabled:
            cls.warning('%s hook has been disabled', cls)
            return
        done = set()
        assert isinstance(cls.events, (tuple, list)), \
               '%s: events is expected to be a tuple, not %s' % (
            cls, type(cls.events))
        for event in cls.events:
            if event in SYSTEM_HOOKS:
                assert not cls.accepts or cls.accepts == ('Any',), \
                       '%s doesnt make sense on %s' % (cls.accepts, event)
                cls.accepts = ('Any',)
            for ertype in cls.accepts:
                if (event, ertype) in done:
                    continue
                yield event, ertype
                done.add((event, ertype))
                try:
                    eschema = cls.schema.eschema(ertype)
                except KeyError:
                    # relation schema
                    pass
                else:
                    for eetype in eschema.specialized_by():
                        if (event, eetype) in done:
                            continue
                        yield event, str(eetype)
                        done.add((event, eetype))


    def make_callback(self, event):
        if len(self.events) == 1:
            return self.call
        return self.__class__(event=event).call

    def call(self):
        raise NotImplementedError

class SystemHook(Hook):
    accepts = ()

from logging import getLogger
from cubicweb import set_log_methods
set_log_methods(HooksManager, getLogger('cubicweb.hooksmanager'))
set_log_methods(Hook, getLogger('cubicweb.hooks'))