Browse Source

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
pull/30294/head
Sam Brannen 2 years ago
parent
commit
e746230de6
  1. 4
      spring-context/src/main/java/org/springframework/validation/FieldError.java
  2. 6
      spring-context/src/test/java/org/springframework/format/datetime/DateFormattingTests.java
  3. 4
      spring-context/src/test/java/org/springframework/format/datetime/standard/DateTimeFormattingTests.java
  4. 4
      spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java
  5. 77
      spring-core/src/main/java/org/springframework/util/ObjectUtils.java
  6. 139
      spring-core/src/test/java/org/springframework/util/ObjectUtilsTests.java

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -125,7 +125,7 @@ public class FieldError extends ObjectError { @@ -125,7 +125,7 @@ public class FieldError extends ObjectError {
@Override
public String toString() {
return "Field error in object '" + getObjectName() + "' on field '" + this.field +
"': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " +
"': rejected value [" + ObjectUtils.nullSafeConciseToString(this.rejectedValue) + "]; " +
resolvableToString();
}

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -133,7 +133,7 @@ public class DateFormattingTests { @@ -133,7 +133,7 @@ public class DateFormattingTests {
assertThat(exception)
.hasMessageContaining("for property 'styleDate'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '99/01/01'")
.hasMessageContaining("for value [99/01/01]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [99/01/01]")
.hasCauseInstanceOf(ParseException.class).cause()
@ -353,7 +353,7 @@ public class DateFormattingTests { @@ -353,7 +353,7 @@ public class DateFormattingTests {
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'patternDateWithFallbackPatterns'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '210302'")
.hasMessageContaining("for value [210302]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [210302]")
.hasCauseInstanceOf(ParseException.class).cause()

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

@ -368,7 +368,7 @@ class DateTimeFormattingTests { @@ -368,7 +368,7 @@ class DateTimeFormattingTests {
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'isoLocalDate'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '2009-31-10'")
.hasMessageContaining("for value [2009-31-10]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [2009-31-10]")
.hasCauseInstanceOf(DateTimeParseException.class).cause()
@ -606,7 +606,7 @@ class DateTimeFormattingTests { @@ -606,7 +606,7 @@ class DateTimeFormattingTests {
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'patternLocalDateWithFallbackPatterns'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '210302'")
.hasMessageContaining("for value [210302]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [210302]")
.hasCauseInstanceOf(DateTimeParseException.class).cause()

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -49,7 +49,7 @@ public class ConversionFailedException extends ConversionException { @@ -49,7 +49,7 @@ public class ConversionFailedException extends ConversionException {
@Nullable Object value, Throwable cause) {
super("Failed to convert from type [" + sourceType + "] to type [" + targetType +
"] for value '" + ObjectUtils.nullSafeToString(value) + "'", cause);
"] for value [" + ObjectUtils.nullSafeConciseToString(value) + "]", cause);
this.sourceType = sourceType;
this.targetType = targetType;
this.value = value;

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

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,8 +17,13 @@ @@ -17,8 +17,13 @@
package org.springframework.util;
import java.lang.reflect.Array;
import java.net.URI;
import java.net.URL;
import java.time.temporal.Temporal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.StringJoiner;
@ -630,6 +635,7 @@ public abstract class ObjectUtils { @@ -630,6 +635,7 @@ public abstract class ObjectUtils {
* Returns a {@code "null"} String if {@code obj} is {@code null}.
* @param obj the object to build a String representation for
* @return a String representation of {@code obj}
* @see #nullSafeConciseToString(Object)
*/
public static String nullSafeToString(@Nullable Object obj) {
if (obj == null) {
@ -885,4 +891,73 @@ public abstract class ObjectUtils { @@ -885,4 +891,73 @@ public abstract class ObjectUtils {
return stringJoiner.toString();
}
/**
* Generate a null-safe, concise string representation of the supplied object
* as described below.
* <p>Favor this method over {@link #nullSafeToString(Object)} when you need
* the length of the generated string to be limited.
* <p>Returns:
* <ul>
* <li>{@code "null"} if {@code obj} is {@code null}</li>
* <li>{@linkplain Class#getName() Class name} if {@code obj} is a {@link Class}</li>
* <li>Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string}
* if {@code obj} is a {@link String} or {@link CharSequence}</li>
* <li>Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string}
* if {@code obj} is a <em>simple type</em> whose {@code toString()} method returns
* a non-null value.</li>
* <li>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</li>
* </ul>
* <p>In the context of this method, a <em>simple type</em> is any of the following:
* a primitive or primitive wrapper (excluding {@code Void} and {@code void}),
* an enum, a Number, a Date, a Temporal, a URI, a URL, or a Locale.
* @param obj the object to build a string representation for
* @return a concise string representation of the supplied object
* @since 5.3.27
* @see #nullSafeToString(Object)
* @see StringUtils#truncate(CharSequence)
*/
public static String nullSafeConciseToString(@Nullable Object obj) {
if (obj == null) {
return "null";
}
if (obj instanceof Class<?> clazz) {
return clazz.getName();
}
if (obj instanceof CharSequence charSequence) {
return StringUtils.truncate(charSequence);
}
Class<?> type = obj.getClass();
if (isSimpleValueType(type)) {
String str = obj.toString();
if (str != null) {
return StringUtils.truncate(str);
}
}
return type.getTypeName() + "@" + getIdentityHexString(obj);
}
/**
* Copy of {@link org.springframework.beans.BeanUtils#isSimpleValueType(Class)}.
* <p>Check if the given type represents a "simple" value type: a primitive or
* primitive wrapper, an enum, a String or other CharSequence, a Number, a
* Date, a Temporal, a URI, a URL, a Locale, or a Class.
* <p>{@code Void} and {@code void} are not considered simple value types.
* @param type the type to check
* @return whether the given type represents a "simple" value type
*/
private static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
}

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

@ -17,15 +17,24 @@ @@ -17,15 +17,24 @@
package org.springframework.util;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.springframework.util.ObjectUtils.isEmpty;
@ -791,7 +800,135 @@ class ObjectUtilsTests { @@ -791,7 +800,135 @@ class ObjectUtilsTests {
.withMessage("Constant [bogus] does not exist in enum type org.springframework.util.ObjectUtilsTests$Tropes");
}
private void assertEqualHashCodes(int expected, Object array) {
@Nested
class NullSafeConciseToStringTests {
private static final String truncated = " (truncated)...";
private static final int truncatedLength = 100 + truncated.length();
@Test
void nullSafeConciseToStringForNull() {
assertThat(ObjectUtils.nullSafeConciseToString(null)).isEqualTo("null");
}
@Test
void nullSafeConciseToStringForClass() {
assertThat(ObjectUtils.nullSafeConciseToString(String.class)).isEqualTo("java.lang.String");
}
@Test
void nullSafeConciseToStringForStrings() {
String repeat100 = "X".repeat(100);
String repeat101 = "X".repeat(101);
assertThat(ObjectUtils.nullSafeConciseToString("foo")).isEqualTo("foo");
assertThat(ObjectUtils.nullSafeConciseToString(repeat100)).isEqualTo(repeat100);
assertThat(ObjectUtils.nullSafeConciseToString(repeat101)).hasSize(truncatedLength).endsWith(truncated);
}
@Test
void nullSafeConciseToStringForStringBuilders() {
String repeat100 = "X".repeat(100);
String repeat101 = "X".repeat(101);
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder("foo"))).isEqualTo("foo");
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder(repeat100))).isEqualTo(repeat100);
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder(repeat101))).hasSize(truncatedLength).endsWith(truncated);
}
@Test
void nullSafeConciseToStringForEnum() {
assertThat(ObjectUtils.nullSafeConciseToString(Tropes.FOO)).isEqualTo("FOO");
}
@Test
void nullSafeConciseToStringForNumber() {
assertThat(ObjectUtils.nullSafeConciseToString(42L)).isEqualTo("42");
assertThat(ObjectUtils.nullSafeConciseToString(99.1234D)).isEqualTo("99.1234");
}
@Test
void nullSafeConciseToStringForDate() {
Date date = new Date();
assertThat(ObjectUtils.nullSafeConciseToString(date)).isEqualTo(date.toString());
}
@Test
void nullSafeConciseToStringForTemporal() {
LocalDate localDate = LocalDate.now();
assertThat(ObjectUtils.nullSafeConciseToString(localDate)).isEqualTo(localDate.toString());
}
@Test
void nullSafeConciseToStringForUri() {
String uri = "https://www.example.com/?foo=1&bar=2&baz=3";
assertThat(ObjectUtils.nullSafeConciseToString(URI.create(uri))).isEqualTo(uri);
uri += "&qux=" + "4".repeat(60);
assertThat(ObjectUtils.nullSafeConciseToString(URI.create(uri)))
.hasSize(truncatedLength)
.startsWith(uri.subSequence(0, 100))
.endsWith(truncated);
}
@Test
void nullSafeConciseToStringForUrl() throws Exception {
String url = "https://www.example.com/?foo=1&bar=2&baz=3";
assertThat(ObjectUtils.nullSafeConciseToString(new URL(url))).isEqualTo(url);
url += "&qux=" + "4".repeat(60);
assertThat(ObjectUtils.nullSafeConciseToString(new URL(url)))
.hasSize(truncatedLength)
.startsWith(url.subSequence(0, 100))
.endsWith(truncated);
}
@Test
void nullSafeConciseToStringForLocale() {
assertThat(ObjectUtils.nullSafeConciseToString(Locale.GERMANY)).isEqualTo("de_DE");
}
@Test
void nullSafeConciseToStringForArraysAndCollections() {
List<String> list = List.of("a", "b", "c");
assertThat(ObjectUtils.nullSafeConciseToString(new int[][] {{1, 2}, {3, 4}})).startsWith(prefix(int[][].class));
assertThat(ObjectUtils.nullSafeConciseToString(list.toArray())).startsWith(prefix(Object[].class));
assertThat(ObjectUtils.nullSafeConciseToString(list.toArray(String[]::new))).startsWith(prefix(String[].class));
assertThat(ObjectUtils.nullSafeConciseToString(new ArrayList<>(list))).startsWith(prefix(ArrayList.class));
assertThat(ObjectUtils.nullSafeConciseToString(new HashSet<>(list))).startsWith(prefix(HashSet.class));
}
@Test
void nullSafeConciseToStringForCustomTypes() {
class ExplosiveType {
@Override
public String toString() {
throw new UnsupportedOperationException("no-go");
}
}
ExplosiveType explosiveType = new ExplosiveType();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(explosiveType::toString);
assertThat(ObjectUtils.nullSafeConciseToString(explosiveType)).startsWith(prefix(ExplosiveType.class));
class WordyType {
@Override
public String toString() {
return "blah blah".repeat(20);
}
}
WordyType wordyType = new WordyType();
assertThat(wordyType).asString().hasSizeGreaterThanOrEqualTo(180 /* 9x20 */);
assertThat(ObjectUtils.nullSafeConciseToString(wordyType)).startsWith(prefix(WordyType.class));
}
private static String prefix(Class<?> clazz) {
return clazz.getTypeName() + "@";
}
}
private static void assertEqualHashCodes(int expected, Object array) {
int actual = ObjectUtils.nullSafeHashCode(array);
assertThat(actual).isEqualTo(expected);
assertThat(array.hashCode()).isNotEqualTo(actual);

Loading…
Cancel
Save