Skip to content

Commit d9785e0

Browse files
Filter service loading candidates using new @CheckAvailability annotation
1 parent e7a693a commit d9785e0

File tree

4 files changed

+293
-2
lines changed

4 files changed

+293
-2
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.
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+
}

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

Lines changed: 49 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,7 +13,8 @@
813

914
public class IntegrationsLoader {
1015

11-
private IntegrationsLoader(){}
16+
private IntegrationsLoader() {
17+
}
1218

1319
/**
1420
* Loads the best suited service, i.e. the one with the highest priority that is supported.
@@ -34,8 +40,10 @@ public static <T> Stream<T> loadAll(Class<T> clazz) {
3440
return ServiceLoader.load(clazz, ClassLoaderFactory.forPluginDir())
3541
.stream()
3642
.filter(IntegrationsLoader::isSupportedOperatingSystem)
43+
.filter(IntegrationsLoader::passesStaticAvailabilityCheck)
3744
.sorted(Comparator.comparingInt(IntegrationsLoader::getPriority).reversed())
38-
.map(ServiceLoader.Provider::get);
45+
.map(ServiceLoader.Provider::get)
46+
.filter(IntegrationsLoader::passesInstanceAvailabilityCheck);
3947
}
4048

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

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+
5198
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package org.cryptomator.integrations.common;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.DisplayName;
5+
import org.junit.jupiter.api.Nested;
6+
import org.junit.jupiter.api.Test;
7+
8+
public class IntegrationsLoaderTest {
9+
10+
@Nested
11+
@DisplayName("@CheckAvailability on static methods")
12+
public class StaticAvailabilityChecks {
13+
14+
@CheckAvailability
15+
private static class StaticTrue {
16+
@CheckAvailability
17+
public static boolean test() {
18+
return true;
19+
}
20+
}
21+
22+
@CheckAvailability
23+
private static class StaticFalse {
24+
@CheckAvailability
25+
public static boolean test() {
26+
return false;
27+
}
28+
}
29+
30+
@Test
31+
@DisplayName("no @CheckAvailability will always pass")
32+
public void testPassesAvailabilityCheck0() {
33+
// @formatter:off
34+
class C1 {}
35+
@CheckAvailability class C2 {}
36+
class C3 {
37+
@CheckAvailability public static boolean test() { return false; }
38+
}
39+
// @formatter:on
40+
41+
Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class));
42+
Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C2.class));
43+
Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C3.class));
44+
}
45+
46+
@Test
47+
@DisplayName("@CheckAvailability on non-conforming methods will be skipped")
48+
public void testPassesAvailabilityCheck1() {
49+
// @formatter:off
50+
@CheckAvailability class C1 {
51+
@CheckAvailability private static boolean test1() { return false; }
52+
@CheckAvailability public static boolean test2(String foo) { return false; }
53+
@CheckAvailability public static String test3() { return "false"; }
54+
}
55+
// @formatter:on
56+
57+
Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class));
58+
}
59+
60+
@Test
61+
@DisplayName("@CheckAvailability on static method")
62+
public void testPassesAvailabilityCheck2() {
63+
Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(StaticTrue.class));
64+
Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(StaticFalse.class));
65+
}
66+
67+
@Test
68+
@DisplayName("@CheckAvailability on inherited static method")
69+
public void testPassesAvailabilityCheck3() {
70+
// @formatter:off
71+
class C1 extends StaticTrue {}
72+
class C2 extends StaticFalse {}
73+
// @formatter:on
74+
75+
Assertions.assertTrue(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class));
76+
Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C2.class));
77+
}
78+
79+
@Test
80+
@DisplayName("multiple @CheckAvailability methods")
81+
public void testPassesAvailabilityCheck4() {
82+
// @formatter:off
83+
class C1 extends StaticTrue {
84+
@CheckAvailability public static boolean test1() { return false; }
85+
}
86+
class C2 extends StaticFalse {
87+
@CheckAvailability public static boolean test1() { return true; }
88+
}
89+
@CheckAvailability class C3 {
90+
@CheckAvailability public static boolean test1() { return true; }
91+
@CheckAvailability public static boolean test2() { return false; }
92+
}
93+
// @formatter:on
94+
95+
Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class));
96+
Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C2.class));
97+
Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C3.class));
98+
}
99+
100+
101+
}
102+
103+
@Nested
104+
@DisplayName("@CheckAvailability on instance methods")
105+
public class InstanceAvailabilityChecks {
106+
107+
@CheckAvailability
108+
private static class InstanceTrue {
109+
@CheckAvailability
110+
public boolean test() {
111+
return true;
112+
}
113+
}
114+
115+
@CheckAvailability
116+
private static class InstanceFalse {
117+
@CheckAvailability
118+
public boolean test() {
119+
return false;
120+
}
121+
}
122+
123+
@Test
124+
@DisplayName("no @CheckAvailability will always pass")
125+
public void testPassesAvailabilityCheck0() {
126+
// @formatter:off
127+
class C1 {}
128+
@CheckAvailability class C2 {}
129+
class C3 {
130+
@CheckAvailability public boolean test() { return false; }
131+
}
132+
// @formatter:on
133+
134+
Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1()));
135+
Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2()));
136+
Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C3()));
137+
}
138+
139+
@Test
140+
@DisplayName("@CheckAvailability on non-conforming instance methods will be skipped")
141+
public void testPassesAvailabilityCheck1() {
142+
// @formatter:off
143+
@CheckAvailability class C1 {
144+
@CheckAvailability private boolean test1() { return false; }
145+
@CheckAvailability public boolean test2(String foo) { return false; }
146+
@CheckAvailability public String test3() { return "false"; }
147+
}
148+
// @formatter:on
149+
150+
Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(C1.class));
151+
}
152+
153+
@Test
154+
@DisplayName("@CheckAvailability on instance method")
155+
public void testPassesAvailabilityCheck2() {
156+
Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new InstanceTrue()));
157+
Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new InstanceFalse()));
158+
}
159+
160+
@Test
161+
@DisplayName("@CheckAvailability on inherited instance method")
162+
public void testPassesAvailabilityCheck3() {
163+
// @formatter:off
164+
class C1 extends InstanceTrue {}
165+
class C2 extends InstanceFalse {}
166+
// @formatter:on
167+
168+
Assertions.assertTrue(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1()));
169+
Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2()));
170+
}
171+
172+
@Test
173+
@DisplayName("multiple @CheckAvailability methods")
174+
public void testPassesAvailabilityCheck4() {
175+
// @formatter:off
176+
class C1 extends InstanceTrue {
177+
@CheckAvailability public boolean test1() { return false; }
178+
}
179+
class C2 extends InstanceFalse {
180+
@CheckAvailability public boolean test1() { return true; }
181+
}
182+
@CheckAvailability class C3 {
183+
@CheckAvailability public boolean test1() { return true; }
184+
@CheckAvailability public boolean test2() { return false; }
185+
}
186+
// @formatter:on
187+
188+
Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1()));
189+
Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2()));
190+
Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C3()));
191+
}
192+
193+
}
194+
195+
}

0 commit comments

Comments
 (0)