]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/registry.rb
[registry] migrate tc directory name to registry_tc
[user/henk/code/ruby/rbot.git] / lib / rbot / registry.rb
index 2a7edbb6fc07bff163799936760cb9a423687fb4..0eefc9da2155f1f1ae83a2d211e87514ea97c818 100644 (file)
-require 'rbot/dbhash'
+#-- vim:sw=2:et
+#++
+#
+# :title: Registry: Persistent storage interface and factory
+# 
+# This class provides persistent storage for plugins via a hash interface.
+# The default mode is an object store, so you can store ruby objects and
+# reference them with hash keys. This is because the default store/restore
+# methods of the plugins' RegistryAccessor are calls to Marshal.dump and
+# Marshal.restore,
+# for example:
+#   blah = Hash.new
+#   blah[:foo] = "fum"
+#   @registry[:blah] = blah
+# then, even after the bot is shut down and disconnected, on the next run you
+# can access the blah object as it was, with:
+#   blah = @registry[:blah]
+# The registry can of course be used to store simple strings, fixnums, etc as
+# well, and should be useful to store or cache plugin data or dynamic plugin
+# configuration.
+#
+# WARNING:
+# in object store mode, don't make the mistake of treating it like a live
+# object, e.g. (using the example above)
+#   @registry[:blah][:foo] = "flump"
+# will NOT modify the object in the registry - remember that Registry#[]
+# returns a Marshal.restore'd object, the object you just modified in place
+# will disappear. You would need to:
+#   blah = @registry[:blah]
+#   blah[:foo] = "flump"
+#   @registry[:blah] = blah
+#
+# If you don't need to store objects, and strictly want a persistant hash of
+# strings, you can override the store/restore methods to suit your needs, for
+# example (in your plugin):
+#   def initialize
+#     class << @registry
+#       def store(val)
+#         val
+#       end
+#       def restore(val)
+#         val
+#       end
+#     end
+#   end
+# Your plugins section of the registry is private, it has its own namespace
+# (derived from the plugin's class name, so change it and lose your data).
+# Calls to registry.each etc, will only iterate over your namespace.
 
 module Irc
+class Bot
 
-  # this is the backend of the RegistryAccessor class, which ties it to a
-  # DBHash object called plugin_registry(.db). All methods are delegated to
-  # the DBHash.
-  class BotRegistry
-    def initialize(bot)
-      @bot = bot
-      upgrade_data
-      @db = DBTree.new @bot, "plugin_registry"
-    end
+class Registry
+
+  # Dynamically loads the specified registry type library.
+  def initialize(format=nil)
+    @libpath = File.join(File.dirname(__FILE__), 'registry')
+    @format = format
+    load File.join(@libpath, @format+'.rb') if format
+  end
 
-    # delegation hack
-    def method_missing(method, *args, &block)
-      @db.send(method, *args, &block)
+  # Returns a list of supported registry database formats.
+  def discover
+    Dir.glob(File.join(@libpath, '*.rb')).map do |name|
+      File.basename(name, File.extname(name))
     end
+  end
 
-    # check for older versions of rbot with data formats that require updating
-    # NB this function is called _early_ in init(), pretty much all you have to
-    # work with is @bot.botclass.
-    def upgrade_data
-      if File.exist?("#{@bot.botclass}/registry.db")
-        puts "upgrading old-style (rbot 0.9.5 or earlier) plugin registry to new format"
-        old = BDB::Hash.open "#{@bot.botclass}/registry.db", nil, 
-                             "r+", 0600, "set_pagesize" => 1024,
-                             "set_cachesize" => [0, 32 * 1024, 0]
-        new = BDB::CIBtree.open "#{@bot.botclass}/plugin_registry.db", nil, 
-                                BDB::CREATE | BDB::EXCL | BDB::TRUNCATE,
-                                0600, "set_pagesize" => 1024,
-                                "set_cachesize" => [0, 32 * 1024, 0]
-        old.each {|k,v|
-          new[k] = v
-        }
-        old.close
-        new.close
-        File.delete("#{@bot.botclass}/registry.db")
-      end
+  # Creates a new Accessor object for the specified database filename.
+  def create(path, filename)
+    # The get_impl method will return a list of all the classes that
+    # implement the accessor interface, since we only ever load one
+    # (the configured one) accessor implementation, we can just assume
+    # it to be the correct accessor to use.
+    cls = AbstractAccessor.get_impl.first
+    db = cls.new(File.join(path, 'registry_' + @format, filename.downcase))
+    db.optimize
+    db
+  end
+
+  # Helper method that will return a list of supported registry formats.
+  def self.formats
+    @@formats ||= Registry.new.discover
+  end
+
+  # Will detect tokyocabinet registry location: ~/.rbot/registry/*.tdb
+  #  and move it to its new location ~/.rbot/registry_tc/*.tdb
+  def migrate_registry_folder(path)
+    old_name = File.join(path, 'registry')
+    new_name = File.join(path, 'registry_tc')
+    if @format == 'tc' and File.exists?(old_name) and
+        not File.exists?(new_name) and
+        not Dir.glob(File.join(old_name, '*.tdb')).empty?
+      File.rename(old_name, new_name)
     end
   end
 
