CustomObject.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 » CustomObject.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.
#-----------------------------------------------------------------------------


"""
CustomObject

  CVS information

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

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

# framework
import ClassDescription
from DatabaseObject import DatabaseObject
from KeyValueCoding import KeyValueCoding
import Validation
from RelationshipManipulation import RelationshipManipulation
from FaultHandler import FaultHandler,AccessArrayFaultHandler
import ObserverCenter
from EditingContext import ObjectNotRegisteredError
from utils import capitalizeFirstLetter

# interfaces
from interfaces.DatabaseObject import DatabaseObjectInterface


class Snapshot_ToManyFault:
  """
  Snapshot_ToManyFault is used in CustomObject.snapshot(), when returning
  the value for a to-many relationship which is still a fault.

  It should not be mistaken for FaultHandler.AccessArrayFaultHandler, which
  holds the real to-many fault. An instance of this class is just a mean for
  CustomObject.snapshot() to tell that it found a fault. If you need the real
  to-many fault, use getToManyFault().

  See also: CustomObject.snapshot() for additional details.
  """
  def __init__(self, sourceGlobalID, key):
    """Initializer

    Parameter:

      sourceGlobalID -- a non temporary GlobalID (this is a non sense to have
        a to many fault for an object that has just been inserted

      key -- the corresponding to-many relationship's name
      
    Raises ValueError is sourceGlobalID.isTemporary() is true.

    """
    if sourceGlobalID.isTemporary():
      raise ValueError, 'sourceGlobalID cannot be a temporary global id'
    self.sourceGlobalID=sourceGlobalID
    self.key=key
    
  def getToManyFault(self, ec):
    """
    Returns the real to-many fault that this object represents.

    Parameter:

      ec -- an EditingContext
      
    """
    return ec.arrayFaultWithSourceGlobalID(self.sourceGlobalID, self.key,
                                           ec)

from Modeling.utils import base_object
class CustomObject(base_object, RelationshipManipulation, DatabaseObject):
  """

  Validation

    See Validation for details

  """

  __implements__ = (DatabaseObjectInterface,)+DatabaseObject.__implements__

  _globalID=None
  __editingContext=lambda self: None
  # The following is needed: when a fault is triggered and belongs to an
  # entity participating to an inheritance hierarchy, the possibly already
  # cached classDescription should be reset.
  # (see also: DatabaseChannel.fetchObject)
  _v_classDescription=None
    
  def awakeFromFetch(self, anEditingContext):
    """
    Automatically called by the framework when an object is initialized from
    its database row.

    Implementation simply sends 'awakeObjectFromFetch()' to the object's
    class description.

    Subclasses can override this method ; if they do, they must make sure that
    this method is called before making their own initialization.

    See also: ClassDescription.awakeObjectFromFetch(), awakeFromInsertion()
    """
    self.classDescription().awakeObjectFromFetch(self, anEditingContext)
  
  def awakeFromInsertion(self, anEditingContext):
    """
    Automatically called by the framework when an object is inserted
    in an EditingContext.

    Implementation simply sends 'awakeObjectFromInsertion()' to the object's
    class description.

    Subclasses can override this method ; if they do, they must make sure that
    this method is called before making their own initialization.

    See also: ClassDescription.awakeObjectFromInsertion(), awakeFromFetch()
    """
    self.classDescription().awakeObjectFromInsertion(self, anEditingContext)
    
  def allPropertyKeys(self):
    """
    Returns the whole set of properties, made of attributesKeys(),
    toManyRelationshipKeys() and toOneRelationshipKeys().
    """
    return self.attributesKeys()+\
           self.toManyRelationshipKeys()+self.toOneRelationshipKeys()
    
  def attributesKeys(self):
    """
    Forwards the message to the object's classDescription() and returns the
    result.

    In most cases, if not all, the underlying classDescription() is an
    EntityClassDescription instance, which only returns attributes that are
    marked as 'class properties' in the model.

    See also: toOneRelationshipKeys, toManyRelationshipKeys
    """
    return self.classDescription().attributesKeys()
  
  def classDescription(self):
    """
    Returns the object's corresponding ClassDescription ; in most cases
    this is an EntityClassDescription instance.
    """
    cd=getattr(self, '_v_classDescription', None)
    if not cd:
      cd=ClassDescription.classDescriptionForName(self.entityName())
      self._v_classDescription=cd
    return cd
  
  def classDescriptionForDestinationKey(self, aKey):
    """
    Returns the ClassDescription used for objects that can be referenced
    by the relationship 'aKey'.

    CustomObject's implementation simply forwards the message to the
    underlying classDescription() and returns the result.
    """
    return self.classDescription().classDescriptionForDestinationKey(aKey)

  #def clearProperties(self):
  #  DatabaseObject.clearProperties.im_func(self)
  
  def deleteRuleForRelationshipKey(self, aKey):
    """

    CustomObject's implementation simply forwards the message to the
    underlying classDescription() and returns the result.
    """
    return self.classDescription().deleteRuleForRelationshipKey(aKey)
  
  def entityName(self):
    raise 'MethodShouldBeOverridden', 'Subclass should override this method'

  def editingContext(self):
    """
    Returns the EditingContext in which the object was registered, or 'None'
    """
    return self.__editingContext()
      
  def globalID(self):
    """Returns the object's globalID, or None if the object is not registered
    within an EditingContext"""
    ec=self.editingContext()
    return ec and ec.globalIDForObject(self) or None
  
  def inverseForRelationshipKey(self, aKey):
    """
    CustomObject's implementation simply forwards the message to the
    underlying classDescription() and returns the result.

    Sometimes the framework is not able to calculate the correct relationship.
    For example, it might be that a relationship and its inverse are not based
    on inverse joins, such as in a one-to-one association, say between a
    Writer and its PostalAddress, where both relationships store the primary
    key of the destination entity in a foreign key of their own (I do not want
    to argue here about such a design, this can happen if you have to work on
    an existing database). In such a case, you will need to override this
    method so that the framework can find the inverse relationship. This could
    be done that way in class Writer::

      def inverseForRelationshipKey(self, aKey):
        \"\"\"
        Overrides CustomObject.inverseForRelationshipKey() so that
        'toPostalAddress' relationship uses the inverse relationship
        'PostalAddress.toAuthor'
        \"\"\"
        if aKey=='toPostalAddress': return 'toAuthor'
        else: # call super
          return CustomObject.inverseForRelationshipKey(self, aKey)

    Of course, the same needs to be done in class PostalAddress.
    
    """
    return self.classDescription().inverseForRelationshipKey(aKey)

  def isToManyKey(self, aKey):
    return aKey in self.toManyRelationshipKeys()
  
  def propagateDeleteWithEditingContext(self, editingContext):
    """
    Handles the deletion of the current object according to the delete
    rules which apply to its relationships, as defined in the model.

    CustomObject's implementation simply forwards the message to the
    underlying classDescription() [using propagateDeleteForObject] and returns
    the result.

    Subclasses overriding this method should call this (superclass)
    method.

    See also: ClassDescription.propagateDeleteForObject()
              Relationship.deleteRule()
    """
    self.classDescription().propagateDeleteForObject(self, editingContext)
  
  def propagateInsertionWithEditingContext(self, editingContext):
    """
    Called by an EditingContext when a object is marked as inserted or
    updated.

    CustomObject's implementation simply forwards the method to the underlying
    classDescription() (sending it a propagateInsertionForObject message).

    Subclasses overriding this method should call this (superclass)
    method.

    See also: ClassDescription.propagateInsertionForObject()
    """
    self.classDescription().propagateInsertionForObject(self, editingContext)
  
  def snapshot(self):
    """
    Returns the snapshot for the current object, i.e. a dictionary whose keys
    are allPropertyKeys(), and whose values are :

      - for attribute, the corresponding value,

      - for toOne relationships, the GlobalID of the related object (or None).
        If you want to get the real object corresponding to that global id,
        simply call EditingContext.faultForGlobalID() on
        self.editingContext().

      - For toMany relationships, the returned value depends on whether the
        set of related objects is still a fault or not:

          - if it is still a to-many fault, you'll get an instance of
            Snapshot_ToManyFault. To get the real to-many fault (instance of
            FaultHandler.AccessArrayFaultHandler), simply call
            getToManyFault() on this instance,

          - otherwise, you get the list of the GlobalIDs of the related
            objects

        Why are to-many faults handled this way? We do not want snapshot() to
        trigger any round-trip to the database. One could say, okay, but then
        you could return the to-many fault as well. True, but this won't be
        consistent with the other values returned by snapshot: values for
        to-one relationship are globalIDs with which you must explicitely call
        ec.faultForGlobalID() to get the real object. Same for to-many faults:
        you also need to explicitely call Snapshot_ToManyFault's
        getToManyFault() to get the whole (faulted) array. Last, this also
        prevents a to-many fault to be cleared by mistake (because simply
        looking at one of the fault properties, such as itys length, triggers
        a round-trip to the database).
        
    Raises ObjectNotRegisteredError if the object itself is not registered in
      an EditingContext, or if any of the objects it is in relation w/ is not
      known to the object's editingContext().
    """
    ec=self.editingContext()
    if ec is None:
      raise ObjectNotRegisteredError, 'Unable to compute snapshot: no editingContext()'
    res={}
    toOneKeys=self.toOneRelationshipKeys()
    toManyKeys=self.toManyRelationshipKeys()
    relKeys=toOneKeys+toManyKeys

    for key in self.allPropertyKeys():
      value=self.storedValueForKey(key)

      if key in toOneKeys:
        if value is None:
          res[key]=None
        elif isinstance(value, FaultHandler):
          res[key]=value.globalID()
        else:
          res[key]=ec.globalIDForObject(value)
        if value and res[key] is None:
          raise ObjectNotRegisteredError,\
                "Object %s at %s references object %s at %s for key '%s' but "\
                "the latter is not registered within the EditingContext %s "\
                "at %s"%\
                (str(self), hex(id(self)),str(value), hex(id(value)), key,
                 ec, hex(id(ec)))
        
      elif key in toManyKeys:
        if not isinstance(value, FaultHandler):
          res[key]=map(lambda o, ec=ec: ec.globalIDForObject(o), value)
          if None in res[key]:
            raise ObjectNotRegisteredError,\
                  "Object %s at %s references a list of object (%s) for "\
                  "key '%s' but the latter holds at least one object that is "\
                  "not registered within the EditingContext %s at %s"%\
                  (str(self), hex(id(self)),str(value), key, ec, hex(id(ec)))

        else:
          res[key]=Snapshot_ToManyFault(self.globalID(), key)

      else:
        res[key]=value

    return res
  
  def snapshot_raw(self):
    """
    Returns the raw snapshot for the current object, i.e. a dictionary whose
    keys are allAttributesKeys() with their corresponding values.

    Special care should be taken when examining the values for primary keys
    and foreign keys:

      - if your object and all its related objects are already saved in the
        database, you'll get the PK's and FKs' values, as expected (a special
        case exists where a FK value can get out-of-sync, see below)

      - if your object is inserted in an EditingContext but has not been saved
        yet, the PK value will be its TemporaryGlobalID, since there is no way
        to know the value it will get when it is saved,

      - if your object has a to-one relationship to an other object that is
        inserted in an EditingContext but not saved yet, the FK's value will
        be that related object's TemporaryGlobalID --for the same reason then
        above.

      - Last, if the object's entity defines a foreign key involved in a
        to-many relationship pointing to it but with no inverse (i.e. self's
        entity has no to-one relationship using this foreign key), the
        returned value will its value stored in the Database's cache managed
        by the framework. In other words, this value will be exact after each
        saveChanges(), and the first time the object is fetched; afterwards,
        if the object on the other side modifies its relation pointing to
        'self' (e.g. removes it), the value will be out-of-sync with the
        object graph and will remain as-is until the EditingContext saves its
        changes again.

    Note: this method does not care about PKs or FKs being marked as class
    properties. Even when they are class properties, it does NOT look at their
    values, instead the returned values are extracted by examining the objects
    related to 'self'.
      
    Raises ObjectNotRegisteredError if the object itself is not registered in
      an EditingContext, or if any of the objects it is in relation w/ is not
      known to the object's editingContext().
    """
    ec=self.editingContext()
    if ec is None:
      raise ObjectNotRegisteredError, 'Unable to compute snapshot_raw: the object is not registered in an EditingContext'
    if self.isFault():
      self.willRead()
    res={}
    cd=self.classDescription()
    fks=cd.foreignKeys()
    pks=cd.primaryKeys()
    toOneRels=filter(lambda rel: rel.isToOne(), cd.entity().relationships())
    for key in cd.allAttributesKeys():
      if key in fks or key in pks: # we take care of this later
        continue

      try:
        res[key]=self.storedValueForKey(key)
      except AttributeError:
        # This happens when it comes to fill the value for a FK used in a
        # relationship pointing to the object, with no to-one inverse
        # relationship defined in the object's entity (thus, this fk is not
        # in fks=ClassDescription.foreignKeys()
        attr=cd.entity().attributeNamed(key)
        if attr and not attr.isClassProperty():
          if self.globalID().isTemporary():
            # cannot compute, insert None
            res[key]=None
          else:
            db=ec.rootObjectStore().objectStoreForObject(self).database()
            snap=db.snapshotForGlobalID(self.globalID())
            res[key]=snap[key]
            
    # Handle PKs
    gid=self.globalID()
    if gid.isTemporary():
      for pk in pks:
        res[pk]=gid
    else:
      res.update(gid.keyValues())

    # Handle FKs
    for rel in toOneRels:
      key=rel.name()
      rel_obj=self.storedValueForKey(key)
      if rel_obj is None:
        for src_attr in rel.sourceAttributes():
          res[src_attr.name()]=None
        continue

      rel_gid=rel_obj.globalID()
      if rel_gid.isTemporary(): # not saved yet: save its TemporaryGID
        for src_attr in rel.sourceAttributes():
          res[src_attr.name()]=rel_gid
          
      else: # we have a KeyGlobalID, values can be filled in
        rel_gid_kv=rel_gid.keyValues()
        for src_attr in rel.sourceAttributes():
          dst_attr=rel.destinationAttributeForSourceAttribute(src_attr)
          res[src_attr.name()]=rel_gid_kv[dst_attr.name()]

    return res

  def toManyRelationshipKeys(self):
    """
    Forwards the message to the object's classDescription() and returns the
    result.

    In most cases, if not all, the underlying classDescription() is an
    EntityClassDescription instance, which only returns toMany relationships
    that are marked as 'class properties' in the model.

    See also: attributesKeys, toOneRelationshipKeys
    """
    return self.classDescription().toManyRelationshipKeys()
    
  def toOneRelationshipKeys(self):
    """
    Forwards the message to the object's classDescription() and returns the
    result.

    In most cases, if not all, the underlying classDescription() is an
    EntityClassDescription instance, which only returns toOne relationships
    that are marked as 'class properties' in the model.

    See also: attributesKeys, toManyRelationshipKeys
    """
    return self.classDescription().toOneRelationshipKeys()
    
  def updateFromSnapshot(self, aSnapshotDictionary):
    """
    Given a snapshot, update the current object ; note that if the object is
    a fault, that fault will be cleared and the object initialized before the
    changes are made.

    Parameter:

      aSnapshotDictionary -- a dictionary, as returned by snapshot()

    See also: snapshot
    """
    ec=self.editingContext()
    if ec is None:
      raise ObjectNotRegisteredError, 'Unable to compute snapshot: no editingContext()'
    snap=aSnapshotDictionary
    self.willRead() # trigger the fault
    toOneKeys=self.toOneRelationshipKeys()
    toManyKeys=self.toManyRelationshipKeys()
    relKeys=toOneKeys+toManyKeys

    for key in self.allPropertyKeys():
      value=snap[key]
      if key in toOneKeys:
        if value is not None:
          value=ec.faultForGlobalID(value, ec)

      elif key in toManyKeys:
        if isinstance(value, Snapshot_ToManyFault):
          #gID=ec.globalIDForObject(self)
          #value=ec.arrayFaultWithSourceGlobalID(gID, key, ec)
          value=value.getToManyFault(ec)
        else:
          try:
            value=map(lambda gID, ec=ec: ec.faultForGlobalID(gID, ec), value)
          except:
            import pdb ; pdb.set_trace()
      self.takeStoredValueForKey(value, key)
    
  
  def willChange(self):
    """
    Notifies the observers that the object is about to change.

    CustomObject's default implementation calls willRead() on itself before
    notifying the ObserverCenter
    """
    self.willRead()
    ObserverCenter.notifyObserversObjectWillChange(self)

  ##
  ## Validation
  ##
  def validateForDelete(self):
    """
    Default implementation simply sends validateObjectForDelete() to the
    receiver's ClassDescription.

    Subclasses overriding this method should call it prior to any other
    checks, and exceptions raised here should be aggegated with custom
    validation.

    See also: EntityClassDescription.validateObjectForDelete()
    """
    self.classDescription().validateObjectForDelete(self)

  def validateForInsert(self):
    """
    Simply calls validateForSave()
    """
    self.validateForSave(self)
    
  def validateForUpdate(self):
    """
    Simply calls validateForSave()
    """
    self.classDescription().validateForSave(self)
    
  def validateForSave(self):
    """
    
    First forwards forwards the message to the object's ClassDescription,
    then iterates on class properties so that they are individually
    checked --this is done by sending the message 'validateValueForKey()'
    to oneself.

    Subclasses overriding this method *must* call it prior to any other
    checks, and exceptions raised here *must* be aggegated with custom
    validation.

    See also: allPropertyKeys(), classDescription()
              EntityClassDescription.validateObjectForSave(),
              EntityClassDescription.classProperties()
    """
    self.classDescription().validateObjectForSave(self)
    error=Validation.ValidationException()
    hasError=0
    for key in self.allPropertyKeys():
      try:
        self.validateValueForKey(self.storedValueForKey(key), key)
      except Validation.ValidationException, exc:
        error.aggregateException(exc)
        hasError=1
    if hasError:
      error.aggregateError(Validation.OBJECT_WIDE%repr(self),
                           Validation.OBJECT_WIDE_KEY)

    error.finalize()


  def validateValueForKey(self, aValue, aKey):
    """
    Validate the value hold by 'aKey' attribute/relationship in the object.

    Default implementation first forwards the message to the object's
    ClassDescription, which checks that the constraints defined in the
    underlying model are respected.

    Then it searches if the object defines a method named 'validate<AKey>'
    (e.g., with 'aKey=lastName', that method's name would be
    'validateLastName': you have already noticed that the first letter of the
    key is capitalized in the name of the method). If such a method is
    actually defined by the object, then it is called. You will typically
    define such a method to define your own validation mechanism (per-key
    validation).

    Subclasses should never override this method. If you want to specify your
    own validation mechanism for a given key, say 'name', you should define a
    method named 'validateName()' in your class instead, as described above.
    
    See: EntityClassDescription.validateValueForKey()
    """
    validationError=Validation.ValidationException()
    try:
      self.classDescription().validateValueForKey(aValue, aKey)
    except Validation.ValidationException, exc:
      validationError.aggregateException(exc)

    # Custom bizness logic: validate<AttributeName>
    validateBizLogic_methodName='validate'+capitalizeFirstLetter(aKey)
    validateFunction=None
    try:
      validateFunction=getattr(self, validateBizLogic_methodName)
    except AttributeError:
      # Function is not defined, fine
      pass
    
    if validateFunction and callable(validateFunction):
      try:
        validateFunction(aValue)
      except Validation.ValidationException, exc:
        validationError.aggregateException(exc)
        validationError.addErrorForKey(Validation.CUSTOM_KEY_VALIDATION,
                                       aKey)
    validationError.finalize()
www.java2java.com | Contact Us
Copyright 2009 - 12 Demo Source and Support. All rights reserved.
All other trademarks are property of their respective owners.