Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add capacity for ReflectionToStringBuilder to include methods annotated with @ToStringInclude #1082

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/main/java/org/apache/commons/lang3/builder/Reflection.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -635,6 +636,19 @@ protected boolean accept(final Field field) {
return !field.isAnnotationPresent(ToStringExclude.class);
}

/**
* Returns whether to append the given {@link Method}.
* <ul>
* <li> Only appends when the method is annotated with {@link ToStringInclude}
* </ul>
*
* @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.
*
Expand Down Expand Up @@ -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.
*
* <p>
* If a cycle is detected as an object is &quot;toString()'ed&quot;, such an object is rendered as if
* {@code Object.toString()} had been called and not implemented by the object.
* </p>
*
* @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.
*
Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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));
}

}