From 04dc4f9ba9f797bc77a699641d2197c350a70cc2 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Sat, 2 Oct 2021 16:53:28 +0200 Subject: [PATCH 01/30] Widen integrations-api to provide a name for a given key --- .../integrations/keychain/KeychainAccessProvider.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index 4dc2477..f86756d 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -21,6 +21,16 @@ public interface KeychainAccessProvider { */ void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException; + /** + * Associates a passphrase with a given key and a name for that key. + * + * @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}. + * @param name The according name to the key. + * @param passphrase The secret to store in this keychain. + * @throws KeychainAccessException If storing the password failed + */ + default void storePassphrase(String key, String name, CharSequence passphrase) throws KeychainAccessException { } + /** * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. * @return The stored passphrase for the given key or null if no value for the given key could be found. From b7f8613d1e2ccffb9bc6365f58110acbf04e0d07 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Mon, 4 Oct 2021 19:30:24 +0200 Subject: [PATCH 02/30] Call mandatory two parameter version of storePassphrase to be used in integrations-mac integrations-linux integrations-windows --- pom.xml | 3 ++- .../integrations/keychain/KeychainAccessProvider.java | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 9a60185..86898b0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.cryptomator integrations-api - 1.1.0-SNAPSHOT + 1.1.0 Cryptomator Integrations API Defines optional service interfaces that may be used by Cryptomator @@ -72,6 +72,7 @@ true + ${java.home}/bin/javadoc 11 diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index f86756d..116a55a 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -24,12 +24,14 @@ public interface KeychainAccessProvider { /** * Associates a passphrase with a given key and a name for that key. * - * @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}. - * @param name The according name to the key. - * @param passphrase The secret to store in this keychain. + * @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}. + * @param displayName The according name to the key. + * @param passphrase The secret to store in this keychain. * @throws KeychainAccessException If storing the password failed */ - default void storePassphrase(String key, String name, CharSequence passphrase) throws KeychainAccessException { } + default void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { + storePassphrase(key, passphrase); + } /** * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. From 1c63a74a50993c84d50f909be77c849e3806103c Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Wed, 6 Oct 2021 07:29:28 +0200 Subject: [PATCH 03/30] Integrations-api needs to be extended for changePassphrase as well as this uses storePassphrase internally in keepassxc-proxy-access --- .../keychain/KeychainAccessProvider.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index 116a55a..b61ca79 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -57,6 +57,18 @@ default void storePassphrase(String key, String displayName, CharSequence passph */ void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException; + /** + * Updates a passphrase with a given key and stores a name for that key. Noop, if there is no item for the given key. + * + * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. + * @param displayName The according name to the key. + * @param passphrase The secret to be updated in this keychain. + * @throws KeychainAccessException If changing the password failed + */ + default void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { + changePassphrase(key, passphrase); + } + /** * @return true if this KeychainAccessIntegration works on the current machine. * @implSpec This method must not throw any exceptions and should fail fast From 03d747f19de35da7c86b5ce62ca87f669c98408f Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Thu, 7 Oct 2021 16:30:31 +0200 Subject: [PATCH 04/30] Revert artifact version for release Revert javadoc config from local build environment --- pom.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 86898b0..2335f30 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.cryptomator integrations-api - 1.1.0 + 1.1.0-SNAPSHOT Cryptomator Integrations API Defines optional service interfaces that may be used by Cryptomator @@ -72,7 +72,6 @@ true - ${java.home}/bin/javadoc 11 From bb3696fb154a897a64e8922fafc742970305e716 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Thu, 7 Oct 2021 16:57:25 +0200 Subject: [PATCH 05/30] Improve Javadoc --- .../integrations/keychain/KeychainAccessProvider.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index b61ca79..4f70540 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -25,7 +25,9 @@ public interface KeychainAccessProvider { * Associates a passphrase with a given key and a name for that key. * * @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}. - * @param displayName The according name to the key. + * @param displayName The according name to the key. That's the name of the vault displayed in the UI. + * It's passed to the keychain as an additional information about the vault besides the key. + * The parameter does not need to be unique or be checked by the keychain. * @param passphrase The secret to store in this keychain. * @throws KeychainAccessException If storing the password failed */ @@ -61,7 +63,9 @@ default void storePassphrase(String key, String displayName, CharSequence passph * Updates a passphrase with a given key and stores a name for that key. Noop, if there is no item for the given key. * * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. - * @param displayName The according name to the key. + * @param displayName The according name to the key. That's the name of the vault displayed in the UI. + * It's passed to the keychain as an additional information about the vault besides the key. + * The parameter does not need to be unique or be checked by the keychain. * @param passphrase The secret to be updated in this keychain. * @throws KeychainAccessException If changing the password failed */ From 8c779e460062227af3c9ef994999f86f0d4621a2 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Thu, 7 Oct 2021 17:12:49 +0200 Subject: [PATCH 06/30] Deprecate methods that are superseded by their three-param variants --- .../integrations/keychain/KeychainAccessProvider.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index 4f70540..e6ecafc 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -18,7 +18,9 @@ public interface KeychainAccessProvider { * @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}. * @param passphrase The secret to store in this keychain. * @throws KeychainAccessException If storing the password failed + * @deprecated Please use {@link #storePassphrase(String, String, CharSequence)} instead */ + @Deprecated void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException; /** @@ -56,7 +58,9 @@ default void storePassphrase(String key, String displayName, CharSequence passph * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. * @param passphrase The secret to be updated in this keychain. * @throws KeychainAccessException If changing the password failed + * @deprecated Please use {@link #changePassphrase(String, String, CharSequence)} instead */ + @Deprecated void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException; /** From 420254d30120e744f00a0bfe3ed4b982eeb757ee Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Thu, 7 Oct 2021 17:51:08 +0200 Subject: [PATCH 07/30] Fix spacing --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2335f30..9a60185 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.cryptomator integrations-api - 1.1.0-SNAPSHOT + 1.1.0-SNAPSHOT Cryptomator Integrations API Defines optional service interfaces that may be used by Cryptomator From 6b78f371ca72b143613d314be603c78176286148 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 8 Oct 2021 09:54:41 +0200 Subject: [PATCH 08/30] prepare version 1.1.0 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9a60185..d692dd0 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 org.cryptomator integrations-api - 1.1.0-SNAPSHOT + 1.1.0 Cryptomator Integrations API Defines optional service interfaces that may be used by Cryptomator From 6fd93ef73bc94be09b433d81c4296d54da60cd13 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 8 Oct 2021 10:28:47 +0200 Subject: [PATCH 09/30] refined javadoc --- .../integrations/keychain/KeychainAccessProvider.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index e6ecafc..c280895 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -38,7 +38,7 @@ default void storePassphrase(String key, String displayName, CharSequence passph } /** - * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. + * @param key Unique key previously used while {@link #storePassphrase(String, String, CharSequence)} storing a passphrase}. * @return The stored passphrase for the given key or null if no value for the given key could be found. * @throws KeychainAccessException If loading the password failed */ @@ -47,7 +47,7 @@ default void storePassphrase(String key, String displayName, CharSequence passph /** * Deletes a passphrase with a given key. * - * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. + * @param key Unique key previously used while {@link #storePassphrase(String, String, CharSequence)} storing a passphrase}. * @throws KeychainAccessException If deleting the password failed */ void deletePassphrase(String key) throws KeychainAccessException; @@ -55,7 +55,7 @@ default void storePassphrase(String key, String displayName, CharSequence passph /** * Updates a passphrase with a given key. Noop, if there is no item for the given key. * - * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. + * @param key Unique key previously used while {@link #storePassphrase(String, String, CharSequence)} storing a passphrase}. * @param passphrase The secret to be updated in this keychain. * @throws KeychainAccessException If changing the password failed * @deprecated Please use {@link #changePassphrase(String, String, CharSequence)} instead @@ -66,7 +66,7 @@ default void storePassphrase(String key, String displayName, CharSequence passph /** * Updates a passphrase with a given key and stores a name for that key. Noop, if there is no item for the given key. * - * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. + * @param key Unique key previously used while {@link #storePassphrase(String, String, CharSequence)} storing a passphrase}. * @param displayName The according name to the key. That's the name of the vault displayed in the UI. * It's passed to the keychain as an additional information about the vault besides the key. * The parameter does not need to be unique or be checked by the keychain. From 9598bf1ec6e934533b24193ff16e15421b38c3fb Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 15 Nov 2021 16:12:34 +0100 Subject: [PATCH 10/30] update pom.xml Updated OSSRH staging repo (see cryptomator/cryptomator#1910) --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9a60185..0922667 100644 --- a/pom.xml +++ b/pom.xml @@ -137,7 +137,7 @@ ossrh Maven Central - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ @@ -149,7 +149,7 @@ true ossrh - https://oss.sonatype.org/ + https://s01.oss.sonatype.org/ true From a7cdfc83808eb9f78eb76cb1c7b454dbb00a40d8 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 18 Jan 2022 14:13:31 +0100 Subject: [PATCH 11/30] bump minimum required JDK version --- .github/workflows/build.yml | 12 ++++-------- .github/workflows/codeql-analysis.yml | 12 ++++-------- .github/workflows/publish-central.yml | 12 ++++-------- .github/workflows/publish-github.yml | 12 ++++-------- pom.xml | 8 ++++---- 5 files changed, 20 insertions(+), 36 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16ee783..36cb45e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,15 +8,11 @@ jobs: if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')" steps: - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: - java-version: 11 - - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- + distribution: 'temurin' + java-version: 17 + cache: 'maven' - name: Ensure to use tagged version if: startsWith(github.ref, 'refs/tags/') run: mvn versions:set --file ./pom.xml -DnewVersion=${GITHUB_REF##*/} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a46ddd6..c95b54d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,15 +18,11 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 2 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: - java-version: 11 - - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- + distribution: 'temurin' + java-version: 17 + cache: 'maven' - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml index f22b7a9..fee8726 100644 --- a/.github/workflows/publish-central.yml +++ b/.github/workflows/publish-central.yml @@ -13,20 +13,16 @@ jobs: - uses: actions/checkout@v2 with: ref: "refs/tags/${{ github.event.inputs.tag }}" - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: - java-version: 11 + distribution: 'temurin' + java-version: 17 + cache: 'maven' server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml server-username: MAVEN_USERNAME # env variable for username in deploy server-password: MAVEN_PASSWORD # env variable for token in deploy gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase - - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - name: Enforce project version ${{ github.event.inputs.tag }} run: mvn versions:set -B -DnewVersion=${{ github.event.inputs.tag }} - name: Deploy diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml index c74c504..885c659 100644 --- a/.github/workflows/publish-github.yml +++ b/.github/workflows/publish-github.yml @@ -8,17 +8,13 @@ jobs: if: startsWith(github.ref, 'refs/tags/') # only allow publishing tagged versions steps: - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/setup-java@v2 with: - java-version: 11 + distribution: 'temurin' + java-version: 17 + cache: 'maven' gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase - - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - name: Enforce project version ${{ github.event.release.tag_name }} run: mvn versions:set -B -DnewVersion=${{ github.event.release.tag_name }} - name: Deploy diff --git a/pom.xml b/pom.xml index 0922667..dc00bca 100644 --- a/pom.xml +++ b/pom.xml @@ -42,9 +42,9 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.9.0 - 11 + 17 @@ -72,7 +72,7 @@ true - 11 + 17 @@ -110,7 +110,7 @@ maven-gpg-plugin - 1.6 + 3.0.1 sign-artifacts From a53d887c59bee8f79a6cdfb49f5045033204465e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 18 Jan 2022 14:14:55 +0100 Subject: [PATCH 12/30] move creation of tray icon to integrations-api this will allow non-awt based tray icons --- src/main/java/module-info.java | 7 +++ .../integrations/tray/ActionItem.java | 4 ++ .../tray/AwtTrayMenuController.java | 45 +++++++++++++++++++ .../integrations/tray/SeparatorItem.java | 4 ++ .../integrations/tray/SubMenuItem.java | 6 +++ .../integrations/tray/TrayMenuController.java | 25 +++++++++++ .../integrations/tray/TrayMenuItem.java | 4 ++ 7 files changed, 95 insertions(+) create mode 100644 src/main/java/org/cryptomator/integrations/tray/ActionItem.java create mode 100644 src/main/java/org/cryptomator/integrations/tray/AwtTrayMenuController.java create mode 100644 src/main/java/org/cryptomator/integrations/tray/SeparatorItem.java create mode 100644 src/main/java/org/cryptomator/integrations/tray/SubMenuItem.java create mode 100644 src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java create mode 100644 src/main/java/org/cryptomator/integrations/tray/TrayMenuItem.java diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 4cadffc..f3381f4 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,13 @@ +import org.cryptomator.integrations.tray.AwtTrayMenuController; +import org.cryptomator.integrations.tray.TrayMenuController; + module org.cryptomator.integrations.api { + requires java.desktop; + exports org.cryptomator.integrations.autostart; exports org.cryptomator.integrations.keychain; exports org.cryptomator.integrations.tray; exports org.cryptomator.integrations.uiappearance; + + provides TrayMenuController with AwtTrayMenuController; } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/integrations/tray/ActionItem.java b/src/main/java/org/cryptomator/integrations/tray/ActionItem.java new file mode 100644 index 0000000..383618b --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/tray/ActionItem.java @@ -0,0 +1,4 @@ +package org.cryptomator.integrations.tray; + +public record ActionItem(String title, Runnable action) implements TrayMenuItem { +} diff --git a/src/main/java/org/cryptomator/integrations/tray/AwtTrayMenuController.java b/src/main/java/org/cryptomator/integrations/tray/AwtTrayMenuController.java new file mode 100644 index 0000000..85325ff --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/tray/AwtTrayMenuController.java @@ -0,0 +1,45 @@ +package org.cryptomator.integrations.tray; + + +import java.awt.*; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public class AwtTrayMenuController implements TrayMenuController { + + private TrayIcon trayIcon; + + @Override + public void showTrayIcon(InputStream iconData, String tooltip) throws IOException { + var image = Toolkit.getDefaultToolkit().createImage(iconData.readAllBytes()); + trayIcon = new TrayIcon(image, tooltip); + } + + @Override + public void setTrayMenu(List items) { + if (trayIcon != null) { + var menu = new PopupMenu(); + addChildren(menu, items); + trayIcon.setPopupMenu(menu); + } + } + + private void addChildren(Menu menu, List items) { + for (var item : items) { + // TODO: use Pattern Matching for switch, once available + if (item instanceof ActionItem a) { + var menuItem = new MenuItem(a.title()); + menuItem.addActionListener(evt -> a.action().run()); + menu.add(menuItem); + } else if (item instanceof SeparatorItem) { + menu.addSeparator(); + } else if (item instanceof SubMenuItem s) { + var submenu = new Menu(s.title()); + addChildren(submenu, s.items()); + menu.add(submenu); + } + } + } + +} diff --git a/src/main/java/org/cryptomator/integrations/tray/SeparatorItem.java b/src/main/java/org/cryptomator/integrations/tray/SeparatorItem.java new file mode 100644 index 0000000..7a4eab2 --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/tray/SeparatorItem.java @@ -0,0 +1,4 @@ +package org.cryptomator.integrations.tray; + +public record SeparatorItem() implements TrayMenuItem { +} diff --git a/src/main/java/org/cryptomator/integrations/tray/SubMenuItem.java b/src/main/java/org/cryptomator/integrations/tray/SubMenuItem.java new file mode 100644 index 0000000..82a65bf --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/tray/SubMenuItem.java @@ -0,0 +1,6 @@ +package org.cryptomator.integrations.tray; + +import java.util.List; + +public record SubMenuItem(String title, List items) implements TrayMenuItem { +} diff --git a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java new file mode 100644 index 0000000..b840f53 --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java @@ -0,0 +1,25 @@ +package org.cryptomator.integrations.tray; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public interface TrayMenuController { + + /** + * Adds an icon to the system tray. + * + * @param iconData What image to show + * @param tooltip Text shown when hovering + * @throws IOException thrown when interacting with the given iconData + */ + void showTrayIcon(InputStream iconData, String tooltip) throws IOException; + + /** + * Show the given options in the tray menu. + * + * @param items Menu items + */ + void setTrayMenu(List items); + +} diff --git a/src/main/java/org/cryptomator/integrations/tray/TrayMenuItem.java b/src/main/java/org/cryptomator/integrations/tray/TrayMenuItem.java new file mode 100644 index 0000000..a174c15 --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/tray/TrayMenuItem.java @@ -0,0 +1,4 @@ +package org.cryptomator.integrations.tray; + +public sealed interface TrayMenuItem permits ActionItem, SubMenuItem, SeparatorItem { +} From 9c6c76a8e93b96a01d456296b631704d16da4caf Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Jan 2022 15:39:44 +0100 Subject: [PATCH 13/30] added new annotations for priorization and filtering --- .../integrations/common/OperatingSystem.java | 51 +++++++++++++++++++ .../integrations/common/Priority.java | 23 +++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/main/java/org/cryptomator/integrations/common/OperatingSystem.java create mode 100644 src/main/java/org/cryptomator/integrations/common/Priority.java diff --git a/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java b/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java new file mode 100644 index 0000000..c6ec0e1 --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java @@ -0,0 +1,51 @@ +package org.cryptomator.integrations.common; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Restricts the annotated integration provider to one or more operating system(s). + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Repeatable(OperatingSystem.OperatingSystems.class) +public @interface OperatingSystem { + Value value() default Value.UNKNOWN; + + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.TYPE}) + @interface OperatingSystems { + OperatingSystem[] value(); + } + + enum Value { + LINUX, + MAC, + WINDOWS, + UNKNOWN; + + private static final String OS_NAME = System.getProperty("os.name").toLowerCase(); + + public static Value current() { + if (OS_NAME.contains("linux")) { + return LINUX; + } else if (OS_NAME.contains("mac")) { + return MAC; + } else if (OS_NAME.contains("windows")) { + return WINDOWS; + } else { + return UNKNOWN; + } + } + + public static boolean isCurrent(OperatingSystem os) { + return current().equals(os.value()); + } + } +} diff --git a/src/main/java/org/cryptomator/integrations/common/Priority.java b/src/main/java/org/cryptomator/integrations/common/Priority.java new file mode 100644 index 0000000..8bb9a00 --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/common/Priority.java @@ -0,0 +1,23 @@ +package org.cryptomator.integrations.common; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Integration Priority. + *

+ * If multiple implementations for an integration can be provided, the provider with the highest priority will be used. + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface Priority { + int DEFAULT = 0; + int FALLBACK = Integer.MIN_VALUE; + + int value() default DEFAULT; +} From df15e0dbbe13a9c18c786e04154f59c035a3b524 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Jan 2022 15:40:26 +0100 Subject: [PATCH 14/30] added improved ServiceLoader that respects annotations --- .../common/IntegrationsLoader.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java diff --git a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java new file mode 100644 index 0000000..7415abe --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java @@ -0,0 +1,38 @@ +package org.cryptomator.integrations.common; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; +import java.util.ServiceLoader; + +public class IntegrationsLoader { + + /** + * Loads the best suited service, 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 + */ + public static Optional load(Class clazz) { + return ServiceLoader.load(clazz) + .stream() + .filter(IntegrationsLoader::isSupportedOperatingSystem) + .sorted(Comparator.comparingInt(IntegrationsLoader::getPriority)) + .map(ServiceLoader.Provider::get) + .findFirst(); + } + + private static int getPriority(ServiceLoader.Provider provider) { + var prio = provider.type().getAnnotation(Priority.class); + return prio == null ? Priority.DEFAULT : prio.value(); + } + + private static boolean isSupportedOperatingSystem(ServiceLoader.Provider provider) { + var annotations = provider.type().getAnnotationsByType(OperatingSystem.class); + return annotations.length == 0 || Arrays.stream(annotations).anyMatch(OperatingSystem.Value::isCurrent); + } + +} From 998423be927ee3cd4acb9bc81833b8127ee5d82f Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Jan 2022 15:50:14 +0100 Subject: [PATCH 15/30] add static provider methods to all API interfaces --- src/main/java/module-info.java | 10 ++++++++++ .../autostart/AutoStartProvider.java | 8 ++++++++ .../integrations/common/IntegrationsLoader.java | 17 ++++++++++++++--- .../keychain/KeychainAccessProvider.java | 8 ++++++++ .../tray/TrayIntegrationProvider.java | 8 ++++++++ .../uiappearance/UiAppearanceProvider.java | 8 ++++++++ 6 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 4cadffc..0f3f469 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,16 @@ +import org.cryptomator.integrations.autostart.AutoStartProvider; +import org.cryptomator.integrations.keychain.KeychainAccessProvider; +import org.cryptomator.integrations.tray.TrayIntegrationProvider; +import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; + module org.cryptomator.integrations.api { exports org.cryptomator.integrations.autostart; exports org.cryptomator.integrations.keychain; exports org.cryptomator.integrations.tray; exports org.cryptomator.integrations.uiappearance; + + uses AutoStartProvider; + uses KeychainAccessProvider; + uses TrayIntegrationProvider; + uses UiAppearanceProvider; } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java b/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java index ade5871..80bf638 100644 --- a/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java +++ b/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java @@ -1,7 +1,15 @@ package org.cryptomator.integrations.autostart; +import org.cryptomator.integrations.common.IntegrationsLoader; + +import java.util.Optional; + public interface AutoStartProvider { + static Optional get() { + return IntegrationsLoader.load(AutoStartProvider.class); + } + void enable() throws ToggleAutoStartFailedException; void disable() throws ToggleAutoStartFailedException; diff --git a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java index 7415abe..f184d8c 100644 --- a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java +++ b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java @@ -4,6 +4,7 @@ import java.util.Comparator; import java.util.Optional; import java.util.ServiceLoader; +import java.util.stream.Stream; public class IntegrationsLoader { @@ -17,12 +18,22 @@ public class IntegrationsLoader { * @return Highest priority service or empty if no supported service was found */ public static Optional load(Class clazz) { + return loadAll(clazz).findFirst(); + } + + /** + * Loads all suited services ordered by priority in descending order. + * + * @param clazz Service class + * @param Type of the service + * @return An ordered stream of all suited service candidates + */ + public static Stream loadAll(Class clazz) { return ServiceLoader.load(clazz) .stream() .filter(IntegrationsLoader::isSupportedOperatingSystem) - .sorted(Comparator.comparingInt(IntegrationsLoader::getPriority)) - .map(ServiceLoader.Provider::get) - .findFirst(); + .sorted(Comparator.comparingInt(IntegrationsLoader::getPriority).reversed()) + .map(ServiceLoader.Provider::get); } private static int getPriority(ServiceLoader.Provider provider) { diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index e6ecafc..3a18d66 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -1,10 +1,18 @@ package org.cryptomator.integrations.keychain; +import org.cryptomator.integrations.common.IntegrationsLoader; + +import java.util.stream.Stream; + /** * This is the interface used by Cryptomator to store passwords securely in external keychains, such as system keychains or password managers. */ public interface KeychainAccessProvider { + static Stream get() { + return IntegrationsLoader.loadAll(KeychainAccessProvider.class).filter(KeychainAccessProvider::isSupported); + } + /** * A name to display in UI elements. If required, this should be localized. * diff --git a/src/main/java/org/cryptomator/integrations/tray/TrayIntegrationProvider.java b/src/main/java/org/cryptomator/integrations/tray/TrayIntegrationProvider.java index cf91862..0c330e5 100644 --- a/src/main/java/org/cryptomator/integrations/tray/TrayIntegrationProvider.java +++ b/src/main/java/org/cryptomator/integrations/tray/TrayIntegrationProvider.java @@ -1,7 +1,15 @@ package org.cryptomator.integrations.tray; +import org.cryptomator.integrations.common.IntegrationsLoader; + +import java.util.Optional; + public interface TrayIntegrationProvider { + static Optional get() { + return IntegrationsLoader.load(TrayIntegrationProvider.class); + } + /** * Performs tasks required when the application is no longer showing any window and only accessible via * system tray (or comparable facilities). diff --git a/src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceProvider.java b/src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceProvider.java index c97ec31..8dc0e7d 100644 --- a/src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceProvider.java +++ b/src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceProvider.java @@ -1,10 +1,18 @@ package org.cryptomator.integrations.uiappearance; +import org.cryptomator.integrations.common.IntegrationsLoader; + +import java.util.Optional; + /** * This is the interface used by Cryptomator to get os specific UI appearances and themes. */ public interface UiAppearanceProvider { + static Optional get() { + return IntegrationsLoader.load(UiAppearanceProvider.class); + } + /** * Gets the best-matching theme for the OS's current L&F. This might be an approximation, as the OS might support more variations than we do. * From 6263463872c02f74341e1f11846ef844c64de207 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Jan 2022 16:06:11 +0100 Subject: [PATCH 16/30] avoid NPE (even though os.name should always be defined on any JVM) --- .../org/cryptomator/integrations/common/OperatingSystem.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java b/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java index c6ec0e1..f618ac8 100644 --- a/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java +++ b/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java @@ -30,7 +30,7 @@ enum Value { WINDOWS, UNKNOWN; - private static final String OS_NAME = System.getProperty("os.name").toLowerCase(); + private static final String OS_NAME = System.getProperty("os.name", "").toLowerCase(); public static Value current() { if (OS_NAME.contains("linux")) { From 96e3251a9e8b7c3184fa2c47b1edf637ac1c3211 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 7 Mar 2022 09:11:56 +0100 Subject: [PATCH 17/30] removed fallback implementation (will be part of the main app) --- src/main/java/module-info.java | 5 -- .../tray/AwtTrayMenuController.java | 48 ------------------- 2 files changed, 53 deletions(-) delete mode 100644 src/main/java/org/cryptomator/integrations/tray/AwtTrayMenuController.java diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 529e1fb..2ac9eaf 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,4 +1,3 @@ -import org.cryptomator.integrations.tray.AwtTrayMenuController; import org.cryptomator.integrations.tray.TrayMenuController; import org.cryptomator.integrations.autostart.AutoStartProvider; import org.cryptomator.integrations.keychain.KeychainAccessProvider; @@ -7,8 +6,6 @@ module org.cryptomator.integrations.api { - requires java.desktop; - exports org.cryptomator.integrations.autostart; exports org.cryptomator.integrations.keychain; exports org.cryptomator.integrations.tray; @@ -19,6 +16,4 @@ uses TrayIntegrationProvider; uses TrayMenuController; uses UiAppearanceProvider; - - provides TrayMenuController with AwtTrayMenuController; } \ No newline at end of file diff --git a/src/main/java/org/cryptomator/integrations/tray/AwtTrayMenuController.java b/src/main/java/org/cryptomator/integrations/tray/AwtTrayMenuController.java deleted file mode 100644 index 0038dd3..0000000 --- a/src/main/java/org/cryptomator/integrations/tray/AwtTrayMenuController.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.cryptomator.integrations.tray; - - -import org.cryptomator.integrations.common.Priority; - -import java.awt.*; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -@Priority(Priority.FALLBACK) -public class AwtTrayMenuController implements TrayMenuController { - - private TrayIcon trayIcon; - - @Override - public void showTrayIcon(InputStream iconData, String tooltip) throws IOException { - var image = Toolkit.getDefaultToolkit().createImage(iconData.readAllBytes()); - trayIcon = new TrayIcon(image, tooltip); - } - - @Override - public void setTrayMenu(List items) { - if (trayIcon != null) { - var menu = new PopupMenu(); - addChildren(menu, items); - trayIcon.setPopupMenu(menu); - } - } - - private void addChildren(Menu menu, List items) { - for (var item : items) { - // TODO: use Pattern Matching for switch, once available - if (item instanceof ActionItem a) { - var menuItem = new MenuItem(a.title()); - menuItem.addActionListener(evt -> a.action().run()); - menu.add(menuItem); - } else if (item instanceof SeparatorItem) { - menu.addSeparator(); - } else if (item instanceof SubMenuItem s) { - var submenu = new Menu(s.title()); - addChildren(submenu, s.items()); - menu.add(submenu); - } - } - } - -} From e4137ffb4d6506bcd1d968fe1b05962882e09a81 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 7 Mar 2022 09:33:43 +0100 Subject: [PATCH 18/30] api fine tuning and documentation --- .../tray/TrayIntegrationProvider.java | 9 +++++++++ .../integrations/tray/TrayMenuController.java | 20 +++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/cryptomator/integrations/tray/TrayIntegrationProvider.java b/src/main/java/org/cryptomator/integrations/tray/TrayIntegrationProvider.java index 0c330e5..8650a2f 100644 --- a/src/main/java/org/cryptomator/integrations/tray/TrayIntegrationProvider.java +++ b/src/main/java/org/cryptomator/integrations/tray/TrayIntegrationProvider.java @@ -4,8 +4,17 @@ import java.util.Optional; +/** + * Allows to perform OS-specific tasks when the app gets minimized to or restored from a tray icon. + */ public interface TrayIntegrationProvider { + /** + * Loads the best-suited TrayIntegrationProvider. + * + * @return preferred TrayIntegrationProvider (if any) + * @since 1.1.0 + */ static Optional get() { return IntegrationsLoader.load(TrayIntegrationProvider.class); } diff --git a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java index 199be1b..48833af 100644 --- a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java +++ b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java @@ -7,6 +7,11 @@ import java.util.List; import java.util.Optional; +/** + * Displays a tray icon and menu + * + * @since 1.1.0 + */ public interface TrayMenuController { static Optional get() { @@ -14,19 +19,22 @@ static Optional get() { } /** - * Adds an icon to the system tray. + * Displays an icon on the system tray. * - * @param iconData What image to show - * @param tooltip Text shown when hovering - * @throws IOException thrown when interacting with the given iconData + * @param rawImageData 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 rawImageData */ - void showTrayIcon(InputStream iconData, String tooltip) throws IOException; + void showTrayIcon(InputStream rawImageData, Runnable defaultAction, String tooltip) throws IOException; /** * Show the given options in the tray menu. + *

+ * This method may be called multiple times, e.g. when the vault list changes. * * @param items Menu items */ - void setTrayMenu(List items); + void updateTrayMenu(List items); } From de302ae2db57ef22429cbe0374cd2080b3d780eb Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 7 Mar 2022 09:35:11 +0100 Subject: [PATCH 19/30] javadoc --- .../integrations/autostart/AutoStartProvider.java | 6 ++++++ .../cryptomator/integrations/common/OperatingSystem.java | 2 ++ .../java/org/cryptomator/integrations/common/Priority.java | 2 ++ .../integrations/keychain/KeychainAccessProvider.java | 6 ++++++ .../integrations/uiappearance/UiAppearanceProvider.java | 6 ++++++ 5 files changed, 22 insertions(+) diff --git a/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java b/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java index 80bf638..49f6e3a 100644 --- a/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java +++ b/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java @@ -6,6 +6,12 @@ public interface AutoStartProvider { + /** + * Loads the best-suited AutoStartProvider. + * + * @return preferred AutoStartProvider (if any) + * @since 1.1.0 + */ static Optional get() { return IntegrationsLoader.load(AutoStartProvider.class); } diff --git a/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java b/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java index f618ac8..3369011 100644 --- a/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java +++ b/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java @@ -9,6 +9,8 @@ /** * Restricts the annotated integration provider to one or more operating system(s). + * + * @since 1.1.0 */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/java/org/cryptomator/integrations/common/Priority.java b/src/main/java/org/cryptomator/integrations/common/Priority.java index 8bb9a00..81ee75d 100644 --- a/src/main/java/org/cryptomator/integrations/common/Priority.java +++ b/src/main/java/org/cryptomator/integrations/common/Priority.java @@ -11,6 +11,8 @@ * Integration Priority. *

* If multiple implementations for an integration can be provided, the provider with the highest priority will be used. + * + * @since 1.1.0 */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index 3a18d66..ef0cfae 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -9,6 +9,12 @@ */ public interface KeychainAccessProvider { + /** + * Loads all available KeychainAccessProvider. + * + * @return a stream of {@link #isSupported() supported} KeychainAccessProviders + * @since 1.1.0 + */ static Stream get() { return IntegrationsLoader.loadAll(KeychainAccessProvider.class).filter(KeychainAccessProvider::isSupported); } diff --git a/src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceProvider.java b/src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceProvider.java index 8dc0e7d..21e805c 100644 --- a/src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceProvider.java +++ b/src/main/java/org/cryptomator/integrations/uiappearance/UiAppearanceProvider.java @@ -9,6 +9,12 @@ */ public interface UiAppearanceProvider { + /** + * Loads the best-suited UiAppearanceProvider. + * + * @return preferred UiAppearanceProvider (if any) + * @since 1.1.0 + */ static Optional get() { return IntegrationsLoader.load(UiAppearanceProvider.class); } From 70fb01cccfde5211ad590a9ca25f76a65392f0d9 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 7 Mar 2022 09:49:42 +0100 Subject: [PATCH 20/30] added jetbrains annotations --- pom.xml | 9 +++++++++ src/main/java/module-info.java | 2 ++ .../integrations/autostart/AutoStartProvider.java | 3 +++ .../integrations/keychain/KeychainAccessProvider.java | 9 +++++++++ .../integrations/tray/TrayMenuController.java | 2 ++ 5 files changed, 25 insertions(+) diff --git a/pom.xml b/pom.xml index dc00bca..5d03a19 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,15 @@ + + + org.jetbrains + annotations + 23.0.0 + provided + + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 2ac9eaf..9d69841 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -6,6 +6,8 @@ module org.cryptomator.integrations.api { + requires static org.jetbrains.annotations; + exports org.cryptomator.integrations.autostart; exports org.cryptomator.integrations.keychain; exports org.cryptomator.integrations.tray; diff --git a/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java b/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java index 49f6e3a..3a71d20 100644 --- a/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java +++ b/src/main/java/org/cryptomator/integrations/autostart/AutoStartProvider.java @@ -1,6 +1,7 @@ package org.cryptomator.integrations.autostart; import org.cryptomator.integrations.common.IntegrationsLoader; +import org.jetbrains.annotations.Blocking; import java.util.Optional; @@ -16,8 +17,10 @@ static Optional get() { return IntegrationsLoader.load(AutoStartProvider.class); } + @Blocking void enable() throws ToggleAutoStartFailedException; + @Blocking void disable() throws ToggleAutoStartFailedException; boolean isEnabled(); diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index ef0cfae..8116316 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -1,6 +1,9 @@ package org.cryptomator.integrations.keychain; import org.cryptomator.integrations.common.IntegrationsLoader; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.Nls; import java.util.stream.Stream; @@ -24,6 +27,7 @@ static Stream get() { * * @return user-friendly name (must not be null or empty) */ + @Nls(capitalization = Nls.Capitalization.Title) String displayName(); /** @@ -35,6 +39,7 @@ static Stream get() { * @deprecated Please use {@link #storePassphrase(String, String, CharSequence)} instead */ @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "1.2.0") void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException; /** @@ -47,6 +52,7 @@ static Stream get() { * @param passphrase The secret to store in this keychain. * @throws KeychainAccessException If storing the password failed */ + @Blocking default void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { storePassphrase(key, passphrase); } @@ -56,6 +62,7 @@ default void storePassphrase(String key, String displayName, CharSequence passph * @return The stored passphrase for the given key or null if no value for the given key could be found. * @throws KeychainAccessException If loading the password failed */ + @Blocking char[] loadPassphrase(String key) throws KeychainAccessException; /** @@ -75,6 +82,7 @@ default void storePassphrase(String key, String displayName, CharSequence passph * @deprecated Please use {@link #changePassphrase(String, String, CharSequence)} instead */ @Deprecated + @ApiStatus.ScheduledForRemoval(inVersion = "1.2.0") void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException; /** @@ -87,6 +95,7 @@ default void storePassphrase(String key, String displayName, CharSequence passph * @param passphrase The secret to be updated in this keychain. * @throws KeychainAccessException If changing the password failed */ + @Blocking default void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { changePassphrase(key, passphrase); } diff --git a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java index 48833af..fadb980 100644 --- a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java +++ b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java @@ -1,6 +1,7 @@ package org.cryptomator.integrations.tray; import org.cryptomator.integrations.common.IntegrationsLoader; +import org.jetbrains.annotations.ApiStatus; import java.io.IOException; import java.io.InputStream; @@ -12,6 +13,7 @@ * * @since 1.1.0 */ +@ApiStatus.Experimental public interface TrayMenuController { static Optional get() { From fad104ccc52e4606002e21c005c3c29031b9f5d2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 7 Mar 2022 10:25:52 +0100 Subject: [PATCH 21/30] export `org.cryptomator.integrations.common` --- src/main/java/module-info.java | 1 + .../org/cryptomator/integrations/common/OperatingSystem.java | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 9d69841..89b0918 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -9,6 +9,7 @@ requires static org.jetbrains.annotations; exports org.cryptomator.integrations.autostart; + exports org.cryptomator.integrations.common; exports org.cryptomator.integrations.keychain; exports org.cryptomator.integrations.tray; exports org.cryptomator.integrations.uiappearance; diff --git a/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java b/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java index 3369011..39b0ec3 100644 --- a/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java +++ b/src/main/java/org/cryptomator/integrations/common/OperatingSystem.java @@ -1,5 +1,7 @@ package org.cryptomator.integrations.common; +import org.jetbrains.annotations.ApiStatus; + import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; @@ -16,6 +18,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Repeatable(OperatingSystem.OperatingSystems.class) +@ApiStatus.Experimental public @interface OperatingSystem { Value value() default Value.UNKNOWN; From 0749e3fb53c399b76b8aa3c3575cc2c375814d10 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 7 Mar 2022 11:03:08 +0100 Subject: [PATCH 22/30] implement workaround for broken nexus-staging-plugin --- .github/workflows/publish-central.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml index fee8726..f3897af 100644 --- a/.github/workflows/publish-central.yml +++ b/.github/workflows/publish-central.yml @@ -28,6 +28,11 @@ jobs: - name: Deploy run: mvn deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress env: + MAVEN_OPTS: > + --add-opens=java.base/java.util=ALL-UNNAMED + --add-opens=java.base/java.lang.reflect=ALL-UNNAMED + --add-opens=java.base/java.text=ALL-UNNAMED + --add-opens=java.desktop/java.awt.font=ALL-UNNAMED MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} \ No newline at end of file From 8a83ae27a7cfb6b5949056ccd95427f53acf4f0c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 8 Mar 2022 14:36:37 +0100 Subject: [PATCH 23/30] adjust IDE settings to use JDK 17 [ci skip] --- .idea/misc.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 92a9595..482738c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -7,5 +7,5 @@ - + \ No newline at end of file From 9272d89fe78b91c47e29095dac5d1d57d75ee362 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 8 Mar 2022 14:37:36 +0100 Subject: [PATCH 24/30] restore ability to load implementations from `cryptomator.pluginDir` --- pom.xml | 12 ++ .../common/ClassLoaderFactory.java | 66 +++++++++ .../common/IntegrationsLoader.java | 4 +- .../common/ClassLoaderFactoryTest.java | 135 ++++++++++++++++++ .../integrations/common/JarBuilder.java | 39 +++++ .../org.mockito.plugins.MockMaker | 1 + 6 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/cryptomator/integrations/common/ClassLoaderFactory.java create mode 100644 src/test/java/org/cryptomator/integrations/common/ClassLoaderFactoryTest.java create mode 100644 src/test/java/org/cryptomator/integrations/common/JarBuilder.java create mode 100644 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/pom.xml b/pom.xml index 5d03a19..3778449 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,18 @@ 23.0.0 provided + + org.junit.jupiter + junit-jupiter + 5.8.2 + test + + + org.mockito + mockito-core + 4.3.1 + test + diff --git a/src/main/java/org/cryptomator/integrations/common/ClassLoaderFactory.java b/src/main/java/org/cryptomator/integrations/common/ClassLoaderFactory.java new file mode 100644 index 0000000..89dab2d --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/common/ClassLoaderFactory.java @@ -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); + } + +} diff --git a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java index f184d8c..fb5381c 100644 --- a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java +++ b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java @@ -8,6 +8,8 @@ public class IntegrationsLoader { + private IntegrationsLoader(){} + /** * Loads the best suited service, i.e. the one with the highest priority that is supported. *

@@ -29,7 +31,7 @@ public static Optional load(Class clazz) { * @return An ordered stream of all suited service candidates */ public static Stream loadAll(Class clazz) { - return ServiceLoader.load(clazz) + return ServiceLoader.load(clazz, ClassLoaderFactory.forPluginDir()) .stream() .filter(IntegrationsLoader::isSupportedOperatingSystem) .sorted(Comparator.comparingInt(IntegrationsLoader::getPriority).reversed()) diff --git a/src/test/java/org/cryptomator/integrations/common/ClassLoaderFactoryTest.java b/src/test/java/org/cryptomator/integrations/common/ClassLoaderFactoryTest.java new file mode 100644 index 0000000..a5a8db5 --- /dev/null +++ b/src/test/java/org/cryptomator/integrations/common/ClassLoaderFactoryTest.java @@ -0,0 +1,135 @@ +package org.cryptomator.integrations.common; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Comparator; + +public class ClassLoaderFactoryTest { + + @Nested + @DisplayName("When two .jars exist in the plugin dir") + public class WithJars { + + private static final byte[] FOO_CONTENTS = "foo = 42".getBytes(); + private static final byte[] BAR_CONTENTS = "bar = 23".getBytes(); + private Path pluginDir; + + @BeforeEach + public void setup(@TempDir Path tmpDir) throws IOException { + Files.createDirectory(tmpDir.resolve("plugin1")); + try (var out = Files.newOutputStream(tmpDir.resolve("plugin1/foo.jar")); + var jar = JarBuilder.withTarget(out)) { + jar.addFile("foo.properties", new ByteArrayInputStream(FOO_CONTENTS)); + } + + Files.createDirectory(tmpDir.resolve("plugin2")); + try (var out = Files.newOutputStream(tmpDir.resolve("plugin2/bar.jar")); + var jar = JarBuilder.withTarget(out)) { + jar.addFile("bar.properties", new ByteArrayInputStream(BAR_CONTENTS)); + } + + this.pluginDir = tmpDir; + } + + @Test + @DisplayName("can load resources from both jars") + public void testForPluginDirWithPath() throws IOException { + var cl = ClassLoaderFactory.forPluginDirWithPath(pluginDir); + var fooContents = cl.getResourceAsStream("foo.properties").readAllBytes(); + var barContents = cl.getResourceAsStream("bar.properties").readAllBytes(); + + Assertions.assertArrayEquals(FOO_CONTENTS, fooContents); + Assertions.assertArrayEquals(BAR_CONTENTS, barContents); + } + + @Test + @DisplayName("can load resources when path is set in cryptomator.pluginDir") + public void testForPluginDirFromSysProp() throws IOException { + System.setProperty("cryptomator.pluginDir", pluginDir.toString()); + + var cl = ClassLoaderFactory.forPluginDir(); + var fooContents = cl.getResourceAsStream("foo.properties").readAllBytes(); + var barContents = cl.getResourceAsStream("bar.properties").readAllBytes(); + + Assertions.assertArrayEquals(FOO_CONTENTS, fooContents); + Assertions.assertArrayEquals(BAR_CONTENTS, barContents); + } + } + + @Test + @DisplayName("read path from cryptomator.pluginDir") + public void testReadPluginDirFromSysProp() { + var ucl = Mockito.mock(URLClassLoader.class, "ucl"); + var absPath = "/there/will/be/plugins"; + try (var mockedClass = Mockito.mockStatic(ClassLoaderFactory.class)) { + mockedClass.when(() -> ClassLoaderFactory.forPluginDir()).thenCallRealMethod(); + mockedClass.when(() -> ClassLoaderFactory.forPluginDirWithPath(Path.of(absPath))).thenReturn(ucl); + + System.setProperty("cryptomator.pluginDir", absPath); + var result = ClassLoaderFactory.forPluginDir(); + + Assertions.assertSame(ucl, result); + } + } + + @Test + @DisplayName("read path from cryptomator.pluginDir and replace ~/ with user.home") + public void testReadPluginDirFromSysPropAndReplaceHome() { + var ucl = Mockito.mock(URLClassLoader.class, "ucl"); + var relPath = "~/there/will/be/plugins"; + var absPath = Path.of(System.getProperty("user.home")).resolve("there/will/be/plugins"); + try (var mockedClass = Mockito.mockStatic(ClassLoaderFactory.class)) { + mockedClass.when(() -> ClassLoaderFactory.forPluginDir()).thenCallRealMethod(); + mockedClass.when(() -> ClassLoaderFactory.forPluginDirWithPath(absPath)).thenReturn(ucl); + + System.setProperty("cryptomator.pluginDir", relPath); + var result = ClassLoaderFactory.forPluginDir(); + + Assertions.assertSame(ucl, result); + } + } + + @Test + @DisplayName("findJars returns empty list if not containing jars") + public void testFindJars1(@TempDir Path tmpDir) throws IOException { + Files.createDirectories(tmpDir.resolve("dir1")); + Files.createFile(tmpDir.resolve("file1")); + + var urls = ClassLoaderFactory.findJars(tmpDir); + + Assertions.assertArrayEquals(new URL[0], urls); + } + + @Test + @DisplayName("findJars returns urls of found jars") + public void testFindJars2(@TempDir Path tmpDir) throws IOException { + Files.createDirectories(tmpDir.resolve("dir1")); + Files.createDirectories(tmpDir.resolve("dir2")); + Files.createDirectories(tmpDir.resolve("dir1").resolve("dir2")); + Files.createFile(tmpDir.resolve("a.jar")); + Files.createFile(tmpDir.resolve("a.txt")); + Files.createFile(tmpDir.resolve("dir2").resolve("b.jar")); + + var urls = ClassLoaderFactory.findJars(tmpDir); + + Arrays.sort(urls, Comparator.comparing(URL::toString)); + Assertions.assertArrayEquals(new URL[]{ + new URL(tmpDir.toUri() + "a.jar"), + new URL(tmpDir.toUri() + "dir2/b.jar") + }, urls); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/integrations/common/JarBuilder.java b/src/test/java/org/cryptomator/integrations/common/JarBuilder.java new file mode 100644 index 0000000..b6bf276 --- /dev/null +++ b/src/test/java/org/cryptomator/integrations/common/JarBuilder.java @@ -0,0 +1,39 @@ +package org.cryptomator.integrations.common; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +public class JarBuilder implements AutoCloseable { + + private final Manifest manifest = new Manifest(); + private final JarOutputStream jos; + + public JarBuilder(JarOutputStream jos) { + this.jos = jos; + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + } + + public static JarBuilder withTarget(OutputStream out) throws IOException { + return new JarBuilder(new JarOutputStream(out)); + } + + public void addFile(String path, InputStream content) throws IOException { + jos.putNextEntry(new JarEntry(path)); + content.transferTo(jos); + jos.closeEntry(); + } + + @Override + public void close() throws IOException { + jos.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME)); + manifest.write(jos); + jos.closeEntry(); + jos.close(); + } +} diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..ca6ee9c --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file From e7a693a69ddf84652fc48ea4d6a6965183e03aaf Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 8 Mar 2022 14:37:59 +0100 Subject: [PATCH 25/30] minor tray menu API adjustments --- .../cryptomator/integrations/tray/ActionItem.java | 6 +++++- .../integrations/tray/TrayMenuController.java | 9 +++++---- .../integrations/tray/TrayMenuException.java | 13 +++++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 src/main/java/org/cryptomator/integrations/tray/TrayMenuException.java diff --git a/src/main/java/org/cryptomator/integrations/tray/ActionItem.java b/src/main/java/org/cryptomator/integrations/tray/ActionItem.java index 383618b..b11a7c5 100644 --- a/src/main/java/org/cryptomator/integrations/tray/ActionItem.java +++ b/src/main/java/org/cryptomator/integrations/tray/ActionItem.java @@ -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); + } } diff --git a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java index fadb980..2d8b864 100644 --- a/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java +++ b/src/main/java/org/cryptomator/integrations/tray/TrayMenuController.java @@ -23,12 +23,12 @@ static Optional 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 rawImageData + * @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. @@ -36,7 +36,8 @@ static Optional get() { * 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 items); + void updateTrayMenu(List items) throws TrayMenuException; } diff --git a/src/main/java/org/cryptomator/integrations/tray/TrayMenuException.java b/src/main/java/org/cryptomator/integrations/tray/TrayMenuException.java new file mode 100644 index 0000000..e9b61da --- /dev/null +++ b/src/main/java/org/cryptomator/integrations/tray/TrayMenuException.java @@ -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); + } + +} \ No newline at end of file From d9785e0950496d05cf6f4050e292e4fcc2b792c9 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 8 Mar 2022 21:19:40 +0100 Subject: [PATCH 26/30] Filter service loading candidates using new `@CheckAvailability` annotation --- .idea/codeStyles/Project.xml | 1 + .../common/CheckAvailability.java | 48 +++++ .../common/IntegrationsLoader.java | 51 ++++- .../common/IntegrationsLoaderTest.java | 195 ++++++++++++++++++ 4 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/cryptomator/integrations/common/CheckAvailability.java create mode 100644 src/test/java/org/cryptomator/integrations/common/IntegrationsLoaderTest.java diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 7056c82..de2d1a2 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,5 +1,6 @@ +

    + *
  • Both the type and the method(s) must be annotated with {@code @CheckAvailability}
  • + *
  • Only public no-arg boolean methods are considered
  • + *
  • Methods may be {@code static}, in which case they get invoked before instantiating the service
  • + *
  • Should the method throw an exception, it has the same effect as returning {@code false}
  • + *
  • No specific execution order is guaranteed in case of multiple annotated methods
  • + *
  • Annotations must be present on classes or ancestor classes, not on interfaces
  • + *
+ * + * Example: + *
+ * {@code
+ * @CheckAvailability
+ * public class Foo {
+ *	@CheckAvailability
+ *	public static boolean isSupported() {
+ *		return "enabled".equals(System.getProperty("plugin.status"));
+ *	}
+ * }
+ * }
+ * 
+ *

+ * 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 { +} diff --git a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java index fb5381c..2fef2a3 100644 --- a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java +++ b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java @@ -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; @@ -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. @@ -34,8 +40,10 @@ public static Stream loadAll(Class 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) { @@ -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 boolean passesAvailabilityCheck(Class 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; + } + } diff --git a/src/test/java/org/cryptomator/integrations/common/IntegrationsLoaderTest.java b/src/test/java/org/cryptomator/integrations/common/IntegrationsLoaderTest.java new file mode 100644 index 0000000..5c7c41a --- /dev/null +++ b/src/test/java/org/cryptomator/integrations/common/IntegrationsLoaderTest.java @@ -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())); + } + + } + +} \ No newline at end of file From bdbe03e9ea272ef82417376ef33dd0000cef31a2 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 3 Apr 2022 15:16:15 +0200 Subject: [PATCH 27/30] add some debug logging to plugin loader --- pom.xml | 12 +++++++++ src/main/java/module-info.java | 1 + .../common/ClassLoaderFactory.java | 12 ++++++++- .../common/IntegrationsLoader.java | 25 ++++++++++++++++++- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 3778449..d4d7369 100644 --- a/pom.xml +++ b/pom.xml @@ -38,12 +38,24 @@ + + org.slf4j + slf4j-api + 1.7.36 + + org.jetbrains annotations 23.0.0 provided + + org.slf4j + slf4j-simple + 1.7.36 + test + org.junit.jupiter junit-jupiter diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 89b0918..12d8bd4 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -7,6 +7,7 @@ module org.cryptomator.integrations.api { requires static org.jetbrains.annotations; + requires org.slf4j; exports org.cryptomator.integrations.autostart; exports org.cryptomator.integrations.common; diff --git a/src/main/java/org/cryptomator/integrations/common/ClassLoaderFactory.java b/src/main/java/org/cryptomator/integrations/common/ClassLoaderFactory.java index 89dab2d..06abfb3 100644 --- a/src/main/java/org/cryptomator/integrations/common/ClassLoaderFactory.java +++ b/src/main/java/org/cryptomator/integrations/common/ClassLoaderFactory.java @@ -2,6 +2,8 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.UncheckedIOException; @@ -10,9 +12,12 @@ import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.stream.Collectors; class ClassLoaderFactory { + private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderFactory.class); 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"; @@ -38,7 +43,12 @@ public static URLClassLoader forPluginDir() { @VisibleForTesting @Contract(value = "_ -> new", pure = true) static URLClassLoader forPluginDirWithPath(Path path) throws UncheckedIOException { - return URLClassLoader.newInstance(findJars(path)); + var jars = findJars(path); + if (LOG.isDebugEnabled()) { + String jarList = Arrays.stream(jars).map(URL::getPath).collect(Collectors.joining(", ")); + LOG.debug("Found jars in cryptomator.pluginDir: {}", jarList); + } + return URLClassLoader.newInstance(jars); } @VisibleForTesting diff --git a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java index 2fef2a3..a1a570b 100644 --- a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java +++ b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java @@ -2,6 +2,8 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.lang.reflect.Method; import java.lang.reflect.Modifier; @@ -13,6 +15,8 @@ public class IntegrationsLoader { + private static final Logger LOG = LoggerFactory.getLogger(IntegrationsLoader.class); + private IntegrationsLoader() { } @@ -39,11 +43,19 @@ public static Optional load(Class clazz) { public static Stream loadAll(Class clazz) { return ServiceLoader.load(clazz, ClassLoaderFactory.forPluginDir()) .stream() + .peek(service -> logFoundService(clazz, service.type())) .filter(IntegrationsLoader::isSupportedOperatingSystem) .filter(IntegrationsLoader::passesStaticAvailabilityCheck) .sorted(Comparator.comparingInt(IntegrationsLoader::getPriority).reversed()) .map(ServiceLoader.Provider::get) - .filter(IntegrationsLoader::passesInstanceAvailabilityCheck); + .filter(IntegrationsLoader::passesInstanceAvailabilityCheck) + .peek(impl -> logServiceIsAvailable(clazz, impl.getClass())); + } + + private static void logFoundService(Class apiType, Class implType) { + if (LOG.isDebugEnabled()) { + LOG.debug("{}: Found implementation: {} in jar {}", apiType.getSimpleName(), implType.getName(), implType.getProtectionDomain().getCodeSource().getLocation().getPath()); + } } private static int getPriority(ServiceLoader.Provider provider) { @@ -70,10 +82,20 @@ static boolean passesInstanceAvailabilityCheck(Object instance) { return passesAvailabilityCheck(instance.getClass(), instance); } + private static void logServiceIsAvailable(Class apiType, Class implType) { + if (LOG.isDebugEnabled()) { + LOG.debug("{}: Implementation is available: {}", apiType.getSimpleName(), implType.getName()); + } + } + private static boolean passesAvailabilityCheck(Class type, @Nullable T instance) { if (!type.isAnnotationPresent(CheckAvailability.class)) { return true; // if type is not annotated, skip tests } + if (!type.getModule().isExported(type.getPackageName(), IntegrationsLoader.class.getModule())) { + LOG.warn("Can't run @CheckAvailability tests for class {}. Make sure to export {} to {}!", type.getName(), type.getPackageName(), IntegrationsLoader.class.getPackageName()); + return false; + } return Arrays.stream(type.getMethods()) .filter(m -> isAvailabilityCheck(m, instance == null)) .allMatch(m -> passesAvailabilityCheck(m, instance)); @@ -84,6 +106,7 @@ private static boolean passesAvailabilityCheck(Method m, @Nullable Object instan try { return (boolean) m.invoke(instance); } catch (ReflectiveOperationException e) { + LOG.warn("Failed to invoke @CheckAvailability test {}#{}", m.getDeclaringClass(), m.getName(), e); return false; } } From 984a03878d5f04dc1d4ad04793e73cf38f84753e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 3 Apr 2022 15:38:40 +0200 Subject: [PATCH 28/30] increase log severity in case of malconfigured plugin --- .../org/cryptomator/integrations/common/IntegrationsLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java index a1a570b..64a3977 100644 --- a/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java +++ b/src/main/java/org/cryptomator/integrations/common/IntegrationsLoader.java @@ -93,7 +93,7 @@ private static boolean passesAvailabilityCheck(Class type, @Nul return true; // if type is not annotated, skip tests } if (!type.getModule().isExported(type.getPackageName(), IntegrationsLoader.class.getModule())) { - LOG.warn("Can't run @CheckAvailability tests for class {}. Make sure to export {} to {}!", type.getName(), type.getPackageName(), IntegrationsLoader.class.getPackageName()); + LOG.error("Can't run @CheckAvailability tests for class {}. Make sure to export {} to {}!", type.getName(), type.getPackageName(), IntegrationsLoader.class.getPackageName()); return false; } return Arrays.stream(type.getMethods()) From 1ee3e204aeb4bc61485c295380480209ebd54c06 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 13 Apr 2022 06:00:10 +0200 Subject: [PATCH 29/30] invert defaults for deprecated methods --- .../keychain/KeychainAccessProvider.java | 16 +++++------ .../keychain/KeychainAccessProviderTest.java | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/cryptomator/integrations/keychain/KeychainAccessProviderTest.java diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index 8116316..f9f2e53 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -40,7 +40,9 @@ static Stream get() { */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.2.0") - void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException; + default void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { + storePassphrase(key, null, passphrase); + } /** * Associates a passphrase with a given key and a name for that key. @@ -53,9 +55,7 @@ static Stream get() { * @throws KeychainAccessException If storing the password failed */ @Blocking - default void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { - storePassphrase(key, passphrase); - } + void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException; /** * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. @@ -83,7 +83,9 @@ default void storePassphrase(String key, String displayName, CharSequence passph */ @Deprecated @ApiStatus.ScheduledForRemoval(inVersion = "1.2.0") - void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException; + default void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException { + changePassphrase(key, null, passphrase); + } /** * Updates a passphrase with a given key and stores a name for that key. Noop, if there is no item for the given key. @@ -96,9 +98,7 @@ default void storePassphrase(String key, String displayName, CharSequence passph * @throws KeychainAccessException If changing the password failed */ @Blocking - default void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException { - changePassphrase(key, passphrase); - } + void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException; /** * @return true if this KeychainAccessIntegration works on the current machine. diff --git a/src/test/java/org/cryptomator/integrations/keychain/KeychainAccessProviderTest.java b/src/test/java/org/cryptomator/integrations/keychain/KeychainAccessProviderTest.java new file mode 100644 index 0000000..1124fea --- /dev/null +++ b/src/test/java/org/cryptomator/integrations/keychain/KeychainAccessProviderTest.java @@ -0,0 +1,28 @@ +package org.cryptomator.integrations.keychain; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class KeychainAccessProviderTest { + + @Test + public void testStorePassphrase() throws KeychainAccessException { + var provider = Mockito.mock(KeychainAccessProvider.class); + Mockito.doCallRealMethod().when(provider).storePassphrase(Mockito.anyString(), Mockito.anyString()); + + provider.storePassphrase("key", "pass"); + + Mockito.verify(provider).storePassphrase("key", null, "pass"); + } + + @Test + public void testChangePassphrase() throws KeychainAccessException { + var provider = Mockito.mock(KeychainAccessProvider.class); + Mockito.doCallRealMethod().when(provider).changePassphrase(Mockito.anyString(), Mockito.anyString()); + + provider.changePassphrase("key", "pass"); + + Mockito.verify(provider).changePassphrase("key", null, "pass"); + } + +} \ No newline at end of file From 878bccb65ab02afd6b029e8b1f8bd0ed869125d0 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 27 Apr 2022 14:14:42 +0200 Subject: [PATCH 30/30] mark displayName as nullable --- .../integrations/keychain/KeychainAccessProvider.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java index 4850e8f..3099528 100644 --- a/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java +++ b/src/main/java/org/cryptomator/integrations/keychain/KeychainAccessProvider.java @@ -4,6 +4,7 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.Nullable; import java.util.stream.Stream; @@ -55,7 +56,7 @@ default void storePassphrase(String key, CharSequence passphrase) throws Keychai * @throws KeychainAccessException If storing the password failed */ @Blocking - void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException; + void storePassphrase(String key, @Nullable String displayName, CharSequence passphrase) throws KeychainAccessException; /** * @param key Unique key previously used while {@link #storePassphrase(String, String, CharSequence)} storing a passphrase}. @@ -98,7 +99,7 @@ default void changePassphrase(String key, CharSequence passphrase) throws Keycha * @throws KeychainAccessException If changing the password failed */ @Blocking - void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException; + void changePassphrase(String key, @Nullable String displayName, CharSequence passphrase) throws KeychainAccessException; /** * @return true if this KeychainAccessIntegration works on the current machine.