Skip to content

Commit 266a4b6

Browse files
Merge branch 'develop' into release/1.1.0
2 parents 0749e3f + d9785e0 commit 266a4b6

File tree

13 files changed

+572
-8
lines changed

13 files changed

+572
-8
lines changed

.idea/codeStyles/Project.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package org.cryptomator.integrations.common;
2+
3+
import org.jetbrains.annotations.ApiStatus;
4+
5+
import java.lang.annotation.Documented;
6+
import java.lang.annotation.ElementType;
7+
import java.lang.annotation.Inherited;
8+
import java.lang.annotation.Retention;
9+
import java.lang.annotation.RetentionPolicy;
10+
import java.lang.annotation.Target;
11+
12+
/**
13+
* Identifies 0..n public methods to check preconditions for the integration to work. These are the rules:
14+
*
15+
* <ul>
16+
* <li>Both the type and the method(s) must be annotated with {@code @CheckAvailability}</li>
17+
* <li>Only public no-arg boolean methods are considered</li>
18+
* <li>Methods <em>may</em> be {@code static}, in which case they get invoked before instantiating the service</li>
19+
* <li>Should the method throw an exception, it has the same effect as returning {@code false}</li>
20+
* <li>No specific execution order is guaranteed in case of multiple annotated methods</li>
21+
* <li>Annotations must be present on classes or ancestor classes, not on interfaces</li>
22+
* </ul>
23+
*
24+
* Example:
25+
* <pre>
26+
* {@code
27+
* @CheckAvailability
28+
* public class Foo {
29+
* @CheckAvailability
30+
* public static boolean isSupported() {
31+
* return "enabled".equals(System.getProperty("plugin.status"));
32+
* }
33+
* }
34+
* }
35+
* </pre>
36+
* <p>
37+
* Annotations are discovered at runtime using reflection, so make sure to make relevant classes accessible to this
38+
* module ({@code opens X to org.cryptomator.integrations.api}).
39+
*
40+
* @since 1.1.0
41+
*/
42+
@Documented
43+
@Retention(RetentionPolicy.RUNTIME)
44+
@Target({ElementType.TYPE, ElementType.METHOD})
45+
@Inherited
46+
@ApiStatus.Experimental
47+
public @interface CheckAvailability {
48+
}
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: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package org.cryptomator.integrations.common;
22

3+
import org.jetbrains.annotations.Nullable;
4+
import org.jetbrains.annotations.VisibleForTesting;
5+
6+
import java.lang.reflect.Method;
7+
import java.lang.reflect.Modifier;
38
import java.util.Arrays;
49
import java.util.Comparator;
510
import java.util.Optional;
@@ -8,6 +13,9 @@
813

