From 08bce69d3de429b9e70435439423b836071f7c1e Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 4 Jul 2023 10:16:34 +0200 Subject: [PATCH 1/2] Add tests for status quo in ObjectUtils.nullSafeConciseToString() --- .../util/ObjectUtilsTests.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java index 3ec2373bb80..0593944bb83 100644 --- a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java @@ -17,6 +17,8 @@ package org.springframework.util; import java.io.IOException; +import java.math.BigDecimal; +import java.math.BigInteger; import java.net.URI; import java.net.URL; import java.sql.SQLException; @@ -28,6 +30,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.UUID; @@ -232,7 +235,7 @@ class ObjectUtilsTests { } @Test - void addObjectToNullArray() throws Exception { + void addObjectToNullArray() { String newElement = "foo"; String[] newArray = ObjectUtils.addObjectToArray(null, newElement); assertThat(newArray).hasSize(1); @@ -240,14 +243,14 @@ class ObjectUtilsTests { } @Test - void addNullObjectToNullArray() throws Exception { + void addNullObjectToNullArray() { Object[] newArray = ObjectUtils.addObjectToArray(null, null); assertThat(newArray).hasSize(1); assertThat(newArray[0]).isNull(); } @Test - void nullSafeEqualsWithArrays() throws Exception { + void nullSafeEqualsWithArrays() { assertThat(ObjectUtils.nullSafeEquals(new String[] {"a", "b", "c"}, new String[] {"a", "b", "c"})).isTrue(); assertThat(ObjectUtils.nullSafeEquals(new int[] {1, 2, 3}, new int[] {1, 2, 3})).isTrue(); } @@ -855,11 +858,19 @@ class ObjectUtilsTests { } @Test - void nullSafeConciseToStringForNumber() { + void nullSafeConciseToStringForPrimitivesAndWrappers() { + assertThat(ObjectUtils.nullSafeConciseToString(true)).isEqualTo("true"); + assertThat(ObjectUtils.nullSafeConciseToString('X')).isEqualTo("X"); assertThat(ObjectUtils.nullSafeConciseToString(42L)).isEqualTo("42"); assertThat(ObjectUtils.nullSafeConciseToString(99.1234D)).isEqualTo("99.1234"); } + @Test + void nullSafeConciseToStringForBigNumbers() { + assertThat(ObjectUtils.nullSafeConciseToString(BigInteger.valueOf(42L))).isEqualTo("42"); + assertThat(ObjectUtils.nullSafeConciseToString(BigDecimal.valueOf(99.1234D))).isEqualTo("99.1234"); + } + @Test void nullSafeConciseToStringForDate() { Date date = new Date(); @@ -917,6 +928,12 @@ class ObjectUtilsTests { assertThat(ObjectUtils.nullSafeConciseToString(new HashSet<>(list))).startsWith(prefix(HashSet.class)); } + @Test + void nullSafeConciseToStringForMaps() { + Map map = Map.of("a", 1, "b", 2, "c", 3); + assertThat(ObjectUtils.nullSafeConciseToString(map)).startsWith(prefix(map.getClass())); + } + @Test void nullSafeConciseToStringForCustomTypes() { class ExplosiveType { From 3ef1b7d83c3ff15c5327de8b964f62ff9dbc8354 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Tue, 4 Jul 2023 13:15:34 +0200 Subject: [PATCH 2/2] Extend supported types in ObjectUtils.nullSafeConciseToString() This commit extends the list of explicitly supported types in ObjectUtils.nullSafeConciseToString() with the following. - Optional - File - Path - InetAddress - Charset - Currency - TimeZone - ZoneId - Pattern Closes gh-30805 --- .../org/springframework/util/ObjectUtils.java | 54 ++++++-- .../util/ObjectUtilsTests.java | 115 ++++++++++++++++++ 2 files changed, 162 insertions(+), 7 deletions(-) diff --git a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java index bf873018f16..e760306e055 100644 --- a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.java @@ -16,18 +16,26 @@ package org.springframework.util; +import java.io.File; import java.lang.reflect.Array; +import java.net.InetAddress; import java.net.URI; import java.net.URL; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.time.ZoneId; import java.time.temporal.Temporal; import java.util.Arrays; import java.util.Collection; +import java.util.Currency; import java.util.Date; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.StringJoiner; +import java.util.TimeZone; import java.util.UUID; +import java.util.regex.Pattern; import org.springframework.lang.Nullable; @@ -900,19 +908,27 @@ public abstract class ObjectUtils { *

Returns: *

    *
  • {@code "null"} if {@code obj} is {@code null}
  • + *
  • {@code"Optional.empty"} if {@code obj} is an empty {@link Optional}
  • + *
  • {@code"Optional[]"} if {@code obj} is a non-empty {@code Optional}, + * where {@code } is the result of invoking {@link #nullSafeConciseToString} + * on the object contained in the {@code Optional}
  • *
  • {@linkplain Class#getName() Class name} if {@code obj} is a {@link Class}
  • + *
  • {@linkplain Charset#name() Charset name} if {@code obj} is a {@link Charset}
  • + *
  • {@linkplain TimeZone#getID() TimeZone ID} if {@code obj} is a {@link TimeZone}
  • + *
  • {@linkplain ZoneId#getId() Zone ID} if {@code obj} is a {@link ZoneId}
  • *
  • Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string} * if {@code obj} is a {@link String} or {@link CharSequence}
  • *
  • Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string} * if {@code obj} is a simple value type whose {@code toString()} method - * returns a non-null value.
  • + * returns a non-null value *
  • Otherwise, a string representation of the object's type name concatenated - * with {@code @} and a hex string form of the object's identity hash code
  • + * with {@code "@"} and a hex string form of the object's identity hash code *
*

In the context of this method, a simple value type is any of the following: - * a primitive wrapper (excluding {@code Void}), an {@code Enum}, a {@code Number}, - * a {@code Date}, a {@code Temporal}, a {@code UUID}, a {@code URI}, a {@code URL}, - * or a {@code Locale}. + * primitive wrapper (excluding {@link Void}), {@link Enum}, {@link Number}, + * {@link Date}, {@link Temporal}, {@link File}, {@link Path}, {@link URI}, + * {@link URL}, {@link InetAddress}, {@link Currency}, {@link Locale}, + * {@link UUID}, {@link Pattern}. * @param obj the object to build a string representation for * @return a concise string representation of the supplied object * @since 5.3.27 @@ -923,9 +939,22 @@ public abstract class ObjectUtils { if (obj == null) { return "null"; } + if (obj instanceof Optional optional) { + return (optional.isEmpty() ? "Optional.empty" : + "Optional[%s]".formatted(nullSafeConciseToString(optional.get()))); + } if (obj instanceof Class clazz) { return clazz.getName(); } + if (obj instanceof Charset charset) { + return charset.name(); + } + if (obj instanceof TimeZone timeZone) { + return timeZone.getID(); + } + if (obj instanceof ZoneId zoneId) { + return zoneId.getId(); + } if (obj instanceof CharSequence charSequence) { return StringUtils.truncate(charSequence); } @@ -941,7 +970,10 @@ public abstract class ObjectUtils { /** * Derived from {@link org.springframework.beans.BeanUtils#isSimpleValueType}. - * As of 5.3.28, considering {@code UUID} in addition to the bean-level check. + *

As of 5.3.28, considering {@link UUID} in addition to the bean-level check. + *

As of 5.3.29, additionally considering {@link File}, {@link Path}, + * {@link InetAddress}, {@link Charset}, {@link Currency}, {@link TimeZone}, + * {@link ZoneId}, {@link Pattern}. */ private static boolean isSimpleValueType(Class type) { return (Void.class != type && void.class != type && @@ -951,10 +983,18 @@ public abstract class ObjectUtils { Number.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type) || Temporal.class.isAssignableFrom(type) || - UUID.class == type || + ZoneId.class.isAssignableFrom(type) || + TimeZone.class.isAssignableFrom(type) || + File.class.isAssignableFrom(type) || + Path.class.isAssignableFrom(type) || + Charset.class.isAssignableFrom(type) || + Currency.class.isAssignableFrom(type) || + InetAddress.class.isAssignableFrom(type) || URI.class == type || URL.class == type || + UUID.class == type || Locale.class == type || + Pattern.class == type || Class.class == type)); } diff --git a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java index 0593944bb83..025c3a99f5c 100644 --- a/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java @@ -16,23 +16,34 @@ package org.springframework.util; +import java.io.File; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; +import java.net.InetAddress; import java.net.URI; import java.net.URL; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.sql.SQLException; import java.time.LocalDate; +import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; +import java.util.Currency; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.TimeZone; import java.util.UUID; +import java.util.regex.Pattern; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -826,6 +837,41 @@ class ObjectUtilsTests { assertThat(ObjectUtils.nullSafeConciseToString(null)).isEqualTo("null"); } + @Test + void nullSafeConciseToStringForEmptyOptional() { + Optional optional = Optional.empty(); + assertThat(ObjectUtils.nullSafeConciseToString(optional)).isEqualTo("Optional.empty"); + } + + @Test + void nullSafeConciseToStringForNonEmptyOptionals() { + Optional optionalEnum = Optional.of(Tropes.BAR); + String expected = "Optional[BAR]"; + assertThat(ObjectUtils.nullSafeConciseToString(optionalEnum)).isEqualTo(expected); + + String repeat100 = "X".repeat(100); + String repeat101 = "X".repeat(101); + + Optional optionalString = Optional.of(repeat100); + expected = "Optional[%s]".formatted(repeat100); + assertThat(ObjectUtils.nullSafeConciseToString(optionalString)).isEqualTo(expected); + + optionalString = Optional.of(repeat101); + expected = "Optional[%s]".formatted(repeat100 + truncated); + assertThat(ObjectUtils.nullSafeConciseToString(optionalString)).isEqualTo(expected); + } + + @Test + void nullSafeConciseToStringForNonEmptyOptionalCustomType() { + class CustomType { + } + + CustomType customType = new CustomType(); + Optional optional = Optional.of(customType); + String expected = "Optional[%s]".formatted(ObjectUtils.nullSafeConciseToString(customType)); + assertThat(ObjectUtils.nullSafeConciseToString(optional)).isEqualTo(expected); + } + @Test void nullSafeConciseToStringForClass() { assertThat(ObjectUtils.nullSafeConciseToString(String.class)).isEqualTo("java.lang.String"); @@ -889,6 +935,30 @@ class ObjectUtilsTests { assertThat(ObjectUtils.nullSafeConciseToString(id)).isEqualTo(id.toString()); } + @Test + void nullSafeConciseToStringForFile() { + String path = "/tmp/file.txt"; + assertThat(ObjectUtils.nullSafeConciseToString(new File(path))).isEqualTo(path); + + path = "/tmp/" + "xyz".repeat(32); + assertThat(ObjectUtils.nullSafeConciseToString(new File(path))) + .hasSize(truncatedLength) + .startsWith(path.subSequence(0, 100)) + .endsWith(truncated); + } + + @Test + void nullSafeConciseToStringForPath() { + String path = "/tmp/file.txt"; + assertThat(ObjectUtils.nullSafeConciseToString(Path.of(path))).isEqualTo(path); + + path = "/tmp/" + "xyz".repeat(32); + assertThat(ObjectUtils.nullSafeConciseToString(Path.of(path))) + .hasSize(truncatedLength) + .startsWith(path.subSequence(0, 100)) + .endsWith(truncated); + } + @Test void nullSafeConciseToStringForURI() { String uri = "https://www.example.com/?foo=1&bar=2&baz=3"; @@ -913,11 +983,56 @@ class ObjectUtilsTests { .endsWith(truncated); } + @Test + void nullSafeConciseToStringForInetAddress() { + InetAddress localhost = getLocalhost(); + assertThat(ObjectUtils.nullSafeConciseToString(localhost)).isEqualTo(localhost.toString()); + } + + private static InetAddress getLocalhost() { + try { + return InetAddress.getLocalHost(); + } + catch (UnknownHostException ex) { + return InetAddress.getLoopbackAddress(); + } + } + + @Test + void nullSafeConciseToStringForCharset() { + Charset charset = StandardCharsets.UTF_8; + assertThat(ObjectUtils.nullSafeConciseToString(charset)).isEqualTo(charset.name()); + } + + @Test + void nullSafeConciseToStringForCurrency() { + Currency currency = Currency.getInstance(Locale.US); + assertThat(ObjectUtils.nullSafeConciseToString(currency)).isEqualTo(currency.toString()); + } + @Test void nullSafeConciseToStringForLocale() { assertThat(ObjectUtils.nullSafeConciseToString(Locale.GERMANY)).isEqualTo("de_DE"); } + @Test + void nullSafeConciseToStringForRegExPattern() { + Pattern pattern = Pattern.compile("^(foo|bar)$"); + assertThat(ObjectUtils.nullSafeConciseToString(pattern)).isEqualTo(pattern.toString()); + } + + @Test + void nullSafeConciseToStringForTimeZone() { + TimeZone timeZone = TimeZone.getDefault(); + assertThat(ObjectUtils.nullSafeConciseToString(timeZone)).isEqualTo(timeZone.getID()); + } + + @Test + void nullSafeConciseToStringForZoneId() { + ZoneId zoneId = ZoneId.systemDefault(); + assertThat(ObjectUtils.nullSafeConciseToString(zoneId)).isEqualTo(zoneId.getId()); + } + @Test void nullSafeConciseToStringForArraysAndCollections() { List list = List.of("a", "b", "c");