]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/registry.rb
799e69ee4f0a72d9876b1257842dcd41b3915e84
[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   # Abstract database accessor (a hash-like interface).
87   class AbstractAccessor
88
89     # lets the user define a recovery procedure in case the Marshal
90     # deserialization fails, it might be manually recover data.
91     # NOTE: weird legacy stuff, used by markov plugin (WTH?)
92     attr_accessor :recovery
93
94     def initialize(filename)
95       debug 'init registry accessor for: ' + filename
96       @filename = filename
97       @name = File.basename filename
98       @registry = nil
99       @default = nil
100       @recovery = nil
101     end
102
103     def sub_registry(prefix)
104       path = File.join(@filename.gsub(/\.[^\/\.]+$/,''), prefix.to_s)
105       return self.class.new(path)
106     end
107
108     # creates the registry / subregistry folders
109     def create_folders
110       debug 'create folders for: ' + @filename
111       dirs = File.dirname(@filename).split("/")
112       dirs.length.times { |i|
113         dir = dirs[0,i+1].join("/")+"/"
114         unless File.exist?(dir)
115           Dir.mkdir(dir)
116         end
117       }
118     end
119
120     # Will return true if the database file exists.
121     def dbexists?
122       File.exists? @filename
123     end
124
125     # convert value to string form for storing in the registry
126     # defaults to Marshal.dump(val) but you can override this in your module's
127     # registry object to use any method you like.
128     # For example, if you always just handle strings use:
129     #   def store(val)
130     #     val
131     #   end
132     def store(val)
133       Marshal.dump(val)
134     end
135
136     # restores object from string form, restore(store(val)) must return val.
137     # If you override store, you should override restore to reverse the
138     # action.
139     # For example, if you always just handle strings use:
140     #   def restore(val)
141     #     val
142     #   end
143     def restore(val)
144       begin
145         Marshal.restore(val)
146       rescue Exception => e
147         error _("failed to restore marshal data for #{val.inspect}, attempting recovery or fallback to default")
148         debug e
149         if defined? @recovery and @recovery
150           begin
151             return @recovery.call(val)
152           rescue Exception => ee
153             error _("marshal recovery failed, trying default")
154             debug ee
155           end
156         end
157         return default
158       end
159     end
160
161     # Returned instead of nil if key wasnt found.
162     def set_default (default)
163       @default = default
164     end
165
166     def default
167       @default && (@default.dup rescue @default)
168     end
169
170     # Opens the database (if not already open) for read/write access.
171     def registry
172       create_folders unless dbexists?
173     end
174
175     # Forces flush/sync the database on disk.
176     def flush
177       return unless @registry
178       @registry.flush
179     end
180
181     # Should optimize/vacuum the database.
182     def optimize
183       return unless @registry
184       @registry.optimize
185     end
186
187     # Closes the database.
188     def close
189       return unless @registry
190       @registry.close
191       @registry = nil
192     end
193
194     # lookup a key in the registry
195     def [](key)
196       if dbexists? and registry.has_key?(key.to_s)
197         return restore(registry[key.to_s])
198       else
199         return default
200       end
201     end
202
203     # set a key in the registry
204     def []=(key,value)
205       registry[key.to_s] = store(value)
206     end
207
208     # like Hash#each
209     def each(&block)
210       return nil unless dbexists?
211       registry.each do |key|
212         block.call(key, self[key])
213       end
214     end
215
216     alias each_pair each
217
218     # like Hash#each_key
219     def each_key(&block)
220       self.each do |key|
221         block.call(key)
222       end
223     end
224
225     # like Hash#each_value
226     def each_value(&block)
227       self.each do |key, value|
228         block.call(value)
229       end
230     end
231
232     # just like Hash#has_key?
233     def has_key?(key)
234       return nil unless dbexists?
235       return registry.has_key?(key.to_s)
236     end
237
238     alias include? has_key?
239     alias member? has_key?
240     alias key? has_key?
241
242     # just like Hash#has_value?
243     def has_value?(value)
244       return nil unless dbexists?
245       return registry.has_value?(store(value))
246     end
247
248     # just like Hash#index?
249     def index(value)
250       self.each do |k,v|
251         return k if v == value
252       end
253       return nil
254     end
255
256     # delete a key from the registry
257     def delete(key)
258       return default unless dbexists?
259       return registry.delete(key.to_s)
260     end
261
262     # returns a list of your keys
263     def keys
264       return [] unless dbexists?
265       return registry.keys
266     end
267
268     # just like Hash#has_both?
269     def has_both?(key, value)
270       return false unless dbexists?
271       registry.has_key?(key.to_s) and registry.has_value?(store(value))
272     end
273
274     # Return an array of all associations [key, value] in your namespace
275     def to_a
276       return [] unless dbexists?
277       ret = Array.new
278       self.each {|key, value|
279         ret << [key, value]
280       }
281       return ret
282     end
283
284     # Return an hash of all associations {key => value} in your namespace
285     def to_hash
286       return {} unless dbexists?
287       ret = Hash.new
288       self.each {|key, value|
289         ret[key] = value
290       }
291       return ret
292     end
293
294     # empties the registry (restricted to your namespace)
295     def clear
296       return unless dbexists?
297       registry.clear
298     end
299     alias truncate clear
300
301     # returns an array of the values in your namespace of the registry
302     def values
303       return [] unless dbexists?
304       ret = Array.new
305       self.each {|k,v|
306         ret << v
307       }
308       return ret
309     end
310
311     # returns the number of keys in your registry namespace
312     def length
313       return 0 unless dbexists?
314       registry.length
315     end
316     alias size length
317
318     # Returns all classes from the namespace that implement this interface
319     def self.get_impl
320       ObjectSpace.each_object(Class).select { |klass| klass < self }
321     end
322   end
323
324 end # Registry
325
326 end # Bot
327 end # Irc
328