914
public class IntegrationsLoader {
1015

16+
private IntegrationsLoader() {
17+
}
18+
1119
/**
1220
* Loads the best suited service, i.e. the one with the highest priority that is supported.
1321
* <p>
@@ -29,11 +37,13 @@ public static <T> Optional<T> load(Class<T> clazz) {
2937
* @return An ordered stream of all suited service candidates
3038
*/
3139
public static <T> Stream<T> loadAll(Class<T> clazz) {
32-
return ServiceLoader.load(clazz)
40+
return ServiceLoader.load(clazz, ClassLoaderFactory.forPluginDir())
3341
.stream()
3442
.filter(IntegrationsLoader::isSupportedOperatingSystem)
43+
.filter(IntegrationsLoader::passesStaticAvailabilityCheck)
3544
.sorted(Comparator.comparingInt(IntegrationsLoader::getPriority).reversed())
36-
.map(ServiceLoader.Provider::get);
45+
.map(ServiceLoader.Provider::get)
46+
.filter(IntegrationsLoader::passesInstanceAvailabilityCheck);
3747
}
3848

3949
private static int getPriority(ServiceLoader.Provider<?> provider) {
@@ -46,4 +56,43 @@ private static boolean isSupportedOperatingSystem(ServiceLoader.Provider<?> prov
4656
return annotations.length == 0 || Arrays.stream(annotations).anyMatch(OperatingSystem.Value::isCurrent);
4757
}
4858

59+
private static boolean passesStaticAvailabilityCheck(ServiceLoader.Provider<?> provider) {
60+
return passesStaticAvailabilityCheck(provider.type());
61+
}
62+
63+
@VisibleForTesting
64+
static boolean passesStaticAvailabilityCheck(Class<?> type) {
65+
return passesAvailabilityCheck(type, null);
66+
}
67+
68+
@VisibleForTesting
69+
static boolean passesInstanceAvailabilityCheck(Object instance) {
70+
return passesAvailabilityCheck(instance.getClass(), instance);
71+
}
72+
73+
private static <T> boolean passesAvailabilityCheck(Class<? extends T> type, @Nullable T instance) {
74+
if (!type.isAnnotationPresent(CheckAvailability.class)) {
75+
return true; // if type is not annotated, skip tests
76+
}
77+
return Arrays.stream(type.getMethods())
78+
.filter(m -> isAvailabilityCheck(m, instance == null))
79+
.allMatch(m -> passesAvailabilityCheck(m, instance));
80+
}
81+
82+
private static boolean passesAvailabilityCheck(Method m, @Nullable Object instance) {
83+
assert Boolean.TYPE.equals(m.getReturnType());
84+
try {
85+
return (boolean) m.invoke(instance);
86+
} catch (ReflectiveOperationException e) {
87+
return false;
88+
}
89+
}
90+
91+
private static boolean isAvailabilityCheck(Method m, boolean isStatic) {
92+
return m.isAnnotationPresent(CheckAvailability.class)
93+
&& Boolean.TYPE.equals(m.getReturnType())
94+
&& m.getParameterCount() == 0
95+
&& Modifier.isStatic(m.getModifiers()) == isStatic;
96+
}
97+
4998
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
package org.cryptomator.integrations.tray;
22

3-
public record ActionItem(String title, Runnable action) implements TrayMenuItem {
3+
public record ActionItem(String title, Runnable action, boolean enabled) implements TrayMenuItem {
4+
5+
public ActionItem(String title, Runnable action) {
6+
this(title, action, true);
7+
}
48
}

src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,21 @@ static Optional<TrayMenuController> get() {
2323
/**
2424
* Displays an icon on the system tray.
2525
*
26-
* @param rawImageData What image to show
26+
* @param imageData What image to show
2727
* @param defaultAction Action to perform when interacting with the icon directly instead of its menu
2828
* @param tooltip Text shown when hovering
29-
* @throws IOException thrown when interacting with the given <code>rawImageData</code>
29+
* @throws TrayMenuException thrown when adding the tray icon failed
3030
*/
31-
void showTrayIcon(InputStream rawImageData, Runnable defaultAction, String tooltip) throws IOException;
31+
void showTrayIcon(byte[] imageData, Runnable defaultAction, String tooltip) throws TrayMenuException;
3232

3333
/**
3434
* Show the given options in the tray menu.
3535
* <p>
3636
* This method may be called multiple times, e.g. when the vault list changes.
3737
*
3838
* @param items Menu items
39+
* @throws TrayMenuException thrown when updating the tray menu failed
3940
*/
40-
void updateTrayMenu(List<TrayMenuItem> items);
41+
void updateTrayMenu(List<TrayMenuItem> items) throws TrayMenuException;
4142

4243
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.cryptomator.integrations.tray;
2+
3+
public class TrayMenuException extends Exception {
4+
5+
public TrayMenuException(String message) {
6+
super(message);
7+
}
8+
9+
public TrayMenuException(String message, Throwable cause) {
10+
super(message, cause);
11+
}
12+
13+
}

0 commit comments

Comments
 (0)