1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package com.healthmarketscience.jackcess.impl;
17
18 import java.lang.reflect.AccessibleObject;
19 import java.lang.reflect.Array;
20 import java.lang.reflect.Field;
21 import java.lang.reflect.Modifier;
22 import java.nio.ByteBuffer;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.Comparator;
26 import java.util.Map;
27 import java.util.Optional;
28 import java.util.WeakHashMap;
29 import java.util.function.Function;
30
31
32
33
34
35
36
37
38 public final class ToStringBuilder
39 {
40
41 private static final ThreadLocal<Map<Object, Object>> REGISTRY = new ThreadLocal<>();
42
43 private static final int MAX_BYTE_DETAIL_LEN = 20;
44 private static final String NULL_TEXT = "<null>";
45 private static final String IMPL_SUFF = "Impl";
46 private static final String LINE_SEP = System.lineSeparator();
47
48 private final StringBuilder _buffer;
49 private final Object _object;
50 private final String _fieldSeparator;
51 private final boolean _fieldSeparatorSurround;
52 private final String _fieldNameValueSeparator;
53 private final String _arraySeparator;
54 private final String _contentStart;
55 private final String _contentEnd;
56 private final boolean _useIdentityHashCode;
57 private final Function<Object,String> _valueFormatter;
58
59 private ToStringBuilder(
60 Object object, String fieldSeparator, boolean fieldSeparatorSurround,
61 String fieldNameValueSeparator, String arraySeparator,
62 String contentStart, String contentEnd, boolean useIdentityHashCode) {
63 _buffer = new StringBuilder(512);
64 _object = object;
65 _fieldSeparator = Optional.ofNullable(fieldSeparator).orElse(",");
66 _fieldSeparatorSurround = fieldSeparatorSurround;
67 _fieldNameValueSeparator = Optional.ofNullable(fieldNameValueSeparator).orElse("=");
68 _arraySeparator = Optional.ofNullable(arraySeparator).orElse(",");
69 _contentStart = Optional.ofNullable(contentStart).orElse("[");
70 _contentEnd = Optional.ofNullable(contentEnd).orElse("]");
71 _useIdentityHashCode = useIdentityHashCode;
72
73 _valueFormatter = (_fieldSeparatorSurround &&
74 (_fieldSeparator.length() > LINE_SEP.length()) &&
75 _fieldSeparator.startsWith(LINE_SEP)) ?
76 this::indentValue : Object::toString;
77
78 if (_object != null) {
79 register(_object);
80 _buffer.append(getShortClassName(_object));
81 if (_useIdentityHashCode) {
82 appendIdentityHashCode(_object, _buffer);
83 }
84 _buffer.append(_contentStart);
85 }
86 }
87
88 public static ToStringBuilder valueBuilder(Object obj) {
89 return new ToStringBuilder(obj, null, false, null, null, null, null, false);
90 }
91
92 public static ToStringBuilder builder(Object obj) {
93 String fieldSep = LINE_SEP + " ";
94 return new ToStringBuilder(obj, fieldSep, true, ": ", "," + fieldSep,
95 "[" + fieldSep, LINE_SEP + "]", true);
96 }
97
98 public ToStringBuilder append(String fieldName, Object value) {
99 if (fieldName != null) {
100 _buffer.append(fieldName).append(_fieldNameValueSeparator);
101 }
102
103 appendValue(value, _buffer);
104
105 _buffer.append(_fieldSeparator);
106 return this;
107 }
108
109 public ToStringBuilder appendIfNotNull(String fieldName, Object value) {
110 if(value == null) {
111 return this;
112 }
113 return append(fieldName, value);
114 }
115
116 public String reflectionToString() {
117
118 Class<?> clazz = _object.getClass();
119 while(clazz != null) {
120 appendFieldsIn(clazz);
121 clazz = clazz.getSuperclass();
122 }
123
124 return toString();
125 }
126
127 @Override
128 public String toString() {
129 if (_object == null) {
130 _buffer.append(NULL_TEXT);
131 } else {
132 removeLastFieldSeparator();
133 _buffer.append(_contentEnd);
134 unregister(_object);
135 }
136 return _buffer.toString();
137 }
138
139 private void appendInternal(Object value, StringBuilder buffer) {
140 boolean primitiveWrapper = (value instanceof Number) || (value instanceof Boolean)
141 || (value instanceof Character);
142
143 if (isRegistered(value) && !primitiveWrapper) {
144 buffer.append(value.getClass().getName());
145 appendIdentityHashCode(value, buffer);
146 return;
147 }
148
149 register(value);
150
151 try {
152
153 if (value instanceof byte[]) {
154
155 appendByteArrayInternal((byte[]) value, buffer);
156
157 } else if (value.getClass().isArray()) {
158
159 appendArrayInternal(value, buffer);
160
161 } else if (value instanceof Collection<?>) {
162
163 appendCollectionInternal((Collection<?>)value, buffer);
164
165 } else if (value instanceof Map<?, ?>) {
166
167 appendMapInternal((Map<?,?>)value, buffer);
168
169 } else {
170
171 buffer.append(_valueFormatter.apply(value));
172 }
173
174 } finally {
175 unregister(value);
176 }
177 }
178
179 private static void appendByteArrayInternal(byte[] bar, StringBuilder buffer) {
180 ByteBuffer bb = PageChannel.wrap(bar);
181 int len = bb.remaining();
182 buffer.append("(").append(len).append(") ").append(
183 ByteUtil.toHexString(bb, bb.position(), Math.min(len, MAX_BYTE_DETAIL_LEN)));
184 if (len > MAX_BYTE_DETAIL_LEN) {
185 buffer.append("...");
186 }
187 }
188
189 private void appendArrayInternal(Object arr, StringBuilder buffer) {
190
191 buffer.append('[');
192
193 int len = Array.getLength(arr);
194 if(len > 0) {
195 StringBuilder valueBuffer = new StringBuilder(512);
196 if(_fieldSeparatorSurround) {
197 valueBuffer.append(_fieldSeparator);
198 }
199 for (int i = 0; i < len; i++) {
200 if (i > 0) {
201 valueBuffer.append(_arraySeparator);
202 }
203 appendValue(Array.get(arr, i), valueBuffer);
204 }
205 buffer.append(_valueFormatter.apply(valueBuffer));
206 if(_fieldSeparatorSurround){
207 buffer.append(_fieldSeparator);
208 }
209 }
210
211 buffer.append(']');
212 }
213
214 private void appendCollectionInternal(Collection<?> col, StringBuilder buffer) {
215
216 buffer.append('[');
217
218 if(!col.isEmpty()) {
219 StringBuilder valueBuffer = new StringBuilder(512);
220 if(_fieldSeparatorSurround) {
221 valueBuffer.append(_fieldSeparator);
222 }
223 boolean isFirst = true;
224 for(Object v : col) {
225 if(!isFirst) {
226 valueBuffer.append(_arraySeparator);
227 }
228 appendValue(v, valueBuffer);
229 isFirst = false;
230 }
231 buffer.append(_valueFormatter.apply(valueBuffer));
232 if(_fieldSeparatorSurround) {
233 buffer.append(_fieldSeparator);
234 }
235 }
236
237 buffer.append(']');
238 }
239
240 private void appendMapInternal(Map<?,?> map, StringBuilder buffer) {
241
242 buffer.append('{');
243
244 if(!map.isEmpty()) {
245 StringBuilder valueBuffer = new StringBuilder(512);
246 if(_fieldSeparatorSurround) {
247 valueBuffer.append(_fieldSeparator);
248 }
249 boolean isFirst = true;
250 for(Map.Entry<?,?> e : map.entrySet()) {
251 if(!isFirst) {
252 valueBuffer.append(_arraySeparator);
253 }
254 valueBuffer.append(e.getKey()).append("=");
255 appendValue(e.getValue(), valueBuffer);
256 isFirst = false;
257 }
258 buffer.append(_valueFormatter.apply(valueBuffer));
259 if(_fieldSeparatorSurround) {
260 buffer.append(_fieldSeparator);
261 }
262 }
263
264 buffer.append('}');
265 }
266
267 private void appendValue(Object value, StringBuilder buffer) {
268 if (value == null) {
269 buffer.append(NULL_TEXT);
270 } else {
271 appendInternal(value, buffer);
272 }
273 }
274
275 private String indentValue(Object value) {
276 String valueStr = value.toString();
277 if(valueStr != null) {
278 valueStr = valueStr.replace(LINE_SEP, _fieldSeparator);
279 }
280 return valueStr;
281 }
282
283 private void appendFieldsIn(final Class<?> clazz) {
284 Field[] fields = clazz.getDeclaredFields();
285 Arrays.sort(fields, Comparator.comparing(Field::getName));
286 AccessibleObject.setAccessible(fields, true);
287 for (final Field field : fields) {
288 String fieldName = field.getName();
289 if (acceptReflectionField(field)) {
290 try {
291 Object value = field.get(_object);
292 if(value != null) {
293 if(fieldName.startsWith("_")) {
294 fieldName = fieldName.substring(1);
295 }
296 append(fieldName, value);
297 }
298 } catch (final IllegalAccessException ex) {
299
300 throw new InternalError("Unexpected IllegalAccessException: " + ex.getMessage());
301 }
302 }
303 }
304 }
305
306 private static void appendIdentityHashCode(Object value, StringBuilder buffer) {
307 buffer.append('@').append(Integer.toHexString(System.identityHashCode(value)));
308 }
309
310 private static boolean acceptReflectionField(final Field field) {
311
312 return ((field.getName().indexOf('$') < 0) &&
313
314 !Modifier.isStatic(field.getModifiers()));
315 }
316
317 private static String getShortClassName(Object value) {
318 if(value instanceof String) {
319
320 return (String)value;
321 }
322 String nm = value.getClass().getSimpleName();
323 if (nm.endsWith(IMPL_SUFF)) {
324 nm = nm.substring(0, nm.length() - IMPL_SUFF.length());
325 }
326 int idx = nm.lastIndexOf('.');
327 return idx >= 0 ? nm.substring(idx + 1) : nm;
328 }
329
330 private void removeLastFieldSeparator() {
331 int len = _buffer.length();
332 int sepLen = _fieldSeparator.length();
333 if (len > 0 && sepLen > 0 && len >= sepLen) {
334 for (int i = 0; i < sepLen; i++) {
335 if (_buffer.charAt(len - 1 - i) != _fieldSeparator.charAt(sepLen - 1 - i)) {
336 return;
337 }
338 }
339 _buffer.setLength(len - sepLen);
340 }
341 }
342
343 private static boolean isRegistered(Object value) {
344 final Map<Object, Object> m = getRegistry();
345 return m != null && m.containsKey(value);
346 }
347
348 private static void register(Object value) {
349 if (value != null) {
350 Map<Object, Object> m = getRegistry();
351 if (m == null) {
352 REGISTRY.set(new WeakHashMap<>());
353 }
354 getRegistry().put(value, null);
355 }
356 }
357
358 private static void unregister(Object value) {
359 if (value != null) {
360 Map<Object, Object> m = getRegistry();
361 if (m != null) {
362 m.remove(value);
363 if (m.isEmpty()) {
364 REGISTRY.remove();
365 }
366 }
367 }
368 }
369
370 private static Map<Object, Object> getRegistry() {
371 return REGISTRY.get();
372 }
373 }