EditingContext.py :  » Database » Modeling-Framework » Modeling-0.9 » Modeling » Python Open Source

Home
Python Open Source
1.3.1.2 Python
2.Ajax
3.Aspect Oriented
4.Blog
5.Build
6.Business Application
7.Chart Report
8.Content Management Systems
9.Cryptographic
10.Database
11.Development
12.Editor
13.Email
14.ERP
15.Game 2D 3D
16.GIS
17.GUI
18.IDE
19.Installer
20.IRC
21.Issue Tracker
22.Language Interface
23.Log
24.Math
25.Media Sound Audio
26.Mobile
27.Network
28.Parser
29.PDF
30.Project Management
31.RSS
32.Search
33.Security
34.Template Engines
35.Test
36.UML
37.USB Serial
38.Web Frameworks
39.Web Server
40.Web Services
41.Web Unit
42.Wiki
43.Windows
44.XML
Python Open Source » Database » Modeling Framework 
Modeling Framework » Modeling 0.9 » Modeling » EditingContext.py
# -*- 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.
#-----------------------------------------------------------------------------


"""
EditingContext


  Notifications

    ObjectsChangedInStoreNotification -- declared in 'ObjectStore', refer to
      its documentation for details

    ObjectsChangedInEditingContextNotification -- Same as the previous one,
      except that the values posted in the 'info' field along with the
      notification are objects, not GlobalIDs.

    *Important*: Listeners for these notification should take care not to hold
    the transmitted objects or globalIDs ; if they need to store them, e.g. to
    process things later, please keep weak-references on them.

    Implementation note: (__TBD) maybe this ('weakref') should be done before
    posting the notifications.

  CVS information

    $Id: EditingContext.py 932 2004-07-20 06:21:57Z sbigaret $
  
"""

__version__='$Revision: 932 $'[11:-2]

from Modeling import ObjectStoreCoordinator
from Modeling.ObjectStore import ObjectStore,\
     InvalidatedAllObjectsInStoreNotification,\
     ObjectsChangedInStoreNotification, \
     DeletedKey, InsertedKey, InvalidatedKey, UpdatedKey

from Modeling.GlobalID import GlobalIDChangedNotification
from Modeling.GlobalID import globalIDWithEntityName
from Modeling.Qualifier import filteredArrayWithQualifier
from Modeling import ObserverCenter
from Modeling.utils import toBoolean
import ClassDescription

# interfaces
from interfaces.EditingContext import IEditingContext
from interfaces.ObservingInterface import ObservingInterface

from logging import info,warn,trace
import weakref

## Notifications
from NotificationFramework import NotificationCenter
NC=NotificationCenter

from ObjectStore import ObjectsChangedInStoreNotification
ObjectsChangedInEditingContextNotification='ObjectsChangedInEditingContextNotification'


## you should *NOT* directly access to module's variable
## '__defaultCoordinator' since it is considered private (MT-safety)
from threading import RLock
__defaultParentObjectStore=None
defaultParentObjectStore_lock=RLock()
lock=defaultParentObjectStore_lock.acquire
unlock=defaultParentObjectStore_lock.release

class ObjectNotRegisteredError(Exception):
  """
  Raised whenever an object is accessed that should be inserted in an
  EditingContext but is not.
  """
  pass

def defaultParentObjectStore():
  "See interfaces.EditingContext for details"
  lock()
  try:
    global __defaultParentObjectStore
    if __defaultParentObjectStore is None:
      return ObjectStoreCoordinator.defaultCoordinator()
    return __defaultParentObjectStore
  finally:
    unlock()
    
def setDefaultParentObjectStore(anObjectStore):
  "See interfaces.EditingContext for details"
  lock()
  try:
    __defaultParentObjectStore=anObjectStore 
  finally:
    unlock()


## Implementation notes
## This module needs refactoring for sure ; for example, the internal states
## representing the managed graph of objects could be separated (UniquingTable
## is the very first step!), especially for everything concerning changes
## within the graph of objects
## [related methods: insertedObjects(), updatedObjects(), deletedObjects(),
##  has(Unprocessed)Changes(), processRecentChanges(), insertObject(),
##  recordObject(), deleteObject(), objectWillChange()]

