diff --git a/src/main/java/org/kiwiproject/base/KiwiPreconditions.java b/src/main/java/org/kiwiproject/base/KiwiPreconditions.java index df0e9e49..0dc75019 100644 --- a/src/main/java/org/kiwiproject/base/KiwiPreconditions.java +++ b/src/main/java/org/kiwiproject/base/KiwiPreconditions.java @@ -29,8 +29,8 @@ * If you're looking for preconditions related to validating arguments using Jakarta Beans Validation, they * are in {@link org.kiwiproject.validation.KiwiValidations KiwiValidations}. * - * @implNote Several methods in this class use Lombok's {@link lombok.SneakyThrows} so that they do not need to declare - * that they throw exceptions of type T, for the case that T is a checked exception. Read more details about + * @implNote Several methods in this class use Lombok {@link lombok.SneakyThrows} so that they do not need to declare + * that they throw {@code Exception}s of type T, for the case that T is a checked exception. Read more details about * how this works in {@link lombok.SneakyThrows}. Most notably, this should give you more insight into how the JVM (versus * Java the language) actually work: "The JVM does not check for the consistency of the checked exception system; * javac does, and this annotation lets you opt out of its mechanism." @@ -44,12 +44,12 @@ public class KiwiPreconditions { /** * Ensures the truth of an expression involving one or more parameters to the calling method. *

- * Throws an exception of type T if {@code expression} is false. + * Throws an {@code Exception} of type T if {@code expression} is false. * * @param expression a boolean expression * @param exceptionType the type of exception to be thrown if {@code expression} is false * @param the type of exception - * @implNote This uses Lombok's {@link lombok.SneakyThrows} to throw any checked exceptions without declaring them. + * @implNote This uses Lombok {@link lombok.SneakyThrows} to throw any checked exceptions without declaring them. */ @SneakyThrows(Throwable.class) public static void checkArgument(boolean expression, Class exceptionType) { @@ -61,13 +61,13 @@ public static void checkArgument(boolean expression, Class /** * Ensures the truth of an expression involving one or more parameters to the calling method. *

- * Throws an exception of type T if {@code expression} is false. + * Throws an {@code Exception} of type T if {@code expression} is false. * * @param expression a boolean expression * @param exceptionType the type of exception to be thrown if {@code expression} is false * @param errorMessage the exception message to use if the check fails * @param the type of exception - * @implNote This uses Lombok's {@link lombok.SneakyThrows} to throw any checked exceptions without declaring them. + * @implNote This uses Lombok {@link lombok.SneakyThrows} to throw any checked exceptions without declaring them. */ @SneakyThrows(Throwable.class) public static void checkArgument(boolean expression, @@ -82,7 +82,7 @@ public static void checkArgument(boolean expression, /** * Ensures the truth of an expression involving one or more parameters to the calling method. *

- * Throws an exception of type T if {@code expression} is false. + * Throws an {@code Exception} of type T if {@code expression} is false. * * @param expression a boolean expression * @param exceptionType the type of exception to be thrown if {@code expression} is false @@ -93,7 +93,7 @@ public static void checkArgument(boolean expression, * @param the type of exception * @throws NullPointerException if the check fails and either {@code errorMessageTemplate} or * {@code errorMessageArgs} is null (don't let this happen) - * @implNote This uses Lombok's {@link lombok.SneakyThrows} to throw any checked exceptions without declaring them. + * @implNote This uses Lombok {@link lombok.SneakyThrows} to throw any checked exceptions without declaring them. */ @SneakyThrows(Throwable.class) public static void checkArgument(boolean expression, @@ -152,7 +152,7 @@ public static String requireNotBlank(String value, String errorMessageTemplate, /** * Ensures that an object reference passed as a parameter to the calling method is not null, throwing - * an {@link IllegalArgumentException} if null or returning the (non null) reference otherwise. + * an {@link IllegalArgumentException} if null or returning the (non-null) reference otherwise. * * @param reference an object reference * @param the type of object @@ -165,7 +165,7 @@ public static T requireNotNull(T reference) { /** * Ensures that an object reference passed as a parameter to the calling method is not null, throwing - * an {@link IllegalArgumentException} if null or returning the (non null) reference otherwise. + * an {@link IllegalArgumentException} if null or returning the (non-null) reference otherwise. * * @param reference an object reference * @param errorMessageTemplate a template for the exception message should the check fail, according to how @@ -421,12 +421,6 @@ public static void checkArgumentNotEmpty(Map map, } } - private static IllegalArgumentException newIllegalArgumentException(String errorMessageTemplate, - Object... errorMessageArgs) { - var errorMessage = format(errorMessageTemplate, errorMessageArgs); - return new IllegalArgumentException(errorMessage); - } - /** * Ensures that a collection of items has an even count, throwing an {@link IllegalArgumentException} if * items is null or there is an odd number of items. @@ -954,4 +948,105 @@ public static int requireValidNonZeroPort(int port, String errorMessageTemplate, return port; } + /** + * Ensures the argument has the expected type. + * + * @param argument the argument to check + * @param requiredType the type that the argument is required to be + * @param the class of the required type + */ + public static void checkArgumentInstanceOf(T argument, Class requiredType) { + Preconditions.checkArgument(isInstanceOf(argument, requiredType)); + } + + /** + * Ensures the argument has the expected type. + * + * @param argument the argument to check + * @param requiredType the type that the argument is required to be + * @param errorMessage the error message to put in the exception if the argument is not the required type + * @param the class of the required type + */ + public static void checkArgumentInstanceOf(T argument, Class requiredType, String errorMessage) { + Preconditions.checkArgument(isInstanceOf(argument, requiredType), errorMessage); + } + + /** + * Ensures the argument has the expected type. + * + * @param argument the argument to check + * @param requiredType the type that the argument is required to be + * @param errorMessageTemplate the error message template to use in the exception if the argument is not the + * required type, according to how {@link KiwiStrings#format(String, Object...)} + * handles placeholders + * @param errorMessageArgs the arguments to populate into the error message template + * @param the class of the required type + */ + public static void checkArgumentInstanceOf(T argument, + Class requiredType, + String errorMessageTemplate, + Object... errorMessageArgs) { + + if (isNotInstanceOf(argument, requiredType)) { + throw newIllegalArgumentException(errorMessageTemplate, errorMessageArgs); + } + } + + /** + * Ensures the argument type is not the restricted type. + * + * @param argument the argument to check + * @param restrictedType the type that the argument must not be + * @param the class of the restricted type + */ + public static void checkArgumentNotInstanceOf(T argument, Class restrictedType) { + Preconditions.checkArgument(isNotInstanceOf(argument, restrictedType)); + } + + /** + * Ensures the argument type is not the restricted type. + * + * @param argument the argument to check + * @param restrictedType the type that the argument must not be + * @param errorMessage the error message to put in the exception if the argument is of the restricted type + * @param the class of the restricted type + */ + public static void checkArgumentNotInstanceOf(T argument, Class restrictedType, String errorMessage) { + Preconditions.checkArgument(isNotInstanceOf(argument, restrictedType), errorMessage); + } + + /** + * Ensures the argument type is not the restricted type. + * + * @param argument the argument to check + * @param restrictedType the type that the argument must not be + * @param errorMessageTemplate the error message to use in the exception if the argument is of the restricted type, + * according to how {@link KiwiStrings#format(String, Object...)} handles placeholders + * @param errorMessageArgs the arguments to populate into the error message template + * @param the class of the restricted type + */ + public static void checkArgumentNotInstanceOf(T argument, + Class restrictedType, + String errorMessageTemplate, + Object... errorMessageArgs) { + + if (isInstanceOf(argument, restrictedType)) { + throw newIllegalArgumentException(errorMessageTemplate, errorMessageArgs); + } + } + + private static boolean isInstanceOf(T argument, Class requiredType) { + return nonNull(argument) && requiredType.isAssignableFrom(argument.getClass()); + } + + private static boolean isNotInstanceOf(T argument, Class restrictedType) { + return isNull(argument) || !restrictedType.isAssignableFrom(argument.getClass()); + } + + private static IllegalArgumentException newIllegalArgumentException(String errorMessageTemplate, + Object... errorMessageArgs) { + var errorMessage = format(errorMessageTemplate, errorMessageArgs); + return new IllegalArgumentException(errorMessage); + } + } diff --git a/src/test/java/org/kiwiproject/base/KiwiPreconditionsTest.java b/src/test/java/org/kiwiproject/base/KiwiPreconditionsTest.java index 4d3cbaaa..5dd3e6bc 100644 --- a/src/test/java/org/kiwiproject/base/KiwiPreconditionsTest.java +++ b/src/test/java/org/kiwiproject/base/KiwiPreconditionsTest.java @@ -14,6 +14,7 @@ import static org.kiwiproject.base.KiwiPreconditions.requireNotNullElse; import static org.kiwiproject.base.KiwiPreconditions.requireNotNullElseGet; +import org.apache.commons.lang3.RandomStringUtils; import org.assertj.core.api.SoftAssertions; import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; import org.junit.jupiter.api.DisplayName; @@ -21,16 +22,25 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; import org.kiwiproject.util.BlankStringArgumentsProvider; import java.math.BigInteger; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; @DisplayName("KiwiPreconditions") @ExtendWith(SoftAssertionsExtension.class) @@ -104,6 +114,7 @@ void testCheckArgument_WhenNoErrorMessage(SoftAssertions softly) { KiwiPreconditions.checkArgument(true, SomeCheckedException.class))) .isNull(); + //noinspection DataFlowIssue softly.assertThat(catchThrowable(() -> KiwiPreconditions.checkArgument(false, SomeCheckedException.class))) .isExactlyInstanceOf(SomeCheckedException.class) @@ -113,6 +124,7 @@ void testCheckArgument_WhenNoErrorMessage(SoftAssertions softly) { KiwiPreconditions.checkArgument(true, SomeRuntimeException.class))) .isNull(); + //noinspection DataFlowIssue softly.assertThat(catchThrowable(() -> KiwiPreconditions.checkArgument(false, SomeRuntimeException.class))) .isExactlyInstanceOf(SomeRuntimeException.class) @@ -127,6 +139,7 @@ void testCheckArgument_WhenHasMessageConstant(SoftAssertions softly) { KiwiPreconditions.checkArgument(true, SomeCheckedException.class, message))) .isNull(); + //noinspection DataFlowIssue softly.assertThat(catchThrowable(() -> KiwiPreconditions.checkArgument(false, SomeCheckedException.class, message))) .isExactlyInstanceOf(SomeCheckedException.class) @@ -136,6 +149,7 @@ void testCheckArgument_WhenHasMessageConstant(SoftAssertions softly) { KiwiPreconditions.checkArgument(true, SomeRuntimeException.class, message))) .isNull(); + //noinspection DataFlowIssue softly.assertThat(catchThrowable(() -> KiwiPreconditions.checkArgument(false, SomeRuntimeException.class, message))) .isExactlyInstanceOf(SomeRuntimeException.class) @@ -151,6 +165,7 @@ void testCheckArgument_WhenHasMessageTemplateWithArgs(SoftAssertions softly) { KiwiPreconditions.checkArgument(true, SomeCheckedException.class, template, args))) .isNull(); + //noinspection DataFlowIssue softly.assertThat(catchThrowable(() -> KiwiPreconditions.checkArgument(false, SomeCheckedException.class, template, args))) .isExactlyInstanceOf(SomeCheckedException.class) @@ -160,6 +175,7 @@ void testCheckArgument_WhenHasMessageTemplateWithArgs(SoftAssertions softly) { KiwiPreconditions.checkArgument(true, SomeRuntimeException.class, template, args))) .isNull(); + //noinspection DataFlowIssue softly.assertThat(catchThrowable(() -> KiwiPreconditions.checkArgument(false, SomeRuntimeException.class, template, args))) .isExactlyInstanceOf(SomeRuntimeException.class) @@ -1165,4 +1181,155 @@ void shouldNotThrowException_WithCustomMessageTemplate_WhenPort_IsValid() { } } + + @Nested + class CheckArgumentInstanceOf { + + @Nested + class WithNoMessage { + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#instanceOfArguments") + void shouldNotThrow_WhenArgumentIsExpectedType(Object argument, Class expectedType) { + assertThatCode(() -> KiwiPreconditions.checkArgumentInstanceOf(argument, expectedType)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#notInstanceOfArguments") + void shouldThrow_WhenArgumentIsNotExpectedType(Object argument, Class expectedType) { + assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiPreconditions.checkArgumentInstanceOf(argument, expectedType)); + } + } + + @Nested + class WithMessage { + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#instanceOfArguments") + void shouldNotThrow_WhenArgumentIsExpectedType(Object argument, Class expectedType) { + assertThatCode(() -> + KiwiPreconditions.checkArgumentInstanceOf(argument, expectedType, "not expected type")) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#notInstanceOfArguments") + void shouldThrow_WhenArgumentIsNotExpectedType(Object argument, Class expectedType) { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiPreconditions.checkArgumentInstanceOf(argument, expectedType, "not expected type")) + .withMessage("not expected type"); + } + } + + @Nested + class WithMessageTemplateAndArguments { + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#instanceOfArguments") + void shouldNotThrow_WhenArgumentIsExpectedType(Object argument, Class expectedType) { + assertThatCode(() -> + KiwiPreconditions.checkArgumentInstanceOf(argument, expectedType, "arg not expected type {}", expectedType)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#notInstanceOfArguments") + void shouldThrow_WhenArgumentIsNotExpectedType(Object argument, Class expectedType) { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiPreconditions.checkArgumentInstanceOf(argument, expectedType, "arg not expected {}", expectedType)) + .withMessage("arg not expected %s", expectedType); + } + } + } + + @Nested + class CheckArgumentNotInstanceOf { + + @Nested + class WithNoMessage { + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#notInstanceOfArguments") + void shouldNotThrow_WhenIsNotInstanceOfRestrictedType(Object argument, Class restrictedType) { + assertThatCode(() -> KiwiPreconditions.checkArgumentNotInstanceOf(argument, restrictedType)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#instanceOfArguments") + void shouldThrow_WhenArgumentIsInstanceOfRestrictedType(Object argument, Class restrictedType) { + assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiPreconditions.checkArgumentNotInstanceOf(argument, restrictedType)); + } + } + + @Nested + class WithMessage { + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#notInstanceOfArguments") + void shouldNotThrow_WhenIsNotInstanceOfRestrictedType(Object argument, Class restrictedType) { + assertThatCode(() -> + KiwiPreconditions.checkArgumentNotInstanceOf(argument, restrictedType, "has restricted type")) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#instanceOfArguments") + void shouldThrow_WhenArgumentIsInstanceOfRestrictedType(Object argument, Class restrictedType) { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiPreconditions.checkArgumentNotInstanceOf(argument, restrictedType, "has restricted type")) + .withMessage("has restricted type"); + } + } + + @Nested + class WithMessageTemplateAndArguments { + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#notInstanceOfArguments") + void shouldNotThrow_WhenIsNotInstanceOfRestrictedType(Object argument, Class restrictedType) { + assertThatCode(() -> + KiwiPreconditions.checkArgumentNotInstanceOf(argument, restrictedType, "has restricted type {}", restrictedType)) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.base.KiwiPreconditionsTest#instanceOfArguments") + void shouldThrow_WhenArgumentIsInstanceOfRestrictedType(Object argument, Class restrictedType) { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiPreconditions.checkArgumentNotInstanceOf(argument, restrictedType, "has restricted type {}", restrictedType)) + .withMessage("has restricted type %s", restrictedType); + } + } + } + + static Stream instanceOfArguments() { + return Stream.of( + Arguments.of(RandomStringUtils.random(5), Object.class), + Arguments.of(RandomStringUtils.random(15), String.class), + Arguments.of(RandomStringUtils.random(10), CharSequence.class), + Arguments.of(random().nextLong(), Long.class), + Arguments.of(random().nextInt(), Integer.class), + Arguments.of(random().nextInt(), Number.class), + Arguments.of(new java.sql.Timestamp(System.currentTimeMillis()), Date.class), + Arguments.of(Instant.now(), Instant.class), + Arguments.of(LocalDateTime.now(), LocalDateTime.class) + ); + } + + static Stream notInstanceOfArguments() { + return Stream.of( + Arguments.of(RandomStringUtils.random(15), Long.class), + Arguments.of(random().nextLong(), String.class), + Arguments.of(random().nextInt(), Collection.class), + Arguments.of(Instant.now(), Date.class), + Arguments.of(LocalDateTime.now(), ZonedDateTime.class) + ); + } + + private static ThreadLocalRandom random() { + return ThreadLocalRandom.current(); + } }