You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by gg...@apache.org on 2022/07/05 23:37:29 UTC
[commons-lang] 01/02: [LANG-1662] Let ReflectionToStringBuilder only reflect given field names #849
This is an automated email from the ASF dual-hosted git repository.
ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
commit 5f22e0451457abc62c87c83526ac129f89cc6d8f
Author: Gary Gregory <ga...@gmail.com>
AuthorDate: Tue Jul 5 18:49:38 2022 -0400
[LANG-1662] Let ReflectionToStringBuilder only reflect given field
names #849
Applied PR and:
- Added missing @since tags to new public and protected methods and
fields
- Fixed Javadocs
- Reuse own ArrayUtils API for validation
- Move methods to AB order
---
src/changes/changes.xml | 1 +
.../lang3/builder/ReflectionToStringBuilder.java | 90 +++++++-
.../ReflectionToStringBuilderIncludeTest.java | 239 +++++++++++++++++++++
3 files changed, 328 insertions(+), 2 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 26fe8b46e..495477086 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -155,6 +155,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="add" dev="ggregory" due-to="Gary Gregory">Add SystemUtils.IS_JAVA_17.</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add SystemUtils.IS_JAVA_18.</action>
<action issue="LANG-1627" type="add" dev="ggregory" due-to="Alberto Scotto, Avijit Chakraborty, Steve Bosman, Bruno P. Kinoshita, Gary Gregory">Add ArrayUtils.oneHot().</action>
+ <action issue="LANG-1662" type="add" dev="ggregory" due-to="Daniel Augusto Veronezi Salvador, Gary Gregory, Bruno P. Kinoshita">Let ReflectionToStringBuilder only reflect given field names #849.</action>
<!-- UPDATE -->
<action type="update" dev="ggregory" due-to="Dependabot, XenoAmess, Gary Gregory">Bump actions/cache from 2.1.4 to 3.0.4 #742, #752, #764, #833, #867.</action>
<action type="update" dev="ggregory" due-to="Dependabot">Bump actions/checkout from 2 to 3 #819, #825, #859.</action>
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 1e2fd132c..0f91ed429 100644
--- a/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java
+++ b/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java
@@ -24,11 +24,14 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import org.apache.commons.lang3.ArraySorter;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.Validate;
/**
@@ -385,6 +388,35 @@ public class ReflectionToStringBuilder extends ToStringBuilder {
return toStringExclude(object, toNoNullStringArray(excludeFieldNames));
}
+ /**
+ * Builds a String for a toString method including the given field names.
+ *
+ * @param object
+ * The object to "toString".
+ * @param includeFieldNames
+ * {@code null} or empty means all fields are included. All fields are included by default. This method will override the default behavior.
+ * @return The toString value.
+ * @since 3.13.0
+ */
+ public static String toStringInclude(final Object object, final Collection<String> includeFieldNames) {
+ return toStringInclude(object, toNoNullStringArray(includeFieldNames));
+ }
+
+ /**
+ * Builds a String for a toString method including the given field names.
+ *
+ * @param object
+ * The object to "toString".
+ * @param includeFieldNames
+ * The field names to include. {@code null} or empty means all fields are included. All fields are included by default. This method will override the default
+ * behavior.
+ * @return The toString value.
+ * @since 3.13.0
+ */
+ public static String toStringInclude(final Object object, final String... includeFieldNames) {
+ return new ReflectionToStringBuilder(object).setIncludeFieldNames(includeFieldNames).toString();
+ }
+
/**
* Converts the given Collection into an array of Strings. The returned array does not contain {@code null}
* entries. Note that {@link Arrays#sort(Object[])} will throw an {@link NullPointerException} if an array element
@@ -460,6 +492,13 @@ public class ReflectionToStringBuilder extends ToStringBuilder {
*/
protected String[] excludeFieldNames;
+ /**
+ * Field names that will be included in the output. All fields are included by default.
+ *
+ * @since 3.13.0
+ */
+ protected String[] includeFieldNames;
+
/**
* The last super class to stop appending fields for.
*/
@@ -614,11 +653,18 @@ public class ReflectionToStringBuilder extends ToStringBuilder {
// Reject static fields.
return false;
}
+
if (this.excludeFieldNames != null
&& Arrays.binarySearch(this.excludeFieldNames, field.getName()) >= 0) {
// Reject fields from the getExcludeFieldNames list.
return false;
}
+
+ if (ArrayUtils.isNotEmpty(includeFieldNames)) {
+ // Accept fields from the getIncludeFieldNames list. {@code null} or empty means all fields are included. All fields are included by default.
+ return Arrays.binarySearch(this.includeFieldNames, field.getName()) >= 0;
+ }
+
return !field.isAnnotationPresent(ToStringExclude.class);
}
@@ -665,12 +711,24 @@ public class ReflectionToStringBuilder extends ToStringBuilder {
}
/**
- * @return Returns the excludeFieldNames.
+ * Gets the excludeFieldNames.
+ *
+ * @return the excludeFieldNames.
*/
public String[] getExcludeFieldNames() {
return this.excludeFieldNames.clone();
}
+ /**
+ * Gets the includeFieldNames
+ *
+ * @return the includeFieldNames.
+ * @since 3.13.0
+ */
+ public String[] getIncludeFieldNames() {
+ return this.includeFieldNames.clone();
+ }
+
/**
* <p>
* Gets the last super class to stop appending fields for.
@@ -800,12 +858,30 @@ public class ReflectionToStringBuilder extends ToStringBuilder {
if (excludeFieldNamesParam == null) {
this.excludeFieldNames = null;
} else {
- //clone and remove nulls
+ // clone and remove nulls
this.excludeFieldNames = ArraySorter.sort(toNoNullStringArray(excludeFieldNamesParam));
}
return this;
}
+ /**
+ * Sets the field names to include. {@code null} or empty means all fields are included. All fields are included by default. This method will override the default behavior.
+ *
+ * @param includeFieldNamesParam
+ * The includeFieldNames that must be on toString or {@code null}.
+ * @return {@code this}
+ * @since 3.13.0
+ */
+ public ReflectionToStringBuilder setIncludeFieldNames(final String... includeFieldNamesParam) {
+ if (includeFieldNamesParam == null) {
+ this.includeFieldNames = null;
+ } else {
+ // clone and remove nulls
+ this.includeFieldNames = ArraySorter.sort(toNoNullStringArray(includeFieldNamesParam));
+ }
+ return this;
+ }
+
/**
* <p>
* Sets the last super class to stop appending fields for.
@@ -836,6 +912,9 @@ public class ReflectionToStringBuilder extends ToStringBuilder {
if (this.getObject() == null) {
return this.getStyle().getNullText();
}
+
+ validate();
+
Class<?> clazz = this.getObject().getClass();
this.appendFieldsIn(clazz);
while (clazz.getSuperclass() != null && clazz != this.getUpToClass()) {
@@ -845,4 +924,11 @@ public class ReflectionToStringBuilder extends ToStringBuilder {
return super.toString();
}
+ private void validate() {
+ if (ArrayUtils.containsAny(this.excludeFieldNames, (Object[]) this.includeFieldNames)) {
+ ToStringStyle.unregister(this.getObject());
+ throw new IllegalStateException("includeFieldNames and excludeFieldNames must not intersect");
+ }
+ }
+
}
diff --git a/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeTest.java b/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeTest.java
new file mode 100644
index 000000000..e5141732e
--- /dev/null
+++ b/src/test/java/org/apache/commons/lang3/builder/ReflectionToStringBuilderIncludeTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.commons.lang3.AbstractLangTest;
+import org.apache.commons.lang3.ArrayUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class ReflectionToStringBuilderIncludeTest extends AbstractLangTest {
+
+ class TestFeature {
+ @SuppressWarnings("unused")
+ private final String field1 = VALUES[0];
+
+ @SuppressWarnings("unused")
+ private final String field2 = VALUES[1];
+
+ @SuppressWarnings("unused")
+ private final String field3 = VALUES[2];
+
+ @SuppressWarnings("unused")
+ private final String field4 = VALUES[3];
+
+ @SuppressWarnings("unused")
+ private final String field5 = VALUES[4];
+ }
+
+ private static final String[] FIELDS = {"field1", "field2", "field3", "field4", "field5"};
+
+ private static final String[] VALUES = {"value 1", "value 2", "value 3", "value 4", "value 5"};
+
+ private static final String SINGLE_FIELD_TO_SHOW = FIELDS[2];
+
+ private static final String SINGLE_FIELD_VALUE_TO_SHOW = VALUES[2];
+
+ private static final String[] FIELDS_TO_SHOW = {FIELDS[0], FIELDS[3]};
+
+ private static final String[] FIELDS_VALUES_TO_SHOW = {VALUES[0], VALUES[3]};
+
+ @Test
+ public void test_toStringInclude() {
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), SINGLE_FIELD_TO_SHOW);
+ this.validateIncludeFieldsPresent(toString, new String[]{ SINGLE_FIELD_TO_SHOW }, new String[]{ SINGLE_FIELD_VALUE_TO_SHOW });
+ }
+
+ @Test
+ public void test_toStringIncludeArray() {
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), FIELDS_TO_SHOW);
+ this.validateIncludeFieldsPresent(toString, FIELDS_TO_SHOW, FIELDS_VALUES_TO_SHOW);
+ }
+
+ @Test
+ public void test_toStringIncludeWithoutInformingFields() {
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature());
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringIncludeArrayWithNull() {
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), new String[]{null});
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringIncludeArrayWithNulls() {
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), null, null);
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringIncludeCollection() {
+ final List<String> includeList = new ArrayList<>();
+ includeList.add(SINGLE_FIELD_TO_SHOW);
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), includeList);
+ this.validateIncludeFieldsPresent(toString, new String[]{ SINGLE_FIELD_TO_SHOW }, new String[]{ SINGLE_FIELD_VALUE_TO_SHOW });
+ }
+
+ @Test
+ public void test_toStringIncludeCollectionWithNull() {
+ final List<String> includeList = new ArrayList<>();
+ includeList.add(null);
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), includeList);
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringIncludeCollectionWithNulls() {
+ final List<String> includeList = new ArrayList<>();
+ includeList.add(null);
+ includeList.add(null);
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), includeList);
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringIncludeEmptyArray() {
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), ArrayUtils.EMPTY_STRING_ARRAY);
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringIncludeEmptyCollection() {
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), new ArrayList<>());
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringIncludeNullArray() {
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), (String[]) null);
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringIncludeNullArrayMultiplesValues() {
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), new String[] {null, null, null, null});
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringIncludeNullCollection() {
+ final String toString = ReflectionToStringBuilder.toStringInclude(new TestFeature(), (Collection<String>) null);
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringDefaultBehavior() {
+ ReflectionToStringBuilder builder = new ReflectionToStringBuilder(new TestFeature());
+ final String toString = builder.toString();
+ this.validateAllFieldsPresent(toString);
+ }
+
+ @Test
+ public void test_toStringSetIncludeAndExcludeWithoutIntersection() {
+ ReflectionToStringBuilder builder = new ReflectionToStringBuilder(new TestFeature());
+ builder.setExcludeFieldNames(FIELDS[1], FIELDS[4]);
+ builder.setIncludeFieldNames(FIELDS_TO_SHOW);
+ final String toString = builder.toString();
+ this.validateIncludeFieldsPresent(toString, FIELDS_TO_SHOW, FIELDS_VALUES_TO_SHOW);
+ }
+
+ @Test
+ public void test_toStringSetIncludeAndExcludeWithIntersection() {
+ ReflectionToStringBuilder builder = new ReflectionToStringBuilder(new TestFeature());
+ builder.setExcludeFieldNames(FIELDS[1], FIELDS[4]);
+ builder.setIncludeFieldNames(FIELDS[0], FIELDS[1]);
+ Assertions.assertThrows(IllegalStateException.class, () -> {
+ builder.toString();
+ });
+ }
+
+ @Test
+ public void test_toStringSetIncludeWithMultipleNullFields() {
+ ReflectionToStringBuilder builder = new ReflectionToStringBuilder(new TestFeature());
+ builder.setExcludeFieldNames(FIELDS[1], FIELDS[4]);
+ builder.setIncludeFieldNames(null, null, null);
+ final String toString = builder.toString();
+ this.validateIncludeFieldsPresent(toString, new String[]{FIELDS[0], FIELDS[2], FIELDS[3]}, new String[]{VALUES[0], VALUES[2], VALUES[3]});
+ }
+
+ @Test
+ public void test_toStringSetIncludeWithArrayWithMultipleNullFields() {
+ ReflectionToStringBuilder builder = new ReflectionToStringBuilder(new TestFeature());
+ builder.setExcludeFieldNames(new String[] {FIELDS[1], FIELDS[4]});
+ builder.setIncludeFieldNames(new String[] {null, null, null});
+ final String toString = builder.toString();
+ this.validateIncludeFieldsPresent(toString, new String[]{FIELDS[0], FIELDS[2], FIELDS[3]}, new String[]{VALUES[0], VALUES[2], VALUES[3]});
+ }
+
+ @Test
+ public void test_toStringSetIncludeAndExcludeWithRandomFieldsWithIntersection() {
+ ReflectionToStringBuilder builder = new ReflectionToStringBuilder(new TestFeature());
+ builder.setExcludeFieldNames(FIELDS[1], "random1");
+ builder.setIncludeFieldNames("random1");
+ Assertions.assertThrows(IllegalStateException.class, () -> {
+ builder.toString();
+ });
+ }
+
+ @Test
+ public void test_toStringSetIncludeAndExcludeWithRandomFieldsWithoutIntersection() {
+ ReflectionToStringBuilder builder = new ReflectionToStringBuilder(new TestFeature());
+ builder.setExcludeFieldNames(FIELDS[1], "random1");
+ builder.setIncludeFieldNames("random2", FIELDS[2]);
+ final String toString = builder.toString();
+ this.validateIncludeFieldsPresent(toString, new String[]{FIELDS[2]}, new String[]{VALUES[2]});
+ }
+
+ private void validateAllFieldsPresent(String toString) {
+ validateIncludeFieldsPresent(toString, FIELDS, VALUES);
+ }
+
+ private void validateIncludeFieldsPresent(final String toString, final String[] fieldsToShow, final String[] valuesToShow) {
+ for (String includeField : fieldsToShow) {
+ assertTrue(toString.indexOf(includeField) > 0);
+ }
+
+ for (String includeValue : valuesToShow) {
+ assertTrue(toString.indexOf(includeValue) > 0);
+ }
+
+ this.validateNonIncludeFieldsAbsent(toString, fieldsToShow, valuesToShow);
+ }
+
+ private void validateNonIncludeFieldsAbsent(String toString, String[] IncludeFields, String[] IncludeFieldsValues) {
+ String[] nonIncludeFields = ArrayUtils.removeElements(FIELDS.clone(), IncludeFields);
+ String[] nonIncludeFieldsValues = ArrayUtils.removeElements(VALUES.clone(), IncludeFieldsValues);
+
+ for (String nonIncludeField : nonIncludeFields) {
+ assertEquals(ArrayUtils.INDEX_NOT_FOUND, toString.indexOf(nonIncludeField));
+ }
+
+ for (String nonIncludeValue : nonIncludeFieldsValues) {
+ assertEquals(ArrayUtils.INDEX_NOT_FOUND, toString.indexOf(nonIncludeValue));
+ }
+ }
+}