Skip to content

Commit

Permalink
restore ability to load implementations from cryptomator.pluginDir
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Mar 8, 2022
1 parent 8a83ae2 commit 9272d89
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 1 deletion.
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@
<version>23.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.3.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package org.cryptomator.integrations.common;

import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.VisibleForTesting;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;

class ClassLoaderFactory {

private static final String USER_HOME = System.getProperty("user.home");
private static final String PLUGIN_DIR_KEY = "cryptomator.pluginDir";
private static final String JAR_SUFFIX = ".jar";

/**
* Attempts to find {@code .jar} files in the path specified in {@value #PLUGIN_DIR_KEY} system property.
* A new class loader instance is returned that loads classes from the given classes.
*
* @return A new URLClassLoader that is aware of all {@code .jar} files in the plugin dir
*/
@Contract(value = "-> new", pure = true)
public static URLClassLoader forPluginDir() {
String val = System.getProperty(PLUGIN_DIR_KEY, "");
final Path p;
if (val.startsWith("~/")) {
p = Path.of(USER_HOME).resolve(val.substring(2));
} else {
p = Path.of(val);
}
return forPluginDirWithPath(p);
}

@VisibleForTesting
@Contract(value = "_ -> new", pure = true)
static URLClassLoader forPluginDirWithPath(Path path) throws UncheckedIOException {
return URLClassLoader.newInstance(findJars(path));
}

@VisibleForTesting
static URL[] findJars(Path path) {
try (var stream = Files.walk(path)) {
return stream.filter(ClassLoaderFactory::isJarFile).map(ClassLoaderFactory::toUrl).toArray(URL[]::new);
} catch (IOException | UncheckedIOException e) {
// unable to locate any jars // TODO: log a warning?
return new URL[0];
}
}

private static URL toUrl(Path path) throws UncheckedIOException {
try {
return path.toUri().toURL();
} catch (MalformedURLException e) {
throw new UncheckedIOException(e);
}
}

private static boolean isJarFile(Path path) {
return Files.isRegularFile(path) && path.getFileName().toString().toLowerCase().endsWith(JAR_SUFFIX);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

public class IntegrationsLoader {

private IntegrationsLoader(){}

/**
* Loads the best suited service, i.e. the one with the highest priority that is supported.
* <p>
Expand All @@ -29,7 +31,7 @@ public static <T> Optional<T> load(Class<T> clazz) {
* @return An ordered stream of all suited service candidates
*/
public static <T> Stream<T> loadAll(Class<T> clazz) {
return ServiceLoader.load(clazz)
return ServiceLoader.load(clazz, ClassLoaderFactory.forPluginDir())
.stream()
.filter(IntegrationsLoader::isSupportedOperatingSystem)
.sorted(Comparator.comparingInt(IntegrationsLoader::getPriority).reversed())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package org.cryptomator.integrations.common;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Comparator;

public class ClassLoaderFactoryTest {

@Nested
@DisplayName("When two .jars exist in the plugin dir")
public class WithJars {

private static final byte[] FOO_CONTENTS = "foo = 42".getBytes();
private static final byte[] BAR_CONTENTS = "bar = 23".getBytes();
private Path pluginDir;

@BeforeEach
public void setup(@TempDir Path tmpDir) throws IOException {
Files.createDirectory(tmpDir.resolve("plugin1"));
try (var out = Files.newOutputStream(tmpDir.resolve("plugin1/foo.jar"));
var jar = JarBuilder.withTarget(out)) {
jar.addFile("foo.properties", new ByteArrayInputStream(FOO_CONTENTS));
}

Files.createDirectory(tmpDir.resolve("plugin2"));
try (var out = Files.newOutputStream(tmpDir.resolve("plugin2/bar.jar"));
var jar = JarBuilder.withTarget(out)) {
jar.addFile("bar.properties", new ByteArrayInputStream(BAR_CONTENTS));
}

this.pluginDir = tmpDir;
}

@Test
@DisplayName("can load resources from both jars")
public void testForPluginDirWithPath() throws IOException {
var cl = ClassLoaderFactory.forPluginDirWithPath(pluginDir);
var fooContents = cl.getResourceAsStream("foo.properties").readAllBytes();
var barContents = cl.getResourceAsStream("bar.properties").readAllBytes();

Assertions.assertArrayEquals(FOO_CONTENTS, fooContents);
Assertions.assertArrayEquals(BAR_CONTENTS, barContents);
}

@Test
@DisplayName("can load resources when path is set in cryptomator.pluginDir")
public void testForPluginDirFromSysProp() throws IOException {
System.setProperty("cryptomator.pluginDir", pluginDir.toString());

var cl = ClassLoaderFactory.forPluginDir();
var fooContents = cl.getResourceAsStream("foo.properties").readAllBytes();
var barContents = cl.getResourceAsStream("bar.properties").readAllBytes();

Assertions.assertArrayEquals(FOO_CONTENTS, fooContents);
Assertions.assertArrayEquals(BAR_CONTENTS, barContents);
}
}

@Test
@DisplayName("read path from cryptomator.pluginDir")
public void testReadPluginDirFromSysProp() {
var ucl = Mockito.mock(URLClassLoader.class, "ucl");
var absPath = "/there/will/be/plugins";
try (var mockedClass = Mockito.mockStatic(ClassLoaderFactory.class)) {
mockedClass.when(() -> ClassLoaderFactory.forPluginDir()).thenCallRealMethod();
mockedClass.when(() -> ClassLoaderFactory.forPluginDirWithPath(Path.of(absPath))).thenReturn(ucl);

System.setProperty("cryptomator.pluginDir", absPath);
var result = ClassLoaderFactory.forPluginDir();

Assertions.assertSame(ucl, result);
}
}

@Test
@DisplayName("read path from cryptomator.pluginDir and replace ~/ with user.home")
public void testReadPluginDirFromSysPropAndReplaceHome() {
var ucl = Mockito.mock(URLClassLoader.class, "ucl");
var relPath = "~/there/will/be/plugins";
var absPath = Path.of(System.getProperty("user.home")).resolve("there/will/be/plugins");
try (var mockedClass = Mockito.mockStatic(ClassLoaderFactory.class)) {
mockedClass.when(() -> ClassLoaderFactory.forPluginDir()).thenCallRealMethod();
mockedClass.when(() -> ClassLoaderFactory.forPluginDirWithPath(absPath)).thenReturn(ucl);

System.setProperty("cryptomator.pluginDir", relPath);
var result = ClassLoaderFactory.forPluginDir();

Assertions.assertSame(ucl, result);
}
}

@Test
@DisplayName("findJars returns empty list if not containing jars")
public void testFindJars1(@TempDir Path tmpDir) throws IOException {
Files.createDirectories(tmpDir.resolve("dir1"));
Files.createFile(tmpDir.resolve("file1"));

var urls = ClassLoaderFactory.findJars(tmpDir);

Assertions.assertArrayEquals(new URL[0], urls);
}

@Test
@DisplayName("findJars returns urls of found jars")
public void testFindJars2(@TempDir Path tmpDir) throws IOException {
Files.createDirectories(tmpDir.resolve("dir1"));
Files.createDirectories(tmpDir.resolve("dir2"));
Files.createDirectories(tmpDir.resolve("dir1").resolve("dir2"));
Files.createFile(tmpDir.resolve("a.jar"));
Files.createFile(tmpDir.resolve("a.txt"));
Files.createFile(tmpDir.resolve("dir2").resolve("b.jar"));

var urls = ClassLoaderFactory.findJars(tmpDir);

Arrays.sort(urls, Comparator.comparing(URL::toString));
Assertions.assertArrayEquals(new URL[]{
new URL(tmpDir.toUri() + "a.jar"),
new URL(tmpDir.toUri() + "dir2/b.jar")
}, urls);
}

}
39 changes: 39 additions & 0 deletions src/test/java/org/cryptomator/integrations/common/JarBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.cryptomator.integrations.common;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

public class JarBuilder implements AutoCloseable {

private final Manifest manifest = new Manifest();
private final JarOutputStream jos;

public JarBuilder(JarOutputStream jos) {
this.jos = jos;
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
}

public static JarBuilder withTarget(OutputStream out) throws IOException {
return new JarBuilder(new JarOutputStream(out));
}

public void addFile(String path, InputStream content) throws IOException {
jos.putNextEntry(new JarEntry(path));
content.transferTo(jos);
jos.closeEntry();
}

@Override
public void close() throws IOException {
jos.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME));
manifest.write(jos);
jos.closeEntry();
jos.close();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mock-maker-inline

0 comments on commit 9272d89

Please sign in to comment.