from Modeling.utils import base_object
class EditingContext(base_object, ObjectStore): # ???
  """
  EditingContext: an in-memory world/graph of objects, with the ability
  to ensure its own persistence, to check its consistency, etc.

  Parent/child EditingContext configuration -- this allow the developper to
    get the notion of transactions for an in-memory graph of objects.
    See:  parentObjectStore, __init__()


  Unsupported features:

    Undo -- this is yet unsupported. However, this is now clear how this
      should be done, given that there is an UndoManager or something like
      that. The undo stack will grow each time 'processRecentChanges()' is
      called (usually automatically, at some point where it make sense, for
      example, in an HTTP-based application, when the HTTP request/response
      loop is about to end).

  """
  __implements__ = (IEditingContext,)
  _is_an_editingcontext_=1 # remove this a.s.a. parent/child ECs
                           # configuration is supported
                           # see: __init__()
  ########
  class UniquingTable(base_object):
    """
    The uniquing table - encapsulate the dictionary structure
    Defines the standard behaviour for methods: 'globalIDForObject()',
    'objectForGlobalID', etc.

    Its goal is to guarantee that the uniquing principle is enforced, i.e.
    that one given row in the database cannot be represented by more than one
    object within the EditingContext.

    Since faults / ghosted object are usually identified by their so-called
    ``root GlobalID'' (a root GlobalID has exactly the same keyValues() than
    the real GlobalID but has an entityName() referring to the root Entity,
    not the object's actual Entity ; this is because the actual Entity for an
    object can only be known when data for that object is actually fetched
    against the database), the UniquingTable also keeps track of root
    globalIDs in order to be able to serve objects from globalID being either
    the root and the real GlobalID.
    
    """
    def __init__(self):
      self._table={}       # GlobalIDs -> objects
      self._invTable={}    # objects   -> GlobalIDs
      self._rootGids={}    # gIDs      -> root_gIDs
      self._invRootGids={} # root_gIDs -> gIDs
      
    def globalIDs(self):
      return self._table.keys()
    
    def objects(self):
      return self._table.values()

    def addObjectForGlobalID(self, anObject, aGlobalID):
      #trace('Adding globalID: %s'%str(aGlobalID))
      if self._table.has_key(aGlobalID):
        warn('Try to register an already registered object')
        return # or raise?
      
      # generate root_keyGlobalID
      root_GlobalID=None
      if not aGlobalID.isTemporary():
        from Modeling import ClassDescription
        from Modeling.GlobalID import KeyGlobalID
        entityName=aGlobalID.entityName()
        cd=ClassDescription.classDescriptionForName(entityName)
        cd_root=cd.rootClassDescription()
        if cd_root.entityName() != entityName:
          root_GlobalID=KeyGlobalID(cd_root.entityName(),
                                    aGlobalID.keyValues())
          self._rootGids[aGlobalID]=root_GlobalID
          self._invRootGids[root_GlobalID]=aGlobalID


      ### Additional test, to be removed
      #def rootkGid(gid):
      #  from Modeling.GlobalID import KeyGlobalID
      #  from Modeling import ClassDescription
      #  cd=ClassDescription.classDescriptionForName(gid.entityName())
      #  cd_root=cd.rootClassDescription()
      #  return KeyGlobalID(cd_root.entityName(), gid.keyValues())
      #
      #if not aGlobalID.isTemporary():
      #  if root_GlobalID in [rootkGid(g) for g in self._table.keys()
      #                       if not g.isTemporary()]:
      #    alreadyRegisteredGids=[g for g in self._table.keys()
      #                           if not g.isTemporary() and \
      #                              rootkGid(g)==root_aGlobalID]
      #    alreadyRegisteredObjs=[self._table[g] for g in alreadyRegisteredGids]
      #    alreadyRegisteredGids=[str(g) for g in alreadyRegisteredGids]
      #    
      #    raise ValueError, 'Inconsistency: trying to register in EC uniquing table has two different objects and gIDs corresponding to the same row: existing: (%s,%s) / new: (%s,%s)'%(alreadyRegisteredGids, alreadyRegisteredObjs, str(aGlobalID), anObject)
      ### /Additional test

      self._table[aGlobalID]=anObject
      self._invTable[anObject]=aGlobalID
      
    def clear(self):
      "Clear all entries"
      self._table.clear()
      self._invTable.clear()
      self._rootGids.clear()
      self._invRootGids.clear()
      
    def forgetObject(self, object):
      """
      Removes the object and the corresponding GlobalID from the uniquing
      table
      """
      gid=self._invTable[object]
      del self._table[self._invTable[object]]
      del self._invTable[object]
      try: del self._rootGids[aGlobalID]
      except: pass
      try: del self._invRootGids[aGlobalID]
      except: pass
        
    def forgetObjectForGlobalID(self, aGlobalID):
      """
      Removes the GlobalID and the corresponding object from the uniquing
      table
      """
      del self._invTable[self._table[aGlobalID]]
      del self._table[aGlobalID]
      try: del self._rootGids[aGlobalID]
      except: pass
      try: del self._invRootGids[aGlobalID]
      except: pass
      
    def globalIDForObject(self, anObject):
      """
      Returns the GlobalId registered with that object, or 'None' if it cannot
      be found
      """
      return self._invTable.get(anObject, None)
      #if self.hasObject(anObject):
      #  for key in self._table.keys():
      #    if self._table[key]==anObject:
      #      return key
      #return None

    def hasGlobalID(self, aGlobalID):
      return aGlobalID in self._table.keys() or \
             aGlobalID in self._rootGids.values()

    def hasObject(self, anObject):
      return anObject in self._table.values()
    
    def objectForGlobalID(self, aGlobalID):
      """
      Returns the object corresponding to the given 'aGlobalID', or 'None' if
      it cannot be found in the receiver's uniquing table.
      """
      #trace('Searching globalID: %s'%str(aGlobalID))
      return self._table.get(aGlobalID, None) or \
             self._table.get(self._invRootGids.get(aGlobalID, None),None)

  ########
  # Class    
  invalidatesObjectsWhenFinalized_default=toBoolean(1)

  def __init__(self, parentObjectStore=None):
    """
    Initializes an 'EditingContext'.
    
    Parameter 'parentObjectStore'

      determines which 'ObjectStore' the receiver will use to fetch its data
      and to save changes made in it ; the default (i.e. when the parameter is
      omitted or equals to 'None') is to use the one returned by
      'defaultParentObjectStore()' --which in turn defaults to the
      application-wide shared 'CooperatingObjectStore.defaultCoordinator()'.

      Note that the parent 'ObjectStore' of an 'EditingContext' cannot be
      changed during the whole object's lifetime.

      You will typically use the default behaviour, or provide an parent
      'EditingContext' to make the newly created 'EditingContext' a child of
      the former one (not supported yet __TBD).

    Implementation Note:

       The object graph is hold by 4 variables:

       - the UniquingTable holds the whole graphs

       - _insertedObjects

       - _deletedObjects

       - _updatedObjects: these three variables only reference GlobaldIDs
             More than that: there should *only* be empty intersections
             between each one and an other, or many methods could fail or
             mis-behave in an uncontrolled way!!
             Last, but not least, please note that the union of these 3 sets
             is a subset of the whole graph (which additionally holds
             objects that were fetched but are still unmodified).

       - Additionally, each of these last three attributes has its equivalent
         for changes that were not yet processed (e.g. _pendingUpdatedObjects).
         These changes are moved to the 

      Additional notes::

        __TBD This is not that good... in fact there should be some
        __TBD 'ObjectGraph' object here to manage these.
        
        __TBD Plus: the way it is implemented here is naive and certainly
        __TBD sub-sub-optimal in terms of run-time efficiency (data structures
        __TBD imply time-consuming treatments). refactoring will be done in
        __TBD the future, for the moment I need to concentrate on features...

    """
    self.__instanceLock=RLock()
    self._pendingDeletedObjects=[] # only holds GlobalIDs
    self._pendingInsertedObjects=[]
    self._pendingUpdatedObjects=[]
    self._deletedObjects=[] # only holds GlobalIDs
    self._insertedObjects=[]
    self._updatedObjects=[]
    self._uniquingTable=EditingContext.UniquingTable()
    self.__resolvedFaultsGIDs={} # rootGid -> resolved Gid (inheritance)
    self._propagatesInsertionForRelatedObjects=0
    if parentObjectStore is None:
      parentObjectStore=defaultParentObjectStore()

    # remove this statement a.s.a. parent/child ECs configuration is supported
    #if getattr(parentObjectStore, '_is_an_editingcontext_', None):
    #  raise 'Unimplemented', 'Parent/child ECs configuration not supported yet'

    self._parentObjectStore=parentObjectStore

    # Registering for notifications __TBD
    
  
  # Changes within the editing context
  def allDeletedGlobalIDs(self):
    """
    Returns a sequence made of GlobalIDs of objects that have been deleted
    into the EditingContext and that have not been made persistent yet.

    The GlobalIDs returned are those identifying objects who were marked as
    deleted within the EditingContext, while deletedGlobalIDs() only returns
    the GlobalIDs for objects that were marked as deleted before the last
    invocation of processRecentChanges() (or saveChanges()).
    
    See also: deletedGlobalIDs(), allDeletedObjects()
    """
    return tuple(self._deletedObjects+self._pendingDeletedObjects)
    
  def deletedGlobalIDs(self):
    """
    Returns a sequence made of GlobalIDs of objects that were removed from the
    graph of objects managed by the EditingContext.

    The GlobalIDs returned are those indentifying objects who were marked as
    deleted before the last invocation of processRecentChanges() (or
    saveChanges()). If you want to get all deleted GlobalIDs, see
    allDeletedGlobalIDs().

    See also: deletedObjects()
    """
    return tuple(self._deletedObjects)
  
  def allDeletedObjects(self):
    """
    Returns a sequence made of objects that were removed from the graph of
    objects managed by the EditingContext.

    allDeletedObjects() returns all the objects that have been marked as
    deleted within the EditingContext, while deletedObjects() only returns
    the objects that were marked as deleted before the last invocation of
    processRecentChanges() (or saveChanges()).
    
    See also: insertedObjects(), updatedObjects(), hasChanges
    """
    return map(self.objectForGlobalID,
               self._deletedObjects+self._pendingDeletedObjects)
  
  def deletedObjects(self):
    """
    Returns a sequence made of objects that were removed from the graph of
    objects managed by the EditingContext.

    The objects returned are those who were marked as deleted before the last
    invocation of processRecentChanges() (typically after saveChanges()). If
    you want to get all deleted objects, see allDeletedObjects().
    
    See also: insertedObjects(), updatedObjects(), hasChanges
    """
    return map(self.objectForGlobalID, self._deletedObjects)
  
  def allInsertedGlobalIDs(self):
    """
    Returns a sequence made of GlobalIDs of objects that have been inserted
    into the EditingContext and that have not been made persistent yet.

    The GlobalIDs returned are those identifying objects who were marked as
    inserted within the EditingContext, while insertedGlobalIDs() only returns
    the GlobalIDs for objects that were marked as inserted before the last
    invocation of processRecentChanges() (or saveChanges()).
    
    See also: insertedObjects(), allInsertedObjects()
    """
    return tuple(self._insertedObjects+self._pendingInsertedObjects)
    
  def insertedGlobalIDs(self):
    """

    Returns a sequence made of GlobalIDs of objects that have been inserted
    into the EditingContext and that have not been made persistent yet.

    The GlobalIDs returned are those indentifying objects who were marked as
    inserted before the last invocation of processRecentChanges() (or
    saveChanges()). If you want to get all inserted GlobalIDs, see
    allInsertedGlobalIDs().
    
    See also: insertedObjects()
    """
    return tuple(self._insertedObjects)
    
  def allInsertedObjects(self):
    """
    Returns a sequence made of the objects that have been inserted into the
    EditingContext and that have not been made persistent yet.
    
    allInsertedObjects() returns all the objects that have been marked as
    inserted within the EditingContext, while insertedObjects() only returns
    the objects that were marked as inserted before the last invocation of
    processRecentChanges() (or saveChanges()).
    
    See also: deletedObjects(), updatedObjects(), hasChanges()
    """
    return map(self.objectForGlobalID,
               self._insertedObjects+self._pendingInsertedObjects)
    
  def insertedObjects(self):
    """
    Returns a sequence made of the objects that have been inserted into the
    EditingContext and that have not been made persistent yet.

    The objects returned are those who were marked as inserted before the last
    invocation of processRecentChanges() (typically, after saveChanges()). If
    you want to get all inserted objects, see allInsertedObjects().
    
    See also: deletedObjects(), updatedObjects(), hasChanges()
    """
    return map(self.objectForGlobalID, self._insertedObjects)
    
  def allUpdatedGlobalIDs(self):
    """
    Returns a sequence made of GlobalIDs of objects that have been updated
    into the EditingContext and that have not been made persistent yet.

    The GlobalIDs returned are those identifying objects who were marked as
    updated within the EditingContext, while updatedGlobalIDs() only returns
    the GlobalIDs for objects that were marked as updated before the last
    invocation of processRecentChanges() (or saveChanges()).
    
    See also: updatedObjects(), allUpdatedObjects()
    """
    return tuple(self._updatedObjects+self._pendingUpdatedObjects)
    
  def updatedGlobalIDs(self):
    """
    Returns a sequence made of GlobalIDs of objects that have changed since
    the last saveChanges(), or since the EditingContext was created.

    The GlobalIDs returned are those indentifying objects who were marked as
    updated before the last invocation of processRecentChanges() (or
    saveChanges()). If you want to get all updated GlobalIDs, see
    allUpdatedGlobalIDs().

    See also: allUpdatedGlobalIDs(), updatedObjects()
    """
    return tuple(self._updatedObjects)

  def allUpdatedObjects(self):
    """
    Returns a sequence made of the objects that are marked for update.

    allUpdatedObjects() returns all the objects that have been marked for
    update within the EditingContext, while updatedObjects() only returns
    the objects that were marked for update before the last invocation of
    processRecentChanges() (typically, after saveChanges()).
    
    See also: deletedObjects(), insertedObjects(), hasChanges()
    """
    return map(self.objectForGlobalID,
               self._updatedObjects+self._pendingUpdatedObjects)

  def updatedObjects(self):
    """
    Returns a sequence made of the objects that have changed since the last
    invocation of processRecentChanges (typically, after the last
    saveChanges()).

    If you want to get all updated objects, including those marked after the
    last invocation of processRecentChanges(), see allUpdatedObjects().
    
    See also: deletedObjects(), insertedObjects(), hasChanges()
    """
    return map(self.objectForGlobalID, self._updatedObjects)

  def hasChanges(self):
    """
    Tells whether some changes have been made in the graph of objects the
    EditingContext manages, whose have not been made persistent yet
    (i.e. newly inserted objects, some that have been modified or some that
    have been removed from the graph of objects

    This method does not make any difference between processed and unprocessed
    changes ; to know if there are some unprocessed changes, use
    'hasUnprocessedChanges()' instead.

    See also: deletedObjects(), insertedObjects(), updatedObjects(),
              hasUnprocessedChanges()
    """
    return self._insertedObjects or self._updatedObjects or \
           self._deletedObjects or \
           self._pendingInsertedObjects or self._pendingUpdatedObjects or \
           self._pendingDeletedObjects
  
  def hasUnprocessedChanges(self):
    """
    Tells whether the EditingContext knows that some changes were made to its
    graph of objects that were not processed yet

    See also: processRecentChanges()
    """
    return self._pendingInsertedObjects or self._pendingUpdatedObjects or \
           self._pendingDeletedObjects
    
  # Processing changes
  def processRecentChanges(self):
    """
    Examines the graph of objects and take appropriate actions.

    This includes:

      - registers all unprocessed changes: inserted, updated and deleted
        objects

      - if propagatesInsertionForRelatedObjects() is true, it searches the
        inserted and updated objects's relationships for newly created
        objects, which are in turn inserted into the EditingContext, if
        needed.
      
      - for all deleted objects, cascade the delete if this needs to be done
      
      - __TBD registers undo informations

      - posts 'ObjectsChangedInEditingContextNotification' and
        'ObjectsChangedInStoreNotification'

    This method is also automatically called when the EditingContext is
    instructed to save changes.

    See also: saveChanges(), saveChangesInEditingContext()
              ClassDescription.propagateDeleteForObject()
              ClassDescription.propagateInsertionForObject()
    """
    _changedGids={DeletedKey: [],
                  InsertedKey: [],
                  UpdatedKey: []}
    _changedObjects={DeletedKey:[]}

    while self.hasUnprocessedChanges():
      for gID in self._pendingUpdatedObjects:
        if gID not in self._updatedObjects:
          self._updatedObjects.append(gID)
          _changedGids[UpdatedKey].append(gID)
        object=self.objectForGlobalID(gID)
        #http://mail.python.org/pipermail-21/persistence-sig/2002-July/000152.html
        if self.propagatesInsertionForRelatedObjects():
          #may insert some objects
          object.propagateInsertionWithEditingContext(self)
          ObserverCenter.ensureSubsequentChangeWillBeNotifiedForObject(object)
      
      self._pendingUpdatedObjects=[]

      while self._pendingInsertedObjects:
        # We suppose here that an inserted objects modified after its
        # insertion has been moved from self._insertedObjects back to
        # self._pendingInsertedObjects: examining the whole set of inserted
        # objects could be really time-consuming, hence we use such a policy
        # to reduce the number of objects to be checked.
        # See objectWillChange() which is responsible for that policy.
        _copy_pendingInsertedObjects=list(self._pendingInsertedObjects) # copy
        self._pendingInsertedObjects=[]
        for gID in _copy_pendingInsertedObjects:
          if gID in self._deletedObjects:
            self._deletedObjects.remove(gID)
          self._insertedObjects.append(gID)
          if gID not in _changedGids[InsertedKey]:
            _changedGids[InsertedKey].append(gID)
            
          if self.propagatesInsertionForRelatedObjects():
            object=self.objectForGlobalID(gID)
            object.propagateInsertionWithEditingContext(self)
            ObserverCenter.ensureSubsequentChangeWillBeNotifiedForObject(object)
        
        
      self._pendingInsertedObjects=[]
      
      # Now check the deleted objects and propagate the deletes
      _copy_pendingDeletedObjects=list(self._pendingDeletedObjects) # copy
      self._pendingDeletedObjects=[]
      
      for gID in _copy_pendingDeletedObjects:
        try: self._insertedObjects.remove(gID)
        except ValueError: pass
        try: self._updatedObjects.remove(gID)
        except ValueError: pass
        try: _changedGids[InsertedKey].remove(gID)
        except ValueError: pass
        try: _changedGids[UpdatedKey].remove(gID)
        except ValueError: pass
        
        object=self.objectForGlobalID(gID)
      
        # ClassDescription.propagateDeleteForObject(), called by
        # propagateDeleteWithEditingContext() populates
        # self._pendingDeletedObjects (when calling deleteObject())

        object.propagateDeleteWithEditingContext(self)
        try:
          # The propagation of the deletion can modify the object, e.g. when
          # one the relationship is marked as NULLIFY. Since
          # self._pendingDeletedObjects is empty when
          # propagateDeleteWithEditingContext is triggered, it will be marked
          # as changed [cf. self.objectWillChange()], but we now this cannot
          # be the case, so we remove it, unconditionally [Bug #599602].
          self._pendingUpdatedObjects.remove(gID)
        except ValueError:
          pass
      
        if gID not in self._deletedObjects: #
          self._deletedObjects.append(gID)
          _changedGids[DeletedKey].append(gID)
          _changedObjects[DeletedKey].append(object)
            
    # Notifications
    # finish initializing dict (userInfo) for Obj.ChangedInEditingContext
    if _changedGids[InsertedKey]:
      _changedObjects[InsertedKey]=map(lambda o, s=self: s.objectForGlobalID,
                                       _changedGids[InsertedKey])
    if _changedGids[UpdatedKey]:
      _changedObjects[UpdatedKey]=map(lambda o, s=self: s.objectForGlobalID,
                                      _changedGids[UpdatedKey])
    NC.postNotification(ObjectsChangedInEditingContextNotification,
                        self, _changedObjects)
    NC.postNotification(ObjectsChangedInStoreNotification,
                        self, _changedGids)
                        
  def propagatesInsertionForRelatedObjects(self):
    """
    If true, processRecentChanges() will search for newly created objects
    related to updated&inserted objects in the EditingContext, and these new
    objects will in turn be automatically inserted in the EditingContext.

    Default is false.
    """
    return self._propagatesInsertionForRelatedObjects
  autoInsertion=propagatesInsertionForRelatedObjects
  
  def setPropagatesInsertionForRelatedObjects(self, bool):
    """
    See propagatesInsertionForRelatedObjects() for details.

    If 'bool' is true and self.propagatesInsertionForRelatedObjects() is
    false, all updated and inserted objects are sent the
    'propagateInsertionWithEditingContext' message ; this can take some time.

    We suggest that you do not change the behaviour after you started working
    with objects within the EditingContext ; it is probably better to choose
    to set it to true or false right after the EditingContext is created.

    Parameter:

      bool -- a boolean value
      
    """
    if bool and not self._propagatesInsertionForRelatedObjects:
      for object in self.updatedObjects()+self.insertedObjects():
        object.propagateInsertionWithEditingContext(self)
    self._propagatesInsertionForRelatedObjects=not not bool
  setAutoInsertion=setPropagatesInsertionForRelatedObjects
  
  def saveChanges(self):
    """
    Tells the EditingContext to save the changes made within the graph of
    objects it is responsible for. It first sends the 'processRecentChanges'
    message to itself, then validates the changes against the constraints
    defined both in the model and in possible custom validation methods, and
    finally calls its parent objects store's 'saveChangesInEditingContext()'
    method.

    Raises Validation.ValidationException if the validation process failed, or
    a RuntimeError if something failed during the process of saving changes in
    the underlying database(s).
    
    See also: saveChangesInEditingContext(), processRecentChanges(),
              parentObjectStore(), validateChanges()
    """
    self.processRecentChanges()
    self.validateChanges()
    self.parentObjectStore().saveChangesInEditingContext(self)

    for obj in self._uniquingTable.objects():
      ObserverCenter.ensureSubsequentChangeWillBeNotifiedForObject(obj)
    # remove deleted objects
    for obj in self.deletedObjects():
      self.forgetObject(obj)
      
    self._deletedObjects=[]
    self._insertedObjects=[]
    self._updatedObjects=[]
    

  def validateChanges(self):
    """
    Performs validation on all objects in the EditingContext.

    This is automatically called by saveChanges() before changes are forwarded
    to the database(s). You should rarely need to call it yourself. If you do,
    you must make sure that processRecentChanges() is called before, since
    this method does not process pending changes.

    The validation process stops after the first error.

    See also: processRecentChanges(), saveChanges()
    """
    for gID in self._insertedObjects+self._updatedObjects:
      self.objectForGlobalID(gID).validateForSave()
    for gID in self._deletedObjects:
      self.objectForGlobalID(gID).validateForDelete()
    
  # Object identification
  def globalIDForObject(self, anObject):
    """
    Returns the GlobalID for 'anObject', or 'None' if 'anObject' is not
    under the receiver's control.
    """
    return self._uniquingTable.globalIDForObject(anObject)
    
  def objectForGlobalID(self, aGlobalID):
    """
    Returns the object corresponding to the given 'aGlobalID', or 'None' if
    it cannot be found in the receiver's uniquing table.
    """
    obj = self._uniquingTable.objectForGlobalID(aGlobalID)
    possibleNewGid=self.__resolvedFaultsGIDs.get(aGlobalID)
    if not obj and possibleNewGid:
      obj=self._uniquingTable.objectForGlobalID(possibleNewGid)
    return obj

  # Object and graph of objects manipulation
  def deleteObject(self, anObject):
    """
    Informs the 'EditingContext' that the supplied object should be marked as
    deleted, so that it will actually be deleted when the receiver saves
    changes. This is not an error to mark an object as deleted more than
    once: this will be silently ignored.

    Parameter 'anObject' should already be registered within the
    'EditingContext' ; if not, ObjectNotRegisteredError is raised.

    Both methods deleteObject() and delete() are completely equivalent.
    
    """
    gID=self.globalIDForObject(anObject)
    if gID is None:
      raise ObjectNotRegisteredError
    if anObject.isFault():
      # we need to trigger the fault, because of
      # propagateDeleteWithEditingContext() triggered in
      # processRecentChanges(). This propagation cannot work on a fault since
      # a fault is, by definition, a non-initialized object, hence it has no
      # idea on related objects.
      # __TBD maybe this can be moved to EntityClassDescription, so that
      # __TBD the fault is triggered only when needed??
      anObject.willRead()
      
    try: self._pendingInsertedObjects.remove(gID)
    except ValueError: pass
    else:
      ## TBD note: simply forgetting the object here prevents the
      ## TBD DELETE_ mechanisms to be triggered appropriately when
      ## TBD processRecentChanges() is called. This should be fixed!!
      self.forgetObject(anObject)
      return
    try: self._insertedObjects.remove(gID)
    except ValueError: pass
    else:
      ## TBD: see notes below
      self.forgetObject(anObject)
      return

    try: self._pendingUpdatedObjects.remove(gID)
    except ValueError: pass

    if gID not in self._deletedObjects and \
       gID not in self._pendingDeletedObjects:  # ignore
      self._pendingDeletedObjects.append(gID)

  delete=deleteObject
  
  def forgetObject(self, anObject):
    """
    Inverse for recordObject(): removes itself from the observers of
    'anObject', removes the object and its GlobalID from the UniquingTable,
    and removes the weakref for self stored in 'anObject'.

    Parameter:

      anObject -- a CustomObject instance

    
    You should never call this method by yourself: it is for internal use only
    (e.g. at the end of the process of saving changes, DatabaseContexts uses
    this to inform the EditingContext of any changes that occured on objects'
    globalIDs --which is always the case when an inserted object has been
    saved in the DB)
    """
    trace('Call')
    gID=self.globalIDForObject(anObject)
    self._uniquingTable.forgetObject(anObject)
    ObserverCenter.removeObserver(self, anObject)
    anObject._CustomObject__editingContext=lambda: None
    if not gID.isTemporary() and not anObject.isFault():
      db=self.rootObjectStore().objectStoreForObject(anObject).database()
      db.decrementSnapshotCountForGlobalID(gID)
    # [...] __TBD forgetObject()
    #self.rootObjectStore().objectStoreForObject(anObject).editingContextDidForgetObjectWithGlobalID(self.globalIDForObject(anObject)
    
    
  def insertObject(self, anObject):
    """
    Inserts a new object within the graph of objects. The 'EditingContext'
    generates a 'TemporaryGlobalID' for that object, registers the object as a
    new one in its graph of objects and registers itself as an observer for
    that object (see ObserverCenter).

    An object should not be inserted more than once. The only valid situation
    where an object can be re-inserted is when this object has previously been
    marked as deleted (hence un-deleting the object), otherwise 'ValueError'
    is raised.

    However, if self.propagateInsertionWithEditingContext() is true, a
    RuntimeWarning is issued, rather than raising ValueError. In that case,
    the warning message will refer to the caller of 'insertObject'.  If you
    want to turn these warnings to real exceptions (e.g. for systematic
    tracking of such situations), simply put the following statement in your
    app.'s init. code::
    
        import warnings
        warnings.filterwarnings(action='error', category=RuntimeWarning)

    or start 'python' with argument '-W error::RuntimeWarning::0'.
    (see the documentation for standard module warnings for further details)

    Both methods insertObject() and insert() are completely equivalent.
    
    """
    gID=self._uniquingTable.globalIDForObject(anObject)
    if not gID: # register the new object
      tmpID=globalIDWithEntityName(anObject.entityName())
      self.insertObjectWithGlobalID(anObject, tmpID)
    else:
      # Already registered: only deleted objects are allowed to re-insert
      if not gID in self._pendingDeletedObjects and \
         not gID in self._deletedObjects:
        if self.propagatesInsertionForRelatedObjects():
          import warnings
          warnings.warn('Attempt to insert an already registered object',
                        RuntimeWarning, stacklevel=2)
        else:
          raise ValueError, 'Object already registered'
      else: # cancel the deletion
        if gID in self._pendingDeletedObjects:
          self._pendingDeletedObjects.remove(gID)
        if gID in self._deletedObjects:
          self._deletedObjects.remove(gID)
        # Mark it as updated, because we here have no way to know whether
        # it was updated or not before it was deleted, or even between its
        # deletion and the cancellation of its deletion!
        self._pendingUpdatedObjects.append(gID)
      
  insert=insertObject
  
  def insertObjectWithGlobalID(self, anObject, aTemporaryGlobalID):
    """
    You should normally not call this method directly. To insert an object,
    use 'insertObject()' instead.

    Parameter:

      'aTemporaryGlobalID' -- must respond 'true' to message 'isTemporary'
       (the method raises 'ValueError' otherwise)

    """
    if not aTemporaryGlobalID.isTemporary():
      raise ValueError, "(Parameter) aTemporaryGlobalID.isTemporary() should "\
            "evaluate to false"
    self.recordObject(anObject, aTemporaryGlobalID)
    self._pendingInsertedObjects.append(self.globalIDForObject(anObject))
    if not self.parentObjectStore().handlesObject(anObject):
      raise ValueError, 'Parent Object Store is not able to deal with object'
    anObject.awakeFromInsertion(self)
    
  def recordObject(self, anObject, aGlobalID):
    """
    Make the editing context aware of this object by adding itself as an
    observer for it and by adding it to the uniquing table.

    You should never need to invoke this method directly, it it automatically
    called within the framework when it needs to. To insert an object into the
    graph of objects, use 'insertObject()' instead.

    See also: insertObject()
    """
    ObserverCenter.addObserver(self, anObject)
    anObject._CustomObject__editingContext=weakref.ref(self)
    # + registers it in the uniquing table
    self._uniquingTable.addObjectForGlobalID(anObject, aGlobalID)
    NC.addObserver(self, EditingContext.handleNotification,
                   GlobalIDChangedNotification)

  def registeredObjects(self):
    """
    Returns all objects managed by the EditingContext
    """
    return self._uniquingTable.objects()
  

  # Behaviour the EditingContext is garbage-collected
  def invalidatesObjectsWhenFinalized(self):
    """
    default==true==when finalized, sends 'clearProperties' to all objects
    """
    if hasattr(self, '_invalidatesObjectsWhenFinalized'):
      return self._invalidatesObjectsWhenFinalized
    return self.invalidatesObjectsWhenFinalized_default
  
  def setInvalidatesObjectsWhenFinalized(self, aBool):
    """
    -
    See also: invalidatesObjectsWhenFinalized()
    """
    self._invalidatesObjectsWhenFinalized=toBoolean(aBool)

  def dispose(self):
    """
    -
    """
    trace('Call')
    try: ## TBD check that objects' referneces are decr. whatever happens here
      for object in self._uniquingTable.objects():
        self.forgetObject(object)
        if self.invalidatesObjectsWhenFinalized():
          object.clearProperties()
    except:
      import cStringIO, traceback
      exc=cStringIO.StringIO()
      traceback.print_exc(file=exc)
      trace(exc.getvalue())
      del exc

  def __del__(self):
    """
    cf. invalidatesObjectsWhenFinalized()
    """
    trace('Call')
    self.dispose()


  # ObjectStore hierarchy related methods
  def parentObjectStore(self):
    """
    Returns the parent ObjectStore for the EditingContext ; it is typically
    an 'ObjectStoreCorrdinator'
    """
    return self._parentObjectStore

  def rootObjectStore(self):
    """
    Returns the 'ObjectStoreCoordinator' in the underlying hierarchy of
    'ObjectStore'.
    
    Forwards the 'rootObjectStore' message to the 'parentObjectStore()', which
    in turn does the same thing, until the 'ObjectStoreCoordinator' is found.

    Conformance to the ObjectStore API

    See also: ObjectStoreCoordinator.rootObjectStore()
    """
    return self._parentObjectStore.rootObjectStore()

  ##
  ## Observing API
  ##
  def objectWillChange(self, anObject):
    """
    ObservingInterface implementation

    Automatically invoked when an object is about to change its state.

    Silently ignores changes if they occur on an inserted, deleted or already
    updated objects.

    Simply logs (at warning level) if the EditingContext receives a
    notification of changes for an object which is not in its uniquing table
    (hence, not in its graph of objects) -- __TBD this behaviour should
    probably be changed, since it should never happen at all.
    """
    gID=self.globalIDForObject(anObject)
    #if not self._uniquingTable.hasObject(anObject):
    if gID is None:
      warn("Received 'objectWillChange()' for an object not registered")
      return
    if self.propagatesInsertionForRelatedObjects() \
       and gID in self._insertedObjects:
      # move the object from _insertedObjects back to _pendingInsertedObjects
      # see comments in processRecentChanges() for a complete explanation
      self._insertedObjects.remove(gID)
      self._pendingInsertedObjects.append(gID)
      
    if gID in \
       self._insertedObjects + self._deletedObjects + \
       self._pendingInsertedObjects + self._pendingDeletedObjects:
      return
    if gID not in self._pendingUpdatedObjects:
      self._pendingUpdatedObjects.append(gID)
      

  ##
  ## ObjectStore API
  ##
  def arrayFaultWithSourceGlobalID(self, aGlobalID, aRelationshipName, 
                                   anEditingContext):
    """
    EditingContext implementation just forwards the message to its parent
    object store and returns the result.
    """
    return self.parentObjectStore().arrayFaultWithSourceGlobalID(aGlobalID, aRelationshipName, anEditingContext)
  
  def isaChildOf(self, anEditingContext):
    """
    Tells whether ``self`` is a child of ``anEditingContext``, either directly
    (``anEditingContext`` is its parent) or indirectly (``anEditingContext``
    is its grand-parent, or grand-grand-parent, etc.).

    Note that ``self`` is not a child of ``self``.
    """
    if anEditingContext==self:
      return 0
    
    try:
        currentEC=self
        while 1:
          if currentEC.parentObjectStore()==anEditingContext:
            return 1
          currentEC=currentEC.parentObjectStore()
    except AttributeError:
      return 0

  def faultForGlobalID(self, aGlobalID, anEditingContext=None):
    """
    Searches for an object registered within the EditingContext with the
    supplied GlobalID, and returns it if it is found. If not, forwards the
    message to the parentObjectStore (usually an ObjectStoreCoordinator).

    If anEditingContext is a child of self, 
    """
    if anEditingContext is None:
      anEditingContext=self
      
    if anEditingContext.isaChildOf(self):
      # One of our child ECs forwarded the message
      if aGlobalID.isTemporary():
        # newly inserted object: we create a copy of it, record it in the
        # child ec and finally we return it
        object=self.objectForGlobalID(aGlobalID)
        new_object=object.__class__()
        propertiesKeys=new_object.attributesKeys()+\
                        new_object.toOneRelationshipKeys()+\
                        new_object.toManyRelationshipKeys()
        new_object.prepareForInitializationWithKeys(propertiesKeys)
        anEditingContext.recordObject(new_object, aGlobalID)
        new_object.updateFromSnapshot(object.snapshot())
        return new_object

      else: # forward to the parentObjectStore
        return self.parentObjectStore().faultForGlobalID(aGlobalID,
                                                         anEditingContext)
        
    else:
      obj=self.objectForGlobalID(aGlobalID)
      if obj:
        return obj
      else:
        return self.parentObjectStore().faultForGlobalID(aGlobalID,
                                                         anEditingContext)

  def faultForRawRow(self, row, entityName, anEditingContext=None):
    """
    Turns a row (dictionary) into a real object. Any row, such as the one
    returned by a fetch when raw rows is actvated, can be turned into a
    real object given that it contains the primary keys.

    Parameters:

      row -- a dictionary. This dictionary should have the entity's primary
        keys in its keys (and their corresponding values)

      entityName -- the name of the entity the row represents

      anEditingContext -- (optional) The EditingContext in which the object
        should be registered. Defaults to self if None or omitted.

    """
    if anEditingContext is None:
      anEditingContext=self

    # no particular action for the child, faultForGlobalID() will handle
    # everything automatically
    return self.parentObjectStore().faultForRawRow(row, entityName,
                                                   anEditingContext)
      
  def initializeObject(self, anObject, aGlobalID, anEditingContext):
    """
    Forwards the message to the parentObjectStore(), with the following
    exception: if 'anEditingContext' 's 'parentObjectStore()' is 'self', i.e.
    it is a child of 'self', and if 'self' already has an real object (not a
    fault) with that globalID, the values of this object is used to initialize
    'anObject'.

    Conformance to the ObjectStore API

    Parameters:

      anObject -- a CustomObject instance

      aGlobalID -- the GlobalID of 'anObject'

      anEditingContext -- the EditingContext which holds 'anObject'

    """
    if anEditingContext.parentObjectStore()==self:
      # do we have that one?
      our_obj=self.objectForGlobalID(aGlobalID)
      if our_obj and not our_obj.isFault():
        # Override with our values

        ## snapshot() requires that all objects are inserted within ec
        ## (in fact, especially for toMany relationships)
        ## --> we choose here to propagate the insertion, if appropriate
        if self.propagatesInsertionForRelatedObjects():
          our_obj.propagateInsertionWithEditingContext(self)
          ObserverCenter.ensureSubsequentChangeWillBeNotifiedForObject(our_obj)
        snap=our_obj.snapshot()
        db=self.rootObjectStore().objectStoreForObject(anObject).database()
        db.incrementSnapshotCountForGlobalID(aGlobalID)
        anObject.updateFromSnapshot(snap)
        return

    self.parentObjectStore().initializeObject(anObject, aGlobalID,
                                              anEditingContext)


  def objectsCountWithFetchSpecification(self, aFetchSpecification,
                                         anEditingContext=None):
    """
    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).

    Forwards the message to the parentObjectStore(), usually an
    ObjectStoreCoordinator.

    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.

    """
    if anEditingContext is None:
      anEditingContext=self
    return self.parentObjectStore().objectsCountWithFetchSpecification(aFetchSpecification, anEditingContext)

  def fetchCount(self, entityName,
                 qualifier=None, # either a Qualifier instance or a string
                 #orderBy=None,   # either a SortOrdering or a string
                 isDeep=0,       # should subentities be fetched as well?
                 #refresh=0,      # unsupported yet
                 #lock=0,         # unsupported yet
                 #limit=None,     # \
                 #page=None,      #  > slice parameters, not in core yet
                 #offset=None,    # /  (page/offset: mutually exclusive)
                 ):
    """
    Returns the approximate number of objects that would be returned by
    fetch() 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).

    Returns the objects identified by the following parameters:

      entityName -- the corresponding entity's name (see also isDeep, below)

      qualifier -- a string qualifying the objects to be fetched (a instance
        of Qualifier can also be supplied)

      isDeep -- shall the fetch be made against the entity and its
        sub-entities?
      
    """
    
    if type(qualifier)==type(''):
      from Qualifier import qualifierWithQualifierFormat
      qualifier=qualifierWithQualifierFormat(qualifier)
    #if type(orderBy)==type(''):
    #  from SortOrdering import sortOrderingsWithString
    #  orderBy=sortOrderingsWithString(orderBy)
    from Modeling.FetchSpecification import FetchSpecification
    fs=FetchSpecification(entityName, qualifier=qualifier, deepFlag=isDeep)
    #fs.setLocksObject(lock)
    #fs.setRefreshesRefetchedObjects(refresh)
    return self.objectsCountWithFetchSpecification(fs)

  def objectsWithFetchSpecification(self, aFetchSpecification,
                                    anEditingContext=None):
    """
    Returns the objects matching the supplied FetchSpecification.

    Note that returned objects are:

      - objects that are already stored in the database. If some, if not all,
        of these objects are already registered within the EditingContext,
        they are returned as-is --meaning that, if they have been modified
        after they have been fetched from the database, these modifications
        are NOT overridden.

      - new objects that are inserted in the EditingContext (see
        insertObject()) but that are not saved yet.

    Objects that are marked as deleted (cf. deleteObject()) in the
    EditingContext are not returned.

    When a nested EditingContext fetches objects, the result set will contain
    the objects that are inserted in the parent (or the parent's parent,
    etc.), and objects which are marked as deleted in parent will not be part
    of the result set.
      
    If parameter 'anEditingContext' is omitted or 'None', it defaults to
    'self', then the message is forwarded down the hierarchy of ObjectStores,
    i.e. to the 'parentObjectStore()'

    Conformance to the ObjectStore API

    See also: DatabaseContext.objectsWithFetchSpecifications()
    """
    if anEditingContext is None:
      anEditingContext=self
    fs=aFetchSpecification
    ec=anEditingContext
    # +--> From this point we use 'ec' instead of 'anEditingContext'
    
    objects=self.parentObjectStore().objectsWithFetchSpecification(fs, ec)

    if ec.isaChildOf(self) and fs.fetchesRawRows():
      # filter the returned raw rows: we'd better replace the modified objects
      # with our own version
      fs_entityName=fs.entityName()
      copy_objects=list(objects)
      for o in copy_objects:
        self_obj=self.faultForRawRow(o, fs_entityName)
        if not self_obj.isFault():
          objects[objects.index(o)]=self_obj.snapshot_raw()

    if ec is self or ec.isaChildOf(self):
      entitiesNames=[fs.entityName()]
      if fs.isDeep():
        import ModelSet
        defaultModelSet=ModelSet.defaultModelSet()
        entity=defaultModelSet.entityNamed(fs.entityName())
        entitiesNames+=entity.allSubEntitiesNames()
        
      # do not include deleted objects in the returned set of objects
      if not fs.fetchesRawRows():

        #TBD Implementation note: the two blocks below (if isaChildOf/else)
        #TBD are completely equivalent. Well, at least, the first one works
        #TBD for both. Is it slower/better than 
        
        if ec.isaChildOf(self):
          # If we're returning objects for a child's use, we remove the one
          # that are marked for deletion in the parent (self) However, the
          # deleted objects in self and the ones in the child-ec are distinct,
          # but the GlobalIDs are the same and this is what we do here:
          # compare the gIDs and remove the apropriate objects from the result
          # set
          ec_deletedGids=self._deletedObjects+self._pendingDeletedObjects
          objects=[o for o in objects
                   if ec.globalIDForObject(o) not in ec_deletedGids]
        else:
          # We work for self, so we just remove the ones that are already
          # marked as deleted
          ec_deletedObjects = self.allDeletedObjects()
          objects=[o for o in objects if o not in ec_deletedObjects]

      else: # We're fetching raw rows. Wow, now we have to remove from the
            # list the dictionaries corresponding to the deleted objects.
            # Note: we use the exact same code when we're working for us (self)
            #       or for a child --this is basically manipulations of
            #       GlobalIDs
        ec_deletedGids=self._deletedObjects+self._pendingDeletedObjects
        ec_deleted_raw_pks=[gid.keyValues() for gid in ec_deletedGids]
        #if ec_deletedGids: import pdb ; pdb.set_trace()
        for deleted_raw_pk in ec_deleted_raw_pks:
          for raw_obj in list(objects):
            raw_obj_copy=raw_obj.copy()
            raw_obj_copy.update(deleted_raw_pk)
            if raw_obj_copy==raw_obj:
              objects.remove(raw_obj)

      # now append inserted objects
      for entityName in entitiesNames:
        ec_insertedObjects = self.allInsertedObjects()
        objs=[o for o in ec_insertedObjects if o.entityName()==entityName]
        if fs.qualifier():
          objs=filteredArrayWithQualifier(objs,fs.qualifier())

        # If we are returning objects for a child's use it is NOT POSSIBLE to
        # return our (self's) objects, instead, we return a fault for the
        # given object.
        # Note that faultForGlobalID() is responsible for cloning inserted
        # objects when they are requested by the nested EditingContext
        if ec.isaChildOf(self):
          fault_objs=[ec.faultForGlobalID(self.globalIDForObject(o), ec)
                      for o in objs]
          objs=fault_objs

        if fs.fetchesRawRows():
          objs=[o.snapshot_raw() for o in objs]
          
        objects.extend(objs)

    return objects

  def fetch(self, entityName,
                  qualifier=None, # either a Qualifier instance or a string
                  #orderBy=None,   # either a SortOrdering or a string
                  isDeep=0,       # should subentities be fetched as well?
                  #refresh=0,      # unsupported yet
                  #lock=0,         # unsupported yet
                  #limit=None,     # \
                  #page=None,      #  > slice parameters, not in core yet
                  #offset=None,    # /  (page/offset: mutually exclusive)
                  rawRows=0        # should we return raw rows
            ):
    """
    Returns the objects identified by the following parameters:

      entityName -- the corresponding entity's name (see also isDeep, below)

      qualifier -- a string qualifying the objects to be fetched (a instance
        of Qualifier can also be supplied)

      isDeep -- shall the fetch be made against the entity and its
        sub-entities?
      
    """
    
    if type(qualifier)==type(''):
      from Qualifier import qualifierWithQualifierFormat
      qualifier=qualifierWithQualifierFormat(qualifier)
    #if type(orderBy)==type(''):
    #  from SortOrdering import sortOrderingsWithString
    #  orderBy=sortOrderingsWithString(orderBy)
    from Modeling.FetchSpecification import FetchSpecification
    fs=FetchSpecification(entityName, qualifier=qualifier, deepFlag=isDeep)
    fs.setFetchesRawRows(rawRows)
    #fs.setLocksObject(lock)
    #fs.setRefreshesRefetchedObjects(refresh)
    return self.objectsWithFetchSpecification(fs)

  def ownsObject(self, anObject):
    """
    Forwards the message to the parentObjectStore() ; it usually goes done the
    ObjectStore hierarchy until an ObjectStoreCoordinator or a DatabaseContext
    is hit and answers
    """
    return self.parentObjectStore().ownsObject(anObject)
  
  def saveChangesInEditingContext(self, anEditingContext):
    """
    Called on the parent EditingContext when a child EditingContext saves
    changes.

    Exceptions that can be raised:

      - ValueError if 'anEditingContext' is not a child of self

      -
      
    Note: this method also automatically locks 'self' before actually
          saving the changes ; this makes it possible for two children of a
          single EC to safely save their changes in two different threads
          without having to worry about explicitly locking the parent EC.
    
    See also: saveChanges(), processRecentChanges()
    """
    self.lock()
    try:
      self._savesChangesInEditingContext(anEditingContext)
    finally:
      self.unlock()
      
  def _savesChangesInEditingContext(self, anEditingContext):
    """
    Private method called by saveChangesInEditingContext() after self has
    been locked.
    """
    if anEditingContext.parentObjectStore()!=self:
      raise ValueError, "Parameter 'anEditingContext' should be a child of "\
            "self"

    childEC=anEditingContext
    
    def ensureObjectsInRelationAreRegisteredInEC(object, ec):
      # Hypothesis: anObject is NOT an newly inserted object
      # and objects in relation are already registered
      for key in object.toOneRelationshipKeys():
        relObj=object.storedValueForKey(key)
        if relObj:
          relObjEC=relObj.editingContext()
          if not relObjEC:
            raise ObjectNotRegisteredError,\
                  "Object %s at %s references object %s at %s for key '%s' "\
                  "but the latter is not registered in any EditingContext"%\
                  (str(object), hex(id(object)),
                   str(relObj), hex(id(relObj)), key)
          
          gID=relObjEC.globalIDForObject(relObj)
          if not gID.isTemporary():
            ec.faultForGlobalID(gID, ec)
      for key in object.toManyRelationshipKeys():
        relObjs=object.storedValueForKey(key)
        for relObj in relObjs:
          relObjEC=relObj.editingContext()
          if not relObjEC:
            raise ObjectNotRegisteredError,\
                  "Object %s at %s references object %s at %s for key '%s' "\
                  "but the latter is not registered in any EditingContext"%\
                  (str(object), hex(id(object)),
                   str(relObj), hex(id(relObj)), key)
          
          gID=relObjEC.globalIDForObject(relObj)
          if not gID.isTemporary():
            ec.faultForGlobalID(gID, ec)
    #end/def ensureObjectsInRelationAreRegisteredInEC()
          
    # first, inserts objects that needs to be inserted
    for insertedObject in anEditingContext.insertedObjects():
      gID=childEC.globalIDForObject(insertedObject)
      if not self.objectForGlobalID(gID):
        new_inserted_object=insertedObject.__class__()

        # Note: we could simply call self.insertObjectWithGlobalID() here
        # BUT: we do not want awakeFromInsertion to be triggered
        self.recordObject(new_inserted_object, gID)
        self._pendingInsertedObjects.append(gID)

        
    for updatedObject in anEditingContext.updatedObjects():
      gID=childEC.globalIDForObject(updatedObject)
      self.faultForGlobalID(gID, self)

    # Then, update the objects that were already inserted within the parent
    ## TBD check that objects in relation still exists
    for insertedObject in anEditingContext.insertedObjects():
      gID=childEC.globalIDForObject(insertedObject)
      self.objectForGlobalID(gID).updateFromSnapshot(insertedObject.snapshot())
      ensureObjectsInRelationAreRegisteredInEC(new_inserted_object, self)


    # Updated objects
    for updatedObject in anEditingContext.updatedObjects():
      gID=childEC.globalIDForObject(updatedObject)
      self_obj=self.objectForGlobalID(gID)
      if not self_obj:
        self_obj=self.faultForGlobalID(gID, self)
      if self_obj in self._deletedObjects+self._pendingDeletedObjects:
        raise ParentChildECInconsistencyError, "Object %s at %s is marked as "\
              "updated in the EditingContext %s but it was deleted in its "\
              "parent EditingContext %s in the meantime"%\
              (updatedObject, hex(id(updatedObject)), str(childEC), str(self))

      ensureObjectsInRelationAreRegisteredInEC(updatedObject, self)
      self_obj.willChange()
      self_obj.updateFromSnapshot(updatedObject.snapshot())

    # DeletedObject
    for deletedObject in anEditingContext.deletedObjects():
      gID=childEC.globalIDForObject(deletedObject)
      self_obj=self.objectForGlobalID(gID)
      if not self_obj:
        self_obj=self.faultForGlobalID(gID, self)
      self.deleteObject(self_obj)
      # now we own the snapshot count: deleteObject has triggered the fault,
      # hence the refCount was updated --> nothing more to do.
    
  ##
  ## Notifications
  ##
  def handleNotification(self, notification):
    """
    Handles notification
    """
    notification_name=notification.name()
    if notification_name == ObjectsChangedInStoreNotification:
      raise 'Unimplemented', \
            'EditingContext got ObjectsChangedInStoreNotification'
       # UPDATED: refault objects for these GlobalIDs
       # Others??

    elif notification_name == InvalidatedAllObjectsInStoreNotification:
      raise 'Unimplemented', 'EditingContext got InvalidatedAllObj.InStore'

    elif notification_name == GlobalIDChangedNotification:
      gid=notification.object()
      if gid.isTemporary():
        obj=self.objectForGlobalID(gid)
        if obj:
          key_gid=notification.userInfo().get(gid)
          self.forgetObject(obj)
          self.recordObject(obj, key_gid)
          db=self.rootObjectStore().objectStoreForObject(obj).database()
          db.incrementSnapshotCountForGlobalID(key_gid)

          # Time to update the object if a PK is set as class properties

          # Note: One might say this could have been done earlier, e.g. in
          # DatabaseContext.prepareForSaveWithCoordinator() where PK values
          # are requested and assigned to the inserted objects

          # THIS IS INTENTIONAL: until finalizeCommitChanges() is called by
          # the ObjectStoreCoordinator, we can't be sure that no exception
          # will be raised in the process of saving changes. If an exception
          # is actually raised, the transaction is rolled back and the object
          # is not saved and as a consequence should NOT appear as if it
          # received a valid PK value.

          # Now, since DBContext.finalizeCommitChanges() does notify the EC
          # that inserted objects did receive a valid GlobalID (thus a valid
          # PK value) after all changes were supplied and committed to the
          # database, we know it is safe here to assign the inserted & saved
          # object its PK value --if the PK is a class property, obviously!
          
          from ModelSet import defaultModelSet
          entity=defaultModelSet().entityNamed(obj.entityName())
          pk_values=key_gid.keyValues()
          for pk in entity.primaryKeyAttributes():
            if pk.isClassProperty():
              obj.takeStoredValueForKey(pk_values[pk.name()], pk.name())
      else:
        obj=self.objectForGlobalID(gid)
        new_gid=notification.userInfo().get(gid)
        trace('Got GlobalIDChangedNotification w/ gid %s mapped to %s'%(gid, new_gid))

        if obj and not obj.isFault():
          warn('Got GlobalIDChangedNotification w/ a non temporary GlobalID '\
               'but the corresponding object is not a fault')
          return

        if obj and new_gid:
          self.__resolvedFaultsGIDs[gid] = new_gid
          self.forgetObject(obj)
          self.recordObject(obj, new_gid)
          NC.removeObserver(self, GlobalIDChangedNotification, gid)
    else:
      info('EditingContext got an unhandled notification: %s'%notification_name, ERROR)

  ##
  ## Undo
  ##
  def setUndoManager(self, anUndoManager):
    """
    Unimplemented yet

    See also: undoManager()
    """

  def redo(self):
    """
    Unimplemented yet

    Raises if no UndoManager is assigned to the EditingContext

    See also: redo(), setUndoManager()
    """

  def undo(self):
    """
    Unimplemented yet

    Raises if no UndoManager is assigned to the EditingContext

    See also: redo(), setUndoManager()
    """
    
  def undoManager(self):
    """
    Unimplemented yet: returns 'None'
    """
    return None

  
  ## Instance's lock
  def lock(self):
    """
    Acquire the reentrant-lock for the EditingContext. Calls to 'lock()' should
    be balanced with the same number of 'unlock()' for the lock to be released.
    Use this if you need to use the EditingContext in a multi-threaded
    environment.

    See also: unlock()
    """
    self.__instanceLock.acquire()

  def unlock(self):
    """
    Releases the lock for the EditingContext.

    See also: lock()
    """
    self.__instanceLock.release()


  ##
  ## Special methods
  ##
  def __unimplemented__(self):
    "Raises 'Unimplemented'"
    raise 'Unimplemented'
www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.