-  # This class provides persistent storage for plugins via a hash interface.
-  # The default mode is an object store, so you can store ruby objects and
-  # reference them with hash keys. This is because the default store/restore
-  # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
-  # Marshal.restore,
-  # for example:
-  #   blah = Hash.new
-  #   blah[:foo] = "fum"
-  #   @registry[:blah] = blah
-  # then, even after the bot is shut down and disconnected, on the next run you
-  # can access the blah object as it was, with:
-  #   blah = @registry[:blah]
-  # The registry can of course be used to store simple strings, fixnums, etc as
-  # well, and should be useful to store or cache plugin data or dynamic plugin
-  # configuration. 
-  #
-  # WARNING:
-  # in object store mode, don't make the mistake of treating it like a live
-  # object, e.g. (using the example above)
-  #   @registry[:blah][:foo] = "flump"
-  # will NOT modify the object in the registry - remember that BotRegistry#[]
-  # returns a Marshal.restore'd object, the object you just modified in place
-  # will disappear. You would need to:
-  #   blah = @registry[:blah]
-  #   blah[:foo] = "flump"
-  #   @registry[:blah] = blah
-
-  # If you don't need to store objects, and strictly want a persistant hash of
-  # strings, you can override the store/restore methods to suit your needs, for
-  # example (in your plugin):
-  #   def initialize
-  #     class << @registry
-  #       def store(val)
-  #         val
-  #       end
-  #       def restore(val)
-  #         val
-  #       end
-  #     end
-  #   end
-  # Your plugins section of the registry is private, it has its own namespace
-  # (derived from the plugin's class name, so change it and lose your data).
-  # Calls to registry.each etc, will only iterate over your namespace.
-  class BotRegistryAccessor
-    # plugins don't call this - a BotRegistryAccessor is created for them and
-    # is accessible via @registry.
-    def initialize(bot, prefix)
-      @bot = bot
-      @registry = @bot.registry
-      @orig_prefix = prefix
-      @prefix = prefix + "/"
+  # Abstract database accessor (a hash-like interface).
+  class AbstractAccessor
+
+    # lets the user define a recovery procedure in case the Marshal
+    # deserialization fails, it might be manually recover data.
+    # NOTE: weird legacy stuff, used by markov plugin (WTH?)
+    attr_accessor :recovery
+
+    def initialize(filename)
+      debug 'init registry accessor for: ' + filename
+      @filename = filename
+      @name = File.basename filename
+      @registry = nil
       @default = nil
-      # debug "initializing registry accessor with prefix #{@prefix}"
+      @recovery = nil
     end
 
-    # use this to chop up your namespace into bits, so you can keep and
-    # reference separate object stores under the same registry
     def sub_registry(prefix)
-      return BotRegistryAccessor.new(@bot, @orig_prefix + "+" + prefix)
+      path = File.join(@filename.gsub(/\.[^\/\.]+$/,''), prefix.to_s)
+      return self.class.new(path)
+    end
+
+    # creates the registry / subregistry folders
+    def create_folders
+      debug 'create folders for: ' + @filename
+      dirs = File.dirname(@filename).split("/")
+      dirs.length.times { |i|
+        dir = dirs[0,i+1].join("/")+"/"
+        unless File.exist?(dir)
+          Dir.mkdir(dir)
+        end
+      }
+    end
+
+    # Will return true if the database file exists.
+    def dbexists?
+      File.exists? @filename
     end
 
     # convert value to string form for storing in the registry
@@ -122,163 +155,186 @@ module Irc
     def restore(val)
       begin
         Marshal.restore(val)
-      rescue
-        $stderr.puts "failed to restore marshal data, falling back to default"
-        if @default != nil
+      rescue Exception => e
+        error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
+        debug e
+        if defined? @recovery and @recovery
           begin
-            return Marshal.restore(@default)
-          rescue
-            return nil
+            return @recovery.call(val)
+          rescue Exception => ee
+            error _("marshal recovery failed, trying default")
+            debug ee
           end
-        else
-          return nil
         end
+        return default
       end
     end
 
+    # Returned instead of nil if key wasnt found.
+    def set_default (default)
+      @default = default
+    end
+
+    def default
+      @default && (@default.dup rescue @default)
+    end
+
+    # Opens the database (if not already open) for read/write access.
+    def registry
+      create_folders unless dbexists?
+    end
+
+    # Forces flush/sync the database on disk.
+    def flush
+      return unless @registry
+      @registry.flush
+    end
+
+    # Should optimize/vacuum the database.
+    def optimize
+      return unless @registry
+      @registry.optimize
+    end
+
+    # Closes the database.
+    def close
+      return unless @registry
+      @registry.close
+      @registry = nil
+    end
+
     # lookup a key in the registry
     def [](key)
