# $Id: persistencemgr.rb 265 2003-10-31 17:51:47Z bolzer $
# Author:: Oliver M. Bolzer (mailto:oliver@fakeroot.net)
# Copyright:: (c) Oliver M. Bolzer, 2002
# Licence:: Ruby licence.

require 'vapor/oidgenerator'
require 'vapor/queryparser'
require 'vapor/persistable'
require 'vapor/tuplemgr'
require 'vapor/extent'
require 'vapor/transaction'

module Vapor

  # Central interface to the VAPOR framework. Manages Persistable objects.
  class PersistenceManager
    include Exceptions

    # Initialize a new PersistenceManager. 
    #
    # <tt>properties</tt> is a <tt>Hash</tt>-like object responding to
    # <tt>[key]</tt>. For unknown keys, it should return <tt>nil</tt>.
    # Keys used by the PersistenceManager are:
    #
    # <tt>Vapor.Datastore.Username</tt>:: Username used to authenticate connection
    #                                     to Datastore
    # <tt>Vapor.Datastore.Password</tt>:: Password used to authenticate connection
    #                                     to Datastore
    # <tt>Vapor.Datastore.Name</tt>:: Name of the Datastore
    # <tt>Vapor.Datastore.Host</tt>:: Hostname where the Datastore resides
    # <tt>Vapor.Datastore.Port</tt>:: <em>optional</em>, Port used to connect to
    #                                 host where Datastore resides
    # <tt>Vapor.Autocommit</tt>:: <em>optional</em>If Autocommmit-Mode should be set
    #                             on startup. 
    #                             Defaults to <tt>true</tt> if unspecified.
    #                             All values other then <tt>false</tt> or
    #                             <tt>nil</tt> are treated as <tt>true</tt>.
    #                             
    # <tt>Vapor.Datastore.BackendClass</tt>:: <em>optional</em>, <tt>Class</tt> used
    #                                         to access Datastore, defaults to
    #                                         <tt>TupleManager</tt>, don't specify
    #                                         anything unless absolutely sure 
    def initialize( properties )
      raise TypeError, "property object does not respond to []" unless properties.respond_to? "[]"

      # determine backend class to use
      if !properties['Vapor.Datastore.BackendClass'].nil? then
        backend_class = properties['Vapor.Datastore.BackendClass']
      else
        backend_class = TupleManager
      end
    
      # retrieve properties
      ds_user = properties[ 'Vapor.Datastore.Username']
      ds_pass = properties[ 'Vapor.Datastore.Password' ]
      ds_name = properties[ 'Vapor.Datastore.Name' ]
      ds_host = properties[ 'Vapor.Datastore.Host' ]
      ds_port = properties[ 'Vapor.Datastore.Port' ]
      if properties[ 'Vapor.Autocommit' ].nil? then
        @autocommit = true
      else
        if properties[ 'Vapor.Autocommit' ] then
          @autocommit = true
        else
          @autocommit = false
        end
      end

      # initialize backend
      @backend = backend_class.new( ds_name, ds_user, ds_pass, ds_host, ds_port ) 

      # initialize other instance variables
      @object_cache = Hash.new
      Persistable.persistence_manager = self
      @oid_gen = OIDGenerator.new
      @oid_gen.high_source = @backend
      @query_parser = QueryParser.new
      @transaction = Transaction.new( self, @backend )
      @autocommit_in_progress = false
      
    end # initialize()

    # Transaction instance asssociated with the PersistenceManager.
    # When the optional block is given, pass it to Transaction##do. 
    # <tt>PersistenceManager#transaction{|t| ...}</tt> is equivalent
    # to <tt>PersistenceManager#transaction.do{|t| ...}</tt>.
    def transaction( &block ) #:yields: transaction
      if block_given?   # just return instance if no block given
        @transaction.do( &block )
      else
        return @transaction
      end
    end # transaction
    
    # Returns the Persistable with the specific OID from the Repository.
    def get_object( oid )
      raise TypeError unless oid.is_a? Integer

      ## check Object Cache first
      if @object_cache.include?( oid ) then
        return @object_cache[ oid ]
      end

      ## retrieve from backend
      attributes = @backend.get_tuple( oid )
      if attributes.nil? then  # object with oid not found
        return nil
      end

      ## determinde obj's klass 
      klass = attributes['_type']
      obj = klass.new
      
      ## save in Object Cache
      @object_cache[ oid ] = obj
      
      ## give it it's content
      load_object( obj, attributes )
      obj.vapor_post_commit

      return obj

    end # get_object()

    # Returns an archived version of a persistent object of the specified
    # klass, oid and revision. The object's satate is READONLY and thus
    # non-modifiable. Returns nil if such an object (either the
    # klass, the oid or the version ) can't be found. The returned objects
    # are not cached and two different objects are returned when the method is
    # called twice with the same arguments.
    def get_object_version( klass, oid, revision )
      raise TypeError unless oid.kind_of? Integer and revision.kind_of? Integer
      
      ## retrieve from backend
      attributes = @backend.get_archived_tuple( klass, oid, revision )
      if attributes.nil? then  # object with oid not found
        return nil
      end
      klass = attributes['_type']
      obj = klass.new

      load_object( obj, attributes, true )

    end # get_object_version()

    # Returns an Extent containing all instances of a the Persistable Class.
    # If <tt>subclasses</tt> is <tt>true</tt>, the Extent will contain all instances
    # of persistable subclasses of the Class. 
    def get_extent( klass, subclasses = true )
      raise ArgumentError unless klass.is_a? Class

      extent = Extent.new( self, klass, subclasses )

      @backend.get_all_instances( klass.name, subclasses ).each{|oid|
        extent.add( oid )
      }

      return extent
    end # get_extent()

    # Retursn an previouslyu unused, unique OID.
    def next_oid
      @oid_gen.next_oid
    end # next_oid()

    # Add newly persistent object to the PersistenceManager's administration.
    def new_object( obj ) #:nodoc:
      raise TypeError unless obj.is_a? Persistable
      raise ArgumentError, "Persistable is not NEW but #{obj.state}" unless obj.state == Persistable::NEW
      
      ## give the klass it's metadata if it doesn't know it jet
      if !obj.class.metadata then
        obj.class.metadata = @backend.get_class_attributes( obj.class.name )
      end

      @object_cache[ obj.oid ] = obj
    end # new_object()

    # Iterator over all all persistent objects.
    def each_loaded_object
      @object_cache.each_value{ |obj| yield obj }
    end

    # Flush out changes to all persistent objects, including new and deleted.
    def flush_all #:nodoc:

      @object_cache.each_value{|obj|
        flush_object( obj )
      }
      # set before-transaction-state to PERSISTENT for flushed objects
      @object_cache.each_value{|obj|
        obj.vapor_post_commit
      }
    
    end # flush()

    # Flush a single object to datastore.
    def flush_object( obj ) #:nodoc:
      raise TypeError unless obj.kind_of? Persistable
      
      begin
        
        case obj.state
      
        when Persistable::PERSISTENT  # object hasn't changed since loading
          return                        # do nothing
	  
        when Persistable::NEW         # object is newly marked persistent
          tuple = Hash.new
	 
          @backend.new_tuple( obj.class, obj.oid )
          attributes = persistent_attributes_oid( obj )
          begin
            @backend.update_tuple( obj.class, obj.oid, attributes )
          rescue StaleObjectError, DeletedObjectError => e
            e.causing_object = obj
            raise e
          end
          attributes[ '_revision' ] += 1
          obj.load_attributes( attributes )
	
        when Persistable::DELETED     # object marked to be deleted
          begin
            @backend.delete_tuple( obj.class, obj.oid, obj.revision )
          rescue StaleObjectError => e
            e.causing_object = obj
            raise e
          end
          @object_cache.delete obj.oid         # get rid of it from the cache 
          obj.__send__( :deleted_persistent )  # kludge to keep this private
       
        when Persistable::DIRTY       # object changed
          attributes = persistent_attributes_oid( obj )
          begin
            @backend.update_tuple( obj.class, obj.oid, attributes )
          rescue StaleObjectError, DeletedObjectError => e
            e.causing_object = obj
            raise e
          end
          attributes[ '_revision' ] += 1
          obj.load_attributes( attributes )
        end
        
      end
      
    end # flush_object()

    # Retrieve persistable atributes and convert their
    # Persistable attributes to their OIDs
    def persistent_attributes_oid( obj )
      raise TypeError unless obj.kind_of? Persistable

      attributes = obj.persistent_attributes
      obj.class.metadata.each{|attr|
        if attr.type == ClassAttribute::Reference and not attr.is_array then
          persistable = attributes[ attr.name ]
          next if persistable.nil?
          if persistable.state == Persistable::TRANSIENT then
            persistable.make_persistent
            flush_object( persistable )
          end
          attributes[ attr.name ] = persistable.oid 
        elsif attr.type == ClassAttribute::Reference and attr.is_array then
          persistable_array = attributes[ attr.name ]
          next if persistable_array.nil?
          attributes[ attr.name ] = persistable_array.collect{|e|
            if e.state == Persistable::TRANSIENT then
              e.make_persistent
              flush_object( e )
            end
            e.oid
          }
        end
      }

      return attributes
    end # persistent_attributes_oid()
    private :persistent_attributes_oid

    # Search for persistent objects of a specific class in the Repository.
    # Returns an Extent containing the objects matching the query, empty
    # if none match. Result will include instances of subclasses matching
    # the query, too, if <tt>subclasses</tt> is <tt>true</tt>.
    def query( klass, query, arguments, subclasses = true )
      raise TypeError unless klass.ancestors.include? Persistable
      raise TypeError unless query.is_a? String
      raise TypeError unless arguments.is_a? Array

      begin 
        statement = @query_parser.parse( query, arguments )
      rescue InvalidQueryError
        raise
      end

      oids = @backend.search_tuples( klass, statement, subclasses )
      
      result = Extent.new( self, klass, subclasses )

      if !oids.nil? then
        oids.each{|oid| result.add( oid ) }
      end

      return result
    end # query()

    # Returns <tt>true</tt> if Autocommit-Mode is turned on.
    def autocommit?
      @autocommit
    end # autocommit?

    # Set status of Autocommit-Mode. Flushes all previous changes when
    # setting to <tt>true</tt>. All subsequent changes will be flushed 
    # immediatly.
    def autocommit=( value )
      if value then
        if !@autocommit and @transaction.active?
         @transaction.commit 
        end
        @autocommit = true
      else
        @autocommit = false
      end
    end # autocommit=()

    # Callback for state-changes of Persistable objects. Flushes object
    # immediatly if Autocommit-Mode is on.
    def try_change_state( obj, &block ) #:nodoc:
      raise TypeError unless obj.kind_of? Persistable
      raise ArgumentError unless block_given?
    
      # raise exception and reload contents if object is read-only
      if obj.persistent_readonly? then
        attributes = @backend.get_archived_tuple( obj.class, obj.oid, obj.revision )
        load_object( obj, attributes, true )
        raise PersistableReadOnlyError
      end

      # check transaction activity
      if @transaction.active? then
        block.call
        @transaction.log_entry.object_modified( obj )
      elsif @autocommit
        @autocommit_in_progress = true
        self.transaction.begin
        block.call
        self.transaction.commit
        @autocommit_in_progress = false
      else
        raise StaleTransactionError
      end

    end # state_changed()

    # Returns <tt>true</tt> if a microtransaction of an Autocommit is in progress.
    def autocommit_in_progress?
      @autocommit_in_progress
    end #autocommit_in_progress?

    # Unconditionally refresh an Persistable object with the current values in the
    # datastore.
    def refresh_object( obj ) #:nodoc:
      raise TypeError unless obj.kind_of? Persistable

      # do nothing if obj is transient
      return if obj.state == Persistable::TRANSIENT or obj.state == Persistable::NEW

      # retrieve attributes from backend
      attributes = @backend.get_tuple( obj.oid )
      if attributes.nil?
        raise DeletedObjectError
      end
      
      # give it it's content
      load_object( obj, attributes )

    end # refresh_object()

    # Load an object with the content currently in the Datastore.
    def load_object( obj, attributes, readonly = false )

      attributes = attributes.dup
      klass = attributes['_type']
      attributes.delete('_type')
   
      ## give the klass it's metadata if it doesn't know it jet
      if !klass.metadata then
        klass.metadata = @backend.get_class_attributes( klass.name )
      end

      klass.metadata.each{ |attr|
        if attr.type == ClassAttribute::Reference and not attr.is_array then
          oid = attributes[ attr.name ]
          next if oid.nil?
          attributes[ attr.name ] = self.get_object( oid ) unless oid.nil?
        elsif attr.type == ClassAttribute::Reference and attr.is_array then
          oid_array = attributes[ attr.name ]
          next if oid_array.nil?
          attributes[ attr.name ] = oid_array.collect{|e| self.get_object( e ) unless e.nil?}
        end
      }

      ## get the changelog object
      changelog_oid = attributes[ '_last_change' ]
      attributes.delete( '_last_change' )
      if changelog_oid.nil? then
        attributes[ 'vapor_changelog' ]
      else
        attributes[ 'vapor_changelog' ] = self.get_object( changelog_oid )
      end

      ## give the object it's content
      if readonly then
        obj.load_attributes_readonly( attributes )
      else
        obj.load_attributes( attributes )
      end

      return obj

    end # load_object()
    private :load_object

    # Rollback a single object. Persistent objects return to state PERSISTENT
    # with their attribute's values refreshed to those in the Datastore. NEW
    # objects return to TRANSIENT without their values being changed.
    # Transient objects are not touched at all.
    def rollback_object( obj ) #:nodoc:
      raise TypeError unless obj.kind_of? Persistable

      case obj.state
      when Persistable::TRANSIENT
        return
      when Persistable::NEW
        obj.__send__( :deleted_persistent )
      when Persistable::PERSISTENT, Persistable::DIRTY, Persistable::DELETED
        if obj.vapor_was_transient? then
          obj.__send__( :deleted_persistent )
        else
          obj.refresh_persistent
        end
      end

    end # rollback_object()

    # Rollback all objects in cach. 
    def rollback_all #:nodoc:
      @object_cache.each_value{|obj|
        rollback_object( obj )
      }
    end # rollback_all()

  end # class PersistenceManager

end # module Vapor
