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