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}