server/sources/__init__.py
changeset 6943 406a41c25e13
parent 6931 0af44a38fe41
child 6944 0cf10429ad39
equal deleted inserted replaced
6942:18bdddd3740f 6943:406a41c25e13
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
    18 """cubicweb server sources support"""
    18 """cubicweb server sources support"""
    19 
    19 
    20 __docformat__ = "restructuredtext en"
    20 __docformat__ = "restructuredtext en"
    21 
    21 
       
    22 import itertools
    22 from os.path import join, splitext
    23 from os.path import join, splitext
    23 from datetime import datetime, timedelta
    24 from datetime import datetime, timedelta
    24 from logging import getLogger
    25 from logging import getLogger
    25 import itertools
       
    26 
    26 
    27 from cubicweb import set_log_methods, server
    27 from cubicweb import set_log_methods, server
    28 from cubicweb.schema import VIRTUAL_RTYPES
    28 from cubicweb.schema import VIRTUAL_RTYPES
    29 from cubicweb.server.sqlutils import SQL_PREFIX
    29 from cubicweb.server.sqlutils import SQL_PREFIX
    30 from cubicweb.server.ssplanner import EditedEntity
    30 from cubicweb.server.ssplanner import EditedEntity
   111         self.support_relations['identity'] = False
   111         self.support_relations['identity'] = False
   112         self.eid = None
   112         self.eid = None
   113         self.public_config = source_config.copy()
   113         self.public_config = source_config.copy()
   114         self.remove_sensitive_information(self.public_config)
   114         self.remove_sensitive_information(self.public_config)
   115 
   115 
   116     def init_creating(self):
       
   117         """method called by the repository once ready to create a new instance"""
       
   118         pass
       
   119 
       
   120     def init(self, activated, session=None):
       
   121         """method called by the repository once ready to handle request.
       
   122         `activated` is a boolean flag telling if the source is activated or not.
       
   123         """
       
   124         pass
       
   125 
       
   126     def backup(self, backupfile, confirm):
       
   127         """method called to create a backup of source's data"""
       
   128         pass
       
   129 
       
   130     def restore(self, backupfile, confirm, drop):
       
   131         """method called to restore a backup of source's data"""
       
   132         pass
       
   133 
       
   134     def close_pool_connections(self):
       
   135         for pool in self.repo.pools:
       
   136             pool._cursors.pop(self.uri, None)
       
   137             pool.source_cnxs[self.uri][1].close()
       
   138 
       
   139     def open_pool_connections(self):
       
   140         for pool in self.repo.pools:
       
   141             pool.source_cnxs[self.uri] = (self, self.get_connection())
       
   142 
       
   143     def reset_caches(self):
       
   144         """method called during test to reset potential source caches"""
       
   145         pass
       
   146 
       
   147     def clear_eid_cache(self, eid, etype):
       
   148         """clear potential caches for the given eid"""
       
   149         pass
       
   150 
       
   151     def __repr__(self):
   116     def __repr__(self):
   152         return '<%s source %s @%#x>' % (self.uri, self.eid, id(self))
   117         return '<%s source %s @%#x>' % (self.uri, self.eid, id(self))
   153 
   118 
   154     def __cmp__(self, other):
   119     def __cmp__(self, other):
   155         """simple comparison function to get predictable source order, with the
   120         """simple comparison function to get predictable source order, with the
   161             return 1
   126             return 1
   162         if other.uri == 'system':
   127         if other.uri == 'system':
   163             return -1
   128             return -1
   164         return cmp(self.uri, other.uri)
   129         return cmp(self.uri, other.uri)
   165 
   130 
       
   131     def backup(self, backupfile, confirm):
       
   132         """method called to create a backup of source's data"""
       
   133         pass
       
   134 
       
   135     def restore(self, backupfile, confirm, drop):
       
   136         """method called to restore a backup of source's data"""
       
   137         pass
       
   138 
       
   139     # source initialization / finalization #####################################
       
   140 
   166     def set_schema(self, schema):
   141     def set_schema(self, schema):
   167         """set the instance'schema"""
   142         """set the instance'schema"""
   168         self.schema = schema
   143         self.schema = schema
       
   144 
       
   145     def init_creating(self):
       
   146         """method called by the repository once ready to create a new instance"""
       
   147         pass
       
   148 
       
   149     def init(self, activated, session=None):
       
   150         """method called by the repository once ready to handle request.
       
   151         `activated` is a boolean flag telling if the source is activated or not.
       
   152         """
       
   153         pass
       
   154 
       
   155     PUBLIC_KEYS = ('type', 'uri')
       
   156     def remove_sensitive_information(self, sourcedef):
       
   157         """remove sensitive information such as login / password from source
       
   158         definition
       
   159         """
       
   160         for key in sourcedef.keys():
       
   161             if not key in self.PUBLIC_KEYS:
       
   162                 sourcedef.pop(key)
       
   163 
       
   164     # connections handling #####################################################
       
   165 
       
   166     def get_connection(self):
       
   167         """open and return a connection to the source"""
       
   168         raise NotImplementedError()
       
   169 
       
   170     def check_connection(self, cnx):
       
   171         """Check connection validity, return None if the connection is still
       
   172         valid else a new connection (called when the pool using the given
       
   173         connection is being attached to a session). Do nothing by default.
       
   174         """
       
   175         pass
       
   176 
       
   177     def close_pool_connections(self):
       
   178         for pool in self.repo.pools:
       
   179             pool._cursors.pop(self.uri, None)
       
   180             pool.source_cnxs[self.uri][1].close()
       
   181 
       
   182     def open_pool_connections(self):
       
   183         for pool in self.repo.pools:
       
   184             pool.source_cnxs[self.uri] = (self, self.get_connection())
       
   185 
       
   186     def pool_reset(self, cnx):
       
   187         """the pool using the given connection is being reseted from its current
       
   188         attached session
       
   189 
       
   190         do nothing by default
       
   191         """
       
   192         pass
       
   193 
       
   194     # cache handling ###########################################################
       
   195 
       
   196     def reset_caches(self):
       
   197         """method called during test to reset potential source caches"""
       
   198         pass
       
   199 
       
   200     def clear_eid_cache(self, eid, etype):
       
   201         """clear potential caches for the given eid"""
       
   202         pass
       
   203 
       
   204     # external source api ######################################################
       
   205 
       
   206     def eid2extid(self, eid, session=None):
       
   207         return self.repo.eid2extid(self, eid, session)
       
   208 
       
   209     def extid2eid(self, value, etype, session=None, **kwargs):
       
   210         return self.repo.extid2eid(self, value, etype, session, **kwargs)
   169 
   211 
   170     def support_entity(self, etype, write=False):
   212     def support_entity(self, etype, write=False):
   171         """return true if the given entity's type is handled by this adapter
   213         """return true if the given entity's type is handled by this adapter
   172         if write is true, return true only if it's a RW support
   214         if write is true, return true only if it's a RW support
   173         """
   215         """
   219         #     card 1 relation ? ...)
   261         #     card 1 relation ? ...)
   220         if self.support_relation(rtype):
   262         if self.support_relation(rtype):
   221             return rtype in self.cross_relations
   263             return rtype in self.cross_relations
   222         return rtype not in self.dont_cross_relations
   264         return rtype not in self.dont_cross_relations
   223 
   265 
   224     def eid2extid(self, eid, session=None):
   266     def before_entity_insertion(self, session, lid, etype, eid):
   225         return self.repo.eid2extid(self, eid, session)
   267         """called by the repository when an eid has been attributed for an
   226 
   268         entity stored here but the entity has not been inserted in the system
   227     def extid2eid(self, value, etype, session=None, **kwargs):
   269         table yet.
   228         return self.repo.extid2eid(self, value, etype, session, **kwargs)
   270 
   229 
   271         This method must return the an Entity instance representation of this
   230     PUBLIC_KEYS = ('type', 'uri')
   272         entity.
   231     def remove_sensitive_information(self, sourcedef):
   273         """
   232         """remove sensitive information such as login / password from source
   274         entity = self.repo.vreg['etypes'].etype_class(etype)(session)
   233         definition
   275         entity.eid = eid
   234         """
   276         entity.cw_edited = EditedEntity(entity)
   235         for key in sourcedef.keys():
   277         return entity
   236             if not key in self.PUBLIC_KEYS:
   278 
   237                 sourcedef.pop(key)
   279     def after_entity_insertion(self, session, lid, entity):
   238 
   280         """called by the repository after an entity stored here has been
   239     def _cleanup_system_relations(self, session):
   281         inserted in the system table.
   240         """remove relation in the system source referencing entities coming from
   282         """
   241         this source
   283         pass
   242         """
   284 
   243         cu = session.system_sql('SELECT eid FROM entities WHERE source=%(uri)s',
   285     # user authentication api ##################################################
   244                                 {'uri': self.uri})
       
   245         myeids = ','.join(str(r[0]) for r in cu.fetchall())
       
   246         if not myeids:
       
   247             return
       
   248         # delete relations referencing one of those eids
       
   249         eidcolum = SQL_PREFIX + 'eid'
       
   250         for rschema in self.schema.relations():
       
   251             if rschema.final or rschema.type in VIRTUAL_RTYPES:
       
   252                 continue
       
   253             if rschema.inlined:
       
   254                 column = SQL_PREFIX + rschema.type
       
   255                 for subjtype in rschema.subjects():
       
   256                     table = SQL_PREFIX + str(subjtype)
       
   257                     for objtype in rschema.objects(subjtype):
       
   258                         if self.support_entity(objtype):
       
   259                             sql = 'UPDATE %s SET %s=NULL WHERE %s IN (%s);' % (
       
   260                                 table, column, eidcolum, myeids)
       
   261                             session.system_sql(sql)
       
   262                             break
       
   263                 continue
       
   264             for etype in rschema.subjects():
       
   265                 if self.support_entity(etype):
       
   266                     sql = 'DELETE FROM %s_relation WHERE eid_from IN (%s);' % (
       
   267                         rschema.type, myeids)
       
   268                     session.system_sql(sql)
       
   269                     break
       
   270             for etype in rschema.objects():
       
   271                 if self.support_entity(etype):
       
   272                     sql = 'DELETE FROM %s_relation WHERE eid_to IN (%s);' % (
       
   273                         rschema.type, myeids)
       
   274                     session.system_sql(sql)
       
   275                     break
       
   276 
       
   277     def cleanup_entities_info(self, session):
       
   278         """cleanup system tables from information for entities coming from
       
   279         this source. This should be called when a source is removed to
       
   280         properly cleanup the database
       
   281         """
       
   282         self._cleanup_system_relations(session)
       
   283         # fti / entities tables cleanup
       
   284         # sqlite doesn't support DELETE FROM xxx USING yyy
       
   285         dbhelper = session.pool.source('system').dbhelper
       
   286         session.system_sql('DELETE FROM %s WHERE %s.%s IN (SELECT eid FROM '
       
   287                            'entities WHERE entities.source=%%(uri)s)'
       
   288                            % (dbhelper.fti_table, dbhelper.fti_table,
       
   289                               dbhelper.fti_uid_attr),
       
   290                            {'uri': self.uri})
       
   291         session.system_sql('DELETE FROM entities WHERE source=%(uri)s',
       
   292                            {'uri': self.uri})
       
   293 
       
   294     # abstract methods to override (at least) in concrete source classes #######
       
   295 
       
   296     def get_connection(self):
       
   297         """open and return a connection to the source"""
       
   298         raise NotImplementedError()
       
   299 
       
   300     def check_connection(self, cnx):
       
   301         """check connection validity, return None if the connection is still valid
       
   302         else a new connection (called when the pool using the given connection is
       
   303         being attached to a session)
       
   304 
       
   305         do nothing by default
       
   306         """
       
   307         pass
       
   308 
       
   309     def pool_reset(self, cnx):
       
   310         """the pool using the given connection is being reseted from its current
       
   311         attached session
       
   312 
       
   313         do nothing by default
       
   314         """
       
   315         pass
       
   316 
   286 
   317     def authenticate(self, session, login, **kwargs):
   287     def authenticate(self, session, login, **kwargs):
   318         """if the source support CWUser entity type, it should implement
   288         """if the source support CWUser entity type, it should implement
   319         this method which should return CWUser eid for the given login/password
   289         this method which should return CWUser eid for the given login/password
   320         if this account is defined in this source and valid login / password is
   290         if this account is defined in this source and valid login / password is
   321         given. Else raise `AuthenticationError`
   291         given. Else raise `AuthenticationError`
   322         """
   292         """
   323         raise NotImplementedError()
   293         raise NotImplementedError()
       
   294 
       
   295     # RQL query api ############################################################
   324 
   296 
   325     def syntax_tree_search(self, session, union,
   297     def syntax_tree_search(self, session, union,
   326                            args=None, cachekey=None, varmap=None, debug=0):
   298                            args=None, cachekey=None, varmap=None, debug=0):
   327         """return result from this source for a rql query (actually from a rql
   299         """return result from this source for a rql query (actually from a rql
   328         syntax tree and a solution dictionary mapping each used variable to a
   300         syntax tree and a solution dictionary mapping each used variable to a
   338         .executemany().
   310         .executemany().
   339         """
   311         """
   340         res = self.syntax_tree_search(session, union, args, varmap=varmap)
   312         res = self.syntax_tree_search(session, union, args, varmap=varmap)
   341         session.pool.source('system').manual_insert(res, table, session)
   313         session.pool.source('system').manual_insert(res, table, session)
   342 
   314 
   343     # system source don't have to implement the two methods below
   315     # write modification api ###################################################
   344 
       
   345     def before_entity_insertion(self, session, lid, etype, eid):
       
   346         """called by the repository when an eid has been attributed for an
       
   347         entity stored here but the entity has not been inserted in the system
       
   348         table yet.
       
   349 
       
   350         This method must return the an Entity instance representation of this
       
   351         entity.
       
   352         """
       
   353         entity = self.repo.vreg['etypes'].etype_class(etype)(session)
       
   354         entity.eid = eid
       
   355         entity.cw_edited = EditedEntity(entity)
       
   356         return entity
       
   357 
       
   358     def after_entity_insertion(self, session, lid, entity):
       
   359         """called by the repository after an entity stored here has been
       
   360         inserted in the system table.
       
   361         """
       
   362         pass
       
   363 
       
   364     # read-only sources don't have to implement methods below
   316     # read-only sources don't have to implement methods below
   365 
   317 
   366     def get_extid(self, entity):
   318     def get_extid(self, entity):
   367         """return the external id for the given newly inserted entity"""
   319         """return the external id for the given newly inserted entity"""
   368         raise NotImplementedError()
   320         raise NotImplementedError()