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));
+        }
+    }
+}