Skip to content

Commit 3ef1b7d

Browse files
committed
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
1 parent 08bce69 commit 3ef1b7d

File tree

2 files changed

+162
-7
lines changed

2 files changed

+162
-7
lines changed

spring-core/src/main/java/org/springframework/util/ObjectUtils.java

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,26 @@
1616

1717
package org.springframework.util;
1818

19+
import java.io.File;
1920
import java.lang.reflect.Array;
21+
import java.net.InetAddress;
2022
import java.net.URI;
2123
import java.net.URL;
24+
import java.nio.charset.Charset;
25+
import java.nio.file.Path;
26+
import java.time.ZoneId;
2227
import java.time.temporal.Temporal;
2328
import java.util.Arrays;
2429
import java.util.Collection;
30+
import java.util.Currency;
2531
import java.util.Date;
2632
import java.util.Locale;
2733
import java.util.Map;
2834
import java.util.Optional;
2935
import java.util.StringJoiner;
36+
import java.util.TimeZone;
3037
import java.util.UUID;
38+
import java.util.regex.Pattern;
3139

3240
import org.springframework.lang.Nullable;
3341

@@ -900,19 +908,27 @@ public static String nullSafeToString(@Nullable short[] array) {
900908
* <p>Returns:
901909
* <ul>
902910
* <li>{@code "null"} if {@code obj} is {@code null}</li>
911+
* <li>{@code"Optional.empty"} if {@code obj} is an empty {@link Optional}</li>
912+
* <li>{@code"Optional[<concise-string>]"} if {@code obj} is a non-empty {@code Optional},
913+
* where {@code <concise-string>} is the result of invoking {@link #nullSafeConciseToString}
914+
* on the object contained in the {@code Optional}</li>
903915
* <li>{@linkplain Class#getName() Class name} if {@code obj} is a {@link Class}</li>
916+
* <li>{@linkplain Charset#name() Charset name} if {@code obj} is a {@link Charset}</li>
917+
* <li>{@linkplain TimeZone#getID() TimeZone ID} if {@code obj} is a {@link TimeZone}</li>
918+
* <li>{@linkplain ZoneId#getId() Zone ID} if {@code obj} is a {@link ZoneId}</li>
904919
* <li>Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string}
905920
* if {@code obj} is a {@link String} or {@link CharSequence}</li>
906921
* <li>Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string}
907922
* if {@code obj} is a <em>simple value type</em> whose {@code toString()} method
908-
* returns a non-null value.</li>
923+
* returns a non-null value</li>
909924
* <li>Otherwise, a string representation of the object's type name concatenated
910-
* with {@code @} and a hex string form of the object's identity hash code</li>
925+
* with {@code "@"} and a hex string form of the object's identity hash code</li>
911926
* </ul>
912927
* <p>In the context of this method, a <em>simple value type</em> is any of the following:
913-
* a primitive wrapper (excluding {@code Void}), an {@code Enum}, a {@code Number},
914-
* a {@code Date}, a {@code Temporal}, a {@code UUID}, a {@code URI}, a {@code URL},
915-
* or a {@code Locale}.
928+
* primitive wrapper (excluding {@link Void}), {@link Enum}, {@link Number},
929+
* {@link Date}, {@link Temporal}, {@link File}, {@link Path}, {@link URI},
930+
* {@link URL}, {@link InetAddress}, {@link Currency}, {@link Locale},
931+
* {@link UUID}, {@link Pattern}.
916932
* @param obj the object to build a string representation for
917933
* @return a concise string representation of the supplied object
918934
* @since 5.3.27
@@ -923,9 +939,22 @@ public static String nullSafeConciseToString(@Nullable Object obj) {
923939
if (obj == null) {
924940
return "null";
925941
}
942+
if (obj instanceof Optional<?> optional) {
943+
return (optional.isEmpty() ? "Optional.empty" :
944+
"Optional[%s]".formatted(nullSafeConciseToString(optional.get())));
945+
}
926946
if (obj instanceof Class<?> clazz) {
927947
return clazz.getName();
928948
}
949+
if (obj instanceof Charset charset) {
950+
return charset.name();
951+
}
952+
if (obj instanceof TimeZone timeZone) {
953+
return timeZone.getID();
954+
}
955+
if (obj instanceof ZoneId zoneId) {
956+
return zoneId.getId();
957+
}
929958
if (obj instanceof CharSequence charSequence) {
930959
return StringUtils.truncate(charSequence);
931960
}
@@ -941,7 +970,10 @@ public static String nullSafeConciseToString(@Nullable Object obj) {
941970

942971
/**
943972
* Derived from {@link org.springframework.beans.BeanUtils#isSimpleValueType}.
944-
* As of 5.3.28, considering {@code UUID} in addition to the bean-level check.
973+
* <p>As of 5.3.28, considering {@link UUID} in addition to the bean-level check.
974+
* <p>As of 5.3.29, additionally considering {@link File}, {@link Path},
975+
* {@link InetAddress}, {@link Charset}, {@link Currency}, {@link TimeZone},
976+
* {@link ZoneId}, {@link Pattern}.
945977
*/
946978
private static boolean isSimpleValueType(Class<?> type) {
947979
return (Void.class != type && void.class != type &&
@@ -951,10 +983,18 @@ private static boolean isSimpleValueType(Class<?> type) {
951983
Number.class.isAssignableFrom(type) ||
952984
Date.class.isAssignableFrom(type) ||
953985
Temporal.class.isAssignableFrom(type) ||
954-
UUID.class == type ||
986+
ZoneId.class.isAssignableFrom(type) ||
987+
TimeZone.class.isAssignableFrom(type) ||
988+
File.class.isAssignableFrom(type) ||
989+
Path.class.isAssignableFrom(type) ||
990+
Charset.class.isAssignableFrom(type) ||
991+
Currency.class.isAssignableFrom(type) ||
992+
InetAddress.class.isAssignableFrom(type) ||
955993
URI.class == type ||
956994
URL.class == type ||
995+
UUID.class == type ||
957996
Locale.class == type ||
997+
Pattern.class == type ||
958998
Class.class == type));
959999
}
9601000

spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,34 @@
1616

1717
package org.springframework.util;
1818

19+
import java.io.File;
1920
import java.io.IOException;
2021
import java.math.BigDecimal;
2122
import java.math.BigInteger;
23+
import java.net.InetAddress;
2224
import java.net.URI;
2325
import java.net.URL;
26+
import java.net.UnknownHostException;
27+
import java.nio.charset.Charset;
28+
import java.nio.charset.StandardCharsets;
29+
import java.nio.file.Path;
2430
import java.sql.SQLException;
2531
import java.time.LocalDate;
32+
import java.time.ZoneId;
2633
import java.util.ArrayList;
2734
import java.util.Collections;
35+
import java.util.Currency;
2836
import java.util.Date;
2937
import java.util.HashMap;
3038
import java.util.HashSet;
3139
import java.util.List;
3240
import java.util.Locale;
3341
import java.util.Map;
42+
import java.util.Optional;
3443
import java.util.Set;
44+
import java.util.TimeZone;
3545
import java.util.UUID;
46+
import java.util.regex.Pattern;
3647

3748
import org.junit.jupiter.api.Nested;
3849
import org.junit.jupiter.api.Test;
@@ -826,6 +837,41 @@ void nullSafeConciseToStringForNull() {
826837
assertThat(ObjectUtils.nullSafeConciseToString(null)).isEqualTo("null");
827838
}
828839

840+
@Test
841+
void nullSafeConciseToStringForEmptyOptional() {
842+
Optional<String> optional = Optional.empty();
843+
assertThat(ObjectUtils.nullSafeConciseToString(optional)).isEqualTo("Optional.empty");
844+
}
845+
846+
@Test
847+
void nullSafeConciseToStringForNonEmptyOptionals() {
848+
Optional<Tropes> optionalEnum = Optional.of(Tropes.BAR);
849+
String expected = "Optional[BAR]";
850+
assertThat(ObjectUtils.nullSafeConciseToString(optionalEnum)).isEqualTo(expected);
851+
852+
String repeat100 = "X".repeat(100);
853+
String repeat101 = "X".repeat(101);
854+
855+
Optional<String> optionalString = Optional.of(repeat100);
856+
expected = "Optional[%s]".formatted(repeat100);
857+
assertThat(ObjectUtils.nullSafeConciseToString(optionalString)).isEqualTo(expected);
858+
859+
optionalString = Optional.of(repeat101);
860+
expected = "Optional[%s]".formatted(repeat100 + truncated);
861+
assertThat(ObjectUtils.nullSafeConciseToString(optionalString)).isEqualTo(expected);
862+
}
863+
864+
@Test
865+
void nullSafeConciseToStringForNonEmptyOptionalCustomType() {
866+
class CustomType {
867+
}
868+
869+
CustomType customType = new CustomType();
870+
Optional<CustomType> optional = Optional.of(customType);
871+
String expected = "Optional[%s]".formatted(ObjectUtils.nullSafeConciseToString(customType));
872+
assertThat(ObjectUtils.nullSafeConciseToString(optional)).isEqualTo(expected);
873+
}
874+
829875
@Test
830876
void nullSafeConciseToStringForClass() {
831877
assertThat(ObjectUtils.nullSafeConciseToString(String.class)).isEqualTo("java.lang.String");
@@ -889,6 +935,30 @@ void nullSafeConciseToStringForUUID() {
889935
assertThat(ObjectUtils.nullSafeConciseToString(id)).isEqualTo(id.toString());
890936
}
891937

938+
@Test
939+
void nullSafeConciseToStringForFile() {
940+
String path = "/tmp/file.txt";
941+
assertThat(ObjectUtils.nullSafeConciseToString(new File(path))).isEqualTo(path);
942+
943+
path = "/tmp/" + "xyz".repeat(32);
944+
assertThat(ObjectUtils.nullSafeConciseToString(new File(path)))
945+
.hasSize(truncatedLength)
946+
.startsWith(path.subSequence(0, 100))
947+
.endsWith(truncated);
948+
}
949+
950+
@Test
951+
void nullSafeConciseToStringForPath() {
952+
String path = "/tmp/file.txt";
953+
assertThat(ObjectUtils.nullSafeConciseToString(Path.of(path))).isEqualTo(path);
954+
955+
path = "/tmp/" + "xyz".repeat(32);
956+
assertThat(ObjectUtils.nullSafeConciseToString(Path.of(path)))
957+
.hasSize(truncatedLength)
958+
.startsWith(path.subSequence(0, 100))
959+
.endsWith(truncated);
960+
}
961+
892962
@Test
893963
void nullSafeConciseToStringForURI() {
894964
String uri = "https://www.example.com/?foo=1&bar=2&baz=3";
@@ -913,11 +983,56 @@ void nullSafeConciseToStringForURL() throws Exception {
913983
.endsWith(truncated);
914984
}
915985

986+
@Test
987+
void nullSafeConciseToStringForInetAddress() {
988+
InetAddress localhost = getLocalhost();
989+
assertThat(ObjectUtils.nullSafeConciseToString(localhost)).isEqualTo(localhost.toString());
990+
}
991+
992+
private static InetAddress getLocalhost() {
993+
try {
994+
return InetAddress.getLocalHost();
995+
}
996+
catch (UnknownHostException ex) {
997+
return InetAddress.getLoopbackAddress();
998+
}
999+
}
1000+
1001+
@Test
1002+
void nullSafeConciseToStringForCharset() {
1003+
Charset charset = StandardCharsets.UTF_8;
1004+
assertThat(ObjectUtils.nullSafeConciseToString(charset)).isEqualTo(charset.name());
1005+
}
1006+
1007+
@Test
1008+
void nullSafeConciseToStringForCurrency() {
1009+
Currency currency = Currency.getInstance(Locale.US);
1010+
assertThat(ObjectUtils.nullSafeConciseToString(currency)).isEqualTo(currency.toString());
1011+
}
1012+
9161013
@Test
9171014
void nullSafeConciseToStringForLocale() {
9181015
assertThat(ObjectUtils.nullSafeConciseToString(Locale.GERMANY)).isEqualTo("de_DE");
9191016
}
9201017

1018+
@Test
1019+
void nullSafeConciseToStringForRegExPattern() {
1020+
Pattern pattern = Pattern.compile("^(foo|bar)$");
1021+
assertThat(ObjectUtils.nullSafeConciseToString(pattern)).isEqualTo(pattern.toString());
1022+
}
1023+
1024+
@Test
1025+
void nullSafeConciseToStringForTimeZone() {
1026+
TimeZone timeZone = TimeZone.getDefault();
1027+
assertThat(ObjectUtils.nullSafeConciseToString(timeZone)).isEqualTo(timeZone.getID());
1028+
}
1029+
1030+
@Test
1031+
void nullSafeConciseToStringForZoneId() {
1032+
ZoneId zoneId = ZoneId.systemDefault();
1033+
assertThat(ObjectUtils.nullSafeConciseToString(zoneId)).isEqualTo(zoneId.getId());
1034+
}
1035+
9211036
@Test
9221037
void nullSafeConciseToStringForArraysAndCollections() {
9231038
List<String> list = List.of("a", "b", "c");

0 commit comments

Comments
 (0)