#!/usr/bin/env ruby -w
require 'logger'
### An exception class for PluginFactory specific errors.
class FactoryError < RuntimeError; end
# This module contains the PluginFactory mixin. Including PluginFactory in your
# class turns it into a factory for its derivatives, capable of searching for
# and loading them by name. This is useful when you have an abstract base class
# which defines an interface and basic functionality for a part of a larger
# system, and a collection of subclasses which implement the interface for
# different underlying functionality.
#
# An example of where this might be useful is in a program which talks to a
# database. To avoid coupling it to a specific database, you use a Driver class
# which encapsulates your program's interaction with the database behind a
# useful interface. Now you can create a concrete implementation of the Driver
# class for each kind of database you wish to talk to. If you make the base
# Driver class a PluginFactory, too, you can add new drivers simply by dropping
# them in a directory and using the Driver's create method to
# instantiate them:
#
# == Creation Argument Variants
#
# The +create+ class method added to your class by PluginFactory searches for your module using
#
# == Synopsis
#
# in driver.rb:
#
# require "PluginFactory"
#
# class Driver
# include PluginFactory
# def self::derivative_dirs
# ["drivers"]
# end
# end
#
# in drivers/mysql.rb:
#
# require 'driver'
#
# class MysqlDriver < Driver
# ...implementation...
# end
#
# in /usr/lib/ruby/1.8/PostgresDriver.rb:
#
# require 'driver'
#
# class PostgresDriver < Driver
# ...implementation...
# end
#
# elsewhere
#
# require 'driver'
#
# config[:driver_type] #=> "mysql"
# driver = Driver.create( config[:driver_type] )
# driver.class #=> MysqlDriver
# pgdriver = Driver.create( "PostGresDriver" )
#
# == Subversion ID
#
# $Id: pluginfactory.rb 57 2009-02-25 17:50:55Z deveiant $
#
# == Authors
#
# * Martin Chase
# * Michael Granger
#
# :include: LICENSE
#
#---
#
# Please see the file LICENSE for licensing details.
#
module PluginFactory
VERSION = '1.0.4'
### Logging
@default_logger = Logger.new( $stderr )
@default_logger.level = $DEBUG ? Logger::DEBUG : Logger::WARN
@logger = @default_logger
class << self
# The logger that will be used when the logging subsystem is reset
attr_accessor :default_logger
# The logger that's currently in effect
attr_accessor :logger
alias_method :log, :logger
alias_method :log=, :logger=
end
### Deprecated: use the Logger object at #log to manipulate logging instead of this
### method.
def self::logger_callback=( callback )
if callback.nil?
self.logger.formatter = nil
else
self.logger.formatter = lambda {|lvl, _, _, msg|
callback.call(lvl.downcase.to_sym, msg)
''
}
end
end
### Reset the global logger object to the default
def self::reset_logger
self.logger = self.default_logger
self.logger.level = Logger::WARN
end
### Returns +true+ if the global logger has not been set to something other than
### the default one.
def self::using_default_logger?
return self.logger == self.default_logger
end
### Inclusion callback -- extends the including class. This is here so you can
### either 'include' or 'extend'.
def self::included( klass )
klass.extend( self )
end
### Add the @derivatives instance variable to including classes.
def self::extend_object( obj )
obj.instance_variable_set( :@derivatives, {} )
super
end
#############################################################
### M I X I N M E T H O D S
#############################################################
### Return the Hash of derivative classes, keyed by various versions of
### the class name.
def derivatives
ancestors.each do |klass|
if klass.instance_variables.include?( :@derivatives ) ||
klass.instance_variables.include?( "@derivatives" )
return klass.instance_variable_get( :@derivatives )
end
end
end
### Returns the type name used when searching for a derivative.
def factory_type
base = nil
self.ancestors.each do |klass|
if klass.instance_variables.include?( :@derivatives ) ||
klass.instance_variables.include?( "@derivatives" )
base = klass
break
end
end
raise FactoryError, "Couldn't find factory base for #{self.name}" if
base.nil?
if base.name =~ /^.*::(.*)/
return $1
else
return base.name
end
end
alias_method :factoryType, :factory_type
### Inheritance callback -- Register subclasses in the derivatives hash
### so that ::create knows about them.
def inherited( subclass )
keys = [ subclass ]
# If it's not an anonymous class, make some keys out of variants of its name
if subclass.name
simple_name = subclass.name.sub( /#::/i, '' )
keys << simple_name << simple_name.downcase
# Handle class names like 'FooBar' for 'Bar' factories.
PluginFactory.log.debug "Inherited %p for %p-type plugins" % [ subclass, self.factory_type ]
if subclass.name.match( /(?:.*::)?(\w+)(?:#{self.factory_type})/i )
keys << Regexp.last_match[1].downcase
else
keys << subclass.name.sub( /.*::/, '' ).downcase
end
else
PluginFactory.log.debug " no name-based variants for anonymous subclass %p" % [ subclass ]
end
keys.compact.uniq.each do |key|
PluginFactory.log.info "Registering %s derivative of %s as %p" %
[ subclass.name, self.name, key ]
self.derivatives[ key ] = subclass
end
super
end
### Returns an Array of registered derivatives
def derivative_classes
self.derivatives.values.uniq
end
alias_method :derivativeClasses, :derivative_classes
### Given the class_name of the class to instantiate, and other
### arguments bound for the constructor of the new object, this method
### loads the derivative class if it is not loaded already (raising a
### LoadError if an appropriately-named file cannot be found), and
### instantiates it with the given args. The class_name
### may be the the fully qualified name of the class, the class object
### itself, or the unique part of the class name. The following examples
### would all try to load and instantiate a class called "FooListener"
### if Listener included Factory
### obj = Listener.create( 'FooListener' )
### obj = Listener.create( FooListener )
### obj = Listener.create( 'Foo' )
def create( class_name, *args, &block )
subclass = get_subclass( class_name )
begin
return subclass.new( *args, &block )
rescue => err
nicetrace = err.backtrace.reject {|frame| /#{__FILE__}/ =~ frame}
msg = "When creating '#{class_name}': " + err.message
Kernel.raise( err, msg, nicetrace )
end
end
### Given a class_name like that of the first argument to
### #create, attempt to load the corresponding class if it is not
### already loaded and return the class object.
def get_subclass( class_name )
return self if ( self.name == class_name || class_name == '' )
if class_name.is_a?( Class )
return class_name if class_name <= self
raise ArgumentError, "%s is not a descendent of %s" % [class_name, self]
end
class_name = class_name.to_s
# If the derivatives hash doesn't already contain the class, try to load it
unless self.derivatives.has_key?( class_name.downcase )
self.load_derivative( class_name )
subclass = self.derivatives[ class_name.downcase ]
unless subclass.is_a?( Class )
raise FactoryError,
"load_derivative(%s) added something other than a class "\
"to the registry for %s: %p" %
[ class_name, self.name, subclass ]
end
end
return self.derivatives[ class_name.downcase ]
end
alias_method :getSubclass, :get_subclass
### Calculates an appropriate filename for the derived class using the
### name of the base class and tries to load it via require. If
### the including class responds to a method named
### derivativeDirs, its return value (either a String, or an
### array of Strings) is added to the list of prefix directories to try
### when attempting to require a modules. Eg., if
### class.derivativeDirs returns ['foo','bar'] the
### require line is tried with both 'foo/' and 'bar/'
### prepended to it.
def load_derivative( class_name )
PluginFactory.log.debug "Loading derivative #{class_name}"
# Get the unique part of the derived class name and try to
# load it from one of the derivative subdirs, if there are
# any.
mod_name = self.get_module_name( class_name )
result = self.require_derivative( mod_name )
# Check to see if the specified listener is now loaded. If it
# is not, raise an error to that effect.
unless self.derivatives[ class_name.downcase ]
errmsg = "Require of '%s' succeeded, but didn't load a %s named '%s' for some reason." % [
result,
self.factory_type,
class_name.downcase,
]
PluginFactory.log.error( errmsg )
raise FactoryError, errmsg, caller(3)
end
end
alias_method :loadDerivative, :load_derivative
### Build and return the unique part of the given class_name
### either by stripping leading namespaces if the name already has the
### name of the factory type in it (eg., 'My::FooService' for Service,
### or by appending the factory type if it doesn't.
def get_module_name( class_name )
if class_name =~ /\w+#{self.factory_type}/
mod_name = class_name.sub( /(?:.*::)?(\w+)(?:#{self.factory_type})/, "\\1" )
else
mod_name = class_name
end
return mod_name
end
alias_method :getModuleName, :get_module_name
### If the factory responds to the #derivative_dirs method, call
### it and use the returned array as a list of directories to
### search for the module with the specified mod_name.
def require_derivative( mod_name )
# See if we have a list of special subdirs that derivatives
# live in
if ( self.respond_to?(:derivative_dirs) )
subdirs = self.derivative_dirs
elsif ( self.respond_to?(:derivativeDirs) )
subdirs = self.derivativeDirs
# If not, just try requiring it from $LOAD_PATH
else
subdirs = ['']
end
subdirs = [ subdirs ] unless subdirs.is_a?( Array )
PluginFactory.log.debug "Subdirs are: %p" % [subdirs]
fatals = []
tries = []
# Iterate over the subdirs until we successfully require a
# module.
subdirs.collect {|dir| dir.strip}.each do |subdir|
self.make_require_path( mod_name, subdir ).each do |path|
PluginFactory.log.debug "Trying #{path}..."
tries << path
# Try to require the module, saving errors and jumping
# out of the catch block on success.
begin
require( path.untaint )
rescue LoadError => err
PluginFactory.log.debug "No module at '%s', trying the next alternative: '%s'" %
[ path, err.message ]
rescue Exception => err
fatals << err
PluginFactory.log.error "Found '#{path}', but encountered an error: %s\n\t%s" %
[ err.message, err.backtrace.join("\n\t") ]
else
PluginFactory.log.info "Loaded '#{path}' without error."
return path
end
end
end
PluginFactory.log.debug "fatals = %p" % [ fatals ]
# Re-raise is there was a file found, but it didn't load for
# some reason.
if fatals.empty?
errmsg = "Couldn't find a %s named '%s': tried %p" % [
self.factory_type,
mod_name,
tries
]
PluginFactory.log.error( errmsg )
raise FactoryError, errmsg
else
PluginFactory.log.debug "Re-raising first fatal error"
Kernel.raise( fatals.first )
end
end
alias_method :requireDerivative, :require_derivative
### Make a list of permutations of the given +modname+ for the given
### +subdir+. Called on a +DataDriver+ class with the arguments 'Socket' and
### 'drivers', returns:
### ["drivers/socketdatadriver", "drivers/socketDataDriver",
### "drivers/SocketDataDriver", "drivers/socket", "drivers/Socket"]
def make_require_path( modname, subdir )
path = []
myname = self.factory_type
# Make permutations of the two parts
path << modname
path << modname.downcase
path << modname + myname
path << modname.downcase + myname
path << modname.downcase + myname.downcase
path << modname + '_' + myname
path << modname.downcase + '_' + myname
path << modname.downcase + '_' + myname.downcase
# If a non-empty subdir was given, prepend it to all the items in the
# path
unless subdir.nil? or subdir.empty?
path.collect! {|m| File.join(subdir, m)}
end
PluginFactory.log.debug "Path is: #{path.uniq.reverse.inspect}..."
return path.uniq.reverse
end
alias_method :makeRequirePath, :make_require_path
end # module Factory