View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.lang3.builder;
18  
19  import java.util.Collection;
20  import java.util.concurrent.atomic.AtomicBoolean;
21  import java.util.function.Supplier;
22  
23  import org.apache.commons.lang3.ClassUtils;
24  import org.apache.commons.lang3.mutable.MutableBoolean;
25  
26  /**
27   * Works with {@link ToStringBuilder} to create a "deep" {@code toString}.
28   * <p>
29   * To use this class write code as follows:
30   * </p>
31   *
32   * <pre>
33   * public class Job {
34   *   String title;
35   *   ...
36   * }
37   *
38   * public class Person {
39   *   String name;
40   *   int age;
41   *   boolean smoker;
42   *   Job job;
43   *
44   *   ...
45   *
46   *   public String toString() {
47   *     return new ReflectionToStringBuilder(this, new RecursiveToStringStyle()).toString();
48   *   }
49   * }
50   * </pre>
51   * <p>
52   * This will produce a toString of the format: {@code Person@7f54[name=Stephen,age=29,smoker=false,job=Job@43cd2[title=Manager]]}
53   * </p>
54   * <p>
55   * Graph safety: within one top-level {@code toString()} call, each object is rendered in detail at most once. A second (identity-equal) occurrence of an object
56   * (whether through a true cycle or through a shared (acyclic) reference) is rendered in the abbreviated {@code Object.toString()} format instead of being
57   * re-traversed. This keeps traversal cost linear in the size of the object graph; without it, shared references (reference "diamonds") would be re-traversed
58   * exponentially. An optional output-length limit can be set via {@link RecursiveToStringStyle.Builder#setMaxOutputLength(int)}; once the produced string
59   * reaches the limit, further nested objects are replaced by a {@code "...<truncated>"} marker.
60   * </p>
61   *
62   * @since 3.2
63   */
64  public class RecursiveToStringStyle extends ToStringStyle {
65  
66      /**
67       * Builder for {@link RecursiveToStringStyle} instances.
68       */
69      public static class Builder implements Supplier<RecursiveToStringStyle> {
70  
71          private int maxOutputLength;
72  
73          private Builder() {
74              this.maxOutputLength = 0;
75          }
76  
77          @Override
78          public RecursiveToStringStyle get() {
79              return new RecursiveToStringStyle(this);
80          }
81  
82          /**
83           * Sets the maximum length the output buffer may reach before nested objects are elided with {@link #TRUNCATED_TEXT}; {@code 0} (the default) means
84           * unlimited. This is a throttle, not an exact bound: objects already being rendered may still append their shallow content.
85           *
86           * @param maxOutputLength once the produced string reaches this length, further nested objects are replaced by a {@code "...<truncated>"} marker;
87           *                        {@code 0} means unlimited.
88           * @return this builder for chaining.
89           */
90          public Builder setMaxOutputLength(final int maxOutputLength) {
91              this.maxOutputLength = maxOutputLength;
92              return this;
93          }
94      }
95  
96      /**
97       * Required for serialization support.
98       *
99       * @see java.io.Serializable
100      */
101     private static final long serialVersionUID = 1L;
102 
103     /**
104      * Marker appended in place of a nested object once {@link #maxOutputLength} is reached.
105      */
106     private static final String TRUNCATED_TEXT = "...<truncated>";
107 
108     /**
109      * Creates a new {@link Builder} for {@link RecursiveToStringStyle} instances.
110      *
111      * @return a new {@link Builder} for {@link RecursiveToStringStyle} instances.
112      */
113     public static Builder builder() {
114         return new Builder();
115     }
116 
117     /**
118      * Maximum length the output buffer may reach before nested objects are elided with
119      * {@link #TRUNCATED_TEXT}; {@code 0} (the default) means unlimited. This is a throttle,
120      * not an exact bound: objects already being rendered may still append their shallow content.
121      */
122     private final int maxOutputLength;
123 
124     /**
125      * Constructs a new instance with no output-length limit.
126      */
127     public RecursiveToStringStyle() {
128         this(RecursiveToStringStyle.builder());
129     }
130 
131     private RecursiveToStringStyle(final Builder builder) {
132         this.maxOutputLength = builder.maxOutputLength;
133     }
134 
135     /**
136      * Constructs a new instance with an output-length limit.
137      *
138      * @param maxOutputLength once the produced string reaches this length, further nested
139      *        objects are replaced by a {@code "...<truncated>"} marker; {@code 0} means unlimited.
140      * @since 3.21.0
141      */
142     public RecursiveToStringStyle(final int maxOutputLength) {
143         this.maxOutputLength = maxOutputLength;
144     }
145 
146     /**
147      * Tests whether or not to recursively format the given {@link Class}.
148      * <p>
149      * By default, this method always filters out the following:
150      * </p>
151      * <ul>
152      * <li><a href="https://docs.oracle.com/javase/specs/jls/se25/html/jls-5.html#jls-5.1.7">Boxed primitives</a>, see {@link ClassUtils#isPrimitiveWrapper(Class)}
153      * <li>{@link String}</li>
154      * <li>{@link Number} subclasses</li>
155      * <li>{@link AtomicBoolean}</li>
156      * <li>{@link MutableBoolean}</li>
157      * </ul>
158      *
159      * @param clazz The class to test.
160      * @return Whether or not to recursively format instances of the given {@link Class}.
161      */
162     protected boolean accept(final Class<?> clazz) {
163         // @formatter:off
164         return !ClassUtils.isPrimitiveWrapper(clazz) &&
165                !String.class.equals(clazz) &&
166                !Number.class.isAssignableFrom(clazz) &&
167                !AtomicBoolean.class.equals(clazz) &&
168                !MutableBoolean.class.equals(clazz);
169         // @formatter:on
170     }
171 
172     @Override
173     protected void appendDetail(final StringBuffer buffer, final String fieldName, final Collection<?> coll) {
174         appendClassName(buffer, coll);
175         appendIdentityHashCode(buffer, coll);
176         appendDetail(buffer, fieldName, coll.toArray());
177     }
178 
179     @Override
180     public void appendDetail(final StringBuffer buffer, final String fieldName, final Object value) {
181         if (value != null && accept(value.getClass())) {
182             buffer.append(ReflectionToStringBuilder.toString(value, this));
183         } else {
184             super.appendDetail(buffer, fieldName, value);
185         }
186     }
187 
188     /**
189      * {@inheritDoc}
190      *
191      * <p>
192      * In addition to the cycle check performed by the superclass, this implementation keeps a
193      * per-thread set of objects already rendered in detail during the current top-level call.
194      * Values that this style would traverse structurally (see {@link #accept(Class)}) are rendered
195      * in detail at most once; later identity-equal occurrences are appended in the abbreviated
196      * {@code Object.toString()} format. This bounds the traversal to one visit per object, so
197      * shared (acyclic) references cannot cause exponential re-traversal. If an output-length limit
198      * was configured and the buffer has reached it, a {@code "...<truncated>"} marker is appended
199      * instead of the value.
200      * </p>
201      */
202     @Override
203     protected void appendInternal(final StringBuffer buffer, final String fieldName, final Object value, final boolean detail) {
204         if (detail && value != null && accept(value.getClass())
205                 && !(value instanceof Number || value instanceof Boolean || value instanceof Character)) {
206             if (isVisited(value)) {
207                 appendCyclicObject(buffer, fieldName, value);
208                 return;
209             }
210             if (maxOutputLength > 0 && buffer.length() >= maxOutputLength) {
211                 buffer.append(TRUNCATED_TEXT);
212                 return;
213             }
214             markVisited(value);
215         }
216         super.appendInternal(buffer, fieldName, value, detail);
217     }
218 }