Skip to content

Commit

Permalink
Filter service loading candidates using new @CheckAvailability anno…
Browse files Browse the repository at this point in the history
…tation
  • Loading branch information
overheadhunter committed Apr 3, 2022
1 parent e7a693a commit d9785e0
Show file tree
Hide file tree
Showing 4 changed files with 293 additions and 2 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.

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
@@ -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,7 +13,8 @@

public class IntegrationsLoader {

private IntegrationsLoader(){}
private IntegrationsLoader() {
}

/**
* Loads the best suited service, i.e. the one with the highest priority that is supported.
Expand All @@ -34,8 +40,10 @@ public static <T> Stream<T> loadAll(Class<T> 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 @@ -48,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
@@ -0,0 +1,195 @@
package org.cryptomator.integrations.common;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

public class IntegrationsLoaderTest {

@Nested
@DisplayName("@CheckAvailability on static methods")
public class StaticAvailabilityChecks {

@CheckAvailability
private static class StaticTrue {
@CheckAvailability
public static boolean test() {
return true;
}
}

@CheckAvailability
private static class StaticFalse {
@CheckAvailability
public static boolean test() {
return false;
}
}

@Test
@DisplayName("no @CheckAvailability will always pass")
public void testPassesAvailabilityCheck0() {
// @formatter:off
class C1 {}
@CheckAvailability class C2 {}
class C3 {
@CheckAvailability public static boolean test() { return false; }
}
// @formatter:on

Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class));
Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C2.class));
Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C3.class));
}

@Test
@DisplayName("@CheckAvailability on non-conforming methods will be skipped")
public void testPassesAvailabilityCheck1() {
// @formatter:off
@CheckAvailability class C1 {
@CheckAvailability private static boolean test1() { return false; }
@CheckAvailability public static boolean test2(String foo) { return false; }
@CheckAvailability public static String test3() { return "false"; }
}
// @formatter:on

Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class));
}

@Test
@DisplayName("@CheckAvailability on static method")
public void testPassesAvailabilityCheck2() {
Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(StaticTrue.class));
Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(StaticFalse.class));
}

@Test
@DisplayName("@CheckAvailability on inherited static method")
public void testPassesAvailabilityCheck3() {
// @formatter:off
class C1 extends StaticTrue {}
class C2 extends StaticFalse {}
// @formatter:on

Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class));
Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C2.class));
}

@Test
@DisplayName("multiple @CheckAvailability methods")
public void testPassesAvailabilityCheck4() {
// @formatter:off
class C1 extends StaticTrue {
@CheckAvailability public static boolean test1() { return false; }
}
class C2 extends StaticFalse {
@CheckAvailability public static boolean test1() { return true; }
}
@CheckAvailability class C3 {
@CheckAvailability public static boolean test1() { return true; }
@CheckAvailability public static boolean test2() { return false; }
}
// @formatter:on

Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class));
Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C2.class));
Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C3.class));
}


}

@Nested
@DisplayName("@CheckAvailability on instance methods")
public class InstanceAvailabilityChecks {

@CheckAvailability
private static class InstanceTrue {
@CheckAvailability
public boolean test() {
return true;
}
}

@CheckAvailability
private static class InstanceFalse {
@CheckAvailability
public boolean test() {
return false;
}
}

@Test
@DisplayName("no @CheckAvailability will always pass")
public void testPassesAvailabilityCheck0() {
// @formatter:off
class C1 {}
@CheckAvailability class C2 {}
class C3 {
@CheckAvailability public boolean test() { return false; }
}
// @formatter:on

Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1()));
Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2()));
Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C3()));
}

@Test
@DisplayName("@CheckAvailability on non-conforming instance methods will be skipped")
public void testPassesAvailabilityCheck1() {
// @formatter:off
@CheckAvailability class C1 {
@CheckAvailability private boolean test1() { return false; }
@CheckAvailability public boolean test2(String foo) { return false; }
@CheckAvailability public String test3() { return "false"; }
}
// @formatter:on

Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(C1.class));
}

@Test
@DisplayName("@CheckAvailability on instance method")
public void testPassesAvailabilityCheck2() {
Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new InstanceTrue()));
Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new InstanceFalse()));
}

@Test
@DisplayName("@CheckAvailability on inherited instance method")
public void testPassesAvailabilityCheck3() {
// @formatter:off
class C1 extends InstanceTrue {}
class C2 extends InstanceFalse {}
// @formatter:on

Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1()));
Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2()));
}

@Test
@DisplayName("multiple @CheckAvailability methods")
public void testPassesAvailabilityCheck4() {
// @formatter:off
class C1 extends InstanceTrue {
@CheckAvailability public boolean test1() { return false; }
}
class C2 extends InstanceFalse {
@CheckAvailability public boolean test1() { return true; }
}
@CheckAvailability class C3 {
@CheckAvailability public boolean test1() { return true; }
@CheckAvailability public boolean test2() { return false; }
}
// @formatter:on

Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1()));
Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2()));
Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C3()));
}

}

}

0 comments on commit d9785e0

Please sign in to comment.