Skip to content

Commit e746230

Browse files
committed
Introduce ObjectUtils.nullSafeConciseToString()
ObjectUtils.nullSafeToString(Object) exists for generating a string representation of various objects in a "null-safe" manner, including support for object graphs, collections, etc. However, there are times when we would like to generate a "concise", null-safe string representation that does not include an entire object graph (or potentially a collection of object graphs). This commit introduces ObjectUtils.nullSafeConciseToString(Object) to address this need and makes use of the new feature in FieldError and ConversionFailedException. Closes gh-30286
1 parent 8161316 commit e746230

File tree

6 files changed

+223
-11
lines changed

6 files changed

+223
-11
lines changed

spring-context/src/main/java/org/springframework/validation/FieldError.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -125,7 +125,7 @@ public int hashCode() {
125125
@Override
126126
public String toString() {
127127
return "Field error in object '" + getObjectName() + "' on field '" + this.field +
128-
"': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " +
128+
"': rejected value [" + ObjectUtils.nullSafeConciseToString(this.rejectedValue) + "]; " +
129129
resolvableToString();
130130
}
131131

spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -133,7 +133,7 @@ void styleDateWithInvalidFormat() {
133133
assertThat(exception)
134134
.hasMessageContaining("for property 'styleDate'")
135135
.hasCauseInstanceOf(ConversionFailedException.class).cause()
136-
.hasMessageContaining("for value '99/01/01'")
136+
.hasMessageContaining("for value [99/01/01]")
137137
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
138138
.hasMessageContaining("Parse attempt failed for value [99/01/01]")
139139
.hasCauseInstanceOf(ParseException.class).cause()
@@ -353,7 +353,7 @@ void patternDateWithUnsupportedPattern() {
353353
assertThat(fieldError.unwrap(TypeMismatchException.class))
354354
.hasMessageContaining("for property 'patternDateWithFallbackPatterns'")
355355
.hasCauseInstanceOf(ConversionFailedException.class).cause()
356-
.hasMessageContaining("for value '210302'")
356+
.hasMessageContaining("for value [210302]")
357357
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
358358
.hasMessageContaining("Parse attempt failed for value [210302]")
359359
.hasCauseInstanceOf(ParseException.class).cause()

spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ void isoLocalDateWithInvalidFormat() {
368368
assertThat(fieldError.unwrap(TypeMismatchException.class))
369369
.hasMessageContaining("for property 'isoLocalDate'")
370370
.hasCauseInstanceOf(ConversionFailedException.class).cause()
371-
.hasMessageContaining("for value '2009-31-10'")
371+
.hasMessageContaining("for value [2009-31-10]")
372372
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
373373
.hasMessageContaining("Parse attempt failed for value [2009-31-10]")
374374
.hasCauseInstanceOf(DateTimeParseException.class).cause()
@@ -606,7 +606,7 @@ void patternLocalDateWithUnsupportedPattern() {
606606
assertThat(fieldError.unwrap(TypeMismatchException.class))
607607
.hasMessageContaining("for property 'patternLocalDateWithFallbackPatterns'")
608608
.hasCauseInstanceOf(ConversionFailedException.class).cause()
609-
.hasMessageContaining("for value '210302'")
609+
.hasMessageContaining("for value [210302]")
610610
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
611611
.hasMessageContaining("Parse attempt failed for value [210302]")
612612
.hasCauseInstanceOf(DateTimeParseException.class).cause()

spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -49,7 +49,7 @@ public ConversionFailedException(@Nullable TypeDescriptor sourceType, TypeDescri
4949
@Nullable Object value, Throwable cause) {
5050

5151
super("Failed to convert from type [" + sourceType + "] to type [" + targetType +
52-
"] for value '" + ObjectUtils.nullSafeToString(value) + "'", cause);
52+
"] for value [" + ObjectUtils.nullSafeConciseToString(value) + "]", cause);
5353
this.sourceType = sourceType;
5454
this.targetType = targetType;
5555
this.value = value;

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

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,8 +17,13 @@
1717
package org.springframework.util;
1818

1919
import java.lang.reflect.Array;
20+
import java.net.URI;
21+
import java.net.URL;
22+
import java.time.temporal.Temporal;
2023
import java.util.Arrays;
2124
import java.util.Collection;
25+
import java.util.Date;
26+
import java.util.Locale;
2227
import java.util.Map;
2328
import java.util.Optional;
2429
import java.util.StringJoiner;
@@ -630,6 +635,7 @@ public static String nullSafeClassName(@Nullable Object obj) {
630635
* Returns a {@code "null"} String if {@code obj} is {@code null}.
631636
* @param obj the object to build a String representation for
632637
* @return a String representation of {@code obj}
638+
* @see #nullSafeConciseToString(Object)
633639
*/
634640
public static String nullSafeToString(@Nullable Object obj) {
635641
if (obj == null) {
@@ -885,4 +891,73 @@ public static String nullSafeToString(@Nullable short[] array) {
885891
return stringJoiner.toString();
886892
}
887893

894+
/**
895+
* Generate a null-safe, concise string representation of the supplied object
896+
* as described below.
897+
* <p>Favor this method over {@link #nullSafeToString(Object)} when you need
898+
* the length of the generated string to be limited.
899+
* <p>Returns:
900+
* <ul>
901+
* <li>{@code "null"} if {@code obj} is {@code null}</li>
902+
* <li>{@linkplain Class#getName() Class name} if {@code obj} is a {@link Class}</li>
903+
* <li>Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string}
904+
* if {@code obj} is a {@link String} or {@link CharSequence}</li>
905+
* <li>Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string}
906+
* if {@code obj} is a <em>simple type</em> whose {@code toString()} method returns
907+
* a non-null value.</li>
908+
* <li>Otherwise, a string representation of the object's type name concatenated
909+
* with {@code @} and a hex string form of the object's identity hash code</li>
910+
* </ul>
911+
* <p>In the context of this method, a <em>simple type</em> is any of the following:
912+
* a primitive or primitive wrapper (excluding {@code Void} and {@code void}),
913+
* an enum, a Number, a Date, a Temporal, a URI, a URL, or a Locale.
914+
* @param obj the object to build a string representation for
915+
* @return a concise string representation of the supplied object
916+
* @since 5.3.27
917+
* @see #nullSafeToString(Object)
918+
* @see StringUtils#truncate(CharSequence)
919+
*/
920+
public static String nullSafeConciseToString(@Nullable Object obj) {
921+
if (obj == null) {
922+
return "null";
923+
}
924+
if (obj instanceof Class<?> clazz) {
925+
return clazz.getName();
926+
}
927+
if (obj instanceof CharSequence charSequence) {
928+
return StringUtils.truncate(charSequence);
929+
}
930+
Class<?> type = obj.getClass();
931+
if (isSimpleValueType(type)) {
932+
String str = obj.toString();
933+
if (str != null) {
934+
return StringUtils.truncate(str);
935+
}
936+
}
937+
return type.getTypeName() + "@" + getIdentityHexString(obj);
938+
}
939+
940+
/**
941+
* Copy of {@link org.springframework.beans.BeanUtils#isSimpleValueType(Class)}.
942+
* <p>Check if the given type represents a "simple" value type: a primitive or
943+
* primitive wrapper, an enum, a String or other CharSequence, a Number, a
944+
* Date, a Temporal, a URI, a URL, a Locale, or a Class.
945+
* <p>{@code Void} and {@code void} are not considered simple value types.
946+
* @param type the type to check
947+
* @return whether the given type represents a "simple" value type
948+
*/
949+
private static boolean isSimpleValueType(Class<?> type) {
950+
return (Void.class != type && void.class != type &&
951+
(ClassUtils.isPrimitiveOrWrapper(type) ||
952+
Enum.class.isAssignableFrom(type) ||
953+
CharSequence.class.isAssignableFrom(type) ||
954+
Number.class.isAssignableFrom(type) ||
955+
Date.class.isAssignableFrom(type) ||
956+
Temporal.class.isAssignableFrom(type) ||
957+
URI.class == type ||
958+
URL.class == type ||
959+
Locale.class == type ||
960+
Class.class == type));
961+
}
962+
888963
}

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

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,24 @@
1717
package org.springframework.util;
1818

1919
import java.io.IOException;
20+
import java.net.URI;
21+
import java.net.URL;
2022
import java.sql.SQLException;
23+
import java.time.LocalDate;
24+
import java.util.ArrayList;
2125
import java.util.Collections;
26+
import java.util.Date;
2227
import java.util.HashMap;
2328
import java.util.HashSet;
29+
import java.util.List;
30+
import java.util.Locale;
2431
import java.util.Set;
2532

33+
import org.junit.jupiter.api.Nested;
2634
import org.junit.jupiter.api.Test;
2735

2836
import static org.assertj.core.api.Assertions.assertThat;
37+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2938
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
3039
import static org.springframework.util.ObjectUtils.isEmpty;
3140

@@ -791,7 +800,135 @@ void caseInsensitiveValueOf() {
791800
.withMessage("Constant [bogus] does not exist in enum type org.springframework.util.ObjectUtilsTests$Tropes");
792801
}
793802

794-
private void assertEqualHashCodes(int expected, Object array) {
803+
@Nested
804+
class NullSafeConciseToStringTests {
805+
806+
private static final String truncated = " (truncated)...";
807+
private static final int truncatedLength = 100 + truncated.length();
808+
809+
@Test
810+
void nullSafeConciseToStringForNull() {
811+
assertThat(ObjectUtils.nullSafeConciseToString(null)).isEqualTo("null");
812+
}
813+
814+
@Test
815+
void nullSafeConciseToStringForClass() {
816+
assertThat(ObjectUtils.nullSafeConciseToString(String.class)).isEqualTo("java.lang.String");
817+
}
818+
819+
@Test
820+
void nullSafeConciseToStringForStrings() {
821+
String repeat100 = "X".repeat(100);
822+
String repeat101 = "X".repeat(101);
823+
824+
assertThat(ObjectUtils.nullSafeConciseToString("foo")).isEqualTo("foo");
825+
assertThat(ObjectUtils.nullSafeConciseToString(repeat100)).isEqualTo(repeat100);
826+
assertThat(ObjectUtils.nullSafeConciseToString(repeat101)).hasSize(truncatedLength).endsWith(truncated);
827+
}
828+
829+
@Test
830+
void nullSafeConciseToStringForStringBuilders() {
831+
String repeat100 = "X".repeat(100);
832+
String repeat101 = "X".repeat(101);
833+
834+
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder("foo"))).isEqualTo("foo");
835+
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder(repeat100))).isEqualTo(repeat100);
836+
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder(repeat101))).hasSize(truncatedLength).endsWith(truncated);
837+
}
838+
839+
@Test
840+
void nullSafeConciseToStringForEnum() {
841+
assertThat(ObjectUtils.nullSafeConciseToString(Tropes.FOO)).isEqualTo("FOO");
842+
}
843+
844+
@Test
845+
void nullSafeConciseToStringForNumber() {
846+
assertThat(ObjectUtils.nullSafeConciseToString(42L)).isEqualTo("42");
847+
assertThat(ObjectUtils.nullSafeConciseToString(99.1234D)).isEqualTo("99.1234");
848+
}
849+
850+
@Test
851+
void nullSafeConciseToStringForDate() {
852+
Date date = new Date();
853+
assertThat(ObjectUtils.nullSafeConciseToString(date)).isEqualTo(date.toString());
854+
}
855+
856+
@Test
857+
void nullSafeConciseToStringForTemporal() {
858+
LocalDate localDate = LocalDate.now();
859+
assertThat(ObjectUtils.nullSafeConciseToString(localDate)).isEqualTo(localDate.toString());
860+
}
861+
862+
@Test
863+
void nullSafeConciseToStringForUri() {
864+
String uri = "https://www.example.com/?foo=1&bar=2&baz=3";
865+
assertThat(ObjectUtils.nullSafeConciseToString(URI.create(uri))).isEqualTo(uri);
866+
867+
uri += "&qux=" + "4".repeat(60);
868+
assertThat(ObjectUtils.nullSafeConciseToString(URI.create(uri)))
869+
.hasSize(truncatedLength)
870+
.startsWith(uri.subSequence(0, 100))
871+
.endsWith(truncated);
872+
}
873+
874+
@Test
875+
void nullSafeConciseToStringForUrl() throws Exception {
876+
String url = "https://www.example.com/?foo=1&bar=2&baz=3";
877+
assertThat(ObjectUtils.nullSafeConciseToString(new URL(url))).isEqualTo(url);
878+
879+
url += "&qux=" + "4".repeat(60);
880+
assertThat(ObjectUtils.nullSafeConciseToString(new URL(url)))
881+
.hasSize(truncatedLength)
882+
.startsWith(url.subSequence(0, 100))
883+
.endsWith(truncated);
884+
}
885+
886+
@Test
887+
void nullSafeConciseToStringForLocale() {
888+
assertThat(ObjectUtils.nullSafeConciseToString(Locale.GERMANY)).isEqualTo("de_DE");
889+
}
890+
891+
@Test
892+
void nullSafeConciseToStringForArraysAndCollections() {
893+
List<String> list = List.of("a", "b", "c");
894+
assertThat(ObjectUtils.nullSafeConciseToString(new int[][] {{1, 2}, {3, 4}})).startsWith(prefix(int[][].class));
895+
assertThat(ObjectUtils.nullSafeConciseToString(list.toArray())).startsWith(prefix(Object[].class));
896+
assertThat(ObjectUtils.nullSafeConciseToString(list.toArray(String[]::new))).startsWith(prefix(String[].class));
897+
assertThat(ObjectUtils.nullSafeConciseToString(new ArrayList<>(list))).startsWith(prefix(ArrayList.class));
898+
assertThat(ObjectUtils.nullSafeConciseToString(new HashSet<>(list))).startsWith(prefix(HashSet.class));
899+
}
900+
901+
@Test
902+
void nullSafeConciseToStringForCustomTypes() {
903+
class ExplosiveType {
904+
@Override
905+
public String toString() {
906+
throw new UnsupportedOperationException("no-go");
907+
}
908+
}
909+
ExplosiveType explosiveType = new ExplosiveType();
910+
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(explosiveType::toString);
911+
assertThat(ObjectUtils.nullSafeConciseToString(explosiveType)).startsWith(prefix(ExplosiveType.class));
912+
913+
class WordyType {
914+
@Override
915+
public String toString() {
916+
return "blah blah".repeat(20);
917+
}
918+
}
919+
WordyType wordyType = new WordyType();
920+
assertThat(wordyType).asString().hasSizeGreaterThanOrEqualTo(180 /* 9x20 */);
921+
assertThat(ObjectUtils.nullSafeConciseToString(wordyType)).startsWith(prefix(WordyType.class));
922+
}
923+
924+
private static String prefix(Class<?> clazz) {
925+
return clazz.getTypeName() + "@";
926+
}
927+
928+
}
929+
930+
931+
private static void assertEqualHashCodes(int expected, Object array) {
795932
int actual = ObjectUtils.nullSafeHashCode(array);
796933
assertThat(actual).isEqualTo(expected);
797934
assertThat(array.hashCode()).isNotEqualTo(actual);

0 commit comments

Comments
 (0)