Index: includes/bootstrap.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/bootstrap.inc,v
retrieving revision 1.288
diff -u -p -r1.288 bootstrap.inc
--- includes/bootstrap.inc	1 Jul 2009 12:47:30 -0000	1.288
+++ includes/bootstrap.inc	8 Jul 2009 04:43:28 -0000
@@ -1857,3 +1857,437 @@ function &drupal_static($name, $default_
 function drupal_static_reset($name = NULL) {
   drupal_static($name, NULL, TRUE);
 }
+
+
+/**
+ * @ingroup plugins
+ * @{
+ */
+
+/**
+ * Request a plugin object.
+ *
+ * @param $slot_id
+ *   The machine-readable name of the slot the plugin belongs to.
+ * @param $target
+ *   The target inside the subsytem the plugin belongs to.
+ * @param $reset
+ *   If this is set to TRUE, the internal caches are flushed. Internal use
+ *   only.
+ *
+ * @return
+ *   The associated plugin object.
+ *
+ * @see hook_slot_info()
+ * @see hook_plugin_info()
+ */
+function plugin($slot_id, $target = 'default', $reset = FALSE) {
+  // $plugin_mappings is an array, keyed by $slot_id and $target and the
+  // values are arrays containing class names and a boolean indicating
+  // reusability. plugin_objects keeps the objects themselves.
+  // $default_only[$slot_id] is TRUE if the given slot only uses defaults.
+  $plugin_mappings =& drupal_static(__FUNCTION__ . '_plugin_mappings', array());
+  $plugin_objects =& drupal_static(__FUNCTION__ . '_plugin_objects', array());
+  $default_only =& drupal_static(__FUNCTION__ . '_default_only', array());
+
+  // If we already have the appropriate plugin cached, just use that.
+  if (!empty($plugin_objects[$slot_id][$target])) {
+    return $plugin_objects[$slot_id][$target];
+  }
+
+  // While the objects need to be stored per target, the classnames can be per
+  // slot, if the only attached plugin is attached to default. That includes
+  // any slot that does not make use of targets. We cache the plugin
+  // information for these slots in the variable system to reduce database
+  // lookups. Because the variable system doesn't auto-initialize in
+  // variable_get(), any plugin lookup that runs before the variable system
+  // has been initialized will simply fail this check and use the database
+  // anyway.
+  $mapping_target = isset($default_only[$slot_id]) ? 'default' : $target;
+  // Look up the class for the requested slot/target.
+  if (empty($plugin_mappings[$slot_id][$mapping_target])) {
+    // The variable table caching of simple attachments doesn't work so well
+    // once we introduce configuration objects.  The serialized configuration
+    // object is likely to be too large to use the variable table sanely. I'm
+    // not sure yet how to handle that, so for now the varable table override
+    // is disabled.  That's what the FALSE is about.
+    // @todo: Figure out what to do about this.
+    if (FALSE && $record = variable_get('plugin_default_' . $slot_id, array())) {
+      $default_only[$slot_id] = TRUE;
+      $mapping_target = 'default';
+    }
+    else {
+      // Try to get the associated plugin. If the first query finds an associated
+      // plugin, we use that. If not, the second query will always find the
+      // default plugin. By UNIONing them together we get the fallback default
+      // behavior without having to issue a second request to the database.
+      $record = db_query_range("SELECT class, configuration, configuration_class, reuse FROM {plugin_attachments} WHERE slot = :slot_1 AND target = :target
+        UNION SELECT class, configuration, configuration_class, reuse FROM {plugin_attachments} WHERE slot = :slot_2 AND target = 'default'", array(
+        ':slot_1' => $slot_id,
+        ':slot_2' => $slot_id,
+        ':target' => $target,
+      ), 0, 1)->fetchAssoc();
+
+      // It's possible that this function will be called before the plugin system
+      // has first been initialized, such as during the installer. That's
+      // especially the case for early-running systems such as cache or path,
+      // as they operate during the bootstrap phase. If we have no record at all,
+      // we first rebuild the registry and then try again. That should at least
+      // always give us the slot-defined default and allow the system to proceed.
+      if (!$record) {
+        registry_rebuild();
+        plugins_rebuild();
+        $record = db_query_range("SELECT class, configuration, configuration_class, reuse FROM {plugin_attachments} WHERE slot = :slot_1 AND target = :target
+          UNION SELECT class, configuration, configuration_class, reuse FROM {plugin_attachments} WHERE slot = :slot_2 AND target = 'default'", array(
+          ':slot_1' => $slot_id,
+          ':slot_2' => $slot_id,
+          ':target' => $target,
+        ), 0, 1)->fetchAssoc();
+      }
+    }
+
+    // Deserialize the configuration object before we cache the record.
+    $record['configuration'] = is_null($record['configuration']) ? new $record['configuration_class'] : unserialize($record['configuration']);
+
+    // Statically cache the lookup information so that we don't need to check
+    // for it again.
+    $plugin_mappings[$slot_id][$mapping_target] = $record;
+  }
+
+  $plugin = new $plugin_mappings[$slot_id][$mapping_target]['class']($plugin_mappings[$slot_id][$mapping_target]['configuration'], $target);
+
+  // Cache the plugin object for later use, if flagged to do so.
+  if ($plugin_mappings[$slot_id][$mapping_target]['reuse']) {
+    $plugin_objects[$slot_id][$target] = $plugin;
+  }
+  return $plugin;
+}
+
+/**
+ * Rebuild the plugin and slot registries.
+ */
+function plugins_rebuild() {
+  require_once DRUPAL_ROOT . '/includes/plugins.inc';
+  plugin_slot_rebuild();
+  plugin_plugin_rebuild();
+  plugin_ensure_defaults();
+}
+
+
+/**
+ * Associate a plugin with a specific target for the given slot.
+ *
+ * @param $slot_id
+ *   The machine-readable name of the slot.
+ * @param $plugin_id
+ *   The machine-readable name of the plugin.
+ * @param $target
+ *   The plugin will be associated with this target.
+ * @param $config
+ *   The configuration object for this plugin on this target.  If not specified,
+ *   an empty configuration object will be used with default options.
+ *
+ * @see plugin_get_available_plugins()
+ * @see plugin_detach()
+ */
+function plugin_attach($slot_id, $plugin_id, $target = 'default', PluginConfigurationInterface $config = NULL) {
+  // Lazy-load the utility function.
+  if (drupal_function_exists('plugin_load')) {
+    $plugin = plugin_load($slot_id, $plugin_id);
+    $slot = slot_load($slot_id);
+
+    if ($plugin) {
+      // We store the class name in the database in addition to the plugin ID
+      // so that we can immediately instantiate the class upon lookup.
+      db_merge('plugin_attachments')
+      ->key(array(
+        'slot' => $slot_id,
+        'target' => $target,
+      ))
+      ->fields(array(
+        'plugin' => $plugin->plugin,
+        'class' => $plugin->class,
+        'configuration_class' => $plugin->configuration_class,
+        'configuration' => is_null($config) ? NULL : serialize($config),
+        'reuse' => (int)$slot->reuse,
+      ))
+      ->execute();
+    }
+
+    // If we've associated something other than the default target, disable the
+    // extra variable system caching of plugin attachments.
+    if ($target != 'default') {
+      variable_del('plugin_default_' . $slot_id);
+    }
+
+    // Flush the target cache.
+    drupal_static_reset('plugin_plugin_mappings');
+    drupal_static_reset('plugin_plugin_objects');
+    drupal_static_reset('plugin_default_only');
+  }
+}
+
+/**
+ * Revert a given slot and target to the default plugin.
+ *
+ * @param $slot_id
+ *   The machine-readable name of the slot to reset to the default plugin.
+ * @param $target
+ *   The target to reset to the default plugin.
+ *
+ * @see plugin_attach()
+ * @see plugin_get_available_plugins()
+ */
+function plugin_detach($slot_id, $target = 'default') {
+  // Delete the plugin attachment.
+  db_delete('plugin_attachments')
+    ->condition('slot', $slot_id)
+    ->condition('target', $target)
+    ->execute();
+
+  // If we just deleted the default target for this slot, reattach the
+  // slot-defined default plugin. That ensures that we always have at least
+  // one plugin configured.
+  if ($target == 'default') {
+    if (drupal_function_exists('slot_load')) {
+      $slot = slot_load($slot_id);
+      plugin_attach($slot_id, $slot->default_plugin, 'default');
+    }
+  }
+
+  // If there are no attachments for this slot other than 'default',
+  // cache that to the variable system for faster lookups.
+  $attachments = db_query("SELECT target, class, configuration_class, reuse FROM {plugin_attachments} WHERE slot = :slot", array(':slot' => $slot_id))->fetchAllAssoc('target', PDO::FETCH_ASSOC);
+  if (count($attachments) == 1 && key($attachments) == 'default') {
+    $attachments = current($attachments);
+    unset($attachments['target']);
+    variable_set('plugin_default_' . $slot_id, $attachments);
+  }
+
+  // Flush the target cache.
+  drupal_static_reset('plugin_plugin_mappings');
+  drupal_static_reset('plugin_plugin_objects');
+  drupal_static_reset('plugin_default_only');
+}
+
+/**
+ * Retrieve a list of all available plugins for a given slot.
+ *
+ * @param $slot_id
+ *   The machine-readable name of the slot for which we want a list of
+ *   available plugins.
+ * @return
+ *   An array of plugins that have been defined for this slot.
+ */
+function plugin_get_available_plugins($slot_id) {
+  return db_query('SELECT plugin, title, description, class FROM {plugin_info} WHERE slot = :slot ORDER BY title', array(':slot' => $slot_id))->fetchAllAssoc('plugin');
+}
+
+/**
+ * Get the plugin object for the plugin associated to this slot/target.
+ *
+ * @param $slot_id
+ *   The machine-readable name of the slot for which we want to look up a
+ *   plugin.
+ * @param $target
+ *   The target for which we want to look up a plugin.
+ * @return
+ *   A loaded plugin object or NULL if not defined.
+ *
+ * @see plugin_get_available_plugin()
+ */
+function plugin_get_attached_plugin($slot_id, $target = 'default') {
+  // Lazy-load the utility function.
+  if (drupal_function_exists('plugin_load')) {
+    $plugin_id = db_query_range("SELECT plugin FROM {plugin_attachments} WHERE slot = :slot_1 AND target = :target
+        UNION SELECT plugin FROM {plugin_attachments} WHERE slot = :slot_2 AND target = 'default'", array(
+        ':slot_1' => $slot_id,
+        ':slot_2' => $slot_id,
+        ':target' => $target,
+      ), 0, 1)->fetchField();
+
+    return plugin_load($slot_id, $plugin_id);
+  }
+}
+
+
+/**
+ * Interface for all plugin classes for any slot.
+ *
+ * This is a very thin interface, but does serve to help standardize plugin
+ * behavior and provides a way to type-check a plugin object. Interfaces for
+ * specific slots should extend this interface.
+ */
+interface PluginInterface {
+
+  /**
+   * Constructor
+   *
+   * @param $config
+   *   A configuration object holding configuration options for this plugin.
+   * @param $target
+   *   The target for which this plugin is being called. Some plugins may
+   *   require this information in order to route commands properly.
+   */
+  function __construct(PluginConfigurationInterface $config, $target = 'default');
+}
+
+/**
+ * Interface for all plugin configuration classes.
+ */
+interface PluginConfigurationInterface {
+
+  /**
+   * Declares the options available in this configuration.
+   *
+   * @return
+   *   An array of options, keyed by the option name.  The value of each
+   *   array element is the default value for the option.
+   */
+  public function options();
+
+  /**
+   * Creates the configuration form for this configuration object.
+   *
+   * Unlike a normal form function, you should not return $form from
+   * this method.  Simply alter $form as-is.
+   *
+   * @param $form
+   *   The form object to which to add our fields.
+   * @param $form_state
+   *   The state for this form.
+   */
+  public function optionsForm(&$form, &$form_state);
+
+  /**
+   * Validates the configuration form.
+   *
+   * This method behaves the same as a normal form validate hook.
+   *
+   * @param $form
+   *   The form object.
+   * @param $form_state
+   *   The state for this form.
+   */
+  public function optionsFormValidate($form, &$form_state);
+
+
+  /**
+   * Pre-processes a configuration form before it is submitted.
+   *
+   * Note that this method should not save form information directly.
+   * Configuration is saved automatically by the system.  Use this method
+   * only if you need to manipulate the $form_state before the submitted values
+   * are saved.
+   *
+   * @param $form
+   *   The form object.
+   * @param $form_state
+   *   The state for this form.
+   */
+  public function optionsFormSubmit($form, &$form_state);
+
+  /**
+   * Retrieves the specified option from this configuration object.
+   *
+   * The meaning of a given configuration is slot- and plugin-dependent.
+   *
+   * @param $option
+   *   A string indicating the option to retrieve.
+   * @return
+   *   The stored value for the specified option, or if one is not found then
+   *   the default value specified in the options() method.
+   */
+  public function getOption($option);
+
+  /**
+   *
+   * @param $option
+   *   The name of the option to set.
+   * @param $value
+   *   The value to which to set the option.  This may be any primitive data
+   *   type (int, float, string, array).
+   */
+  public function setOption($option, $value);
+
+}
+
+/**
+ * Base implementation of a plugin configuration object.
+ */
+abstract class PluginConfigurationBase implements PluginConfigurationInterface {
+
+  /**
+   * Keyed array of available options.
+   *
+   * @var array
+   */
+  protected $options;
+
+  public function options() {
+    return $this->options;
+  }
+
+  public function optionsForm(&$form, &$form_state) {}
+
+  public function optionsFormValidate($form, &$form_state) {}
+
+  public function optionsFormSubmit($form, &$form_state) {}
+
+  public function getOption($option) {
+    if (isset($this->options[$option])) {
+      return $this->options[$option];
+    }
+    else {
+      error_log('Got to the default options');
+      $defaults = $this->options();
+      error_log(print_r($defaults, 1));
+      return $defaults[$option];
+    }
+  }
+
+  public function setOption($option, $value) {
+    $this->options[$option] = $value;
+  }
+}
+
+/**
+ * "Null" implementation of plugin configuration.
+ *
+ * Use this configuration class for plugins that have no configuration options.
+ */
+class PluginConfigurationNone extends PluginConfigurationBase {
+
+}
+
+/**
+ * Base implementation of a plugin object.
+ *
+ * Simple plugin objects may choose to inherit from a base class in order to
+ * not reimplement routine functionality. Alternatively they may simply implement
+ * the appropriate interface and implement their own version of common
+ * functionality. Both methods are acceptable depending on the use case.
+ */
+abstract class PluginBase implements PluginInterface {
+
+  /**
+   * The configuration object for this plugin.
+   *
+   * @var PluginConfigurationInterface
+   */
+  protected $config;
+
+  /**
+   * The target for which this plugin is active.
+   */
+  protected $target;
+
+  function __construct(PluginConfigurationInterface $config, $target = 'default') {
+    $this->config = $config;
+    $this->target = $target;
+  }
+}
+
+/**
+ * @} End of "ingroup plugins".
+ */
Index: includes/plugins.inc
===================================================================
RCS file: includes/plugins.inc
diff -N includes/plugins.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/plugins.inc	8 Jul 2009 04:43:28 -0000
@@ -0,0 +1,295 @@
+<?php
+// $Id:$
+
+/**
+ * @file
+ * Plugin utility functions.
+ *
+ * These functions are only needed when triggering a plugin rebuild, and
+ * should not be included otherwise.
+ */
+
+/**
+ * @defgroup plugins Plugins system
+ * @{
+ * Plugins are a standardized mechanism for implementing swappable subsystems.
+ * While hooks are useful for allowing any and all other modules to respond
+ * to or modify some event, Plugins are a way for code to delegate certain
+ * behavior to an easily swappable object. That could be anything from a simple
+ * string operation up through an entire rendering engine.
+ *
+ * The Plugins system includes a number of key parts:
+ *
+ * Slot
+ * A slot describes a swappable system or subsystem, and includes a name, an
+ * interface, and a default plugin. For example, "path_lookup" is a "slot".
+ * All plugins for a given slot must conform to the PHP interface (in the
+ * path_lookup case, PathAliasInterface) defined by the slot.
+ *
+ * Plugin
+ * A plugin is a class that implements the interface for a given slot. For
+ * example, there is a lazy and a pre-caching plugin for the path_lookup slot
+ * defined in system_plugin_info(). Plugins may be transparently swapped out
+ * for one another.
+ *
+ * Target
+ * A target is a specific case within a slot. Every slot may have an unlimited
+ * number of arbitrarily defined targets, these are not defined in the slot or
+ * anywhere else (see more below). Each target may have a different plugin
+ * attached to it. However, many slots only use the default target.
+ * path_lookup, for example, only uses its default target, while cache uses
+ * many, including "page", "menu", and "filter".
+ *
+ * Slots and plugins are defined by hook_slot_info() and hook_plugin_info(),
+ * respectively. Once defined, a plugin may be "attached" to a specific target
+ * like so:
+ *
+ * @code
+ * plugin_attach('cache', 'memcache', 'filter');
+ * @endcode
+ *
+ * Where cache is the slot, memcache is the name of the plugin, filter is the
+ * target. This will overwrite any previously associated plugin, and all
+ * subsequent reads, writes and clears of the filter cache will be handled by
+ * memcache.
+ *
+ * A plugin may then be requested using the plugin() function:
+ *
+ * @code
+ * $plugin = plugin('cache', 'filter');
+ * $plugin->get(...);
+ * @endcode
+ *
+ * If no plugin has been attached to that slot and target, the plugin attached
+ * to the target 'default' is used instead. This allows arbitrarily defined
+ * targets -- plugin can ask for any target and will get a meaningful answer
+ * every time. If no target is specified, the slot's currently assigned default
+ * plugin is used, allowing the target argument to be ommitted for subsystems
+ * with only a single target.
+ *
+ * A slot/target may also be reset:
+ *
+ * @code
+ * plugin_detach('cache', 'filter');
+ * @endcode
+ *
+ * After detach, as explained above, subsequent calls for this target will
+ * return the plugin attached to the target 'default'.
+ *
+ * If the target argument is omitted, both plugin_attach() and plugin_detach()
+ * operate on the 'default' target.
+ */
+
+/**
+ * Rebuild the plugin slot registry.
+ *
+ * @return
+ *   The array of slots just declared.
+ */
+function plugin_slot_rebuild() {
+  $slots = module_invoke_all('slot_info');
+
+  // Set default values, to avoid NULL issues if nothing else.
+  foreach ($slots as $slot => $info) {
+    $slots[$slot] += plugin_slot_defaults();
+  }
+
+  // Let other modules alter the plugin target registry if needed.
+  drupal_alter('slot_info', $slots);
+
+  if ($slots) {
+    // Build the target data.
+    $insert = db_insert('plugin_slot_info')->fields(array('slot', 'title', 'interface', 'configuration_interface', 'reuse', 'default_plugin', 'description'));
+
+    foreach ($slots as $slot => $info) {
+      $insert->values(array(
+       'slot' => $slot,
+       'title' => $info['title'],
+       'interface' => $info['interface'],
+       'configuration_interface' => $info['configuration_interface'],
+       'reuse' => (int)$info['reuse'],
+       'default_plugin' => $info['default_plugin'],
+       'description' => $info['description'],
+      ));
+    }
+
+    // Don't do the deletion until after we've gotten the new data prepared.
+    // That reduces the potential race condition window.
+    db_delete('plugin_slot_info')->execute();
+    $insert->execute();
+  }
+
+  return $slots;
+}
+
+/**
+ * Defines the default info array for plugins.
+ */
+function plugin_plugin_defaults() {
+  return array(
+    'plugin' => '',
+    'slot' => '',
+    'title' => '',
+    'class' => '',
+    'configuration_class' => 'PluginConfigurationNone',
+    'description' => '',
+  );
+}
+
+/**
+ * Defines the default info array for slots.
+ */
+function plugin_slot_defaults() {
+  return array(
+    'slot' => '',
+    'title' => '',
+    'interface' => 'PluginInterface',
+    'configuration_interface' => 'PluginConfigurationInterface',
+    'reuse' => 1,
+    'default_plugin' => 'default',
+    'description' => '',
+  );
+}
+
+/**
+ * Rebuild the plugin registry.
+ *
+ * @return
+ *   The array of plugins just defined.
+ */
+function plugin_plugin_rebuild() {
+  $plugins = module_invoke_all('plugin_info');
+
+  // Set default values, to avoid NULL issues if nothing else.
+  foreach ($plugins as $slot => $plugin_info) {
+    foreach ($plugin_info as $plugin => $info) {
+      $plugins[$slot][$plugin] += plugin_plugin_defaults();
+    }
+  }
+
+  // Let other modules alter the plugin registry if needed.
+  drupal_alter('plugin_info', $plugins);
+
+  if ($plugins) {
+    // Build the plugin data.
+    $insert = db_insert('plugin_info')->fields(array('plugin', 'slot', 'title', 'class', 'configuration_class', 'description'));
+    foreach ($plugins as $slot => $plugin_info) {
+      foreach ($plugin_info as $plugin => $info) {
+        $insert->values(array(
+          'plugin' => $plugin,
+          'slot' => $slot,
+          'title' => $info['title'],
+          'class' => $info['class'],
+          'configuration_class' => $info['configuration_class'],
+          'description' => $info['description'],
+        ));
+      }
+    }
+
+    // Don't do the deletion until after we've gotten the new data. That
+    // reduces the potential race condition window.
+    db_delete('plugin_info')->execute();
+    $insert->execute();
+  }
+
+  return $plugins;
+}
+
+/**
+ * Ensure that every slot has a plugin attached for the default target.
+ *
+ * For every slot, attach whatever plugin is declared the default plugin
+ * to the default target unless one has already been defined. That ensures
+ * that the attachment table always has at least that default record in it for
+ * each slot, which greatly simplifies the lookup logic.
+ */
+function plugin_ensure_defaults() {
+  $result = db_query("SELECT psi.slot, reuse, class, pi.configuration_class, plugin
+   FROM {plugin_slot_info} psi
+     INNER JOIN {plugin_info} pi ON psi.slot = pi.slot AND psi.default_plugin = pi.plugin");
+
+  foreach ($result as $record) {
+    // We can't exclude all fields or the query breaks. However, setting
+    // reuse to itself has no effect on the table so it's safe to do.
+    db_merge('plugin_attachments')
+     ->key(array(
+       'slot' => $record->slot,
+       'target' => 'default',
+     ))
+     ->fields(array(
+       'plugin' => $record->plugin,
+       'class' => $record->class,
+       'configuration_class' => $record->configuration_class,
+       'reuse' => $record->reuse,
+     ))
+    ->updateExcept('class', 'plugin', 'configuration_class')
+    ->execute();
+
+    // If there are no attachments for this slot other than the default, cache
+    // that to the variable system for faster lookups.
+    $attachments = db_query("SELECT target, class, reuse FROM {plugin_attachments} WHERE slot = :slot", array(':slot' => $record->slot))->fetchAllAssoc('target', PDO::FETCH_ASSOC);
+    if (count($attachments) == 1 && key($attachments) == 'default') {
+      $attachments = current($attachments);
+      unset($attachments['target']);
+      variable_set('plugin_default_' . $record->slot, $attachments);
+    }
+  }
+}
+
+/**
+ * Load function for slot objects.
+ *
+ * @param $slot_id
+ *   The machine-readable name of the slot.
+ * @param $refresh
+ *   If this is set to TRUE, the internal slot cache is flushed.
+ * @return
+ *   The slot object or NULL if it is not defined.
+ */
+function slot_load($slot_id, $refresh = FALSE) {
+  static $slots;
+
+  if ($refresh) {
+    $slots = array();
+  }
+
+  if (empty($slots[$slot_id])) {
+    $slots[$slot_id] = db_query("SELECT slot, title, interface, reuse, default_plugin, description FROM {plugin_slot_info} WHERE slot = :slot", array(':slot' => $slot_id))->fetchObject();
+  }
+
+  return $slots[$slot_id];
+}
+
+/**
+ * Load function for plugin info objects.
+ *
+ * @param $slot_id
+ *   The machine-readable name of the slot.
+ * @param $plugin_id
+ *   The machine-readable name of the plugin. All plugins are namespaced
+ *   within their slot.
+ * @param $refresh
+ *   If this is set to TRUE, the internal plugins cache is flushed.
+ * @return
+ *   The plugin object or NULL if not defined.
+ */
+function plugin_load($slot_id, $plugin_id, $refresh = FALSE) {
+  static $plugins;
+
+  if ($refresh) {
+    $plugins = array();
+  }
+
+  if (empty($plugins[$slot_id][$plugin_id])) {
+    $plugins[$slot_id][$plugin_id] = db_query("SELECT plugin, slot, title, class, configuration_class, description FROM {plugin_info} WHERE slot = :slot AND plugin = :plugin", array(
+      ':slot' => $slot_id,
+      ':plugin' => $plugin_id,
+    ))->fetchObject();
+  }
+
+  return $plugins[$slot_id][$plugin_id];
+}
+
+/**
+ * @} End of "ingroup plugins".
+ */
Index: modules/simpletest/simpletest.info
===================================================================
RCS file: /cvs/drupal/drupal/modules/simpletest/simpletest.info,v
retrieving revision 1.7
diff -u -p -r1.7 simpletest.info
--- modules/simpletest/simpletest.info	1 Jul 2009 13:44:53 -0000	1.7
+++ modules/simpletest/simpletest.info	8 Jul 2009 04:43:29 -0000
@@ -25,6 +25,7 @@ files[] = tests/graph.test
 files[] = tests/image.test
 files[] = tests/menu.test
 files[] = tests/module.test
+files[] = tests/plugins.test
 files[] = tests/registry.test
 files[] = tests/schema.test
 files[] = tests/session.test
Index: modules/simpletest/tests/plugins.test
===================================================================
RCS file: modules/simpletest/tests/plugins.test
diff -N modules/simpletest/tests/plugins.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/plugins.test	8 Jul 2009 04:43:29 -0000
@@ -0,0 +1,293 @@
+<?php
+// $Id:$
+
+/**
+ * @file
+ * Unit tests for the Plugins system.
+ */
+
+/**
+ * Test class for the Plugins system.
+ */
+class PluginsNotReusedTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => t('Plugins, Nonreusable'),
+      'description' => t('Test the plugin routing system'),
+      'group' => t('Plugins'),
+    );
+  }
+
+  function setUp() {
+    parent::setUp('plugins_test');
+    $this->sampleString = $this->randomName();
+
+    // We need to rebuild the index at least once, because the module_enable()
+    // routine called by the parent method doesn't call drupal_flush_all_caches().
+    plugins_rebuild();
+  }
+
+  /**
+   * Test that we can retrieve the default plugin if no target is defined.
+   */
+  function testDefaultPlugin() {
+    // 'default' has not been attached to anything, so should return the
+    // default implementation.
+    $plugin = plugin('fancystring', 'default');
+
+    $this->assertTrue($plugin instanceof FancystringInterface, t('Returned plugin has the correct interface.'));
+    $this->assertTrue($plugin instanceof FancystringDefault, t('Correct plugin object returned.'));
+
+    if ($plugin instanceof pluginInterface) {
+      $plugin->setString($this->sampleString);
+      $mangled = $plugin->getString();
+
+      $this->assertEqual($this->sampleString, $mangled, t('Default plugin returned correct string.'));
+    }
+  }
+
+  /**
+   * Test that we can specify a plugin for a target and it loads correctly.
+   */
+  function testSpecifiedPlugin() {
+
+    // Specify a specific plugin for a given target.
+    plugin_attach('fancystring', 'rot13', 'foo');
+
+    // Now request the plugin.
+    $plugin = plugin('fancystring', 'foo');
+
+    $this->assertTrue($plugin instanceof FancystringInterface, t('Returned plugin has the correct interface.'));
+    $this->assertTrue($plugin instanceof FancystringRot13, t('Correct plugin object returned.'));
+
+    if ($plugin instanceof PluginInterface) {
+      $plugin->setString($this->sampleString);
+      $mangled = $plugin->getString();
+
+      $this->assertEqual(str_rot13($this->sampleString), $mangled, t('Specified plugin returned correct string.'));
+    }
+  }
+
+  /**
+   * Test that we can skip using target on a slot and everything still works.
+   */
+  function testUndefinedPlugin() {
+    // An undefined target should always give us the default plugin defined by the slot.
+    $plugin = plugin('fancystring');
+
+    $this->assertTrue($plugin instanceof FancystringInterface, t('Returned plugin has the correct interface.'));
+    $this->assertTrue($plugin instanceof FancystringDefault, t('Correct plugin object returned.'));
+
+    if ($plugin instanceof PluginInterface) {
+      $plugin->setString($this->sampleString);
+      $mangled = $plugin->getString();
+
+      $this->assertEqual($this->sampleString, $mangled, t('Default plugin returned correct string.'));
+    }
+  }
+
+  /**
+   * Test a context-sensitive plugin.
+   */
+  function testContextSensitivePlugin() {
+
+    // This is a poor way of simulating the variable system, but it will have
+    // to do for now.
+    $map = array(
+      'Hello' => 'Goodbye',
+      'World' => 'Cruel World',
+    );
+    $GLOBALS['conf']['fancystring_translate'] = $map;
+
+    // Specify a specific plugin for a given target.
+    plugin_attach('fancystring', 'custom_translate', 'bar');
+
+    // Now request the plugin.
+    $plugin = plugin('fancystring', 'bar');
+
+    $this->assertTrue($plugin instanceof FancystringInterface, t('Returned plugin has the correct interface.'));
+    $this->assertTrue($plugin instanceof FancystringCustom, t('Correct plugin object returned.'));
+
+    if ($plugin instanceof PluginInterface) {
+      $plugin->setString($this->sampleString);
+      $mangled = $plugin->getString();
+
+      $this->assertEqual(strtr($this->sampleString, $map), $mangled, t('Specified plugin returned correct string.'));
+    }
+
+    unset ($GLOBALS['conf']['fancystring_translate']);
+  }
+
+  /**
+   * Test that we can successfully unset a plugin attachment.
+   */
+  function testUnsettingPlugin() {
+    // Specify a specific plugin for a given target.
+    plugin_attach('fancystring', 'rot13', 'foo');
+    // Now unset that attachment.
+    plugin_detach('fancystring', 'foo');
+    // Now request the plugin. We should get back the default.
+    $plugin = plugin('fancystring', 'foo');
+
+   // $this->fail(get_class($plugin));
+
+    $this->assertTrue($plugin instanceof FancystringInterface, t('Returned plugin has the correct interface.'));
+    $this->assertTrue($plugin instanceof FancystringDefault, t('Correct plugin object returned.'));
+
+    if ($plugin instanceof PluginInterface) {
+      $plugin->setString($this->sampleString);
+      $mangled = $plugin->getString();
+
+      $this->assertEqual($this->sampleString, $mangled, t('Unspecified plugin returned correct string.'));
+    }
+  }
+
+  /**
+   * Test that a non-reusable slot returns a new object eacn time.
+   */
+  function testPluginReuse() {
+
+    // Specify a specific plugin for a given target.
+    plugin_attach('fancystring', 'rot13', 'foo');
+
+    // Now request the plugin.
+    $plugin_1 = plugin('fancystring', 'foo');
+
+    $this->assertTrue($plugin_1 instanceof FancystringInterface, t('Returned plugin has the correct interface.'));
+    $this->assertTrue($plugin_1 instanceof FancystringRot13, t('Correct plugin object returned.'));
+
+    $plugin_2 = plugin('fancystring', 'foo');
+
+    $this->assertNotIdentical($plugin_1, $plugin_2, t('A new object was returned.'));
+  }
+}
+
+/**
+ * Test class for the plugins system, part 2.
+ */
+class PluginsReusableTestCase extends DrupalWebTestCase {
+
+  protected $sampleString = 'Hello World';
+
+  public static function getInfo() {
+    return array(
+      'name' => t('Plugins, Reusable'),
+      'description' => t('Test the plugin routing system for reusable plugins.'),
+      'group' => t('Plugins'),
+    );
+  }
+
+  function setUp() {
+    parent::setUp('plugins_test');
+
+    // We need to rebuild the index at least once, because the module_enable()
+    // routine called by the parent method doesn't call drupal_flush_all_caches().
+    plugins_rebuild();
+  }
+
+  /**
+   * Test that a reusable slot returns the same object each time.
+   */
+  function testPluginReuse() {
+
+    // Specify a specific plugin for a given target.
+    plugin_attach('thingie', 'default', 'foo');
+
+    // Now request the plugin.
+    $plugin_1 = plugin('thingie', 'foo');
+
+    $this->assertTrue($plugin_1 instanceof ThingieInterface, t('Returned plugin has the correct interface.'));
+    $this->assertTrue($plugin_1 instanceof ThingieDefault, t('Correct plugin object returned.'));
+
+    $plugin_2 = plugin('thingie', 'foo');
+
+    $this->assertIdentical($plugin_1, $plugin_2, t('The same plugin object was returned.'));
+  }
+}
+
+
+/**
+ * Test plugin configuration ability.
+ */
+class PluginsConfigurationTestCase extends DrupalWebTestCase {
+
+  public static function getInfo() {
+    return array(
+      'name' => t('Plugins, Configuration'),
+      'description' => t('Test the plugin routing system for configuration.'),
+      'group' => t('Plugins'),
+    );
+  }
+
+  function setUp() {
+    parent::setUp('plugins_test');
+
+    // We need to rebuild the index at least once, because the module_enable()
+    // routine called by the parent method doesn't call drupal_flush_all_caches().
+    plugins_rebuild();
+  }
+
+  /**
+   * Test that a plugin responds to its configuration object.
+   */
+  function testPluginConfiguration() {
+
+    // Specify a specific plugin for a given target.
+    plugin_attach('math', 'default', 'multiply');
+
+    // Now request the plugin.
+    $plugin_1 = plugin('math', 'default');
+
+    $this->assertTrue($plugin_1 instanceof MathInterface, t('Returned plugin has the correct interface.'));
+    $this->assertTrue($plugin_1 instanceof MathMultiply, t('Correct plugin object returned.'));
+
+    $this->assertEqual($plugin_1->calculate(5), 10, t('plugin calculates the correct value.'));
+  }
+
+  /**
+   * Test that we can safely serialize and deserialize a configuration object.
+   */
+  function testConfigurationSerialization() {
+
+    $before = new MathConfiguration();
+
+    $serialized = serialize($before);
+
+    $after = unserialize($serialized);
+
+    $this->assertTrue($after instanceof MathConfiguration, t('Configuration object deserialized to the correct type.'));
+
+    foreach (array_keys($before->options()) as $option) {
+      $this->assertEqual($before->getOption($option), $after->getOption($option), t('Serialized value the same after deserialization.'));
+    }
+  }
+
+  /**
+   * Test that configuration objects affect the plugin appropriately.
+   */
+  function testConfigurationSetting() {
+    $config = new MathConfiguration();
+
+    $config->setOption('factor', 3);
+    $plugin = new MathMultiply($config);
+
+    $this->assertEqual($plugin->calculate(5), 15, t('Set option used correctly.'));
+  }
+
+  /**
+   * Test that we can attach a plugin to a slot/target with configuration.
+   */
+  function testConfiguratingSaving() {
+    $config = new MathConfiguration();
+
+    $config->setOption('factor', 3);
+
+    plugin_attach('math', 'multiply', 'default', $config);
+
+    $plugin = plugin('math', 'default');
+
+    $this->assertEqual($plugin->calculate(5), 15, t('Saved option used correctly.'));
+  }
+
+}
Index: modules/simpletest/tests/plugins_test.info
===================================================================
RCS file: modules/simpletest/tests/plugins_test.info
diff -N modules/simpletest/tests/plugins_test.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/plugins_test.info	8 Jul 2009 04:43:29 -0000
@@ -0,0 +1,7 @@
+; $Id:$
+name = Plugins test
+description = "Support module for Plugins tests."
+core = 7.x
+package = Testing
+files[] = plugins_test.module
+hidden = TRUE
Index: modules/simpletest/tests/plugins_test.module
===================================================================
RCS file: modules/simpletest/tests/plugins_test.module
diff -N modules/simpletest/tests/plugins_test.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ modules/simpletest/tests/plugins_test.module	8 Jul 2009 04:43:29 -0000
@@ -0,0 +1,223 @@
+<?php
+// $Id:$
+
+/**
+ * Implementation of hook_slot_info().
+ */
+function plugins_test_slot_info() {
+  return array(
+    'fancystring' => array(
+      'title' => 'Fancy string',
+      'description' => 'Do fancy stuff to strings',
+      'interface' => 'FancystringInterface',
+      'default_plugin' => 'default',
+      'reuse' => FALSE,
+    ),
+    'thingie' => array(
+      'title' => 'Other slot',
+      'description' => "This slot does nothing. It's just to test lookup behavior of reusable slots.",
+      'interface' => 'ThingieInterface',
+    ),
+    'math' => array(
+      'title' => 'Math tests',
+      'description' => "Run a mathematical manipulation on a number.",
+      'interface' => 'MathInterface',
+      'default_plugin' => 'multiply',
+    ),
+  );
+}
+
+/**
+ * Implementation of hook_plugin_info().
+ */
+function plugins_test_plugin_info() {
+  return array(
+    'fancystring' => array(
+       'default' => array(
+         // Translate on load, not define, like hook_menu().
+         'title' => 'No string mutation',
+         'class' => 'FancystringDefault',
+         'description' => "This plugin doesn't do anything to the string. It passes through unaltered.",
+       ),
+       'rot13' => array(
+         'title' => 'Rot13 translation',
+         'class' => 'FancystringRot13',
+         'description' => 'This plugin ROT13 encrypts a string.',
+       ),
+       'custom_translate' => array(
+         'title' => 'Custom mapping',
+         'class' => 'FancystringCustom',
+         'description' => 'This plugin uses a user-specified mapping array.',
+       ),
+     ),
+    'thingie' => array(
+      'default' => array(
+        'title' => 'Do nothing plugin',
+        'description' => 'This plugin does nothing',
+        'class' => 'ThingieDefault',
+      )
+    ),
+    'math' => array(
+       'multiply' => array(
+         'title' => 'Multiply',
+         'class' => 'MathMultiply',
+         'configuration_class' => 'MathConfiguration',
+         'description' => "Multiply a provided number.",
+       ),
+       'add' => array(
+         'title' => 'Add',
+         'class' => 'MathAdd',
+         'description' => "Add a provided number.",
+       ),
+     ),
+  );
+}
+
+interface MathInterface extends PluginInterface {
+
+  /**
+   * Calculates the result of a number and this plugin's operation.
+   *
+   * @param $number
+   *   The number to use for calculations.
+   */
+  public function calculate($number);
+}
+
+class MathMultiply extends PluginBase implements MathInterface {
+  public function calculate($number) {
+    return $number * $this->config->getOption('factor');
+  }
+}
+
+class MathAdd extends PluginBase implements MathInterface {
+  public function calculate($number) {
+    return $number + $this->config->getOption('factor');
+  }
+}
+
+class MathConfiguration extends PluginConfigurationBase {
+
+  public function options() {
+    return array(
+      'factor' => 2,
+    );
+  }
+}
+
+/**
+ * Interface for the example string mangling plugin.
+ */
+interface FancystringInterface extends PluginInterface {
+
+  /**
+   * Set the string for this object to mangle.
+   * @param $string
+   *   The string to mangle.
+   */
+  public function setString($string);
+
+  /**
+   * Get the string back, mangled according to this plugin.
+   * @return
+   *   The mangled string.
+   */
+  public function getString();
+}
+
+/**
+ * Base implementation of Fancystring plugin.
+ */
+abstract class FancystringBase implements FancystringInterface {
+
+  protected $target;
+
+  protected $config;
+
+  protected $string = '';
+
+  function __construct(PluginConfigurationInterface $config, $target = 'default') {
+    $this->config = $config;
+    $this->target = $target;
+  }
+
+  public function setString($string) {
+    $this->string = $string;
+  }
+}
+
+/**
+ * Default implementation of Fancystring plugin.
+ */
+class FancystringDefault extends FancystringBase {
+
+  public function getString() {
+    return $this->string;
+  }
+}
+
+/**
+ * Rot13 implementation of Fancystring plugin.
+ */
+class FancystringRot13 extends FancystringBase {
+
+  public function getString() {
+    return str_rot13($this->string);
+  }
+}
+
+/**
+ * Custom replacement implementation of Fancystring plugin.
+ *
+ * Note that we are implementing the FancystringInterface here directly
+ * rather than inheriting from the base class, mostly to show that we can.
+ * There's no obligation that plugins do anything other than support the
+ * appropriate interface. How they do so is entirely up to them.
+ *
+ */
+class FancystringCustom implements FancystringInterface {
+
+  protected $string = '';
+
+  protected $target;
+
+  function __construct(PluginConfigurationInterface $config, $target = 'default') {
+    $this->config = $config;
+    $this->target = $target;
+  }
+
+  public function setString($string) {
+    $this->string = $string;
+  }
+
+  public function getString() {
+    $map = variable_get('fancystring_translate', array());
+
+    return strtr($this->string, $map);
+  }
+}
+
+/**
+ * Interface for the do-nothing test plugin.
+ */
+interface ThingieInterface extends PluginInterface {
+
+  /**
+   * Do nothing.
+   */
+  function noop();
+}
+
+class ThingieDefault implements ThingieInterface {
+
+  protected $target;
+
+  function __construct(PluginConfigurationInterface $config, $target = 'default') {
+    $this->config = $config;
+    $this->target = $target;
+  }
+
+  function noop() {
+    return;
+  }
+}
Index: modules/system/system.install
===================================================================
RCS file: /cvs/drupal/drupal/modules/system/system.install,v
retrieving revision 1.353
diff -u -p -r1.353 system.install
--- modules/system/system.install	4 Jul 2009 14:45:36 -0000	1.353
+++ modules/system/system.install	8 Jul 2009 04:43:29 -0000
@@ -1158,6 +1158,144 @@ function system_schema() {
     'primary key' => array('mlid'),
   );
 
