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 }