diff --git a/src/main/java/org/kiwiproject/base/KiwiPreconditions.java b/src/main/java/org/kiwiproject/base/KiwiPreconditions.java index cbe854d0..0127fb4f 100644 --- a/src/main/java/org/kiwiproject/base/KiwiPreconditions.java +++ b/src/main/java/org/kiwiproject/base/KiwiPreconditions.java @@ -220,6 +220,54 @@ public static void checkArgumentNotNull(T reference, String errorMessageTemp } } + /** + * Ensures that only one of two given arguments is null. + * Throws {@link IllegalArgumentException} if both are null or both are non-null. + * + * @param first the first argument + * @param second the second argument + * @param the object type + */ + public static void checkOnlyOneArgumentIsNull(T first, T second) { + Preconditions.checkArgument(isOnlyOneNull(first, second)); + } + + /** + * Ensures that only one of two given arguments is null. + * Throws {@link IllegalArgumentException} if both are null or both are non-null. + * + * @param first the first argument + * @param second the second argument + * @param message the error message to use if the check fails + * @param the object type + */ + public static void checkOnlyOneArgumentIsNull(T first, T second, String message) { + Preconditions.checkArgument(isOnlyOneNull(first, second), message); + } + + /** + * Ensures that only one of two given arguments is null. + * Throws {@link IllegalArgumentException} if both are null or both are non-null. + * + * @param first the first argument + * @param second the second argument + * @param errorMessageTemplate a template for the exception message should the check fail, according to how + * {@link KiwiStrings#format(String, Object...)} handles placeholders + * @param errorMessageArgs the arguments to be substituted into the message template. Arguments + * are converted to strings using {@link String#valueOf(Object)}. + * @param the object type + */ + public static void checkOnlyOneArgumentIsNull(T first, + T second, + String errorMessageTemplate, + Object... errorMessageArgs) { + Preconditions.checkArgument(isOnlyOneNull(first, second), errorMessageTemplate, errorMessageArgs); + } + + private static boolean isOnlyOneNull(T left, T right) { + return (nonNull(left) && isNull(right)) || (isNull(left) && nonNull(right)); + } + /** * Ensures that an object reference passed as a parameter to the calling method is null, throwing * an {@link IllegalArgumentException} if not null. diff --git a/src/test/java/org/kiwiproject/base/KiwiPreconditionsTest.java b/src/test/java/org/kiwiproject/base/KiwiPreconditionsTest.java index 9cb1ae84..a245af3b 100644 --- a/src/test/java/org/kiwiproject/base/KiwiPreconditionsTest.java +++ b/src/test/java/org/kiwiproject/base/KiwiPreconditionsTest.java @@ -7,6 +7,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.catchThrowable; import static org.assertj.core.util.Sets.newLinkedHashSet; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.kiwiproject.base.KiwiPreconditions.MAX_PORT_NUMBER; import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull; import static org.kiwiproject.base.KiwiPreconditions.requireNotBlank; @@ -212,6 +213,78 @@ void testCheckArgumentNotNull_MessageWithTemplate(SoftAssertions softly) { softly.assertThat(catchThrowable(() -> checkArgumentNotNull(new Object(), errorMessageTemplate, errorMessageArgs))).isNull(); } + @Nested + class CheckOnlyOneArgumentIsNull { + + @Test + void shouldCheckWithNoErrorMessage() { + assertAll( + () -> assertThatCode(() -> KiwiPreconditions.checkOnlyOneArgumentIsNull("a", null)) + .doesNotThrowAnyException(), + + () -> assertThatCode(() -> KiwiPreconditions.checkOnlyOneArgumentIsNull(null, "b")) + .doesNotThrowAnyException(), + + () -> assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiPreconditions.checkOnlyOneArgumentIsNull(null, null)) + .withMessage(null), + + () -> assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiPreconditions.checkOnlyOneArgumentIsNull("a", "b")) + .withMessage(null) + ); + } + + @Test + void shouldCheckWithErrorMessage() { + var message = "only one can be non-null"; + assertAll( + () -> assertThatCode(() -> + KiwiPreconditions.checkOnlyOneArgumentIsNull("a", null, message)) + .doesNotThrowAnyException(), + + () -> assertThatCode(() -> + KiwiPreconditions.checkOnlyOneArgumentIsNull(null, "b", message)) + .doesNotThrowAnyException(), + + () -> assertThatIllegalArgumentException().isThrownBy(() -> + KiwiPreconditions.checkOnlyOneArgumentIsNull(null, null, message)) + .withMessage(message), + + () -> assertThatIllegalArgumentException().isThrownBy(() -> + KiwiPreconditions.checkOnlyOneArgumentIsNull("a", "b", message)) + .withMessage(message) + ); + } + + @Test + void shouldCheckWithErrorMessageTemplate() { + var errorMessageTemplate = "only %s or %s can be non-null"; + var expectedMessage = "only a or b can be non-null"; + assertAll( + () -> assertThatCode(() -> + KiwiPreconditions.checkOnlyOneArgumentIsNull( + "a", null, errorMessageTemplate, "a", "b")) + .doesNotThrowAnyException(), + + () -> assertThatCode(() -> + KiwiPreconditions.checkOnlyOneArgumentIsNull( + null, "b", errorMessageTemplate, "a", "b")) + .doesNotThrowAnyException(), + + () -> assertThatIllegalArgumentException().isThrownBy(() -> + KiwiPreconditions.checkOnlyOneArgumentIsNull( + null, null, errorMessageTemplate, "a", "b")) + .withMessage(expectedMessage), + + () -> assertThatIllegalArgumentException().isThrownBy(() -> + KiwiPreconditions.checkOnlyOneArgumentIsNull( + "a", "b", errorMessageTemplate, "a", "b")) + .withMessage(expectedMessage) + ); + } + } + @Nested class CheckArgumentIsNull {