]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/registry.rb
[registry] migrate tc directory name to registry_tc
[user/henk/code/ruby/rbot.git] / lib / rbot / registry.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: Registry: Persistent storage interface and factory
5
6 # This class provides persistent storage for plugins via a hash interface.
7 # The default mode is an object store, so you can store ruby objects and
8 # reference them with hash keys. This is because the default store/restore
9 # methods of the plugins' RegistryAccessor are calls to Marshal.dump and
10 # Marshal.restore,
11 # for example:
12 #   blah = Hash.new
13 #   blah[:foo] = "fum"
14 #   @registry[:blah] = blah
15 # then, even after the bot is shut down and disconnected, on the next run you
16 # can access the blah object as it was, with:
17 #   blah = @registry[:blah]
18 # The registry can of course be used to store simple strings, fixnums, etc as
19 # well, and should be useful to store or cache plugin data or dynamic plugin
20 # configuration.
21 #
22 # WARNING:
23 # in object store mode, don't make the mistake of treating it like a live
24 # object, e.g. (using the example above)
25 #   @registry[:blah][:foo] = "flump"
26 # will NOT modify the object in the registry - remember that Registry#[]
27 # returns a Marshal.restore'd object, the object you just modified in place
28 # will disappear. You would need to:
29 #   blah = @registry[:blah]
30 #   blah[:foo] = "flump"
31 #   @registry[:blah] = blah
32 #
33 # If you don't need to store objects, and strictly want a persistant hash of
34 # strings, you can override the store/restore methods to suit your needs, for
35 # example (in your plugin):
36 #   def initialize
37 #     class << @registry
38 #       def store(val)
39 #         val
40 #       end
41 #       def restore(val)
42 #         val
43 #       end
44 #     end
45 #   end
46 # Your plugins section of the registry is private, it has its own namespace
47 # (derived from the plugin's class name, so change it and lose your data).
48 # Calls to registry.each etc, will only iterate over your namespace.
49
50 module Irc
51 class Bot
52
53 class Registry
54
55   # Dynamically loads the specified registry type library.
56   def initialize(format=nil)
57     @libpath = File.join(File.dirname(__FILE__), 'registry')
58     @format = format
59     load File.join(@libpath, @format+'.rb') if format
60   end
61
62   # Returns a list of supported registry database formats.
63   def discover
64     Dir.glob(File.join(@libpath, '*.rb')).map do |name|
65       File.basename(name, File.extname(name))
66     end
67   end
68
69   # Creates a new Accessor object for the specified database filename.
70   def create(path, filename)
71     # The get_impl method will return a list of all the classes that
72     # implement the accessor interface, since we only ever load one
73     # (the configured one) accessor implementation, we can just assume
74     # it to be the correct accessor to use.
75     cls = AbstractAccessor.get_impl.first
76     db = cls.new(File.join(path, 'registry_' + @format, filename.downcase))
77     db.optimize
78     db
79   end
80
81   # Helper method that will return a list of supported registry formats.
82   def self.formats
83     @@formats ||= Registry.new.discover
84   end
85
86   # Will detect tokyocabinet registry location: ~/.rbot/registry/*.tdb
87   #  and move it to its new location ~/.rbot/registry_tc/*.tdb
88   def migrate_registry_folder(path)
89     old_name = File.join(path, 'registry')
90     new_name = File.join(path, 'registry_tc')
91     if @format == 'tc' and File.exists?(old_name) and
92         not File.exists?(new_name) and
93         not Dir.glob(File.join(old_name, '*.tdb')).empty?
94       File.rename(old_name, new_name)
95     end
96   end
97
98   # Abstract database accessor (a hash-like interface).
99   class AbstractAccessor
100
101     # lets the user define a recovery procedure in case the Marshal
102     # deserialization fails, it might be manually recover data.
103     # NOTE: weird legacy stuff, used by markov plugin (WTH?)
104     attr_accessor :recovery
105
106     def initialize(filename)
107       debug 'init registry accessor for: ' + filename
108       @filename = filename
109       @name = File.basename filename
110       @registry = nil
111       @default = nil
112       @recovery = nil
113     end
114
115     def sub_registry(prefix)
116       path = File.join(@filename.gsub(/\.[^\/\.]+$/,''), prefix.to_s)
117       return self.class.new(path)
118     end
119
120     # creates the registry / subregistry folders
121     def create_folders
122       debug 'create folders for: ' + @filename
123       dirs = File.dirname(@filename).split("/")
124       dirs.length.times { |i|
125         dir = dirs[0,i+1].join("/")+"/"
126         unless File.exist?(dir)
127           Dir.mkdir(dir)
128         end
129       }
130     end
131
132     # Will return true if the database file exists.
133     def dbexists?
134       File.exists? @filename
135     end
136
137     # convert value to string form for storing in the registry
138     # defaults to Marshal.dump(val) but you can override this in your module's
139     # registry object to use any method you like.
140     # For example, if you always just handle strings use:
141     #   def store(val)
142     #     val
143     #   end
144     def store(val)
145       Marshal.dump(val)
146     end
147
148     # restores object from string form, restore(store(val)) must return val.
149     # If you override store, you should override restore to reverse the
150     # action.
151     # For example, if you always just handle strings use:
152     #   def restore(val)
153     #     val
154     #   end
155     def restore(val)
156       begin
157         Marshal.restore(val)
158       rescue Exception => e
159         error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
160         debug e
161         if defined? @recovery and @recovery
162           begin
163             return @recovery.call(val)
164           rescue Exception => ee
165             error _("marshal recovery failed, trying default")
166             debug ee
167           end
168         end
169         return default
170       end
171     end
172
173     # Returned instead of nil if key wasnt found.
174     def set_default (default)
175       @default = default
176     end
177
178     def default
179       @default && (@default.dup rescue @default)
180     end
181
182     # Opens the database (if not already open) for read/write access.
183     def registry
184       create_folders unless dbexists?
185     end
186
187     # Forces flush/sync the database on disk.
188     def flush
189       return unless @registry
190       @registry.flush
191     end
192
193     # Should optimize/vacuum the database.
194     def optimize
195       return unless @registry
196       @registry.optimize
197     end
198
199     # Closes the database.
200     def close
201       return unless @registry
202       @registry.close
203       @registry = nil
204     end
205
206     # lookup a key in the registry
207     def [](key)
208       if dbexists? and registry.has_key?(key.to_s)
209         return restore(registry[key.to_s])
210       else
211         return default
212       end
213     end
214
215     # set a key in the registry
216     def []=(key,value)
217       registry[key.to_s] = store(value)
218     end
219
220     # like Hash#each
221     def each(&block)
222       return nil unless dbexists?
223       registry.each do |key|
224         block.call(key, self[key])
225       end
226     end
227
228     alias each_pair each
229
230     # like Hash#each_key
231     def each_key(&block)
232       self.each do |key|
233         block.call(key)
234       end
235     end
236
237     # like Hash#each_value
238     def each_value(&block)
239       self.each do |key, value|
240         block.call(value)
241       end
242     end
243
244     # just like Hash#has_key?
245     def has_key?(key)
246       return nil unless dbexists?
247       return registry.has_key?(key.to_s)
248     end
249
250     alias include? has_key?
251     alias member? has_key?
252     alias key? has_key?
253
254     # just like Hash#has_value?
255     def has_value?(value)
256       return nil unless dbexists?
257       return registry.has_value?(store(value))
258     end
259
260     # just like Hash#index?
261     def index(value)
262       self.each do |k,v|
263         return k if v == value
264       end
265       return nil
266     end
267
268     # delete a key from the registry
269     def delete(key)
270       return default unless dbexists?
271       return registry.delete(key.to_s)
272     end
273
274     # returns a list of your keys
275     def keys
276       return [] unless dbexists?
277       return registry.keys
278     end
279
280     # just like Hash#has_both?
281     def has_both?(key, value)
282       return false unless dbexists?
283       registry.has_key?(key.to_s) and registry.has_value?(store(value))
284     end
285
286     # Return an array of all associations [key, value] in your namespace
287     def to_a
288       return [] unless dbexists?
289       ret = Array.new
290       self.each {|key, value|
291         ret << [key, value]
292       }
293       return ret
294     end
295
296     # Return an hash of all associations {key => value} in your namespace
297     def to_hash
298       return {} unless dbexists?
299       ret = Hash.new
300       self.each {|key, value|
301         ret[key] = value
302       }
303       return ret
304     end
305
306     # empties the registry (restricted to your namespace)
307     def clear
308       return unless dbexists?
309       registry.clear
310     end
311     alias truncate clear
312
313     # returns an array of the values in your namespace of the registry
314     def values
315       return [] unless dbexists?
316       ret = Array.new
317       self.each {|k,v|
318         ret << v
319       }
320       return ret
321     end
322
323     # returns the number of keys in your registry namespace
324     def length
325       return 0 unless dbexists?
326       registry.length
327     end
328     alias size length
329
330     # Returns all classes from the namespace that implement this interface
331     def self.get_impl
332       ObjectSpace.each_object(Class).select { |klass| klass < self }
333     end
334   end
335
336 end # Registry
337
338 end # Bot
339 end # Irc
340