Skip to content

Commit

Permalink
Merge branch 'develop' into release/1.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Apr 3, 2022
2 parents 0749e3f + d9785e0 commit 266a4b6
Show file tree
Hide file tree
Showing 13 changed files with 572 additions and 8 deletions.
1 change: 1 addition & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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,48 @@
package org.cryptomator.integrations.common;

import org.jetbrains.annotations.ApiStatus;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Identifies 0..n public methods to check preconditions for the integration to work. These are the rules:
*
* <ul>
* <li>Both the type and the method(s) must be annotated with {@code @CheckAvailability}</li>
* <li>Only public no-arg boolean methods are considered</li>
* <li>Methods <em>may</em> be {@code static}, in which case they get invoked before instantiating the service</li>
* <li>Should the method throw an exception, it has the same effect as returning {@code false}</li>
* <li>No specific execution order is guaranteed in case of multiple annotated methods</li>
* <li>Annotations must be present on classes or ancestor classes, not on interfaces</li>
* </ul>
*
* Example:
* <pre>
* {@code
* @CheckAvailability
* public class Foo {
* @CheckAvailability
* public static boolean isSupported() {
* return "enabled".equals(System.getProperty("plugin.status"));
* }
* }
* }
* </pre>
* <p>
* Annotations are discovered at runtime using reflection, so make sure to make relevant classes accessible to this
* module ({@code opens X to org.cryptomator.integrations.api}).
*
* @since 1.1.0
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited
@ApiStatus.Experimental
public @interface CheckAvailability {
}
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
@@ -1,5 +1,10 @@
package org.cryptomator.integrations.common;

import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Optional;
Expand All @@ -8,6 +13,9 @@

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,11 +37,13 @@ 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)
.filter(IntegrationsLoader::passesStaticAvailabilityCheck)
.sorted(Comparator.comparingInt(IntegrationsLoader::getPriority).reversed())
.map(ServiceLoader.Provider::get);
.map(ServiceLoader.Provider::get)
.filter(IntegrationsLoader::passesInstanceAvailabilityCheck);
}

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

private static boolean passesStaticAvailabilityCheck(ServiceLoader.Provider<?> provider) {
return passesStaticAvailabilityCheck(provider.type());
}

@VisibleForTesting
static boolean passesStaticAvailabilityCheck(Class<?> type) {
return passesAvailabilityCheck(type, null);
}

@VisibleForTesting
static boolean passesInstanceAvailabilityCheck(Object instance) {
return passesAvailabilityCheck(instance.getClass(), instance);
}

private static <T> boolean passesAvailabilityCheck(Class<? extends T> type, @Nullable T instance) {
if (!type.isAnnotationPresent(CheckAvailability.class)) {
return true; // if type is not annotated, skip tests
}
return Arrays.stream(type.getMethods())
.filter(m -> isAvailabilityCheck(m, instance == null))
.allMatch(m -> passesAvailabilityCheck(m, instance));
}

private static boolean passesAvailabilityCheck(Method m, @Nullable Object instance) {
assert Boolean.TYPE.equals(m.getReturnType());
try {
return (boolean) m.invoke(instance);
} catch (ReflectiveOperationException e) {
return false;
}
}

private static boolean isAvailabilityCheck(Method m, boolean isStatic) {
return m.isAnnotationPresent(CheckAvailability.class)
&& Boolean.TYPE.equals(m.getReturnType())
&& m.getParameterCount() == 0
&& Modifier.isStatic(m.getModifiers()) == isStatic;
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
package org.cryptomator.integrations.tray;

public record ActionItem(String title, Runnable action) implements TrayMenuItem {
public record ActionItem(String title, Runnable action, boolean enabled) implements TrayMenuItem {

public ActionItem(String title, Runnable action) {
this(title, action, true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,21 @@ static Optional<TrayMenuController> get() {
/**
* Displays an icon on the system tray.
*
* @param rawImageData What image to show
* @param imageData What image to show
* @param defaultAction Action to perform when interacting with the icon directly instead of its menu
* @param tooltip Text shown when hovering
* @throws IOException thrown when interacting with the given <code>rawImageData</code>
* @throws TrayMenuException thrown when adding the tray icon failed
*/
void showTrayIcon(InputStream rawImageData, Runnable defaultAction, String tooltip) throws IOException;
void showTrayIcon(byte[] imageData, Runnable defaultAction, String tooltip) throws TrayMenuException;

/**
* Show the given options in the tray menu.
* <p>
* This method may be called multiple times, e.g. when the vault list changes.
*
* @param items Menu items
* @throws TrayMenuException thrown when updating the tray menu failed
*/
void updateTrayMenu(List<TrayMenuItem> items);
void updateTrayMenu(List<TrayMenuItem> items) throws TrayMenuException;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.cryptomator.integrations.tray;

public class TrayMenuException extends Exception {

public TrayMenuException(String message) {
super(message);
}

public TrayMenuException(String message, Throwable cause) {
super(message, cause);
}

}
Loading

0 comments on commit 266a4b6

Please sign in to comment.