diff --git a/src/main/java/org/kiwiproject/jdbc/KiwiJdbc.java b/src/main/java/org/kiwiproject/jdbc/KiwiJdbc.java index f8ef1ab2..4000cd7e 100644 --- a/src/main/java/org/kiwiproject/jdbc/KiwiJdbc.java +++ b/src/main/java/org/kiwiproject/jdbc/KiwiJdbc.java @@ -1,6 +1,7 @@ package org.kiwiproject.jdbc; import static java.util.Objects.isNull; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull; import lombok.experimental.UtilityClass; @@ -128,7 +129,7 @@ public static LocalDateTime localDateTimeFromTimestamp(Timestamp timestamp) { * @param rs the ResultSet * @param columnName the date column name * @return the converted LocalDate or {@code null} if the column was {@code NULL} - * @throws SQLException if there is problem getting the date + * @throws SQLException if there is any error getting the date value from the database */ @Nullable public static LocalDate localDateFromDateOrNull(ResultSet rs, String columnName) throws SQLException { @@ -363,6 +364,44 @@ public static boolean booleanFromInt(ResultSet rs, String columnName, BooleanCon return KiwiPrimitives.booleanFromInt(rs.getInt(columnName), option); } + /** + * Enum representing options for trimming strings. + */ + public enum StringTrimOption { + + /** + * Preserves leading and trailing whitespace. + */ + PRESERVE, + + /** + * Removes any leading and trailing whitespace. + */ + REMOVE + } + + /** + * Returns a String from the specified column in the {@link ResultSet} using the given {@link StringTrimOption}. + * When the database value is {@code NULL} or contains only whitespace, returns {@code null}. + * + * @param rs the ResultSet + * @param columnName the date column name + * @param option how to handle leading and trailing whitespace + * @return the String with the specified trim option applied, or {@code null} if the column was blank or {@code NULL} + * @throws SQLException if there is any error getting the value from the database + */ + @Nullable + public static String stringOrNullIfBlank(ResultSet rs, + String columnName, + StringTrimOption option) throws SQLException { + + var s = rs.getString(columnName); + return isBlank(s) ? null : switch (option) { + case PRESERVE -> s; + case REMOVE -> s.strip(); + }; + } + /** * Sets the {@link Timestamp} value in a null-safe manner by using the {@link PreparedStatement#setNull(int, int)} * method for {@code null} values. Uses {@link Types#TIMESTAMP} as the SQL type. diff --git a/src/test/java/org/kiwiproject/jdbc/KiwiJdbcTest.java b/src/test/java/org/kiwiproject/jdbc/KiwiJdbcTest.java index 02988417..c651309b 100644 --- a/src/test/java/org/kiwiproject/jdbc/KiwiJdbcTest.java +++ b/src/test/java/org/kiwiproject/jdbc/KiwiJdbcTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -16,6 +17,8 @@ import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; import org.kiwiproject.base.KiwiPrimitives.BooleanConversionOption; +import org.kiwiproject.jdbc.KiwiJdbc.StringTrimOption; +import org.kiwiproject.util.BlankStringSource; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -647,6 +650,66 @@ void shouldConvert_WithBooleanConversionOption(int value, } } + @Nested + class StringOrNullIfBlankWithTrimOption { + + @ParameterizedTest + @BlankStringSource + void shouldReturnNull_WhenValue_IsBlank(String value) throws SQLException { + var resultSet = newMockResultSet(); + when(resultSet.getString(anyString())).thenReturn(value); + + assertAll( + () -> assertThat(KiwiJdbc.stringOrNullIfBlank(resultSet, "comment", StringTrimOption.PRESERVE)) + .isNull(), + () -> assertThat(KiwiJdbc.stringOrNullIfBlank(resultSet, "note", StringTrimOption.REMOVE)) + .isNull() + ); + + verify(resultSet).getString("comment"); + verify(resultSet).getString("note"); + verifyNoMoreInteractions(resultSet); + } + + @ParameterizedTest + @ValueSource(strings = { + "The quick", + " brown fox", + "jumps\nover ", + "\t\tthe lazy\t\r\n", + " \tdog\r\n\r\n " + }) + void shouldReturn_ExactString_WhenStringTrimOption_Is_PRESERVE(String phrase) throws SQLException { + var resultSet = newMockResultSet(); + when(resultSet.getString(anyString())).thenReturn(phrase); + + assertThat(KiwiJdbc.stringOrNullIfBlank(resultSet, "phrase", StringTrimOption.PRESERVE)) + .isEqualTo(phrase); + + verify(resultSet).getString("phrase"); + verifyNoMoreInteractions(resultSet); + } + + @ParameterizedTest + @ValueSource(strings = { + "The quick", + " brown fox", + "jumps\nover ", + "\t\tthe lazy\t\r\n", + " \tdog\r\n\r\n " + }) + void shouldReturn_StrippedString_WhenStringTrimOption_Is_REMOVE(String phrase) throws SQLException { + var resultSet = newMockResultSet(); + when(resultSet.getString(anyString())).thenReturn(phrase); + + assertThat(KiwiJdbc.stringOrNullIfBlank(resultSet, "expression", StringTrimOption.REMOVE)) + .isEqualTo(phrase.strip()); + + verify(resultSet).getString("expression"); + verifyNoMoreInteractions(resultSet); + } + } + @Nested class NullSafeSetInt { diff --git a/src/test/java/org/kiwiproject/util/BlankStringSource.java b/src/test/java/org/kiwiproject/util/BlankStringSource.java new file mode 100644 index 00000000..3c40b7a3 --- /dev/null +++ b/src/test/java/org/kiwiproject/util/BlankStringSource.java @@ -0,0 +1,32 @@ +package org.kiwiproject.util; + +import org.junit.jupiter.params.provider.ArgumentsSource; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @BlankStringSource} is an {@link ArgumentsSource} which provides blank strings. + *
+ * Usage: + *
+ * {@literal @}ParameterizedTest + * {@literal @}BlankStringSource + * void testThatEachProvidedArgumentIsBlank(String blankString) 1 + * assertThat(blankString).isBlank(); + * // or whatever else you need to test where you need a blank String... + * } + *+ * + *
+ * NOTE: This is a duplicate of the kiwi-test version. It is included here to prevent a circular dependency. + */ +@Target({ ElementType.ANNOTATION_TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@ArgumentsSource(BlankStringArgumentsProvider.class) +public @interface BlankStringSource { +}