-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
restore ability to load implementations from
cryptomator.pluginDir
- Loading branch information
1 parent
8a83ae2
commit 9272d89
Showing
6 changed files
with
256 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
src/main/java/org/cryptomator/integrations/common/ClassLoaderFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
src/test/java/org/cryptomator/integrations/common/ClassLoaderFactoryTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
39
src/test/java/org/cryptomator/integrations/common/JarBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
mock-maker-inline |