@@ -31,7 +31,7 @@
1.0.0
1.0.0
1.0.0
- 1.0.0
+ 1.0.1
1.3.3
1.3.3
1.2.6
diff --git a/src/main/java/org/cryptomator/common/ErrorCode.java b/src/main/java/org/cryptomator/common/ErrorCode.java
index 51fb355b6..7def1287b 100644
--- a/src/main/java/org/cryptomator/common/ErrorCode.java
+++ b/src/main/java/org/cryptomator/common/ErrorCode.java
@@ -1,5 +1,6 @@
package org.cryptomator.common;
+import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
@@ -80,7 +81,7 @@ public class ErrorCode {
if (causalChain.size() > 1) {
var rootCause = causalChain.get(causalChain.size() - 1);
var parentOfRootCause = causalChain.get(causalChain.size() - 2);
- var rootSpecificFrames = nonOverlappingFrames(parentOfRootCause.getStackTrace(), rootCause.getStackTrace());
+ var rootSpecificFrames = countTopmostFrames(rootCause.getStackTrace(), parentOfRootCause.getStackTrace());
return new ErrorCode(throwable, rootCause, rootSpecificFrames);
} else {
return new ErrorCode(throwable, throwable, ALL_FRAMES);
@@ -107,11 +108,31 @@ public class ErrorCode {
return result;
}
- private static int nonOverlappingFrames(StackTraceElement[] frames, StackTraceElement[] enclosingFrames) {
- // Compute the number of elements in `frames` not contained in `enclosingFrames` by iterating backwards
- // Result should usually be equal to the difference in size of both traces
- var i = reverseStream(enclosingFrames).iterator();
- return (int) reverseStream(frames).dropWhile(f -> i.hasNext() && i.next().equals(f)).count();
+ /**
+ * Counts the number of additional frames contained in allFrames but not in bottomFrames.
+ *
+ * If allFrames does not end with bottomFrames, it is considered distinct and all its frames are counted.
+ *
+ * @param allFrames Some stack frames
+ * @param bottomFrames Other stack frames, potentially forming the bottom of the stack of allFrames
+ * @return The number of additional frames in allFrames. In most cases this should be equal to the difference in size.
+ */
+ // visible for testing
+ static int countTopmostFrames(StackTraceElement[] allFrames, StackTraceElement[] bottomFrames) {
+ if (allFrames.length < bottomFrames.length) {
+ // if frames had been stacked on top of bottomFrames, allFrames would be larger
+ return allFrames.length;
+ } else {
+ return allFrames.length - commonSuffixLength(allFrames, bottomFrames);
+ }
+ }
+
+ // visible for testing
+ static int commonSuffixLength(T[] set, T[] subset) {
+ Preconditions.checkArgument(set.length >= subset.length);
+ // iterate items backwards as long as they are identical
+ var iterator = reverseStream(subset).iterator();
+ return (int) reverseStream(set).takeWhile(item -> iterator.hasNext() && iterator.next().equals(item)).count();
}
private static Stream reverseStream(T[] array) {
diff --git a/src/main/java/org/cryptomator/ipc/IpcCommunicator.java b/src/main/java/org/cryptomator/ipc/IpcCommunicator.java
index 0120389c9..776299549 100644
--- a/src/main/java/org/cryptomator/ipc/IpcCommunicator.java
+++ b/src/main/java/org/cryptomator/ipc/IpcCommunicator.java
@@ -44,7 +44,9 @@ public interface IpcCommunicator extends Closeable {
}
// Didn't get any connection yet? I.e. we're the first app instance, so let's launch a server:
try {
- return Server.create(socketPaths.iterator().next());
+ final var socketPath = socketPaths.iterator().next();
+ Files.deleteIfExists(socketPath); // ensure path does not exist before creating it
+ return Server.create(socketPath);
} catch (IOException e) {
LOG.warn("Failed to create IPC server", e);
return new LoopbackCommunicator();
diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java
index 214ed19b0..4fceaa929 100644
--- a/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java
+++ b/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java
@@ -56,11 +56,10 @@ public class ChooseExistingVaultController implements FxController {
@FXML
public void initialize() {
- final String resource = SystemUtils.IS_OS_MAC ? "/img/select-masterkey-mac.png" : "/img/select-masterkey-win.png";
- try (InputStream in = getClass().getResourceAsStream(resource)) {
- this.screenshot = new Image(in);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
+ if (SystemUtils.IS_OS_MAC) {
+ this.screenshot = new Image(getClass().getResource("/img/select-masterkey-mac.png").toString());
+ } else {
+ this.screenshot = new Image(getClass().getResource("/img/select-masterkey-win.png").toString());
}
}
@@ -73,7 +72,7 @@ public class ChooseExistingVaultController implements FxController {
public void chooseFileAndNext() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
- fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
+ fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Vault", "*.cryptomator"));
File masterkeyFile = fileChooser.showOpenDialog(window);
if (masterkeyFile != null) {
vaultPath.setValue(masterkeyFile.toPath().toAbsolutePath().getParent());
diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
index 578b90969..b71bf0569 100644
--- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
+++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
@@ -44,8 +44,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.SecureRandom;
+import java.util.Comparator;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@@ -195,12 +197,28 @@ public class CreateNewVaultPasswordController implements FxController {
} catch (CryptoException e) {
throw new IOException("Failed initialize vault.", e);
}
+ } finally {
+ AtomicBoolean cleanupFailed = new AtomicBoolean(false);
+ Files.walk(path)
+ .sorted(Comparator.reverseOrder())
+ .forEach(p -> {
+ try {
+ Files.deleteIfExists(p);
+ } catch (IOException e) {
+ cleanupFailed.set(false);
+ }
+ });
+ if(cleanupFailed.get()) {
+ LOG.warn("Failed to cleanup after failed vault creation at {}. Leftovers need to be deleted manually.", path);
+ }
}
// 4. write vault-external readme file:
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
+ } catch (IOException e) {
+ LOG.warn("Unable to create vault storage location readme.", e);
}
LOG.info("Created vault at {}", path);
diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java b/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java
index 6b87ab59a..2c176df76 100644
--- a/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java
+++ b/src/main/java/org/cryptomator/ui/traymenu/TrayIconController.java
@@ -2,9 +2,6 @@ package org.cryptomator.ui.traymenu;
import com.google.common.base.Preconditions;
import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.integrations.uiappearance.Theme;
-import org.cryptomator.integrations.uiappearance.UiAppearanceException;
-import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -12,38 +9,25 @@ import javax.inject.Inject;
import java.awt.AWTException;
import java.awt.SystemTray;
import java.awt.TrayIcon;
-import java.util.Optional;
@TrayMenuScoped
public class TrayIconController {
private static final Logger LOG = LoggerFactory.getLogger(TrayIconController.class);
- private final TrayImageFactory imageFactory;
- private final Optional appearanceProvider;
private final TrayMenuController trayMenuController;
private final TrayIcon trayIcon;
private volatile boolean initialized;
@Inject
- TrayIconController(TrayImageFactory imageFactory, TrayMenuController trayMenuController, Optional appearanceProvider) {
+ TrayIconController(TrayImageFactory imageFactory, TrayMenuController trayMenuController) {
this.trayMenuController = trayMenuController;
- this.imageFactory = imageFactory;
- this.appearanceProvider = appearanceProvider;
this.trayIcon = new TrayIcon(imageFactory.loadImage(), "Cryptomator", trayMenuController.getMenu());
}
public synchronized void initializeTrayIcon() throws IllegalStateException {
Preconditions.checkState(!initialized);
- appearanceProvider.ifPresent(appearanceProvider -> {
- try {
- appearanceProvider.addListener(this::systemInterfaceThemeChanged);
- } catch (UiAppearanceException e) {
- LOG.error("Failed to enable automatic tray icon theme switching.");
- }
- });
-
trayIcon.setImageAutoSize(true);
if (SystemUtils.IS_OS_WINDOWS) {
trayIcon.addActionListener(trayMenuController::showMainWindow);
@@ -61,10 +45,6 @@ public class TrayIconController {
this.initialized = true;
}
- private void systemInterfaceThemeChanged(Theme theme) {
- trayIcon.setImage(imageFactory.loadImage()); // TODO refactor "theme" is re-queried in loadImage()
- }
-
public boolean isInitialized() {
return initialized;
}
diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java b/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java
index d9fcdfc29..aa55ca766 100644
--- a/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java
+++ b/src/main/java/org/cryptomator/ui/traymenu/TrayImageFactory.java
@@ -25,11 +25,7 @@ class TrayImageFactory {
}
private String getMacResourceName() {
- var theme = appearanceProvider.map(UiAppearanceProvider::getSystemTheme).orElse(Theme.LIGHT);
- return switch (theme) {
- case DARK -> "/img/tray_icon_mac_white.png";
- case LIGHT -> "/img/tray_icon_mac_black.png";
- };
+ return "/img/tray_icon_mac.png";
}
private String getWinOrLinuxResourceName() {
diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties
index 61476d9b9..1cb5eb0b8 100644
--- a/src/main/resources/i18n/strings.properties
+++ b/src/main/resources/i18n/strings.properties
@@ -76,9 +76,9 @@ addvault.new.readme.accessLocation.2=This is your vault's access location.
addvault.new.readme.accessLocation.3=Any files added to this volume will be encrypted by Cryptomator. You can work on it like on any other drive/folder. This is only a decrypted view of its content, your files stay encrypted on your hard drive all the time.
addvault.new.readme.accessLocation.4=Feel free to remove this file.
## Existing
-addvaultwizard.existing.instruction=Choose the "masterkey.cryptomator" file of your existing vault.
+addvaultwizard.existing.instruction=Choose the "vault.cryptomator" file of your existing vault. If only a file named "masterkey.cryptomator" exists, select that instead.
addvaultwizard.existing.chooseBtn=Choose…
-addvaultwizard.existing.filePickerTitle=Select Masterkey File
+addvaultwizard.existing.filePickerTitle=Select Vault File
## Success
addvaultwizard.success.nextStepsInstructions=Added vault "%s".\nYou need to unlock this vault to access or add contents. Alternatively you can unlock it at any later point in time.
addvaultwizard.success.unlockNow=Unlock Now
diff --git a/src/main/resources/img/select-masterkey-mac-dark.png b/src/main/resources/img/select-masterkey-mac-dark.png
new file mode 100644
index 000000000..0f1f62a04
Binary files /dev/null and b/src/main/resources/img/select-masterkey-mac-dark.png differ
diff --git a/src/main/resources/img/select-masterkey-mac-dark@2x.png b/src/main/resources/img/select-masterkey-mac-dark@2x.png
new file mode 100644
index 000000000..db81bf652
Binary files /dev/null and b/src/main/resources/img/select-masterkey-mac-dark@2x.png differ
diff --git a/src/main/resources/img/select-masterkey-mac.png b/src/main/resources/img/select-masterkey-mac.png
index c5aacd82f..355814840 100644
Binary files a/src/main/resources/img/select-masterkey-mac.png and b/src/main/resources/img/select-masterkey-mac.png differ
diff --git a/src/main/resources/img/select-masterkey-mac@2x.png b/src/main/resources/img/select-masterkey-mac@2x.png
new file mode 100644
index 000000000..672de4822
Binary files /dev/null and b/src/main/resources/img/select-masterkey-mac@2x.png differ
diff --git a/src/main/resources/img/select-masterkey-win.png b/src/main/resources/img/select-masterkey-win.png
index 81d29214d..d933c9fb6 100644
Binary files a/src/main/resources/img/select-masterkey-win.png and b/src/main/resources/img/select-masterkey-win.png differ
diff --git a/src/main/resources/img/select-masterkey-win@2x.png b/src/main/resources/img/select-masterkey-win@2x.png
new file mode 100644
index 000000000..4de5a0f12
Binary files /dev/null and b/src/main/resources/img/select-masterkey-win@2x.png differ
diff --git a/src/main/resources/img/tray_icon_mac.png b/src/main/resources/img/tray_icon_mac.png
new file mode 100755
index 000000000..b0f2c7894
Binary files /dev/null and b/src/main/resources/img/tray_icon_mac.png differ
diff --git a/src/main/resources/img/tray_icon_mac@2x.png b/src/main/resources/img/tray_icon_mac@2x.png
new file mode 100755
index 000000000..a6cdd32b9
Binary files /dev/null and b/src/main/resources/img/tray_icon_mac@2x.png differ
diff --git a/src/main/resources/img/tray_icon_mac_black.png b/src/main/resources/img/tray_icon_mac_black.png
deleted file mode 100644
index 42d6aad40..000000000
Binary files a/src/main/resources/img/tray_icon_mac_black.png and /dev/null differ
diff --git a/src/main/resources/img/tray_icon_mac_black@2x.png b/src/main/resources/img/tray_icon_mac_black@2x.png
deleted file mode 100644
index c93a96579..000000000
Binary files a/src/main/resources/img/tray_icon_mac_black@2x.png and /dev/null differ
diff --git a/src/main/resources/img/tray_icon_mac_white.png b/src/main/resources/img/tray_icon_mac_white.png
deleted file mode 100644
index d178cd017..000000000
Binary files a/src/main/resources/img/tray_icon_mac_white.png and /dev/null differ
diff --git a/src/main/resources/img/tray_icon_mac_white@2x.png b/src/main/resources/img/tray_icon_mac_white@2x.png
deleted file mode 100644
index 8c721bbce..000000000
Binary files a/src/main/resources/img/tray_icon_mac_white@2x.png and /dev/null differ
diff --git a/src/test/java/org/cryptomator/common/ErrorCodeTest.java b/src/test/java/org/cryptomator/common/ErrorCodeTest.java
index 34c0c2ec0..ebe6d90ce 100644
--- a/src/test/java/org/cryptomator/common/ErrorCodeTest.java
+++ b/src/test/java/org/cryptomator/common/ErrorCodeTest.java
@@ -1,59 +1,119 @@
package org.cryptomator.common;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
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.params.ParameterizedTest;
+import org.junit.jupiter.params.converter.ConvertWith;
+import org.junit.jupiter.params.converter.SimpleArgumentConverter;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.mockito.Mockito;
public class ErrorCodeTest {
- private static ErrorCode codeCaughtFrom(RunnableThrowingException runnable) {
- try {
- runnable.run();
- throw new IllegalStateException("should not reach this point");
- } catch (RuntimeException e) {
- return ErrorCode.of(e);
- }
- }
+ private final StackTraceElement foo = new StackTraceElement("ErrorCodeTest", "foo", null, 0);
+ private final StackTraceElement bar = new StackTraceElement("ErrorCodeTest", "bar", null, 0);
+ private final StackTraceElement baz = new StackTraceElement("ErrorCodeTest", "baz", null, 0);
+ private final Exception fooException = Mockito.mock(NullPointerException.class, "fooException");
@Test
@DisplayName("same exception leads to same error code")
- public void testDifferentErrorCodes() {
- var code1 = codeCaughtFrom(this::throwNpe);
- var code2 = codeCaughtFrom(this::throwNpe);
+ public void testDeterministicErrorCode() {
+ Mockito.doReturn(new StackTraceElement[]{foo, bar, baz}).when(fooException).getStackTrace();
+ var code1 = ErrorCode.of(fooException);
+ var code2 = ErrorCode.of(fooException);
Assertions.assertEquals(code1.toString(), code2.toString());
}
- private void throwNpe() {
- throwException(new NullPointerException());
+ @Test
+ @DisplayName("three error code segments change independently")
+ public void testErrorCodeSegments() {
+ Exception fooBarException = Mockito.mock(IndexOutOfBoundsException.class, "fooBarException");
+ Mockito.doReturn(new StackTraceElement[]{foo, foo, foo}).when(fooBarException).getStackTrace();
+ Mockito.doReturn(fooException).when(fooBarException).getCause();
+ Mockito.doReturn(new StackTraceElement[]{bar, bar, bar, foo, foo, foo}).when(fooException).getStackTrace();
+
+ var code = ErrorCode.of(fooBarException);
+
+ Assertions.assertNotEquals(code.throwableCode(), code.rootCauseCode());
+ Assertions.assertNotEquals(code.rootCauseCode(), code.methodCode());
}
- private void throwException(RuntimeException e) throws RuntimeException {
- throw e;
+ @DisplayName("commonSuffixLength()")
+ @ParameterizedTest
+ @CsvSource({"1 2 3, 1 2 3, 3", "1 2 3, 0 2 3, 2", "1 2 3 4, 3 4, 2", "1 2 3 4, 5 6, 0", "1 2 3 4 5 6,, 0",})
+ public void commonSuffixLength1(@ConvertWith(IntegerArrayConverter.class) Integer[] set, @ConvertWith(IntegerArrayConverter.class) Integer[] subset, int expected) {
+ var result = ErrorCode.commonSuffixLength(set, subset);
+
+ Assertions.assertEquals(expected, result);
}
- @DisplayName("when different cause but same root cause")
+ @DisplayName("commonSuffixLength() with too short array")
+ @ParameterizedTest
+ @CsvSource({"1 2, 3 4 5 6", ",1 2 3 4 5 6",})
+ public void commonSuffixLength2(@ConvertWith(IntegerArrayConverter.class) Integer[] set, @ConvertWith(IntegerArrayConverter.class) Integer[] subset) {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> {
+ ErrorCode.commonSuffixLength(set, subset);
+ });
+ }
+
+ @Test
+ @DisplayName("countTopmostFrames() with partially overlapping suffix")
+ public void testCountTopmostFrames1() {
+ var allFrames = new StackTraceElement[]{foo, bar, baz, bar, foo};
+ var bottomFrames = new StackTraceElement[]{baz, bar, foo};
+
+ int result = ErrorCode.countTopmostFrames(allFrames, bottomFrames);
+
+ Assertions.assertEquals(2, result);
+ }
+
+ @Test
+ @DisplayName("countTopmostFrames() without overlapping suffix")
+ public void testCountTopmostFrames2() {
+ var allFrames = new StackTraceElement[]{foo, foo, foo};
+ var bottomFrames = new StackTraceElement[]{bar, bar, bar};
+
+ int result = ErrorCode.countTopmostFrames(allFrames, bottomFrames);
+
+ Assertions.assertEquals(3, result);
+ }
+
+ @Test
+ @DisplayName("countUniqueFrames() fully overlapping")
+ public void testCountUniqueFrames3() {
+ var allFrames = new StackTraceElement[]{foo, bar, baz};
+ var bottomFrames = new StackTraceElement[]{foo, bar, baz};
+
+ int result = ErrorCode.countTopmostFrames(allFrames, bottomFrames);
+
+ Assertions.assertEquals(0, result);
+ }
+
+ @DisplayName("when different exception with same root cause")
@Nested
- public class SameRootCauseDifferentCause {
+ public class DifferentExceptionWithSameRootCause {
- private final ErrorCode code1 = codeCaughtFrom(this::foo);
- private final ErrorCode code2 = codeCaughtFrom(this::bar);
+ private final Exception fooBarException = Mockito.mock(IllegalArgumentException.class, "fooBarException");
+ private final Exception fooBazException = Mockito.mock(IndexOutOfBoundsException.class, "fooBazException");
- private void foo() throws IllegalArgumentException {
- try {
- throwNpe();
- } catch (NullPointerException e) {
- throw new IllegalArgumentException(e);
- }
- }
+ private ErrorCode code1;
+ private ErrorCode code2;
- private void bar() throws IllegalStateException {
- try {
- throwNpe();
- } catch (NullPointerException e) {
- throw new IllegalStateException(e);
- }
+ @BeforeEach
+ public void setup() {
+ Mockito.doReturn(new StackTraceElement[]{baz, bar, foo}).when(fooException).getStackTrace();
+ Mockito.doReturn(new StackTraceElement[]{foo}).when(fooBarException).getStackTrace();
+ Mockito.doReturn(new StackTraceElement[]{foo}).when(fooBazException).getStackTrace();
+ Mockito.doReturn(fooException).when(fooBarException).getCause();
+ Mockito.doReturn(fooException).when(fooBazException).getCause();
+ this.code1 = ErrorCode.of(fooBarException);
+ this.code2 = ErrorCode.of(fooBazException);
}
@Test
@@ -82,23 +142,21 @@ public class ErrorCodeTest {
}
- @DisplayName("when same cause but different call stack")
+ @DisplayName("when same exception with different call stacks")
@Nested
- public class SameCauseDifferentCallStack {
+ public class SameExceptionDifferentCallStack {
- private final ErrorCode code1 = codeCaughtFrom(this::foo);
- private final ErrorCode code2 = codeCaughtFrom(this::bar);
+ private final Exception barException = Mockito.mock(NullPointerException.class, "barException");
- private void foo() throws NullPointerException {
- try {
- throwNpe();
- } catch (NullPointerException e) {
- throw new IllegalArgumentException(e);
- }
- }
+ private ErrorCode code1;
+ private ErrorCode code2;
- private void bar() throws NullPointerException {
- foo();
+ @BeforeEach
+ public void setup() {
+ Mockito.doReturn(new StackTraceElement[]{foo, bar, baz}).when(fooException).getStackTrace();
+ Mockito.doReturn(new StackTraceElement[]{foo, baz, bar}).when(barException).getStackTrace();
+ this.code1 = ErrorCode.of(fooException);
+ this.code2 = ErrorCode.of(barException);
}
@Test
@@ -114,9 +172,9 @@ public class ErrorCodeTest {
}
@Test
- @DisplayName("rootCauseCodes are equal")
+ @DisplayName("rootCauseCodes are different")
public void testSameRootCauseCodes() {
- Assertions.assertEquals(code1.rootCauseCode(), code2.rootCauseCode());
+ Assertions.assertNotEquals(code1.rootCauseCode(), code2.rootCauseCode());
}
@Test
@@ -127,4 +185,19 @@ public class ErrorCodeTest {
}
+ public static class IntegerArrayConverter extends SimpleArgumentConverter {
+
+ @Override
+ protected Integer[] convert(Object source, Class> targetType) {
+ if (source == null) {
+ return new Integer[0];
+ } else if (source instanceof String s && Integer[].class.isAssignableFrom(targetType)) {
+ return Splitter.on(CharMatcher.inRange('0', '9').negate()).splitToStream(s).map(Integer::valueOf).toArray(Integer[]::new);
+ } else {
+ throw new IllegalArgumentException("Conversion from " + source.getClass() + " to " + targetType + " not supported.");
+ }
+ }
+
+ }
+
}
\ No newline at end of file