Skip to content

Commit 9272d89

Browse files
restore ability to load implementations from cryptomator.pluginDir
1 parent 8a83ae2 commit 9272d89

6 files changed

Lines changed: 256 additions & 1 deletion

File tree

pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@
4444
<version>23.0.0</version>
4545
<scope>provided</scope>
4646
</dependency>
47+
<dependency>
48+
<groupId>org.junit.jupiter</groupId>
49+
<artifactId>junit-jupiter</artifactId>
50+
<version>5.8.2</version>
51+
<scope>test</scope>
52+
</dependency>
53+
<dependency>
54+
<groupId>org.mockito</groupId>
55+
<artifactId>mockito-core</artifactId>
56+
<version>4.3.1</version>
57+
<scope>test</scope>
58+
</dependency>
4759
</dependencies>
4860

4961
<build>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.cryptomator.integrations.common;
2+
3+
import org.jetbrains.annotations.Contract;
4+
import org.jetbrains.annotations.VisibleForTesting;
5+
6+
import java.io.IOException;
7+
import java.io.UncheckedIOException;
8+
import java.net.MalformedURLException;
9+
import java.net.URL;
10+
import java.net.URLClassLoader;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
14+
class ClassLoaderFactory {
15+
16+
private static final String USER_HOME = System.getProperty("user.home");
17+
private static final String PLUGIN_DIR_KEY = "cryptomator.pluginDir";
18+
private static final String JAR_SUFFIX = ".jar";
19+
20+
/**
21+
* Attempts to find {@code .jar} files in the path specified in {@value #PLUGIN_DIR_KEY} system property.
22+
* A new class loader instance is returned that loads classes from the given classes.
23+
*
24+
* @return A new URLClassLoader that is aware of all {@code .jar} files in the plugin dir
25+
*/
26+
@Contract(value = "-> new", pure = true)
27+
public static URLClassLoader forPluginDir() {
28+
String val = System.getProperty(PLUGIN_DIR_KEY, "");
29+
final Path p;
30+
if (val.startsWith("~/")) {
31+
p = Path.of(USER_HOME).resolve(val.substring(2));
32+
} else {
33+
p = Path.of(val);
34+
}
35+
return forPluginDirWithPath(p);
36+
}
37+
38+
@VisibleForTesting
39+
@Contract(value = "_ -> new", pure = true)
40+
static URLClassLoader forPluginDirWithPath(Path path) throws UncheckedIOException {
41+
return URLClassLoader.newInstance(findJars(path));
42+
}
43+
44+
@VisibleForTesting
45+
static URL[] findJars(Path path) {
46+
try (var stream = Files.walk(path)) {
47+
return stream.filter(ClassLoaderFactory::isJarFile).map(ClassLoaderFactory::toUrl).toArray(URL[]::new);
48+
} catch (IOException | UncheckedIOException e) {
49+
// unable to locate any jars // TODO: log a warning?
50+
return new URL[0];
51+
}
52+
}
53+
54+
private static URL toUrl(Path path) throws UncheckedIOException {
55+
try {
56+
return path.toUri().toURL();
57+
} catch (MalformedURLException e) {
58+
throw new UncheckedIOException(e);
59+
}
60+
}
61+
62+
private static boolean isJarFile(Path path) {
63+
return Files.isRegularFile(path) && path.getFileName().toString().toLowerCase().endsWith(JAR_SUFFIX);
64+
}
65+
66+
}

src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
public class IntegrationsLoader {
1010

11+
private IntegrationsLoader(){}
12+
1113
/**
1214
* Loads the best suited service, i.e. the one with the highest priority that is supported.
1315
* <p>
@@ -29,7 +31,7 @@ public static <T> Optional<T> load(Class<T> clazz) {
2931
* @return An ordered stream of all suited service candidates
3032
*/
3133
public static <T> Stream<T> loadAll(Class<T> clazz) {
32-
return ServiceLoader.load(clazz)
34+
return ServiceLoader.load(clazz, ClassLoaderFactory.forPluginDir())
3335
.stream()
3436
.filter(IntegrationsLoader::isSupportedOperatingSystem)
3537
.sorted(Comparator.comparingInt(IntegrationsLoader::getPriority).reversed())
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package org.cryptomator.integrations.common;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.DisplayName;
6+
import org.junit.jupiter.api.Nested;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.io.TempDir;
9+
import org.mockito.Mockito;
10+
11+
import java.io.ByteArrayInputStream;
12+
import java.io.IOException;
13+
import java.net.URL;
14+
import java.net.URLClassLoader;
15+
import java.nio.file.Files;
16+
import java.nio.file.Path;
17+
import java.util.Arrays;
18+
import java.util.Comparator;
19+
20+
public class ClassLoaderFactoryTest {
21+
22+
@Nested
23+
@DisplayName("When two .jars exist in the plugin dir")
24+
public class WithJars {
25+
26+
private static final byte[] FOO_CONTENTS = "foo = 42".getBytes();
27+
private static final byte[] BAR_CONTENTS = "bar = 23".getBytes();
28+
private Path pluginDir;
29+
30+
@BeforeEach
31+
public void setup(@TempDir Path tmpDir) throws IOException {
32+
Files.createDirectory(tmpDir.resolve("plugin1"));
33+
try (var out = Files.newOutputStream(tmpDir.resolve("plugin1/foo.jar"));
34+
var jar = JarBuilder.withTarget(out)) {
35+
jar.addFile("foo.properties", new ByteArrayInputStream(FOO_CONTENTS));
36+
}
37+
38+
Files.createDirectory(tmpDir.resolve("plugin2"));
39+
try (var out = Files.newOutputStream(tmpDir.resolve("plugin2/bar.jar"));
40+
var jar = JarBuilder.withTarget(out)) {
41+
jar.addFile("bar.properties", new ByteArrayInputStream(BAR_CONTENTS));
42+
}
43+
44+
this.pluginDir = tmpDir;
45+
}
46+
47+
@Test
48+
@DisplayName("can load resources from both jars")
49+
public void testForPluginDirWithPath() throws IOException {
50+
var cl = ClassLoaderFactory.forPluginDirWithPath(pluginDir);
51+
var fooContents = cl.getResourceAsStream("foo.properties").readAllBytes();
52+
var barContents = cl.getResourceAsStream("bar.properties").readAllBytes();
53+
54+
Assertions.assertArrayEquals(FOO_CONTENTS, fooContents);
55+
Assertions.assertArrayEquals(BAR_CONTENTS, barContents);
56+
}
57+
58+
@Test
59+
@DisplayName("can load resources when path is set in cryptomator.pluginDir")
60+
public void testForPluginDirFromSysProp() throws IOException {
61+
System.setProperty("cryptomator.pluginDir", pluginDir.toString());
62+
63+
var cl = ClassLoaderFactory.forPluginDir();
64+
var fooContents = cl.getResourceAsStream("foo.properties").readAllBytes();
65+
var barContents = cl.getResourceAsStream("bar.properties").readAllBytes();
66+
67+
Assertions.assertArrayEquals(FOO_CONTENTS, fooContents);
68+
Assertions.assertArrayEquals(BAR_CONTENTS, barContents);
69+
}
70+
}
71+
72+
@Test
73+
@DisplayName("read path from cryptomator.pluginDir")
74+
public void testReadPluginDirFromSysProp() {
75+
var ucl = Mockito.mock(URLClassLoader.class, "ucl");
76+
var absPath = "/there/will/be/plugins";
77+
try (var mockedClass = Mockito.mockStatic(ClassLoaderFactory.class)) {
78+
mockedClass.when(() -> ClassLoaderFactory.forPluginDir()).thenCallRealMethod();
79+
mockedClass.when(() -> ClassLoaderFactory.forPluginDirWithPath(Path.of(absPath))).thenReturn(ucl);
80+
81+
System.setProperty("cryptomator.pluginDir", absPath);
82+
var result = ClassLoaderFactory.forPluginDir();
83+
84+
Assertions.assertSame(ucl, result);
85+
}
86+
}
87+
88+
@Test
89+
@DisplayName("read path from cryptomator.pluginDir and replace ~/ with user.home")
90+
public void testReadPluginDirFromSysPropAndReplaceHome() {
91+
var ucl = Mockito.mock(URLClassLoader.class, "ucl");
92+
var relPath = "~/there/will/be/plugins";
93+
var absPath = Path.of(System.getProperty("user.home")).resolve("there/will/be/plugins");
94+
try (var mockedClass = Mockito.mockStatic(ClassLoaderFactory.class)) {
95+
mockedClass.when(() -> ClassLoaderFactory.forPluginDir()).thenCallRealMethod();
96+
mockedClass.when(() -> ClassLoaderFactory.forPluginDirWithPath(absPath)).thenReturn(ucl);
97+
98+
System.setProperty("cryptomator.pluginDir", relPath);
99+
var result = ClassLoaderFactory.forPluginDir();
100+
101+
Assertions.assertSame(ucl, result);
102+
}
103+
}
104+
105+
@Test
106+
@DisplayName("findJars returns empty list if not containing jars")
107+
public void testFindJars1(@TempDir Path tmpDir) throws IOException {
108+
Files.createDirectories(tmpDir.resolve("dir1"));
109+
Files.createFile(tmpDir.resolve("file1"));
110+
111+
var urls = ClassLoaderFactory.findJars(tmpDir);
112+
113+
Assertions.assertArrayEquals(new URL[0], urls);
114+
}
115+
116+
@Test
117+
@DisplayName("findJars returns urls of found jars")
118+
public void testFindJars2(@TempDir Path tmpDir) throws IOException {
119+
Files.createDirectories(tmpDir.resolve("dir1"));
120+
Files.createDirectories(tmpDir.resolve("dir2"));
121+
Files.createDirectories(tmpDir.resolve("dir1").resolve("dir2"));
122+
Files.createFile(tmpDir.resolve("a.jar"));
123+
Files.createFile(tmpDir.resolve("a.txt"));
124+
Files.createFile(tmpDir.resolve("dir2").resolve("b.jar"));
125+
126+
var urls = ClassLoaderFactory.findJars(tmpDir);
127+
128+
Arrays.sort(urls, Comparator.comparing(URL::toString));
129+
Assertions.assertArrayEquals(new URL[]{
130+
new URL(tmpDir.toUri() + "a.jar"),
131+
new URL(tmpDir.toUri() + "dir2/b.jar")
132+
}, urls);
133+
}
134+
135+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.cryptomator.integrations.common;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.io.OutputStream;
6+
import java.util.jar.Attributes;
7+
import java.util.jar.JarEntry;
8+
import java.util.jar.JarFile;
9+
import java.util.jar.JarOutputStream;
10+
import java.util.jar.Manifest;
11+
12+
public class JarBuilder implements AutoCloseable {
13+
14+
private final Manifest manifest = new Manifest();
15+
private final JarOutputStream jos;
16+
17+
public JarBuilder(JarOutputStream jos) {
18+
this.jos = jos;
19+
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
20+
}
21+
22+
public static JarBuilder withTarget(OutputStream out) throws IOException {
23+
return new JarBuilder(new JarOutputStream(out));
24+
}
25+
26+
public void addFile(String path, InputStream content) throws IOException {
27+
jos.putNextEntry(new JarEntry(path));
28+
content.transferTo(jos);
29+
jos.closeEntry();
30+
}
31+
32+
@Override
33+
public void close() throws IOException {
34+
jos.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME));
35+
manifest.write(jos);
36+
jos.closeEntry();
37+
jos.close();
38+
}
39+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mock-maker-inline

0 commit comments

Comments
 (0)