Skip to content

Instantly share code, notes, and snippets.

@stanio
Created December 11, 2020 06:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stanio/c7e810a8597287d515f89229924624af to your computer and use it in GitHub Desktop.
Save stanio/c7e810a8597287d515f89229924624af to your computer and use it in GitHub Desktop.
Dump object's internal state
package net.example.util;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* Dump of the object internal state.
* <pre>
* Object myObj;
* ...
* ObjectDump.dump(myObj, System.out);</pre>
*/
public class ObjectDump {
private static final int MAX_NEST_LEVEL = 10;
private static final int MAX_COLLECTION_ITEMS = 10;
private static final int MAX_STRING_LENGTH = 80;
private static String lineSep = System.lineSeparator();
private static String indentString = " ";
private Appendable out;
private StringBuilder currentIndent = new StringBuilder();
private Set<Object> dumped = new HashSet<>();
private int nestLevel = 0;
ObjectDump(Appendable out) {
this.out = out;
}
private void increaseIndent() {
currentIndent.append(indentString);
nestLevel += 1;
}
private void decreaseIndent() {
currentIndent.setLength(currentIndent.length() - indentString.length());
nestLevel -= 1;
}
private Appendable indent() throws IOException {
return out.append(currentIndent);
}
public static String toString(Object obj) {
StringBuilder buf = new StringBuilder();
dump(obj, buf);
return buf.toString();
}
public static void dump(Object obj, Appendable out) {
new ObjectDump(out).dump(obj);
}
void dump(Object obj) {
nestLevel = 0;
currentIndent.setLength(0);
try {
dumpValue(obj);
} catch (IOException e) {
e.printStackTrace();
} finally {
dumped.clear();
}
}
private void dumpValue(Object val) throws IOException {
if (val == null || val instanceof Boolean
|| val instanceof Number) {
out.append(String.valueOf(val));
} else if (val instanceof Character) {
dumpChar((char) val);
} else if (val instanceof String) {
dumpString((String) val);
} else if (nestLevel > MAX_NEST_LEVEL) {
dumpObjectID(val, '@');
} else if (dumped.contains(val)) { // prevent cycle and repeat
dumpObjectID(val, '#');
} else if (val.getClass().isArray()) {
dumped.add(val);
dumpArray(val);
} else if (val instanceof Map) {
dumped.add(val);
dumpMap((Map<?,?>) val);
} else if (val instanceof Collection) {
dumped.add(val);
dumpCollection((Collection<?>) val);
} else {
dumped.add(val);
dumpObject(val);
}
}
private void dumpSimpleValue(Object val) throws IOException {
if (val == null || val instanceof Boolean
|| val instanceof Number) {
out.append(String.valueOf(val));
} else if (val instanceof Character) {
dumpChar((char) val);
} else if (val instanceof String) {
dumpString((String) val);
} else {
dumpObjectID(val, '@');
}
}
private void dumpChar(char ch) throws IOException {
out.append('\'');
dumpChar0(ch);
out.append('\'');
}
private static final char[] digits = {
'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
};
private void dumpChar0(char ch) throws IOException {
if (ch < 32 || ch > 126) {
char[] hexDigit = digits;
out.append("\\u").append(hexDigit[(ch >> 12) & 0xf])
.append(hexDigit[(ch >> 8) & 0xf])
.append(hexDigit[(ch >> 4) & 0xf])
.append(hexDigit[ ch & 0xf]);
} else {
out.append(ch);
}
}
private void dumpString(String val) throws IOException {
int length = val.length();
out.append('"');
int max = Math.min(length, MAX_STRING_LENGTH);
for (int i = 0; i < max; i++) {
dumpChar0(val.charAt(i));
}
if (max < length) {
out.append("...");
}
out.append('"');
}
private void dumpMap(Map<?,?> map) throws IOException {
int size = map.size();
dumpObjectID(map, '@').append('(').append(String.valueOf(size)).append(") {").append(lineSep);
increaseIndent();
int max = Math.min(size, MAX_COLLECTION_ITEMS);
for (Map.Entry<?,?> entry : map.entrySet()) {
indent();
dumpSimpleValue(entry.getKey());
out.append(": ");
dumpValue(entry.getValue());
out.append(',').append(lineSep);
if (--max == 0) break;
}
if (size > MAX_COLLECTION_ITEMS) {
indent().append("...").append(lineSep);
}
decreaseIndent();
indent().append("}");
}
private void dumpCollection(Collection<?> col) throws IOException {
int size = col.size();
dumpObjectID(col, '@').append('(').append(String.valueOf(size)).append(") [").append(lineSep);
increaseIndent();
int max = Math.min(size, MAX_COLLECTION_ITEMS);
for (Object item : col) {
indent();
dumpValue(item);
out.append(',').append(lineSep);
if (--max == 0) break;
}
if (size > MAX_COLLECTION_ITEMS) {
indent().append("...").append(lineSep);
}
decreaseIndent();
indent().append("]");
}
private Appendable dumpObjectID(Object obj, char sep) throws IOException {
if (obj.getClass().isArray()) {
out.append(obj.getClass().getComponentType().getName()).append("[]");
} else {
out.append(obj.getClass().getName());
}
return out.append(sep).append(String.valueOf(System.identityHashCode(obj)));
}
private void dumpArray(Object arr) throws IOException {
int length = Array.getLength(arr);
dumpObjectID(arr, '@').append('(').append(String.valueOf(length)).append(") [").append(lineSep);
Class<?> componentType = arr.getClass().getComponentType();
boolean compact = componentType.isPrimitive()
|| componentType == Character.class
|| Number.class.isAssignableFrom(componentType);
increaseIndent();
if (compact) indent();
int max = Math.min(length, MAX_COLLECTION_ITEMS);
for (int i = 0; i < max; i++) {
if (!compact) indent();
dumpValue(Array.get(arr, i));
if (compact) out.append(", ");
else out.append(',').append(lineSep);
}
if (max < length) {
if (compact) out.append("...");
else indent().append("...").append(lineSep);
}
if (compact) out.append(lineSep);
decreaseIndent();
indent().append("]");
}
private void dumpObject(Object obj) throws IOException {
dumpObjectID(obj, '@').append(" {").append(lineSep);
increaseIndent();
Map<String, Collection<Field>> fields = mapFields(obj);
dumpFields(obj, fields.get("public"), "public ");
dumpFields(obj, fields.get("protected"), "protected ");
dumpFields(obj, fields.get(""), "");
dumpFields(obj, fields.get("private"), "");
decreaseIndent();
indent().append("}");
}
private void dumpFields(Object obj, Collection<Field> fields, String accessModifier) throws IOException {
if (fields == null) return;
for (Field field : fields) {
Object value;
try {
field.setAccessible(true);
value = field.get(obj);
} catch (Exception e) {
value = "<error>";
}
indent().append(accessModifier).append(field.getName()).append(": ");
if (field.getClass().isPrimitive()) {
out.append(String.valueOf(value));
} else {
dumpValue(value);
}
out.append(",").append(lineSep);
}
}
private static Map<String, Collection<Field>> mapFields(Object obj) {
Map<String, Collection<Field>> fieldMap = new HashMap<>();
Class<?> currentClass = obj.getClass();
while (currentClass != null && currentClass != Object.class) {
Field[] declaredFields = currentClass.getDeclaredFields();
for (int i = 0, len = declaredFields.length; i < len; i++) {
Field field = declaredFields[i];
if (Modifier.isStatic(field.getModifiers()) || field.isSynthetic())
continue;
fieldMap.computeIfAbsent(getAccessModifier(field.getModifiers()),
k -> new ArrayList<>())
.add(field);
}
currentClass = currentClass.getSuperclass();
}
return fieldMap;
}
private static String getAccessModifier(int modifiers) {
if (Modifier.isPublic(modifiers)) {
return "public";
} else if (Modifier.isPrivate(modifiers)) {
return "private";
} else if (Modifier.isProtected(modifiers)) {
return "protected";
}
return "";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment