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..0792a9bbf28 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,19 @@ protected boolean accept(final Field field) { return !field.isAnnotationPresent(ToStringExclude.class); } + /** + * 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); + } + /** * Appends the fields and values defined by the given object of the given Class. * @@ -666,6 +680,43 @@ 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) { + final Method[] methods = Arrays.stream(clazz.getDeclaredMethods()) + .filter(this::accept) + .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(final Method method) { + if (method.isAnnotationPresent(ToStringInclude.class) && + !ToStringInclude.UNDEFINED.equals(method.getDeclaredAnnotation( + ToStringInclude.class).value())) { + return method.getDeclaredAnnotation(ToStringInclude.class).value(); + } + return method.getName(); + } + /** * Gets the excludeFieldNames. * @@ -851,9 +902,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/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..6ac822e8495 --- /dev/null +++ b/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeWithAnnotationTest.java @@ -0,0 +1,77 @@ +/* + * 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 String methodNotAnnotatedWithToStringInclude() { + return null; + } + } + + 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)); + } + +}