View Javadoc
1   /*
2   Copyright (c) 2025 Markus Spann
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
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   * <p>Builder for {@link Object#toString()} methods.</p>
33   *
34   * Heavily borrowed/adapted/simplified from commons lang ToStringBuilder.
35   *
36   * @author Markus Spann
37   */
38  public final class ToStringBuilder
39  {
40    /** Object registry for avoidance of cycles. */
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           // this shouldn't happen. Would get a Security exception instead
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     // Reject field from inner class.
312     return ((field.getName().indexOf('$') < 0) &&
313             // Reject static fields.
314             !Modifier.isStatic(field.getModifiers()));
315   }
316 
317   private static String getShortClassName(Object value) {
318     if(value instanceof String) {
319       // caller passed in explicit "class" name
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 }