# $Id: persistable.rb 277 2003-11-22 08:25:13Z bolzer $
# Author:: Oliver M. Bolzer (mailto:oliver@fakeroot.net)
# Copyright:: (c) Oliver M. Bolzer, 2002
# Licence:: Ruby licence.

require 'vapor/exceptions'
require 'vapor/utils'

module Vapor

  # Mix-in providing classses with the ability to be persistently stored in a
  # VAPOR Repository. Metadata about the class must be registered with the
  # Repository before actual objects can be made persistent. 
  module Persistable
    include Exceptions

    TRANSIENT  = "TRANSIENT"      # object not in persistent storage
    PERSISTENT = "PERSISTENT"     # object same as in persistent storage
    NEW        = "NEW"            # object marked to be stored, but not stored yet
    DELETED    = 'DELETED'        # object marked to be detached from repository
    DIRTY      = 'DIRTY'          # persistent object changed
    READONLY   = 'READONLY'       # object persistent, but non-modifiable
                                  # (typically old revision)

    ## PersistenceManager to retrieve information from the Repository 
    def self.persistence_manager=( pmgr ) #:nodoc:
      @@pmgr = pmgr
    end # self.persistence_manager=()

    # load/replace the objects attribute values from the Hash given
    def load_attributes( attributes ) #:nodoc:

      # load data
      @vapor_oid = attributes['_oid']
      @vapor_revision = attributes['_revision']
      attributes.each{ |key, value|
        next if key =~ /^_/ 
        eval( "@#{key} = value" )
      }
      @vapor_state = PERSISTENT

    end # load_attributes()
  
    # load the object's attribute from the Hash given and
    # set the object's state to READONLY
    def load_attributes_readonly( attributes ) #:nodoc:
      load_attributes( attributes )
      @vapor_state = READONLY
    end # load_attributes_readonly()
    
    # Returns <tt>true</tt> if the object is stored in the Repository
    # and has not changed after being synched with the Repository.
    def persistent?
      self.state == PERSISTENT
    end # persistent?()

    # Returns <tt>true</tt> if the object's state is READONLY. Normally,
    # objects in this state are of archive copys of older revisions.
    def persistent_readonly?
      self.state == READONLY
    end # persistent_readonly?()
    
    # Returns <tt>String</tt> containing current persistence status.
    def state
      if !defined?( @vapor_state ) || @vapor_state.nil? then
        @vapor_state = TRANSIENT 
        @vapor_was_transient = true
      end

      return @vapor_state
    end # state()

    # Object-ID, <tt>nil</tt> if object is transient 
    def oid
      @vapor_oid ||= nil
      return @vapor_oid
    end

    # Revision number of the object. Starts with <tt>0</tt> when
    # the object is initially made persistent and is increased by one, each
    # time changes to it are committed. Returns <tt>nil</tt> if the object
    # is transient.
    def revision
      @vapor_revision ||= nil
      return @vapor_revision
    end

    # TransactionLog object that describes the transaction that made
    # the last changes to the object, creating the current version of it.
    def vapor_changelog
      return @vapor_changelog
    end # vapor_changelog
    
    # Returns <tt>true</tt> if the object's state was TRANSIENT when the
    # current transaction started.
    def vapor_was_transient? #:nodoc:
      if !defined?( @vapor_was_transient ) || @vapor_was_transient.nil? then
        @vapor_was_transient = true 
      end
      return @vapor_was_transient
    end # was_transient?()

    # Method is called by <tt>PersistenceManager</tt> after all objects of
    # the current transaction have successfully been committed to the datastore.
    # Don't call yourself unless absolutely sure what you are doing. 
    def vapor_post_commit #:nodoc:
      if self.state == PERSISTENT
        @vapor_was_transient = false
      end
    end # vapor_post_commit()
    
    # Make an transient object persistent. Raises an
    # ObjectAlreadyPersistentError if the object is not transient and
    # a RepositoryOfflineError if no PersistenceManager has been
    # instantiated.
    def make_persistent
      raise RepositoryOfflineError, "no PersistenceManager available" unless @@pmgr
      raise PersistableReadOnlyError if self.persistent_readonly?

      if self.state != TRANSIENT then
        raise ObjectAlreadyPersistentError
      end

    @@pmgr.try_change_state( self ){
      @vapor_state = NEW
      @vapor_oid = @@pmgr.next_oid
      @vapor_revision = 0

      @@pmgr.new_object( self )

      ## search reference attributes and mark them persistent, too
      self.class.metadata.nil? or begin
      self.class.metadata.select{|m| m.type == ClassAttribute::Reference }.each{|md|
        attribute = eval "@#{md.name}" 
        next if attribute.nil?

        if md.is_array then  # this is supposed to be an array
          raise VaporTypeError, "non-array where Array of Persistables expected" unless attribute.is_a? Array
          attribute.each{|a|
            raise VaporTypeError, "non-Persistable found in Array of Persistables" unless a.kind_of? Persistable
            # make it persistent, if it's not yet
            if a.state == TRANSIENT then
              a.make_persistent
            end
          }
        else                 # normal Persistable
          raise VaporTypeError, "non-Persistable #{attribute.inspect} at #{md.name} here Persistable expected" unless attribute.kind_of? Persistable
          # make it persistent, if it's not yet
          if attribute.state == TRANSIENT then
            attribute.make_persistent
          end
        end
        
      }
      rescue VaporTypeError => e  # some kind of type error, abort
        @vapor_state = TRANSIENT 
        @vapor_oid = nil
        raise
      end
    }

    end # make_persistent
      
    # Returns a <tt>Hash<tt> with the name of persistent attributes as keys and
    # and their values.
    def persistent_attributes #:nodoc:
      return nil if !self.class.metadata
      attributes = Hash.new
      self.class.metadata.each{|attr|
        case attr.type
        when ClassAttribute::Reference
           attributes[ attr.name ] = eval "@#{attr.name}"
        when ClassAttribute::String
          if attr.is_array then
            array =  eval("@#{attr.name}")
            attributes[ attr.name ] =
              if array.nil? then nil else array.collect{ |e| e.to_s } end
          else
            attributes[ attr.name ] = eval "@#{attr.name}.to_s"
          end
        when ClassAttribute::Integer
          if attr.is_array then
            attributes[ attr.name ] = eval("@#{attr.name}").collect{ |e| e.to_i }
          else
            attributes[ attr.name ] = eval "@#{attr.name}.to_i"
          end
        when ClassAttribute::Boolean
          if attr.is_array then
            attributes[ attr.name ] = eval("@#{attr.name}").collect{ |e|
              if e then true else false end
            }
          else
            attributes[ attr.name ] = 
              if eval "@#{attr.name}" then true else false end
          end
        when ClassAttribute::Float
          if attr.is_array then
            attributes[ attr.name ] = eval("@#{attr.name}").collect{ |e| e.to_F }
          else
            attributes[ attr.name ] = eval "@#{attr.name}.to_f"
          end
        when ClassAttribute::Date
          if attr.is_array then
            attributes[ attr.name ] = eval("@#{attr.name}").collect{ |e|
              if e.kind_of? Time or e.nil? then
                e
              else
                raise VaporTypeError, "Array element at #{attr.name} is not a Time"
              end
            }
          else
            date = eval "@#{attr.name}"
            if date.kind_of? Time or date.nil? then
              attributes[ attr.name ] = date
            else
              raise VaporTypeError, "object at attribute #{self.class.name}.#{attr.name} is not a Time"
            end
          end
        else
          attributes[ attr.name ] = eval "@#{attr.name}"
        end
      }
      attributes['_oid'] = @vapor_oid
      attributes['_revision'] = @vapor_revision
      return attributes
    end

    # Mark an object to be deleted from the repository. The object will still
    # exist in memory; it is only 'detached' from the repository,
    # losing it's persistent identitiy (OID) when the transaction is flushed.
    # Does nothing if the object is TRANSIENT.
    def delete_persistent
      return if self.state == TRANSIENT
      @@pmgr.try_change_state( self ){
        @vapor_state = DELETED 
      } 
    end # delete_persistent()

    # Mark an object as dirty. It will be saved when the transaction
    # commits, which might be instantly if Auto-Commit mode is set. Call this
    # method when the value of an persistent attribute has changed, preferably
    # from setters. Does nothing if the object is TRANSIENT.
    def mark_dirty
      return if self.state != PERSISTENT and self.state != READONLY
      @@pmgr.try_change_state( self ){
        @vapor_state = DIRTY
      }  
    end # mark_dirty()

    # Refresh values of object's persistent attributes to current values
    # in Datastore, setting the object's state to PERSISTENT. Transient attributes
    # are not touched. Does nothing if the object is transient or new. Raises
    # a DeletedObjectError if the object can not be found in the Datastore.
    # Does nothing if the object is TRANSIENT.
    def refresh_persistent
      return if self.state == TRANSIENT or self.state == NEW
      @@pmgr.try_change_state( self ){
        @@pmgr.refresh_object( self )
      }
    end # refresh_persistent()

    # Retrieve an persistently archived version of self. Returns
    # <code>nil</code> if a version with said revision mumber does not exist or the 
    # object is not persistent. The returned object is an Persistable
    # in the state READONLY.
    def get_version( revision_number )
      return nil unless revision_number.respond_to? :to_i and revision_number.to_i.kind_of? Integer

      @@pmgr.get_object_version( self.class, self.oid, revision_number )
    
    end # get_version
    alias :old_revision :get_version

    # Callback, called when object is actually deleted from the repository,
    # don't dare call without knowing the consequences
    def deleted_persistent #:nodoc:
      raise PersistableReadOnlyError if self.persistent_readonly?
      @vapor_oid = nil
      @vapor_revision = nil
      @vapor_state = TRANSIENT
      @vapor_was_transient = true
    end # deleted_persistent()
    private :deleted_persistent

    # when this module is included into a class,
    # extend that class with the PersistableClassMethods
    # module, adding some class-methods
    def self.append_features(klass_or_mod) #:nodoc: 
      super  # still want Persistable to be included

      if klass_or_mod.is_a?( Class ) then
        klass_or_mod.extend( PersistableClassMethods )
      end

    end # self.append_features()


    # module for methods, that are added as classmethods
    # to Persistable classes. These need to be separate
    # because inclusin of a module makes the module's
    # methods to instanece methods of objects of that class
    # (objects of the type Class get extend()ed with this module)
    module PersistableClassMethods #:nodoc:

      # teach the class about it's attributes that are of the Reference-type
      def metadata=( refAttrs )
        raise ArgumentError unless refAttrs.is_a?( Array ) or refAttrs.nil?
  
        # Yes, we know about our metadata
        @vapor_metadata = refAttrs 

      end # metadata=()
  
      # MetaData about the class' persistent attributes. returns an
      # ClassAttribute[]
      def metadata
        @vapor_metadata ||= nil
        @vapor_metadata
      end


    end # module PersistableClassMethods
  
  end # module Persistable

end # module Vapor
