package PXP::PluginRegistry; =pod =head1 NAME PXP::PluginRegistry - Registry of plugins for PXP =cut use strict; use warnings; use constant PLUGIN_INFO_FILE => 'plugin.xml'; use PXP::Plugin; use File::Spec; use Log::Log4perl qw(get_logger); our $logger = get_logger(__PACKAGE__); our $VERSION = '0.1'; our $plugins; =pod " =head1 SYNOPSIS use PXP::PluginRegistry; PXP::init(dir => './plugins', default => 'all', load => [], noload => []); ... my $plugin = PXP::PluginRegistry::getExtension(ref($self))->plugin(); my $extpt = PXP::PluginRegistry::getExtensionPoint('PXP::StaticResourcesHandler'); =head1 ABSTRACT The PluginRegistry groups together a set of plugins, extension points and extensions for the running system. =head1 DESCRIPTION The PluginRegistry manages plugins, extension points and extensions. The PluginRegistry takes care of loading the plugins from file, loading classes and Perl modules and, optionnaly, running 'startup' sub-routines as each plugin is loaded in the system. =over 4 =item I Initialize the plugin registry. Note: only one instance of PluginRegistry is supported (singleton pattern). =back =cut " our $loader = {}; sub init { $loader = shift; loadPluginsFromDirectory($loader->{dir}); } =pod " =item I Return a hash containing all registered plugins. =back =cut " sub getPluginList { return %{$plugins}; } =pod " =over 4 =item I Return a plugin referenced by its id, or undef if no such plugin exists. I : a plugin id can be different from its actual object class, as declared in plugin.xml =back =cut " sub getPlugin { my $id = shift; return $plugins->{$id}; } =pod " =over 4 =item I Load all plugins present in the directory. This is usually called once by by PXP::init. Plugin developpers can call this function to load plugins installed outside of the system default directory ('plugins' at the root of the PXP home directory). See below for details about the loading process. =back =cut " sub loadPluginsFromDirectory { my $directory = shift; # List the plugins inside the directory $directory = File::Spec->canonpath($directory); $logger->debug("looking for plugins in $directory..."); opendir DIR, $directory; my @dirs = readdir DIR; closedir DIR; # Check each sub-directory. my ($pluginFile, @plugins); my $default = $loader->{default}; my $load = $loader->{load}; if ($load && ref $load eq "ARRAY") { $load = join ' ',@$load; } my $noload = $loader->{noload}; if ($noload && ref $noload eq "ARRAY") { $noload = join ' ',@$noload; } my $debugList = ''; foreach my $plugdir (@dirs) { if ($default eq 'none') { next unless $load and $load =~ /$plugdir/i; } else { # load all by default next if $noload and $noload =~ /$plugdir/i; } my $dir = File::Spec->catdir(($directory, $plugdir)); next unless (-d $dir); # Load the plugin xml description file $pluginFile = File::Spec->catfile($dir, PLUGIN_INFO_FILE); # Create plugin files list if (-e $pluginFile) { my $plugin = PXP::Plugin->new('filename' => PLUGIN_INFO_FILE, 'directory' => $dir, 'moduleName' => $plugdir); foreach my $p (@plugins) { if ($p->id eq $plugin->id) { $logger->fatal("Plugin " . $p->id . " loaded twice in " . $p->directory . " and " . $plugin->directory); die "problem detected in the plugin directory"; } } push @plugins, $plugin; $debugList .= "$dir, "; } } $logger->info("preparing to load the following plugins : [$debugList]"); # Load all plugins, considering dependencies loadPlugins(@plugins) if (@plugins); } sub loadPlugins { my @plugins = @_; # Check dependencies to load the plugins in right order. my $tree = {}; my $instances = {}; # Get plugin dependencies & Fill the tree foreach my $plugin (@plugins) { $tree->{$plugin->id()} = $plugin->getDependencies(); $instances->{$plugin->id()} = $plugin; } # Load the dependencies in the right order foreach (@plugins) { _loadAndStartPlugin($_ , $tree, $instances); } return 1; } sub _loadAndStartPlugin { my $plugin = shift; my $tree = shift; my $instances = shift; my @path = @_; # Maybe the plugin is already loaded ? return 1 if (_getRegisteredPlugin($plugin->id())); $logger->debug("loading plugin " . $plugin->id); # Check to find circular dependencies if (grep {$plugin->id() eq $_} @path) { die "Circular dependencies detected with ".$plugin->id().". Correct this first !"; } # Does the plugin have dependencies ? if ($tree->{$plugin->id()} && ref($tree->{$plugin->id()}) eq 'ARRAY') { # Load them ! foreach my $dep (@{$tree->{$plugin->id()}}) { # Check if dependency is right my $insdep = $instances->{$dep} || _getRegisteredPlugin($dep); unless ($insdep) { $logger->warn("Dependency $dep does not exist for plugin ".$plugin->id().'. Loading aborted.'); die "check dependency problem"; } unless (_loadAndStartPlugin($insdep, $tree, $instances, @path, $plugin->id())) { $logger->warn("Error loading dependency $dep for ".$plugin->id().'. Loading aborted.'); die "check dependency problem"; } } } # Load libraries _loadLibraries($plugin->directory(), $plugin->libraries()); # Instantiate the extensions $plugin->instantiateExtensions(); # register everybody registerPlugin($plugin); # and call startup startupPlugin($plugin); } sub _loadLibraries { my $dir = shift; my $libs = shift; foreach my $lib (@{$libs}) { $logger->debug("loading library $lib (in $dir)"); my $filename = File::Spec->catfile($dir, $lib); my $loader = "use PAR \'$filename\';"; eval $loader; } return 1; } sub registerPlugin { my $plugin = shift; my $error = 1; if ($plugin && ref($plugin) && $plugin->isa('PXP::Plugin')) { $error = 0; # Register extensions with their extension point foreach my $ext ($plugin->getAllExtensions()) { # in case the plugin does not declare extensions last if not $ext; # Search the extension point in the global registry or in the own registry of the plugin we're currently registering my $extpt = _getExtensionPoint($ext->point()) || $plugin->getExtensionPoint($ext->point()); if ($extpt) { $logger->debug("registering " . $ext->id . " (" . ref($ext) . ") with " . $extpt->id . " (" . ref($extpt) . ")"); $extpt->register($ext); } else { $logger->warn("No matching extension point " . $ext->point() . ' while registering '.$plugin->id()); # $error = 1; last; } } } else { $logger->error("Could not register the specified plugin. Wrong plugin type."); return 0; } # FIXME : can there really be _detected_ errors now !? unless ($error) { # Store the plugin in registry $plugins->{$plugin->id()} = $plugin; $logger->debug('Plugin '.$plugin->id().'-'.$plugin->version()." successfully registered."); return 1; } } # remember which plugin we started to avoid double starts our $startupRegistry = {}; sub startupPluginWithDependencies { my $plugin = shift; my $tree = shift; my $instances = shift; my @path = @_; # if class is not declared, do not attempt to startup the plugin return 1 if (not $plugin->class); # Maybe the plugin has already been started ? return 1 if ($startupRegistry->{$plugin->id()}); # Does the plugin have dependencies ? if ($tree->{$plugin->id()} && ref($tree->{$plugin->id()}) eq 'ARRAY') { # Load them ! foreach my $dep (@{$tree->{$plugin->id()}}) { # Check if dependency is right my $insdep = $instances->{$dep} || _getRegisteredPlugin($dep); unless ($insdep) { $logger->warn("Dependency $dep does not exist for plugin ".$plugin->id().'. Startup aborted.'); return undef; } unless (startupPluginWithDependencies($insdep, $tree, $instances, @path, $plugin->id())) { $logger->warn("Error starting up dependency $dep for ".$plugin->id().'. Startup aborted.'); return undef; } } } $startupRegistry->{$plugin->id} = 1 if startupPlugin($plugin); } sub startupPlugin { my $plugin = shift; my $module = $plugin->class() || $plugin->id(); my $module_file = $module .".pm"; $module_file =~ s|::|/|g; $module_file = File::Spec->catfile($plugin->directory(), $module_file); # try to load the module if(-e $module_file) { $logger->debug("loading $module"); eval "require $module"; # $logger->warn("$@\n") if $@; die $@ if $@; } # startup { no strict; if (defined &{"${module}::startup"}) { $logger->debug("calling 'startup' for $module"); &{"${module}::startup"}($plugin); } } return 1; } sub getResourceHandle { my $plugin = shift; my $resourceId = shift; return $plugin->getResourceHandle($resourceId); } sub loadResource { my $plugin = shift; my $resourceId = shift; return $plugin->loadResource($resourceId); } =pod " =over 4 =item I Search the registry for an C and return it, based on its id. =back =cut " # public interface : this one returns the _object_ associated with an # extension point, ie the _object_ that actually handles the # extensions, _not_ the internal descriptor. sub getExtensionPoint { my $id = shift; foreach (values %{$plugins}) { return $_->getExtensionPoint($id)->object() if ($_->getExtensionPoint($id)); } return undef; } # private function : returns the internal ExtensionPoint descriptor, # _not_ the object that actually handles the extensions. sub _getExtensionPoint { my $id = shift; #FIXME: Add checks; foreach (values %{$plugins}) { return $_->getExtensionPoint($id) if ($_->getExtensionPoint($id)); } return undef; } =pod " =over 4 =item I Return the internal XML node describing the extension in the plugin.xml file. This is mostly used by plugin developpers to get access to the XML config. =back =cut " sub getExtensionNode { my $id = shift; #FIXME: Add checks; foreach (values %{$plugins}) { return $_->getExtension($id)->node() if ($_->getExtension($id)); } return undef; } =pod " =over 4 =item I Search the registry for an C and return it, based on its id. This returns the internal extension descriptor object, ie an PXP::Extension object, not the real extension See the code for details of the private API. =back =cut " # find the extension object in the registry sub getExtension { my $id = shift; #FIXME: Add checks; foreach (values %{$plugins}) { return $_->getExtension($id) if ($_->getExtension($id)); } return undef; } sub _getRegisteredPlugin { my $id = shift || ''; return $plugins->{$id}; } =pod =head2 Loading process First, the PluginRegistry calls loadPluginFromDirectory to load all the plugins installed in the system directory (usually called 'plugins' inside the PXP hierarchy). The loader calculates the dependencies so that plugin can resolve symbols. For each plugin, extension points are instantiated and registered into the global registry. Then, extension _definitions_ are handed to the extension points to be registered. Extension points choose wether to instantiate a particular objet for each of their extensions. Last, the PluginRegistry calls the 'startup' routine of each plugin that declared a specific class (class="" attribute inside the plugin.xml header). An error during plugin load stops the process for a whole plugin dependency branch, but plugin startup is still called for plugins that have already been loaded. =head1 SEE ALSO See C, C for details about the concepts 1;