-      if @registry.has_key?(@prefix + key)
-        return restore(@registry[@prefix + key])
-      elsif @default != nil
-        return restore(@default)
+      if dbexists? and registry.has_key?(key.to_s)
+        return restore(registry[key.to_s])
       else
-        return nil
+        return default
       end
     end
 
     # set a key in the registry
     def []=(key,value)
-      @registry[@prefix + key] = store(value)
+      registry[key.to_s] = store(value)
     end
 
-    # set the default value for registry lookups, if the key sought is not
-    # found, the default will be returned. The default default (har) is nil.
-    def set_default (default)
-      @default = store(default)
-    end
-
-    # just like Hash#each
+    # like Hash#each
     def each(&block)
-      @registry.each {|key,value|
-        if key.gsub!(/^#{Regexp.escape(@prefix)}/, "")
-          block.call(key, restore(value))
-        end
-      }
+      return nil unless dbexists?
+      registry.each do |key|
+        block.call(key, self[key])
+      end
     end
-    
-    # just like Hash#each_key
+
+    alias each_pair each
+
+    # like Hash#each_key
     def each_key(&block)
-      @registry.each {|key, value|
-        if key.gsub!(/^#{Regexp.escape(@prefix)}/, "")
-          block.call(key)
-        end
-      }
+      self.each do |key|
+        block.call(key)
+      end
     end
-    
-    # just like Hash#each_value
+
+    # like Hash#each_value
     def each_value(&block)
-      @registry.each {|key, value|
-        if key =~ /^#{Regexp.escape(@prefix)}/
-          block.call(restore(value))
-        end
-      }
+      self.each do |key, value|
+        block.call(value)
+      end
     end
 
     # just like Hash#has_key?
     def has_key?(key)
-      return @registry.has_key?(@prefix + key)
+      return nil unless dbexists?
+      return registry.has_key?(key.to_s)
     end
+
     alias include? has_key?
     alias member? has_key?
+    alias key? has_key?
 
-    # just like Hash#has_both?
-    def has_both?(key, value)
-      return @registry.has_both?(@prefix + key, store(value))
-    end
-    
     # just like Hash#has_value?
     def has_value?(value)
-      return @registry.has_value?(store(value))
+      return nil unless dbexists?
+      return registry.has_value?(store(value))
     end
 
     # just like Hash#index?
     def index(value)
-      ind = @registry.index(store(value))
-      if ind && ind.gsub!(/^#{Regexp.escape(@prefix)}/, "")
-        return ind
-      else
-        return nil
+      self.each do |k,v|
+        return k if v == value
       end
+      return nil
     end
-    
+
     # delete a key from the registry
     def delete(key)
-      return @registry.delete(@prefix + key)
+      return default unless dbexists?
+      return registry.delete(key.to_s)
     end
 
     # returns a list of your keys
     def keys
-      return @registry.keys.collect {|key|
-        if key.gsub!(/^#{Regexp.escape(@prefix)}/, "")  
-          key
-        else
-          nil
-        end
-      }.compact
+      return [] unless dbexists?
+      return registry.keys
+    end
+
+    # just like Hash#has_both?
+    def has_both?(key, value)
+      return false unless dbexists?
+      registry.has_key?(key.to_s) and registry.has_value?(store(value))
     end
 
     # Return an array of all associations [key, value] in your namespace
     def to_a
+      return [] unless dbexists?
       ret = Array.new
-      @registry.each {|key, value|
-        if key.gsub!(/^#{Regexp.escape(@prefix)}/, "")
-          ret << [key, restore(value)]
-        end
+      self.each {|key, value|
+        ret << [key, value]
       }
       return ret
     end
-    
+
     # Return an hash of all associations {key => value} in your namespace
     def to_hash
+      return {} unless dbexists?
       ret = Hash.new
-      @registry.each {|key, value|
-        if key.gsub!(/^#{Regexp.escape(@prefix)}/, "")
-          ret[key] = restore(value)
-        end
+      self.each {|key, value|
+        ret[key] = value
       }
       return ret
     end
 
     # empties the registry (restricted to your namespace)
     def clear
-      @registry.each_key {|key|
-        if key =~ /^#{Regexp.escape(@prefix)}/
-          @registry.delete(key)
-        end
-      }
+      return unless dbexists?
+      registry.clear
     end
     alias truncate clear
 
     # returns an array of the values in your namespace of the registry
     def values
+      return [] unless dbexists?
       ret = Array.new
       self.each {|k,v|
-        ret << restore(v)
+        ret << v
       }
       return ret
     end
 
     # returns the number of keys in your registry namespace
     def length
-      self.keys.length
+      return 0 unless dbexists?
+      registry.length
     end
     alias size length
 
-    def flush
-      @registry.flush
+    # Returns all classes from the namespace that implement this interface
+    def self.get_impl
+      ObjectSpace.each_object(Class).select { |klass| klass < self }
     end
-    
   end
 
-end
+end # Registry
+
+end # Bot
+end # Irc
+