PluginManager.java

package org.hammer.audio.pluginhost;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.hammer.audio.plugin.AudioAnalyzerPlugin;
import org.hammer.audio.plugin.PluginDescriptor;

/**
 * Discovers and loads {@link AudioAnalyzerPlugin} implementations via {@link ServiceLoader}.
 *
 * <p>Each plugin is initialized in isolation: a faulty descriptor or constructor cannot prevent
 * other plugins from loading, and never crashes the host application. Failed plugins are still
 * surfaced through the returned {@link PluginRegistry} so the UI/log can show them.
 *
 * <p>This is the only entry point the host application uses to discover plugins. It deliberately
 * does not import or reference any concrete plugin class.
 */
public final class PluginManager {

  private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName());

  private final ClassLoader classLoader;

  /** Use the current thread's context class loader (or this class' loader as fallback). */
  public PluginManager() {
    this(defaultClassLoader());
  }

  /** Use the supplied class loader for service discovery. */
  public PluginManager(ClassLoader classLoader) {
    this.classLoader = Objects.requireNonNull(classLoader, "classLoader");
  }

  /**
   * Discover all plugins on the configured class loader and return a populated {@link
   * PluginRegistry}. Never throws; failures are isolated into individual {@link PluginLoadResult}
   * entries.
   */
  public PluginRegistry loadPlugins() {
    List<PluginLoadResult> results = new ArrayList<>();
    ServiceLoader<AudioAnalyzerPlugin> loader =
        ServiceLoader.load(AudioAnalyzerPlugin.class, classLoader);
    Iterator<ServiceLoader.Provider<AudioAnalyzerPlugin>> providers;
    try {
      providers = loader.stream().iterator();
    } catch (ServiceConfigurationError ex) {
      LOGGER.log(Level.WARNING, "Plugin service discovery failed", ex);
      results.add(PluginLoadResult.failure("<discovery>", ex));
      return new PluginRegistry(results);
    }
    while (true) {
      String providerClassName = "<unknown>";
      try {
        if (!providers.hasNext()) {
          break;
        }
        ServiceLoader.Provider<AudioAnalyzerPlugin> provider = providers.next();
        providerClassName = safeProviderName(provider);
        AudioAnalyzerPlugin plugin = provider.get();
        // Force descriptor evaluation once and reuse it for validation, registration and logging
        // so a flaky descriptor() implementation is exercised exactly once per plugin.
        PluginDescriptor descriptor = Objects.requireNonNull(plugin.descriptor(), "descriptor");
        results.add(PluginLoadResult.success(plugin, descriptor));
        LOGGER.log(
            Level.INFO,
            "Loaded plugin: {0} ({1} {2})",
            new Object[] {descriptor.id(), descriptor.name(), descriptor.version()});
      } catch (ServiceConfigurationError | RuntimeException ex) {
        LOGGER.log(Level.WARNING, "Failed to load plugin " + providerClassName, ex);
        results.add(PluginLoadResult.failure(providerClassName, ex));
      }
    }
    return new PluginRegistry(results);
  }

  private static String safeProviderName(ServiceLoader.Provider<AudioAnalyzerPlugin> provider) {
    try {
      return provider.type().getName();
    } catch (RuntimeException ex) {
      return "<unknown>";
    }
  }

  private static ClassLoader defaultClassLoader() {
    ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
    if (contextLoader != null) {
      return contextLoader;
    }
    return PluginManager.class.getClassLoader();
  }
}