Skip to content

Commit

Permalink
#413: Implement new plugin service loader
Browse files Browse the repository at this point in the history
  • Loading branch information
kaklakariada committed Jun 16, 2024
1 parent bfcb923 commit 887a5ca
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.itsallcode.openfasttrace.core.serviceloader;

import java.util.ServiceLoader;
import java.util.ServiceLoader.Provider;
import java.util.stream.Stream;

final class ClassPathServiceLoader<T> implements Loader<T>
{
private final ServiceLoader<T> serviceLoader;

private ClassPathServiceLoader(final ServiceLoader<T> serviceLoader)
{
this.serviceLoader = serviceLoader;
}

static <T> Loader<T> load(final Class<T> serviceType, final ServiceOrigin serviceOrigin)
{
return new ClassPathServiceLoader<>(ServiceLoader.load(serviceType, serviceOrigin.getClassLoader()));
}

@Override
public Stream<T> load()
{
return this.serviceLoader.stream().map(Provider::get);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.itsallcode.openfasttrace.core.serviceloader;

import java.util.List;
import java.util.stream.Stream;

class DelegatingLoader<T> implements Loader<T>
{
private final List<Loader<T>> delegates;

DelegatingLoader(final List<Loader<T>> delegates)
{
this.delegates = delegates;
}

@Override
public Stream<T> load()
{
return delegates.stream().flatMap(Loader::load);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.itsallcode.openfasttrace.core.serviceloader;

import java.util.*;
import java.util.stream.Stream;

import org.itsallcode.openfasttrace.api.core.serviceloader.Initializable;

Expand All @@ -12,15 +14,15 @@
* @param <C>
* the context type
*/
public class InitializingServiceLoader<T extends Initializable<C>, C> implements Iterable<T>
public final class InitializingServiceLoader<T extends Initializable<C>, C> implements Iterable<T>, Loader<T>
{
private final ServiceLoader<T> serviceLoader;
private final Loader<T> delegate;
private final C context;
private List<T> services;

private InitializingServiceLoader(final ServiceLoader<T> serviceLoader, final C context)
private InitializingServiceLoader(final Loader<T> delegate, final C context)
{
this.serviceLoader = serviceLoader;
this.delegate = delegate;
this.context = context;
}

Expand All @@ -41,27 +43,31 @@ private InitializingServiceLoader(final ServiceLoader<T> serviceLoader, final C
public static <T extends Initializable<C>, C> InitializingServiceLoader<T, C> load(
final Class<T> serviceType, final C context)
{
return new InitializingServiceLoader<>(ServiceLoader.load(serviceType), context);
final PluginLoaderFactory loaderFactory = PluginLoaderFactory.createDefault();
return new InitializingServiceLoader<>(loaderFactory.createLoader(serviceType), context);
}

@Override
public Iterator<T> iterator()
public Stream<T> load()
{
if (this.services == null)
{
this.services = loadServices();
}
return this.services.iterator();
return services.stream();
}

private List<T> loadServices()
{
final List<T> initializedServices = new ArrayList<>();
for (final T service : this.serviceLoader)
{
return this.delegate.load().map(service -> {
service.init(this.context);
initializedServices.add(service);
}
return initializedServices;
return service;
}).toList();
}

@Override
public Iterator<T> iterator()
{
return load().iterator();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.itsallcode.openfasttrace.core.serviceloader;

import java.util.stream.Stream;

interface Loader<T>
{
Stream<T> load();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.itsallcode.openfasttrace.core.serviceloader;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

class PluginLoaderFactory
{
private final Path pluginsDirectory;

PluginLoaderFactory(final Path pluginsDirectory)
{
this.pluginsDirectory = pluginsDirectory;
}

static PluginLoaderFactory createDefault()
{
return new PluginLoaderFactory(getHomeDirectory().resolve(".oft").resolve("plugins"));
}

private static Path getHomeDirectory()
{
return Path.of(System.getProperty("user.home"));
}

<T> Loader<T> createLoader(final Class<T> serviceType)
{
return createLoader(serviceType, findServiceOrigins());
}

private <T> Loader<T> createLoader(final Class<T> serviceType, final List<ServiceOrigin> origins)
{
final List<Loader<T>> loaders = origins.stream().map(origin -> ClassPathServiceLoader.load(serviceType, origin))
.toList();
return new DelegatingLoader<>(loaders);
}

private List<ServiceOrigin> findServiceOrigins()
{
final List<ServiceOrigin> origins = new ArrayList<>();
origins.add(ServiceOrigin.forCurrentClassPath());
origins.addAll(findPluginOrigins());
return origins;
}

Collection<ServiceOrigin> findPluginOrigins()
{
if (!Files.isDirectory(pluginsDirectory))
{
return Collections.emptyList();
}

try
{
return Files.list(pluginsDirectory)
.map(this::originForPath)
.flatMap(Optional::stream)
.toList();
}
catch (final IOException exception)
{
throw new UncheckedIOException(
"Failed to list plugin directories in '" + this.pluginsDirectory + "': " + exception.getMessage(),
exception);
}
}

private Optional<ServiceOrigin> originForPath(final Path path)
{
if (isJarFile(path))
{
return Optional.of(ServiceOrigin.forJars(List.of(path)));
}
if (!Files.isDirectory(path))
{
return Optional.empty();
}
final List<Path> jars = findJarsInDir(path);
if (jars.isEmpty())
{
return Optional.empty();
}
return Optional.of(ServiceOrigin.forJars(jars));
}

private static List<Path> findJarsInDir(final Path path)
{
try
{
return Files.list(path).filter(PluginLoaderFactory::isJarFile).toList();
}
catch (final IOException exception)
{
throw new UncheckedIOException(
"Failed to list files in plugin directory '" + path + "': " + exception.getMessage(),
exception);
}
}

private static boolean isJarFile(final Path path)
{
return path.getFileName().toString().toLowerCase(Locale.ENGLISH).endsWith(".jar");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.itsallcode.openfasttrace.core.serviceloader;

import static java.util.stream.Collectors.joining;

import java.net.*;
import java.nio.file.Path;
import java.util.List;

final class ServiceOrigin
{
private final ClassLoader classLoader;

private ServiceOrigin(final ClassLoader classLoader)
{
this.classLoader = classLoader;
}

public static ServiceOrigin forCurrentClassPath()
{
return new ServiceOrigin(getBaseClassLoader());
}

private static ClassLoader getBaseClassLoader()
{
return Thread.currentThread().getContextClassLoader();
}

public static ServiceOrigin forJar(final Path jar)
{
return forJars(List.of(jar));
}

public static ServiceOrigin forJars(final List<Path> jars)
{
return new ServiceOrigin(createClassLoader(jars));
}

private static ClassLoader createClassLoader(final List<Path> jars)
{
final String name = "ServiceClassLoader-"
+ jars.stream().map(Path::getFileName).map(Path::toString).collect(joining(","));
final URL[] urls = jars.stream().map(ServiceOrigin::toUrl).toArray(URL[]::new);
return new URLClassLoader(name, urls, getBaseClassLoader());
}

private static URL toUrl(final Path path)
{
try
{
return path.toUri().toURL();
}
catch (final MalformedURLException e)
{
throw new IllegalStateException("Error converting path " + path + " to url", e);
}
}

public ClassLoader getClassLoader()
{
return classLoader;
}
}

0 comments on commit 887a5ca

Please # to comment.