From 9b44cf0eb745c5b20f79cc0802cbb372148fe8a2 Mon Sep 17 00:00:00 2001 From: Shashank Sharma Date: Thu, 20 Jul 2023 10:40:45 +0530 Subject: [PATCH 1/3] Add capacity for ReflectionToStringBuilder to include methods annotated with ToStringInclude --- .../commons/lang3/builder/Reflection.java | 19 +++++ .../builder/ReflectionToStringBuilder.java | 26 +++++++ .../lang3/builder/ToStringInclude.java | 37 ++++++++++ ...tringBuilderIncludeWithAnnotationTest.java | 73 +++++++++++++++++++ 4 files changed, 155 insertions(+) create mode 100644 src/main/java/org/apache/commons/lang3/builder/ToStringInclude.java create mode 100644 src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeWithAnnotationTest.java diff --git a/src/main/java/org/apache/commons/lang3/builder/Reflection.java b/src/main/java/org/apache/commons/lang3/builder/Reflection.java index 119bfe9b24f..6989c2a932a 100644 --- a/src/main/java/org/apache/commons/lang3/builder/Reflection.java +++ b/src/main/java/org/apache/commons/lang3/builder/Reflection.java @@ -18,6 +18,8 @@ package org.apache.commons.lang3.builder; import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.Objects; /** @@ -41,4 +43,21 @@ static Object getUnchecked(final Field field, final Object obj) { } } + /** + * Delegates to {@link Method#invoke(Object, Object...)} and rethrows {@link IllegalAccessException} + * and {@link InvocationTargetException} as {@link IllegalArgumentException}. + * + * @param method The receiver of the invoke call. + * @param obj The argument of the invoke call. + * @return The result of the invoke call. + * @throws IllegalArgumentException Thrown after catching {@link IllegalAccessException} and {@link InvocationTargetException}. + */ + static Object getUnchecked(final Method method, final Object obj) { + try { + return Objects.requireNonNull(method, "method").invoke(obj); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException(e); + } + } + } diff --git a/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java b/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java index d6413681b5c..367d8616573 100644 --- a/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java +++ b/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java @@ -19,6 +19,7 @@ import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collection; @@ -635,6 +636,10 @@ protected boolean accept(final Field field) { return !field.isAnnotationPresent(ToStringExclude.class); } + protected boolean acceptMethod(final Method method) { + return method.isAnnotationPresent(ToStringInclude.class); + } + /** * Appends the fields and values defined by the given object of the given Class. * @@ -664,6 +669,27 @@ protected void appendFieldsIn(final Class clazz) { } } } + + final Method[] methods = Arrays.stream(clazz.getDeclaredMethods()) + .filter(this::acceptMethod) + .sorted(Comparator.comparing(this::getMethodFieldName)) + .toArray(Method[]::new); + AccessibleObject.setAccessible(methods, true); + for (final Method method : methods) { + final String fieldName = getMethodFieldName(method); + final Object fieldValue = Reflection.getUnchecked(method, getObject()); + if (!excludeNullValues || fieldValue != null) { + this.append(fieldName, fieldValue, true); + } + } + } + + private String getMethodFieldName(Method method) { + if (method.isAnnotationPresent(ToStringInclude.class) && + !ToStringInclude.UNDEFINED.equals(method.getDeclaredAnnotation(ToStringInclude.class).value())) { + return method.getDeclaredAnnotation(ToStringInclude.class).value(); + } + return method.getName(); } /** diff --git a/src/main/java/org/apache/commons/lang3/builder/ToStringInclude.java b/src/main/java/org/apache/commons/lang3/builder/ToStringInclude.java new file mode 100644 index 00000000000..f87e1e3bdfa --- /dev/null +++ b/src/main/java/org/apache/commons/lang3/builder/ToStringInclude.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.commons.lang3.builder; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Use this annotation to include a method in + * {@link ReflectionToStringBuilder}. + * + * @since 3.6 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ToStringInclude { + String UNDEFINED = "__undefined__"; + + String value() default UNDEFINED; +} diff --git a/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeWithAnnotationTest.java b/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeWithAnnotationTest.java new file mode 100644 index 00000000000..aba7e4e8794 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeWithAnnotationTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.commons.lang3.builder; + +import org.apache.commons.lang3.AbstractLangTest; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +/** + * Test class for ToStringExclude annotation + */ +public class ReflectionToStringBuilderIncludeWithAnnotationTest extends AbstractLangTest { + + class TestFixture { + @ToStringExclude + private final String excludedField = EXCLUDED_FIELD_VALUE; + + @SuppressWarnings("unused") + private final String includedField = INCLUDED_FIELD_VALUE; + + @ToStringInclude + private String toStringExcludedField() { + return EXCLUDED_FIELD_VALUE_MODIFIED; + } + + @ToStringInclude("modifiedExcludedField") + private String toStringModifiedExcludedField() { + return EXCLUDED_FIELD_VALUE_MODIFIED; + } + } + + private static final String INCLUDED_FIELD_NAME = "includedField"; + + private static final String INCLUDED_FIELD_VALUE = "Hello World!"; + + private static final String EXCLUDED_FIELD_NAME = "excludedField"; + + private static final String EXCLUDED_FIELD_VALUE = "excluded field value"; + private static final String EXCLUDED_FIELD_VALUE_MODIFIED = "excluded field modified value"; + + @Test + public void test_toStringInclude() { + final String toString = ReflectionToStringBuilder.toString(new TestFixture()); + + assertThat(toString, not(containsString(EXCLUDED_FIELD_NAME))); + assertThat(toString, not(containsString(EXCLUDED_FIELD_VALUE))); + assertThat(toString, containsString(INCLUDED_FIELD_NAME)); + assertThat(toString, containsString(INCLUDED_FIELD_VALUE)); + + assertThat(toString, containsString("toStringExcludedField")); // method name when annotation value is not set + assertThat(toString, containsString("modifiedExcludedField")); // Annotation value when its set explicitly + assertThat(toString, containsString(EXCLUDED_FIELD_VALUE_MODIFIED)); + } + +} From b8e690b52ef8be3dd8ba6990b41c598537b97be1 Mon Sep 17 00:00:00 2001 From: Shashank Sharma Date: Thu, 20 Jul 2023 15:14:13 +0530 Subject: [PATCH 2/3] Modify the implementation to be more modular --- .../builder/ReflectionToStringBuilder.java | 35 +++++++++++++++++-- ...tringBuilderIncludeWithAnnotationTest.java | 4 +++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java b/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java index 367d8616573..e8d9235b56c 100644 --- a/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java +++ b/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java @@ -636,7 +636,16 @@ protected boolean accept(final Field field) { return !field.isAnnotationPresent(ToStringExclude.class); } - protected boolean acceptMethod(final Method method) { + /** + * Returns whether to append the given {@link Method}. + * + * + * @param method The method to test. + * @return whether to append the given {@link Method}. + */ + protected boolean accept(final Method method) { return method.isAnnotationPresent(ToStringInclude.class); } @@ -669,9 +678,28 @@ protected void appendFieldsIn(final Class clazz) { } } } + } + + /** + * Appends fields and values that are generated by invoking a method. Only the methods + * that are annotated with {@link ToStringInclude}, are added to the output. + * + *

+ * If a cycle is detected as an object is "toString()'ed", such an object is rendered as if + * {@code Object.toString()} had been called and not implemented by the object. + *

+ * + * @param clazz + * The class of object parameter + */ + protected void appendMethodsIn(final Class clazz) { + if (clazz.isArray()) { + this.reflectionAppendArray(this.getObject()); + return; + } final Method[] methods = Arrays.stream(clazz.getDeclaredMethods()) - .filter(this::acceptMethod) + .filter(this::accept) .sorted(Comparator.comparing(this::getMethodFieldName)) .toArray(Method[]::new); AccessibleObject.setAccessible(methods, true); @@ -684,6 +712,7 @@ protected void appendFieldsIn(final Class clazz) { } } + private String getMethodFieldName(Method method) { if (method.isAnnotationPresent(ToStringInclude.class) && !ToStringInclude.UNDEFINED.equals(method.getDeclaredAnnotation(ToStringInclude.class).value())) { @@ -877,9 +906,11 @@ public String toString() { Class clazz = this.getObject().getClass(); this.appendFieldsIn(clazz); + this.appendMethodsIn(clazz); while (clazz.getSuperclass() != null && clazz != this.getUpToClass()) { clazz = clazz.getSuperclass(); this.appendFieldsIn(clazz); + this.appendMethodsIn(clazz); } return super.toString(); } diff --git a/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeWithAnnotationTest.java b/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeWithAnnotationTest.java index aba7e4e8794..6ac822e8495 100644 --- a/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeWithAnnotationTest.java +++ b/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeWithAnnotationTest.java @@ -45,6 +45,10 @@ private String toStringExcludedField() { private String toStringModifiedExcludedField() { return EXCLUDED_FIELD_VALUE_MODIFIED; } + + private String methodNotAnnotatedWithToStringInclude() { + return null; + } } private static final String INCLUDED_FIELD_NAME = "includedField"; From 6a337f84c866aafe2259d6d1e708a82e5f702595 Mon Sep 17 00:00:00 2001 From: Shashank Sharma Date: Fri, 28 Jul 2023 23:18:37 +0530 Subject: [PATCH 3/3] Fix Tests --- .../lang3/builder/ReflectionToStringBuilder.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java b/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java index e8d9235b56c..0792a9bbf28 100644 --- a/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java +++ b/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java @@ -693,11 +693,6 @@ protected void appendFieldsIn(final Class clazz) { * The class of object parameter */ protected void appendMethodsIn(final Class clazz) { - if (clazz.isArray()) { - this.reflectionAppendArray(this.getObject()); - return; - } - final Method[] methods = Arrays.stream(clazz.getDeclaredMethods()) .filter(this::accept) .sorted(Comparator.comparing(this::getMethodFieldName)) @@ -713,9 +708,10 @@ protected void appendMethodsIn(final Class clazz) { } - private String getMethodFieldName(Method method) { + private String getMethodFieldName(final Method method) { if (method.isAnnotationPresent(ToStringInclude.class) && - !ToStringInclude.UNDEFINED.equals(method.getDeclaredAnnotation(ToStringInclude.class).value())) { + !ToStringInclude.UNDEFINED.equals(method.getDeclaredAnnotation( + ToStringInclude.class).value())) { return method.getDeclaredAnnotation(ToStringInclude.class).value(); } return method.getName();