diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f91d6d5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,38 @@ +version: 2 +updates: + - package-ecosystem: "maven" + directory: "/" + schedule: + interval: "monthly" + day: "monday" + time: "06:00" + timezone: "Etc/UTC" + groups: + java-test-dependencies: + patterns: + - "org.junit.jupiter:*" + - "org.mockito:*" + maven-build-plugins: + patterns: + - "org.apache.maven.plugins:*" + - "org.sonatype.plugins:nexus-staging-maven-plugin" + java-production-dependencies: + patterns: + - "*" + exclude-patterns: + - "org.apache.maven.plugins:*" + - "org.sonatype.plugins:nexus-staging-maven-plugin" + - "org.junit.jupiter:*" + - "org.mockito:*" + + + - package-ecosystem: "github-actions" + directory: "/" # even for `.github/workflows` + schedule: + interval: "monthly" + groups: + github-actions: + patterns: + - "*" + labels: + - "ci" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4ae8cec..794888a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,8 +7,8 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: 17 @@ -19,7 +19,7 @@ jobs: - name: Build and Test id: buildAndTest run: mvn -B clean install - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: artifacts path: target/*.jar diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 7eedd35..debd498 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -15,19 +15,19 @@ jobs: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 2 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: 17 cache: 'maven' - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: java - name: Build run: mvn -B compile - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 \ No newline at end of file + uses: github/codeql-action/analyze@v3 \ No newline at end of file diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml index 6cecb12..5d33118 100644 --- a/.github/workflows/publish-central.yml +++ b/.github/workflows/publish-central.yml @@ -10,10 +10,10 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: "refs/tags/${{ github.event.inputs.tag }}" - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: 17 diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index 309a8ac..74e6b9d 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -7,8 +7,8 @@ jobs: runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions steps: - - uses: actions/checkout@v3 - - uses: actions/setup-java@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 with: distribution: 'temurin' java-version: 17 diff --git a/pom.xml b/pom.xml index c686efb..66a349f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.cryptomator integrations-api - 1.3.0 + 1.3.1 Cryptomator Integrations API Defines optional service interfaces that may be used by Cryptomator @@ -41,31 +41,31 @@ org.slf4j slf4j-api - 1.7.36 + 2.0.12 org.jetbrains annotations - 23.0.0 + 24.1.0 provided org.slf4j slf4j-simple - 1.7.36 + 2.0.12 test org.junit.jupiter junit-jupiter - 5.9.0 + 5.10.2 test org.mockito mockito-core - 4.8.0 + 5.10.0 test @@ -75,14 +75,14 @@ org.apache.maven.plugins maven-compiler-plugin - 3.9.0 + 3.12.1 17 maven-source-plugin - 3.2.0 + 3.3.0 attach-sources @@ -95,11 +95,11 @@ org.apache.maven.plugins maven-surefire-plugin - 3.0.0-M7 + 3.2.5 maven-javadoc-plugin - 3.2.0 + 3.6.3 attach-javadocs @@ -148,7 +148,7 @@ maven-gpg-plugin - 3.0.1 + 3.1.0 sign-artifacts @@ -183,7 +183,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.8 + 1.6.13 true ossrh diff --git a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java index 64a3977..6e4ec72 100644 --- a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java +++ b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java @@ -10,6 +10,7 @@ import java.util.Arrays; import java.util.Comparator; import java.util.Optional; +import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.stream.Stream; @@ -21,38 +22,38 @@ private IntegrationsLoader() { } /** - * Loads the best suited service, i.e. the one with the highest priority that is supported. + * Loads the best suited service provider, i.e. the one with the highest priority that is supported. *

