diff --git a/spring-core/src/main/java/org/springframework/core/style/SimpleValueStyler.java b/spring-core/src/main/java/org/springframework/core/style/SimpleValueStyler.java new file mode 100644 index 0000000000..99cb1ac763 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/style/SimpleValueStyler.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.style; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.StringJoiner; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * {@link ValueStyler} that converts objects to String form — generally for + * debugging purposes — using simple styling conventions that mimic the + * {@code toString()} styling conventions for standard JDK implementations of + * collections, maps, and arrays. + * + *

Uses the reflective visitor pattern underneath the hood to nicely + * encapsulate styling algorithms for each type of styled object. + * + *

Favor {@link SimpleValueStyler} over {@link DefaultValueStyler} when you + * wish to use styling similar to the JDK or when you need configurable control + * over the styling of classes and methods. + * + * @author Sam Brannen + * @since 6.0 + */ +public class SimpleValueStyler extends DefaultValueStyler { + + /** + * Default {@link Class} styling function: {@link Class#getCanonicalName()}. + */ + public static final Function, String> DEFAULT_CLASS_STYLER = Class::getCanonicalName; + + /** + * Default {@link Method} styling function: converts the supplied {@link Method} + * to a simple string representation of the method's signature in the form of + * {@code ()}, where {@code } + * is a comma-separated list of the {@linkplain Class#getSimpleName() simple names} + * of the parameter types. + *

For example, if the supplied method is a reference to + * {@link String#getBytes(java.nio.charset.Charset)}, this function will + * return {@code "getBytes(Charset)"}. + */ + public static final Function DEFAULT_METHOD_STYLER = SimpleValueStyler::toSimpleMethodSignature; + + + private final Function, String> classStyler; + + private final Function methodStyler; + + + /** + * Create a {@code SimpleValueStyler} using the {@link #DEFAULT_CLASS_STYLER} + * and {@link #DEFAULT_METHOD_STYLER}. + */ + public SimpleValueStyler() { + this(DEFAULT_CLASS_STYLER, DEFAULT_METHOD_STYLER); + } + + /** + * Create a {@code SimpleValueStyler} using the supplied class and method stylers. + * @param classStyler a function that applies styling to a {@link Class} + * @param methodStyler a function that applies styling to a {@link Method} + */ + public SimpleValueStyler(Function, String> classStyler, Function methodStyler) { + this.classStyler = classStyler; + this.methodStyler = methodStyler; + } + + + @Override + protected String styleNull() { + return "null"; + } + + @Override + protected String styleString(String str) { + return "\"" + str + "\""; + } + + @Override + protected String styleClass(Class clazz) { + return this.classStyler.apply(clazz); + } + + @Override + protected String styleMethod(Method method) { + return this.methodStyler.apply(method); + } + + @Override + protected String styleMap(Map map) { + StringJoiner result = new StringJoiner(", ", "{", "}"); + for (Map.Entry entry : map.entrySet()) { + result.add(style(entry)); + } + return result.toString(); + } + + @Override + protected String styleCollection(Collection collection) { + StringJoiner result = new StringJoiner(", ", "[", "]"); + for (Object element : collection) { + result.add(style(element)); + } + return result.toString(); + } + + @Override + protected String styleArray(Object[] array) { + StringJoiner result = new StringJoiner(", ", "[", "]"); + for (Object element : array) { + result.add(style(element)); + } + return result.toString(); + } + + private static String toSimpleMethodSignature(Method method) { + String parameterList = Arrays.stream(method.getParameterTypes()) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")); + return String.format("%s(%s)", method.getName(), parameterList); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/style/SimpleValueStylerTests.java b/spring-core/src/test/java/org/springframework/core/style/SimpleValueStylerTests.java new file mode 100644 index 0000000000..83f56e4288 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/style/SimpleValueStylerTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.core.style; + +import java.lang.reflect.Method; +import java.nio.charset.Charset; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link SimpleValueStyler}. + * + * @author Sam Brannen + * @since 6.0 + */ +class SimpleValueStylerTests { + + @Nested + class CommonStyling { + + private final SimpleValueStyler styler = new SimpleValueStyler(); + + @Test + void styleBasics() throws NoSuchMethodException { + assertThat(styler.style(null)).isEqualTo("null"); + assertThat(styler.style(true)).isEqualTo("true"); + assertThat(styler.style(99.9)).isEqualTo("99.9"); + assertThat(styler.style("str")).isEqualTo("\"str\""); + } + + @Test + void stylePlainObject() { + Object obj = new Object(); + + assertThat(styler.style(obj)).isEqualTo(String.valueOf(obj)); + } + + @Test + void styleMaps() { + assertThat(styler.style(Map.of())).isEqualTo("{}"); + assertThat(styler.style(Map.of("key", 1))).isEqualTo("{\"key\" -> 1}"); + + Map map = new LinkedHashMap<>() {{ + put("key1", 1); + put("key2", 2); + }}; + assertThat(styler.style(map)).isEqualTo("{\"key1\" -> 1, \"key2\" -> 2}"); + } + + @Test + void styleMapEntries() { + Map map = Map.of("key1", 1, "key2", 2); + + assertThat(map.entrySet()).map(styler::style).containsExactlyInAnyOrder("\"key1\" -> 1", "\"key2\" -> 2"); + } + + @Test + void styleLists() { + assertThat(styler.style(List.of())).isEqualTo("[]"); + assertThat(styler.style(List.of(1))).isEqualTo("[1]"); + assertThat(styler.style(List.of(1, 2))).isEqualTo("[1, 2]"); + } + + @Test + void stylePrimitiveArrays() { + int[] array = new int[0]; + assertThat(styler.style(array)).isEqualTo("[]"); + + array = new int[] { 1 }; + assertThat(styler.style(array)).isEqualTo("[1]"); + + array = new int[] { 1, 2 }; + assertThat(styler.style(array)).isEqualTo("[1, 2]"); + } + + @Test + void styleObjectArrays() { + String[] array = new String[0]; + assertThat(styler.style(array)).isEqualTo("[]"); + + array = new String[] { "str1" }; + assertThat(styler.style(array)).isEqualTo("[\"str1\"]"); + + array = new String[] { "str1", "str2" }; + assertThat(styler.style(array)).isEqualTo("[\"str1\", \"str2\"]"); + } + + } + + @Nested + class DefaultClassAndMethodStylers { + + private final SimpleValueStyler styler = new SimpleValueStyler(); + + @Test + void styleClass() { + assertThat(styler.style(String.class)).isEqualTo("java.lang.String"); + assertThat(styler.style(getClass())).isEqualTo(getClass().getCanonicalName()); + assertThat(styler.style(String[].class)).isEqualTo("java.lang.String[]"); + assertThat(styler.style(int[][].class)).isEqualTo("int[][]"); + } + + @Test + void styleMethod() throws NoSuchMethodException { + assertThat(styler.style(String.class.getMethod("toString"))).isEqualTo("toString()"); + assertThat(styler.style(String.class.getMethod("getBytes", Charset.class))).isEqualTo("getBytes(Charset)"); + } + + @Test + void styleClassMap() { + Map> map = new LinkedHashMap<>() {{ + put("key1", Integer.class); + put("key2", DefaultClassAndMethodStylers.class); + }}; + assertThat(styler.style(map)).isEqualTo( + "{\"key1\" -> java.lang.Integer, \"key2\" -> %s}", + DefaultClassAndMethodStylers.class.getCanonicalName()); + } + + @Test + void styleClassList() { + assertThat(styler.style(List.of(Integer.class, String.class))) + .isEqualTo("[java.lang.Integer, java.lang.String]"); + } + + @Test + void styleClassArray() { + Class[] array = new Class[] { Integer.class, getClass() }; + assertThat(styler.style(array)) + .isEqualTo("[%s, %s]", Integer.class.getCanonicalName(), getClass().getCanonicalName()); + } + + } + + @Nested + class CustomClassAndMethodStylers { + + private final SimpleValueStyler styler = new SimpleValueStyler(Class::getSimpleName, Method::toGenericString); + + @Test + void styleClass() { + assertThat(styler.style(String.class)).isEqualTo("String"); + assertThat(styler.style(getClass())).isEqualTo(getClass().getSimpleName()); + assertThat(styler.style(String[].class)).isEqualTo("String[]"); + assertThat(styler.style(int[][].class)).isEqualTo("int[][]"); + } + + @Test + void styleMethod() throws NoSuchMethodException { + Method method = String.class.getMethod("toString"); + assertThat(styler.style(method)).isEqualTo(method.toGenericString()); + } + + @Test + void styleClassMap() { + Map> map = new LinkedHashMap<>() {{ + put("key1", Integer.class); + put("key2", CustomClassAndMethodStylers.class); + }}; + assertThat(styler.style(map)).isEqualTo( + "{\"key1\" -> %s, \"key2\" -> %s}", + Integer.class.getSimpleName(), CustomClassAndMethodStylers.class.getSimpleName()); + } + @Test + void styleClassList() { + assertThat(styler.style(List.of(Integer.class, String.class))).isEqualTo("[Integer, String]"); + } + + @Test + void styleClassArray() { + Class[] array = new Class[] { Integer.class, getClass() }; + assertThat(styler.style(array)).isEqualTo("[%s, %s]", Integer.class.getSimpleName(), getClass().getSimpleName()); + } + + } + +}