# -*- coding: iso-8859-1 -*-
#-----------------------------------------------------------------------------
# Modeling Framework: an Object-Relational Bridge for python
#
# Copyright (c) 2001-2004 Sbastien Bigaret <sbigaret@users.sourceforge.net>
# All rights reserved.
#
# This file is part of the Modeling Framework.
#
# This code is distributed under a "3-clause BSD"-style license;
# see the LICENSE file for details.
#-----------------------------------------------------------------------------
"""
ObjectStoreCoordinator
__TBD
Notifications
The following notifications are defined and posted:
- 'CooperatingObjectStoreWasAddedNotification'
object: the 'ObjectStoreCoordinator' instance
- 'CooperatingObjectStoreWasRemovedNotification'
object: the removed 'ObjectStoreCoordinator' instance
- 'CooperatingObjectStoreNeededNotification'
object: the ObjectStoreCoordinator posting the request
userInfo: either a GlobalID, an object ('DatabaseObject') or
a FetchSpecification. This object, passed along with the
notification, is assumed to be able to respond to the
message 'entityName()'
CVS information
$Id: ObjectStoreCoordinator.py 966 2006-02-25 13:08:08Z sbigaret $
"""
__version__='$Revision: 966 $'[11:-2]
from logging import error,warn,debug
import sys, traceback, StringIO
from NotificationFramework import NotificationCenter
NC=NotificationCenter
from ObjectStore import ObjectStore
# Modules constants: Notifications
CooperatingObjectStoreWasAddedNotification='CooperatingObjectStoreWasAddedNotification'
CooperatingObjectStoreWasRemovedNotification='CooperatingObjectStoreWasRemovedNotification'
CooperatingObjectStoreNeededNotification='CooperatingObjectStoreNeededNotification'
## The lock is used for module's methods AND class methods
## Remember that you should *NOT* directly access to module's variable
## '__defaultCoordinator' since it is considered private (MT-safety)
from threading import RLock
__defaultCoordinator=None
defaultCoordinator_lock=RLock()
lock=defaultCoordinator_lock.acquire
unlock=defaultCoordinator_lock.release
def defaultCoordinator():
"""
Returns the default coordinator used in the application.
If the coordinator has not been set before this function is called, a
default one is created and returned
"""
lock()
try:
global __defaultCoordinator
if __defaultCoordinator is None:
__defaultCoordinator=ObjectStoreCoordinator()
return __defaultCoordinator
finally:
unlock()
def setDefaultCoordinator(aCoordinator):
"""
Sets the default coordinator
"""
lock()
try:
__defaultCoordinator=aCoordinator
finally:
unlock()
## ObjectStoreCoordinator
class ObjectStoreCoordinator(ObjectStore):
"""
ObjectStoreCoordinator API
Design Pattern: Chain of repsonsability
__TBD
By default only one 'ObjectStoreCoordinator' is created for an entire
application ; however multiple coordinators might exist in an application
(reasons will be details ine the future __TBD)
Implementation of interface 'ObjectStore'
With respect to implemented interface 'ObjectStoreInterface', the
following methods make sense and are consequently overriden:
- arrayFaultWithSourceGlobalID
- faultForGlobalID
- invalidateAllObjects
- invalidateObjectsWithGlobalIDs
- objectsForSourceGlobalID
- objectsWithFetchSpecification
- refaultObject
- savesChangesInEditingContext
These methods' implementations simply forwards to the dedicated
coordinator(s) the message ; determination of the coordinator(s)
involved is done using either 'objectStoreForFetchSpecification',
'objectStoreForGlobalID' or 'objectStoreForObject'.
Implementation notes:
Remember that you should *NOT* access directly to instance variables,
they are considered private (MT-safety)
"""
def __init__(self):
"""
"""
self.__instanceLock=RLock()
self.lock()
try:
self._cooperatingObjectStores=[]
finally:
self.unlock()
def addCooperatingObjectStore(self, aStore):
"""
Adds a cooperating object store
Posts CooperatingObjectStoreWasAdded
See also: removeCooperatingObjectStore
"""
self.lock()
try:
if aStore not in self._cooperatingObjectStores:
self._cooperatingObjectStores.append(aStore)
NC.postNotification(CooperatingObjectStoreWasAddedNotification, aStore)
finally:
self.unlock()
def cooperatingObjectStores(self):
"""
Returns a copy of the list of registered cooperatingObjectStores
"""
self.lock()
try:
return tuple(self._cooperatingObjectStores)
finally:
self.unlock()
def forwardUpdateForObject(self, anObject, aDictionaryOfChanges):
"""
Called by a CooperatingObjectStore (usually a DatabaseContext) when it
needs to notify that some changes were noticed that are not necessarily
already noticed --this is the case, for example, when a toMany
relationship, with no inverseRelationship declared in the model, has been
changed.
The ObjectStoreCoordinator simply locates the appropriate
CooperatingObjectStore for 'anObject' then forwards the notification of
changes to it.
Refer to DatabaseContext.recordUpdateForObject() and DatabaseOperation for
a more complete discussion on this topic.
Paremeters:
anObject -- the object that needs to be notified of some changes.
aDictionary -- dictionary key/values corresponding to changes
See also: objectStoreForObject(),
CooperatingObjectStore.recordUpdateForObject()
"""
self.lock()
try:
store=self.objectStoreForObject(anObject)
store.recordUpdateForObject(anObject, aDictionaryOfChanges)
finally:
self.unlock()
def _objectStoreForFetchSpecification(self, aFetchSpecification):
"""
Private method returning the 1st cooperating store answering positively
to message 'handlesFetchSpecification'.
Called by 'objectStoreForFetchSpecification'.
"""
self.lock()
try:
for store in self._cooperatingObjectStores:
if store.handlesFetchSpecification(aFetchSpecification):
return store
return None
finally:
self.unlock()
def objectStoreForFetchSpecification(self, aFetchSpecification):
"""
Iterates on objectStores and returns the first one who answers positively
to message 'handlesFetchSpecification'.
Posts 'CooperatingObjectStoreNeeded' in case no store can be found at
first glance, so that listeners get a chance to register an adequate
'CooperatingObjectStore' ; then the requested 'ObjectStore' is returned,
or 'None' if it still cannot be found.
"""
self.lock()
try:
store=self._objectStoreForFetchSpecification(aFetchSpecification)
if store is not None:
return store
# We need one, let the observers have a chance to register it
NC.postNotification(CooperatingObjectStoreNeededNotification,
self,
{'fetchSpecification': aFetchSpecification})
return self._objectStoreForFetchSpecification(aFetchSpecification)
finally:
self.unlock()
def _objectStoreForEntityName(self, entityName):
"""
Private method returning the 1st cooperating store answering positively
to message `ownsEntityName`. Called by `objectStoreForEntityName`.
Note: This method does not lock self. Do not call this by hand in a
multi-threaded env.
"""
for store in self._cooperatingObjectStores:
if store.ownsEntityName(entityName):
return store
return None
def objectStoreForEntityName(self, entityName):
"""
Iterates on objectStores and returns the first one who answers positively
to message 'ownsEntityName'
Posts 'CooperatingObjectStoreNeeded' in case no store can be found at
first glance, so that listeners get a chance to register an adequate
'CooperatingObjectStore' ; then the requested 'ObjectStore' is returned,
or 'None' if it still cannot be found.
"""
self.lock()
try:
store=self._objectStoreForEntityName(entityName)
if store is not None:
return store
# We need one, let the observers have a chance to register it
NC.postNotification(CooperatingObjectStoreNeededNotification,
self,
{'entityName': entityName})
return self._objectStoreForEntityName(entityName)
finally:
self.unlock()
def _objectStoreForGlobalID(self, aGlobalID):
"""
Private method returning the 1st cooperating store answering positively
to message 'ownsGlobalID'. Called by 'objectStoreForGlobalID'.
Note: This method does not lock self. Do not call tyis by hand in a
mutli-threaded env.
"""
for store in self._cooperatingObjectStores:
if store.ownsGlobalID(aGlobalID):
return store
return None
def objectStoreForGlobalID(self, aGlobalID):
"""
Iterates on objectStores and returns the first one who answers positively
to message 'ownsGlobalID'
Posts 'CooperatingObjectStoreNeeded' in case no store can be found at
first glance, so that listeners get a chance to register an adequate
'CooperatingObjectStore' ; then the requested 'ObjectStore' is returned,
or 'None' if it still cannot be found.
"""
self.lock()
try:
store=self._objectStoreForGlobalID(aGlobalID)
if store is not None:
return store
# We need one, let the observers have a chance to register it
NC.postNotification(CooperatingObjectStoreNeededNotification,
self,
{'globalID': aGlobalID})
return self._objectStoreForGlobalID(aGlobalID)
finally:
self.unlock()
def _objectStoreForObject(self, anObject):
"""
Private method returning the 1st cooperating store answering positively
to message 'ownsObject'. Called by 'objectStoreForObject'.
"""
self.lock()
try:
for store in self._cooperatingObjectStores:
if store.ownsObject(anObject):
return store
return None
finally:
self.unlock()
def objectStoreForObject(self, anObject):
"""
Iterates on objectStores and returns the first one who answers positively
to message 'ownsObject'
Posts 'CooperatingObjectStoreNeeded' in case no store can be found at
first glance, so that listeners get a chance to register an adequate
'CooperatingObjectStore' ; then the requested 'ObjectStore' is returned,
or 'None' if it still cannot be found.
"""
self.lock()
try:
store=self._objectStoreForObject(anObject)
if store is not None:
return store
# We need one, let the observers have a chance to register it
NC.postNotification(CooperatingObjectStoreNeededNotification,
self,
{'object': anObject})
return self._objectStoreForObject(anObject)
finally:
self.unlock()
def removeCooperatingObjectStore(self, aStore):
"""
Removes a cooperating object store
Posts CooperatingObjectStoreWasRemoved
See also: addCooperatingObjectStore
"""
self.lock()
try:
self._cooperatingObjectStores.remove(aStore)
NC.postNotification(CooperatingObjectStoreWasRemovedNotification,
aStore)
finally:
self.unlock()
## ObjectStore API
## Simply forwards the message to the appropriate cooperating store(s)
def arrayFaultWithSourceGlobalID(self, aGlobalID, aRelationshipName,
anEditingContext):
"See ObjectStore for details"
self.lock()
try:
store=self.objectStoreForGlobalID(aGlobalID)
return store.arrayFaultWithSourceGlobalID(aGlobalID, aRelationshipName,
anEditingContext)
finally:
self.unlock()
def faultForGlobalID(self, aGlobalID, anEditingContext):
"""
Forwards the message to the adequate CooperatingObjectStore, usually a
DatabaseContext, and returns the result.
"""
self.lock()
try:
store=self.objectStoreForGlobalID(aGlobalID)
return store.faultForGlobalID(aGlobalID, anEditingContext)
finally:
self.unlock()
def faultForRawRow(self, row, entityName, anEditingContext):
"""
Forwards the message to the adequate CooperatingObjectStore, usually a
DatabaseContext, and returns the result.
"""
self.lock()
try:
from Modeling.FetchSpecification import FetchSpecification
fs=FetchSpecification(entityName)
store=self.objectStoreForFetchSpecification(fs)
return store.faultForRawRow(row, entityName, anEditingContext)
finally:
self.unlock()
def initializeObject(self, anObject, aGlobalID, anEditingContext):
"See ObjectStore for details"
self.lock()
try:
objectStore=self.objectStoreForGlobalID(aGlobalID)
objectStore.initializeObject(anObject, aGlobalID, anEditingContext)
finally:
self.unlock()
def invalidateAllObjects(self):
"See ObjectStore for details"
self.lock()
try:
for store in self._cooperatingObjectStores:
store.invalidateAllObjects()
finally:
self.unlock()
def invalidateObjectsWithGlobalIDs(self, globalIDs):
"See ObjectStore"
self.lock()
try:
for globalID in globalIDs:
store=self.objectStoreForGlobalID(globalID)
store.invalidateObjectsWithGlobalIDs((globalID,))
finally:
self.unlock()
def objectsForSourceGlobalID(self, aGlobalID, aRelationshipName,
anEditingContext):
"See ObjectStore for details"
lock()
try:
store=self.objectStoreForGlobalID(aGlobalID)
store.objectsForSourceGlobalID(aGlobalID, aRelationshipName,
anEditingContext)
finally:
self.unlock()
def objectsWithFetchSpecification(self, aFetchSpecification,
anEditingContext):
"See ObjectStore for details"
self.lock()
try:
store=self.objectStoreForFetchSpecification(aFetchSpecification)
if store is None:
raise RuntimeError, \
'Unable to find the ObjectStore for FetchSpecification with '\
'entityName: %s'%aFetchSpecification.entityName()
return store.objectsWithFetchSpecification(aFetchSpecification, anEditingContext)
finally:
self.unlock()
def objectsCountWithFetchSpecification(self, aFetchSpecification,
anEditingContext):
"""
Forwards the message to the appropriate CooperatingObjectStore, usually a
DatabaseContext.
Returns the approximate number of objects that would be returned by
objectsWithFetchSpecification() if called with the very same parameters.
About ``approximate'': the number returned is in fact the upper bound ; as
the objects are not actually fetched against the database, it is not
possible to determine whether some have already been deleted within
anEditingContext (in which case objectsWithFetchSpecification() would not
return them --see its documentation as well).
Parameters:
aFetchSpecification -- a FetchSpecification object describing the
objects to be fetched
anEditingContext -- the EditingContext in which the objects would be
fetched. This is in fact an optional argument: since no objects are
fetched it is not really needed.
"""
self.lock()
try:
store=self.objectStoreForFetchSpecification(aFetchSpecification)
if store is None:
raise RuntimeError, \
'Unable to find the ObjectStore for FetchSpecification with '\
'entityName: %s'%aFetchSpecification.entityName()
return store.objectsCountWithFetchSpecification(aFetchSpecification, anEditingContext)
finally:
self.unlock()
def ownsObject(self, anObject):
"""
Tells whether the ObjectStore can handle the supplied
object. ObjectStoreCoordinator determines this by searching a matching
CooperatingObjectStore for that object. If such a CooperatingObjectStore
is found, returns true (1), else return false (0).
Note: this method directly triggers objectStoreForObject() --refer to its
doc. for more info. on its side-effect.
See also: objectStoreForObject()
"""
try:
coopObjStore=self.objectStoreForObject(anObject)
except:
#if 1: # __TBD if DEBUG (somehow)
# exc=StringIO.StringIO()
# traceback.print_exc(file=exc)
# debug(exc.getvalue())
# del exc
return 0
if coopObjStore is not None: return 1
return 0
def refaultObject(self, anObject, aGlobalID, anEditingContext):
"See ObjectStore for details"
self.lock()
try:
store=self.objectStoreForObject(anObject)
store.refaultObject(anObject, aGlobalID, anEditingContext)
finally:
self.unlock()
def rootObjectStore(self):
"""
Returns 'self', since an 'ObjectStoreCoordinator' object is designed to
be the root of an ObjectStore hierarchy.
See also: interface 'ObjectStore' for details
"""
self.lock()
try:
return self
finally:
self.unlock()
def saveChangesInEditingContext(self, anEditingContext):
"See ObjectStore for details"
anEditingContext.lock()
self.lock()
# take a lock on the Coop.Obj.Store's database
for coopObjStore in self._cooperatingObjectStores:
coopObjStore.database().lock()
try: # global try/finally block
_error=""
try:
for coopObjStore in self._cooperatingObjectStores:
coopObjStore.prepareForSaveWithCoordinator(self,
anEditingContext)
except:
warn("Got an exception before performChanges")
_error="prepareForSaveWithCoordinator() failed on %s"%repr(coopObjStore)
raise
try:
for coopObjStore in self._cooperatingObjectStores:
coopObjStore.recordChangesInEditingContext()
except:
warn("Got an exception during recordChangesInEditingContext()")
_error="recordChangesInEditingContext() failed on %s"%repr(coopObjStore)
raise
try:
for coopObjStore in self._cooperatingObjectStores:
coopObjStore.performChanges()
except:
warn("Got an exception: rollback changes")
if 1: # __TBD if DEBUG (somehow)
exc=StringIO.StringIO()
traceback.print_exc(file=exc)
warn(exc.getvalue())
del exc
exctype, value = sys.exc_info()[:2]
_error="performChanges() failed on %s: %s:%s"%(repr(coopObjStore),
exctype, value)
for coopObjStore in self._cooperatingObjectStores:
try:
coopObjStore.rollbackChanges()
except:
exctype, value = sys.exc_info()[:2]
warn("got an exception for rollback() on %s"%str(coopObjStore))
_error+="\nrollbackChanges() failed on %s: %s:%s"%(repr(coopObjStore),exctype,value)
if 1: # __TBD if DEBUG (somehow)
exc=StringIO.StringIO()
traceback.print_exc(file=exc)
debug(exc.getvalue())
del exc
raise RuntimeError, _error
_commitDidFail=0
for coopObjStore in self._cooperatingObjectStores:
try:
coopObjStore.commitChanges()
except:
_commitDidFail=1
if 1: # __TBD if DEBUG (somehow)
exc=StringIO.StringIO()
traceback.print_exc(file=exc)
warn(exc.getvalue())
del exc
break
if _commitDidFail: # One of the commit failed: try to revert everything
warn("Got an exception while committing: trying to rollback changes")
for coopObjStore in self._cooperatingObjectStores:
try:
coopObjStore.rollbackChanges()
except:
warn("got an exception for rollback() on %s"%str(coopObjStore))
if 1: # __TBD if DEBUG (somehow)
exc=StringIO.StringIO()
traceback.print_exc(file=exc)
debug(exc.getvalue())
del exc
raise RuntimeError, "commitChanges() failed"
else:
# All commits did succeed: time to sync. Databases' snapshots
# with their DBContext's
for coopObjStore in self._cooperatingObjectStores:
coopObjStore.finalizeCommitChanges()
finally:
for coopObjStore in self._cooperatingObjectStores:
coopObjStore.database().unlock()
self.unlock()
anEditingContext.unlock()
# Instance lock
def lock(self):
"""
Acquire the lock for the ObjectStoreCoordinator. Calls to 'lock()' should
be balanced with the same number of 'unlock()' for the lock to be released.
You normally do not need to call this method directly.
See also: unlock()
"""
self.__instanceLock.acquire()
def unlock(self):
"""
Releases the lock for the ObjectStoreCoordinator.
See also: lock()
"""
self.__instanceLock.release()
|