* If two services are available with the same priority, it is unspecified which one will be returned. * * @param clazz Service class * @param Type of the service - * @return Highest priority service or empty if no supported service was found + * @return Highest priority service provider or empty if no supported service provider was found */ public static Optional load(Class clazz) { return loadAll(clazz).findFirst(); } /** - * Loads all suited services ordered by priority in descending order. + * Loads all suited service providers ordered by priority in descending order. * * @param clazz Service class * @param Type of the service - * @return An ordered stream of all suited service candidates + * @return An ordered stream of all suited service providers */ public static Stream loadAll(Class clazz) { return ServiceLoader.load(clazz, ClassLoaderFactory.forPluginDir()) .stream() - .peek(service -> logFoundService(clazz, service.type())) + .peek(serviceProvider -> logFoundServiceProvider(clazz, serviceProvider.type())) .filter(IntegrationsLoader::isSupportedOperatingSystem) .filter(IntegrationsLoader::passesStaticAvailabilityCheck) .sorted(Comparator.comparingInt(IntegrationsLoader::getPriority).reversed()) - .map(ServiceLoader.Provider::get) + .flatMap(IntegrationsLoader::instantiateServiceProvider) .filter(IntegrationsLoader::passesInstanceAvailabilityCheck) .peek(impl -> logServiceIsAvailable(clazz, impl.getClass())); } - private static void logFoundService(Class apiType, Class implType) { + private static void logFoundServiceProvider(Class apiType, Class implType) { if (LOG.isDebugEnabled()) { LOG.debug("{}: Found implementation: {} in jar {}", apiType.getSimpleName(), implType.getName(), implType.getProtectionDomain().getCodeSource().getLocation().getPath()); } @@ -68,18 +69,30 @@ private static boolean isSupportedOperatingSystem(ServiceLoader.Provider prov return annotations.length == 0 || Arrays.stream(annotations).anyMatch(OperatingSystem.Value::isCurrent); } + private static Stream instantiateServiceProvider(ServiceLoader.Provider provider) { + try { + return Stream.of(provider.get()); + } catch (ServiceConfigurationError err) { + //ServiceLoader.Provider::get throws this error if (from javadoc) + // * the public static "provider()" method of a provider factory returns null + // * the service provider cannot be instantiated due to an error/throw + LOG.warn("Unable to load service provider {}.", provider.type().getName(), err); + return Stream.empty(); + } + } + private static boolean passesStaticAvailabilityCheck(ServiceLoader.Provider provider) { return passesStaticAvailabilityCheck(provider.type()); } @VisibleForTesting static boolean passesStaticAvailabilityCheck(Class type) { - return passesAvailabilityCheck(type, null); + return silentlyPassesAvailabilityCheck(type, null); } @VisibleForTesting static boolean passesInstanceAvailabilityCheck(Object instance) { - return passesAvailabilityCheck(instance.getClass(), instance); + return silentlyPassesAvailabilityCheck(instance.getClass(), instance); } private static void logServiceIsAvailable(Class apiType, Class implType) { @@ -88,6 +101,15 @@ private static void logServiceIsAvailable(Class apiType, Class implType) { } } + private static boolean silentlyPassesAvailabilityCheck(Class type, @Nullable T instance) { + try { + return passesAvailabilityCheck(type, instance); + } catch (ExceptionInInitializerError | NoClassDefFoundError | RuntimeException e) { + LOG.warn("Unable to load service provider {}.", type.getName(), e); + return false; + } + } + private static boolean passesAvailabilityCheck(Class type, @Nullable T instance) { if (!type.isAnnotationPresent(CheckAvailability.class)) { return true; // if type is not annotated, skip tests diff --git a/src/test/java/org/cryptomator/integrations/common/InitExceptionTestClass.java b/src/test/java/org/cryptomator/integrations/common/InitExceptionTestClass.java new file mode 100644 index 0000000..800e370 --- /dev/null +++ b/src/test/java/org/cryptomator/integrations/common/InitExceptionTestClass.java @@ -0,0 +1,25 @@ +package org.cryptomator.integrations.common; + +@CheckAvailability +public class InitExceptionTestClass { + + private static final String TEST; + + static { + TEST = throwSomething(); + } + + public InitExceptionTestClass() { + + } + + static String throwSomething() { + throw new RuntimeException("STATIC FAIL"); + } + + @CheckAvailability + public static boolean test() { + return true; + } + +} diff --git a/src/test/java/org/cryptomator/integrations/common/IntegrationsLoaderTest.java b/src/test/java/org/cryptomator/integrations/common/IntegrationsLoaderTest.java index 5c7c41a..1fbc614 100644 --- a/src/test/java/org/cryptomator/integrations/common/IntegrationsLoaderTest.java +++ b/src/test/java/org/cryptomator/integrations/common/IntegrationsLoaderTest.java @@ -97,6 +97,18 @@ class C2 extends StaticFalse { Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C3.class)); } + @Test + @DisplayName("throwing @CheckAvailability methods are treated as false") + public void testPassesAvailabilityCheckThrowing() { + + @CheckAvailability class C1 { + @CheckAvailability public static boolean test() { throw new RuntimeException("FAIL"); } + } + + Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(C1.class)); + Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(InitExceptionTestClass.class)); + Assertions.assertFalse(IntegrationsLoader.passesStaticAvailabilityCheck(InitExceptionTestClass.class)); //NoClassDefFoundError due to repated call + } } @@ -190,6 +202,26 @@ class C2 extends InstanceFalse { Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C3())); } + + @Test + @DisplayName("throwing @CheckAvailability methods are treated as false") + public void testPassesAvailabilityCheckThrowing() { + + @CheckAvailability + class C1 { + @CheckAvailability public boolean test1() { throw new RuntimeException("FAIL"); } + } + + @CheckAvailability + class C2 { + @CheckAvailability public boolean test1() { return true; } + @CheckAvailability public boolean test2() { throw new RuntimeException("FAIL"); } + } + + Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C1())); + Assertions.assertFalse(IntegrationsLoader.passesInstanceAvailabilityCheck(new C2())); + } + } } \ No newline at end of file