001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.lang3.builder;
018
019import java.util.Collection;
020import java.util.concurrent.atomic.AtomicBoolean;
021import java.util.function.Supplier;
022
023import org.apache.commons.lang3.ClassUtils;
024import org.apache.commons.lang3.mutable.MutableBoolean;
025
026/**
027 * Works with {@link ToStringBuilder} to create a "deep" {@code toString}.
028 * <p>
029 * To use this class write code as follows:
030 * </p>
031 *
032 * <pre>
033 * public class Job {
034 *   String title;
035 *   ...
036 * }
037 *
038 * public class Person {
039 *   String name;
040 *   int age;
041 *   boolean smoker;
042 *   Job job;
043 *
044 *   ...
045 *
046 *   public String toString() {
047 *     return new ReflectionToStringBuilder(this, new RecursiveToStringStyle()).toString();
048 *   }
049 * }
050 * </pre>
051 * <p>
052 * This will produce a toString of the format: {@code Person@7f54[name=Stephen,age=29,smoker=false,job=Job@43cd2[title=Manager]]}
053 * </p>
054 * <p>
055 * 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
056 * (whether through a true cycle or through a shared (acyclic) reference) is rendered in the abbreviated {@code Object.toString()} format instead of being
057 * re-traversed. This keeps traversal cost linear in the size of the object graph; without it, shared references (reference "diamonds") would be re-traversed
058 * exponentially. An optional output-length limit can be set via {@link RecursiveToStringStyle.Builder#setMaxOutputLength(int)}; once the produced string
059 * reaches the limit, further nested objects are replaced by a {@code "...<truncated>"} marker.
060 * </p>
061 *
062 * @since 3.2
063 */
064public class RecursiveToStringStyle extends ToStringStyle {
065
066    /**
067     * Builder for {@link RecursiveToStringStyle} instances.
068     */
069    public static class Builder implements Supplier<RecursiveToStringStyle> {
070
071        private int maxOutputLength;
072
073        private Builder() {
074            this.maxOutputLength = 0;
075        }
076
077        @Override
078        public RecursiveToStringStyle get() {
079            return new RecursiveToStringStyle(this);
080        }
081
082        /**
083         * Sets the maximum length the output buffer may reach before nested objects are elided with {@link #TRUNCATED_TEXT}; {@code 0} (the default) means
084         * unlimited. This is a throttle, not an exact bound: objects already being rendered may still append their shallow content.
085         *
086         * @param maxOutputLength once the produced string reaches this length, further nested objects are replaced by a {@code "...<truncated>"} marker;
087         *                        {@code 0} means unlimited.
088         * @return this builder for chaining.
089         */
090        public Builder setMaxOutputLength(final int maxOutputLength) {
091            this.maxOutputLength = maxOutputLength;
092            return this;
093        }
094    }
095
096    /**
097     * Required for serialization support.
098     *
099     * @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}