+  $schema['plugin_attachments'] = array(
+    'description' => 'Maps slots and targets to their configured plugin.',
+    'fields' => array(
+      'slot' => array(
+        'description' => 'The machine-readable name of the slot.',
+        'type' => 'varchar',
+        'length' => '50',
+        'not null' => TRUE,
+      ),
+      'target' => array(
+        'description' => 'The target value.',
+        'type' => 'varchar',
+        'length' => '50',
+        'not null' => TRUE,
+      ),
+      'plugin' => array(
+        'description' => 'The plugin attached to this slot/target.',
+        'type' => 'varchar',
+        'length' => '50',
+        'not null' => TRUE,
+      ),
+      'configuration' => array(
+        'description' => 'The configuration object for this attachment.',
+        'type' => 'text',
+        'not null' => FALSE,
+      ),
+      'class' => array(
+        'description' => 'The PHP class for the plugin attached to this slot/target. It is stored in this table to make lookups faster.',
+        'type' => 'varchar',
+        'length' => '100',
+        'not null' => TRUE,
+      ),
+      'configuration_class' => array(
+        'description' => 'The PHP class that defines this plugin\s configuration.  It is stored in this table to make lookups faster.',
+        'type' => 'varchar',
+        'length' => '100',
+        'not null' => TRUE,
+      ),
+      'reuse' => array(
+        'description' => 'Whether or not this plugin may be reused. It is stored in this table to make lookups faster.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 1,
+      ),
+    ),
+    'primary key' => array('slot', 'target'),
+  );
+
+  $schema['plugin_info'] = array(
+    'description' => 'Tracks all available plugins in the system.',
+    'fields' => array(
+      'plugin' => array(
+        'description' => 'The machine-readable name of the plugin.',
+        'type' => 'varchar',
+        'length' => '50',
+        'not null' => TRUE,
+      ),
+      'slot' => array(
+        'description' => 'The machine-readable name of the slot this plugin is for.',
+        'type' => 'varchar',
+        'length' => '50',
+        'not null' => TRUE,
+      ),
+      'title' => array(
+        'description' => 'The human-readable name of the plugin.',
+        'type' => 'varchar',
+        'length' => '100',
+        'not null' => TRUE,
+      ),
+      'class' => array(
+        'description' => 'The PHP class that defines this plugin.',
+        'type' => 'varchar',
+        'length' => '100',
+        'not null' => TRUE,
+      ),
+      'configuration_class' => array(
+        'description' => 'The PHP class that defines this plugin\s configuration.',
+        'type' => 'varchar',
+        'length' => '100',
+        'not null' => TRUE,
+      ),
+      'description' => array(
+        'description' => 'A human-readable description of this plugin.',
+        'type' => 'text',
+        'not null' => TRUE,
+      )
+    ),
+    'primary key' => array('plugin', 'slot'),
+  );
+
+  $schema['plugin_slot_info'] = array(
+    'description' => 'Tracks all registered slots in the system that plugins can fulfill.',
+    'fields' => array(
+      'slot' => array(
+        'description' => 'The machine-readable name of the slot.',
+        'type' => 'varchar',
+        'length' => '50',
+        'not null' => TRUE,
+      ),
+      'title' => array(
+        'description' => 'The human-readable name of the slot.',
+        'type' => 'varchar',
+        'length' => '100',
+        'not null' => TRUE,
+      ),
+      'interface' => array(
+        'description' => 'The PHP interface that defines this slot.',
+        'type' => 'varchar',
+        'length' => '50',
+        'not null' => TRUE,
+      ),
+      'configuration_interface' => array(
+        'description' => 'The PHP interface that defines this slot\'s configuration object.',
+        'type' => 'varchar',
+        'length' => '50',
+        'not null' => TRUE,
+      ),
+      'reuse' => array(
+        'description' => 'Whether or not to reuse plugin objects on this slot when requested multiple times.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 1,
+      ),
+      'default_plugin' => array(
+        'description' => 'The default plugin id for this slot if one is not otherwise configured.',
+        'type' => 'varchar',
+        'length' => '50',
+        'not null' => TRUE,
+      ),
+      'description' => array(
+        'description' => 'A human-readable description of this slot.',
+        'type' => 'text',
+        'not null' => FALSE,
+      ),
+    ),
+    'primary key' => array('slot'),
+  );
+
   $schema['queue'] = array(
     'description' => 'Stores items in queues.',
     'fields' => array(
