/*
* Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the Classpath exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.btrace;
import java.io.PrintStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.*;
import java.util.regex.Pattern;
/**
* Library for constructing a dot file format file containing the state of select
* objects in the current running application.
*
* Introduction
* ============
*
* Sometimes when debugging an complex problem, a picture is worth more than a
* thousand words. Looking at an object set graphically can simplify understanding
* of some of the problems.
*
* DOTWriter is designed to analyze a set of Java objects and generate a
* dot format file. The file can be passed to any number of tools for further
* analysis. The multi-platform tools are described at http://graphviz.org/. The
* most commonly used are the 'dot' command and the 'Graphviz' application.
*
* The 'dot' command can be used to convert a dot format file into any number of
* visual format files; jpg, png, pdf et cetera.
*
* Ex.
* dot -Tpdf -osample.pdf sample.dot
*
* There are also other tools to convert dot format files to other representations
* such gxl (using dot2gxl.)
*
* Using DOTWriter is straight forward. Simply construct an DOTWriter instance,
* add objects to observe, then close the instance.
*
* Ex.
*
* import com.sun.btrace.DOTWriter;
*
* var dot = new DOTWriter("sample.dot");
* dot.addNodes(a, b, c);
* dot.close();
*
* There is also a quick and dirty form to do the same. Note: to get dependency
* edges you need to build the class file with -XDannobindees
*
* Ex.
* DOTWriter.graph("sample.dot", a, b, c);
*
* The other calls on the public interface allows more detailed control of the
* graph. Details below.
*
* You also have the ability to control dot format properties.
*
* Ex.
* DOTWriter.graph("sample.dot", "fillcolor=lightblue", a, "fillcolor=lightpink", b, c);
*
* Will display 'a' in blue and 'b'/'c' in pink. Properties are specified in
* "k=v, ..., k=v" form.
*
*
* Public Interface
* ================
*
* Graphing
* --------
*
* public DOTWriter(String fileName);
*
* The constructor creates a file stream for the dot output. The filename string
* is the name of the file, and should have the .dot extension.
*
*
* public void close();
*
* This method writes the graph to the file stream and closes out the graph. Any
* further operations will be ignored.
*
*
* public static void graph(String fileName, Object... objects);
*
* All-in-one graph objects. Specify which objects you want graphed. If an object
* argument is a String then it is used as the default properties for all object
* arguments following.
*
*
* public void addNodes(Object... objects);
*
* Add a set of objects to the graph. If an object argument is a string then the
* string is used as a property string for the remaining object arguments.
*
*
* public void addNode(String propertyString, Object object);
*
* Add an individual object to the graph. The property string defines the dot
* Properties for the string. If the object is of primitive data type or null then
* it will be ignored.
*
*
* public void addEdge(Object head, Object tail);
* public void addEdge(Object head, Object tail, String propertyString);
* public void addEdge(Object head, int headFieldId, Object tail, int tailFieldId);
* public void addEdge(Object head, int headFieldId, Object tail, int tailFieldId, String propertyString);
*
* Add Edges to the node. Field ids indicate which slot to start/end from, -1
* indicates the whole object.
*
*
*
* Control Display
* ----------------
*
* public void objectLimit(int objectLimit);
*
* The maximum number of objects to display in detail in the graph (default=256.)
* More objects may be displayed but they will be truncated. Having a lower limit
* speeds up the processing of the graph.
*
*
* public void fieldLimit(int fieldLimit);
*
* The maximum number of fields to display for an object (default=64.) Having a
* lower limit speeds up the processing of the graph.
*
*
* public void arrayLimit(int arrayLimit);
*
* The maximum number of slots to display for an array (default=32.) Having a
* lower limit speeds up the processing of the graph.
*
*
* public void stringLimit(int stringLimit);
*
* The maximum length of a string to display (default=32.) Having a
* lower limit speeds up the processing of the graph.
*
*
* public void expandCollections(boolean expandCollections);
*
* Controls the display of collections. By default, collections are displayed
* as simple arrays. expandCollections == true, displays collections as
* individual java objects.
*
* public void displayStatics(boolean displayStatics)
*
* Controls whether static fields are displayed. By default displayStatics == false.
*
*
* public void displayLinks(boolean displayLinks);
*
* Controls whether data links are displayed. By default displayLinks == true.
* You may want to set displayLinks = false, if all you want to look at is
* dependencies.
*
* Filtering
* ---------
*
* public void includeObjects(Object... objects);
*
* Only objects specified are included in the graph.
*
*
* public void excludeObjects(Object... objects)
*
* Objects specified are excluded from the graph. This may truncated data links
* as well.
*
*
* public void includeClasses(Class... clazzes);
*
* Only objects that are instances of the specified classes are included in the
* graph.
*
* public void excludeClasses(Class... clazzes);
*
* Classes to be excluded from the graph.
*
* @author Jim Laskey
*
*/
public class DOTWriter {
// Property settings for fonts.
final static String FONTDEFAULTS = "fontname=Helvetica, fontcolor=black, fontsize=10";
// Default property settings for entire graph.
final static String GRAPHDEFAULTS = FONTDEFAULTS + ", rankdir=LR";
// Default property settings for nodes.
final static String NODEDEFAULTS = FONTDEFAULTS + ", label=\"\\N\", shape=record, style=filled, fillcolor=lightgrey, color=black";
// Default property settings for edges.
final static String EDGEDEFAULTS = FONTDEFAULTS + ", arrowhead=open";
// Style of starting node.
final static String STARTNODESTYLE = "fillcolor=pink";
// Style of intra dependency edge.
final static String INTRAEDGESTYLE = "style=dashed, color=grey";
// Style of inter dependency edge.
final static String INTEREDGESTYLE = "style=dashed, color=darkGrey";
// Maximum number of objects displayed.
private int objectLimit = 256;
// Maximum number of field entries displayed.
private int fieldLimit = 64;
// Maximum number of array entries displayed.
private int arrayLimit = 32;
// Maximum number of string characters displayed.
private int stringLimit = 32;
// Map of visited objects.
private Map<Object, Node> visited = new IdentityHashMap<Object, Node>();
// Cache of field information.
Map<Class, Field[]> fieldCache = new HashMap<Class, Field[]>();
// Graph properties.
private Properties graphProperties = new Properties();
// Default node properties.
private Properties nodeProperties = new Properties();
// Default edge properties.
private Properties edgeProperties = new Properties();
// List of nodes.
private List<Node> nodes = new ArrayList<Node>();
// List of edges.
private List<Edge> edges = new ArrayList<Edge>();
// Output stream.
private PrintStream dotStream;
// True if filters are active.
private boolean filtering = false;
// Include set of instances.
private Set<Object> includeObjects = new HashSet<Object>();
// Exclude set of instances.
private Set<Object> excludeObjects = new HashSet<Object>();
// Include List of classes.
private List<Class> includeClasses = new ArrayList<Class>();
// Include classes which matching name pattern
private Pattern includeClassNames;
// Exclude List of classes.
private List<Class> excludeClasses = new ArrayList<Class>();
// Excluse classes with matching name pattern
private Pattern excludeClassNames;
// True if collections should be expanded.
private boolean expandCollections = false;
// True if static fields should be displayed.
private boolean displayStatics = false;
// True if object links should be shown.
private boolean displayLinks = true;
// This class maintains properties.
static class Properties {
// Property map.
private Map<String, String> properties = new HashMap<String, String>();
// Adds a new property to the map.
void addProperty(String key, String value) {
properties.put(key, value);
}
// Adds new properties from a "key=value, key=value" string.
void addProperties(String propertyString) {
// Split on the commas.
String[] kvs = propertyString.split("\\s*,\\s*");
// For each key=value pair.
for (String kv : kvs) {
// Split on the equals.
String[] pair = kv.split("\\s*=\\s*");
// If no equals.
if (pair.length == 1) {
properties.put(pair[0], "true");
} else {
properties.put(pair[0], pair[1]);
}
}
}
// Adds new information to a property.
void extendProperty(String key, String extension) {
// Get existing property
String value = getProperty(key);
// If no property exists then start one.
if (value == null) value = "";
// Add the extension to the value.
value += extension;
// Update the property.
addProperty(key, value);
}
// Return the value of the property, null if not found.
String getProperty(String key) {
return properties.get(key);
}
// Write properties to a stream in the form "[key=value, ..., key=value]".
void writeProperties(PrintStream dotStream) {
// Only if there are properties.
if (!properties.isEmpty()) {
dotStream.print("[");
String comma = "";
for (String key : properties.keySet()) {
String value = properties.get(key);
// true properties don't require a value.
if (value.equals("true")) {
dotStream.print(comma + key);
} else {
dotStream.print(comma + key + "=" + escapeString(value));
}
comma = ", ";
}
dotStream.print("]");
}
}
}
// This class maintains information about a graph element.
static class Element extends Properties {
// Identifying number.
int id;
Element(int id) {
this.id = id;
}
}
// This class maintains information about a node.
static class Node extends Element {
// Subject of the node.
Object object;
Node(int id, Object object) {
super(id);
this.object = object;
}
}
// This class maintains information about an edge.
static class Edge extends Element {
// Beginning and end of edge.
Node head;
int headFieldId;
Node tail;
int tailFieldId;
Edge(int id, Node head, int headFieldId, Node tail, int tailFieldId) {
super(id);
this.head = head;
this.headFieldId = headFieldId;
this.tail = tail;
this.tailFieldId = tailFieldId;
}
}
// This class makes simplifies the display of values.
class Format {
// String representation of value.
String string = "";
// True if the value is simple and can't be referenced externally.
boolean isSimple = true;
Format(Object value) {
if (value == null) {
// nulls as is, but may be referenced .
string += value;
isSimple = false;
} else if (value instanceof String) {
// Strings in double quotes.
string = formatString((String)value, "\"");
} else if (value instanceof char[]) {
// char arrays as single quote strings.
string = formatString(new String((char[])value), "\'");
} else if (value instanceof byte[] && allASCII((byte[])value)) {
// byte arrays of ascii characters as single quote strings.
string = formatString(new String((byte[])value, StandardCharsets.UTF_8), "\'");
} else if (value instanceof Character) {
// Quote characters.
Character character = (Character)value;
string = "\'" + character + "\'";
} else if (!expandCollections && value instanceof Map.Entry) {
// Map entries as key->value.
Map.Entry entry = (Map.Entry)value;
Format keyFormat = new Format(entry.getKey());
Format valueFormat = new Format(entry.getValue());
if (!keyFormat.isSimple) {
isSimple = false;
} else {
string = keyFormat.string + "-\\>";
if (valueFormat.isSimple) {
string += valueFormat.string;
} else {
isSimple = false;
}
}
} else if (isPrimitive(value)) {
// Primitive types as is.
string += value;
} else {
// Reference to another object.
isSimple = false;
}
}
}
public DOTWriter(String fileName) {
try {
dotStream = new PrintStream(fileName);
} catch (Throwable ex) {
}
// Set up default properties.
graphProperties.addProperties(GRAPHDEFAULTS);
nodeProperties.addProperties(NODEDEFAULTS);
edgeProperties.addProperties(EDGEDEFAULTS);
}
public static final String DOTWRITER_PREFIX = "dotwriter.";
public void customize(java.util.Properties props) {
String prop = props.getProperty(DOTWRITER_PREFIX + "objectlimit");
if (prop != null) {
this.objectLimit(Integer.parseInt(prop));
}
prop = props.getProperty(DOTWRITER_PREFIX + "fieldLimit");
if (prop != null) {
this.fieldLimit(Integer.parseInt(prop));
}
prop = props.getProperty(DOTWRITER_PREFIX + "stringLimit");
if (prop != null) {
this.stringLimit(Integer.parseInt(prop));
}
prop = props.getProperty(DOTWRITER_PREFIX + "arrayLimit");
if (prop != null) {
this.arrayLimit(Integer.parseInt(prop));
}
prop = props.getProperty(DOTWRITER_PREFIX + "expandCollections");
if (prop != null) {
this.expandCollections(Boolean.parseBoolean(prop));
}
prop = props.getProperty(DOTWRITER_PREFIX + "diaplayLinks");
if (prop != null) {
this.displayLinks(Boolean.parseBoolean(prop));
}
prop = props.getProperty(DOTWRITER_PREFIX + "displayStatics");
if (prop != null) {
this.displayStatics(Boolean.parseBoolean(prop));
}
prop = props.getProperty(DOTWRITER_PREFIX + "excludeClassNames");
if (prop != null) {
this.excludeClassNames(Pattern.compile(prop));
}
prop = props.getProperty(DOTWRITER_PREFIX + "includeClassNames");
if (prop != null) {
this.includeClassNames(Pattern.compile(prop));
}
}
// All-in-one graph objects. Specify which objects you want graphed.
// If an object is a String then it is used as the default properties
// for all objects following.
public static void graph(String fileName, Object... objects) {
// Open the dot file.
DOTWriter writer = new DOTWriter(fileName);
// Hilight starting nodes in pink.
String propertyString = STARTNODESTYLE;
// For each argument.
for (Object object : objects) {
// If string the swap default properties.
if (object instanceof String) {
propertyString = (String)object;
} else {
writer.addNode(propertyString, object);
}
}
// Dump and close out the dot file.
writer.close();
}
// Add a set of nodes to the graph. If the object is a string then the string
// is used as a property string for the remaining objects.
public void addNodes(Object... objects) {
String propertyString = null;
for (Object object : objects) {
if (object instanceof String) {
propertyString = (String)object;
} else {
addNode(propertyString, object);
}
}
}
// Add an object to the graph.
public void addNode(String propertyString, Object object) {
// No nulls in the graph.
if (object == null) return;
// No primitive types in the graph.
if (isPrimitive(object)) return;
// Capture where to continue graphing.
int index = nodes.size();
// Add the new node or get the existing one.
Node newNode = getNode(object);
// While the node list is not exhausted.
for ( ; index < nodes.size(); index++) {
// get the next node.
Node node = nodes.get(index);
// Get the node object.
object = node.object;
// Get the object class.
Class clazz = object.getClass();
// Add object header to label.
addHeader(object, clazz, node);
// If the object is an array.
if (clazz.isArray()) {
// Display detail if under object limit and not excluded.
if (index < arrayLimit && shouldDetail(object)) {
addArrayDetail(object, node);
} else {
// Indicate there is more than displayed.
addContinuation(node);
}
} else {
// Display detail if under object limit and not excluded.
if (index < objectLimit && shouldDetail(object)) {
if (!expandCollections && object instanceof Collection) {
// Display collection as an array of enties.
addCollectionDetail(object, clazz, node);
} else if (!expandCollections && object instanceof Map) {
// Display map as an array of key->value.
addMapDetail(object, clazz, node);
} else if (displayStatics && object instanceof Class) {
// Display class static fields.
addClassDetail(object, clazz, node);
} else {
// Display as a detailed java object.
addObjectDetail(object, clazz, node);
}
} else {
// Indicate there is more than displayed.
addContinuation(node);
}
}
}
// Add properties to the node if present.
if (propertyString != null) {
newNode.addProperties(propertyString);
}
}
// Set maximum number of objects displayed in detail.
public void objectLimit(int objectLimit) {
this.objectLimit = objectLimit;
}
// Set maximum number of field entries displayed.
public void fieldLimit(int fieldLimit) {
this.fieldLimit = fieldLimit;
}
// Set maximum number of array entries displayed.
public void arrayLimit(int arrayLimit) {
this.arrayLimit = arrayLimit;
}
// Set maximum number of string characters displayed.
public void stringLimit(int stringLimit) {
this.stringLimit = stringLimit;
}
// Control the switching of collections from detail to expanded.
public void expandCollections(boolean expandCollections) {
this.expandCollections = expandCollections;
}
// Control whether static fields should be shown.
public void displayStatics(boolean displayStatics) {
this.displayStatics = displayStatics;
}
// Control whether object links should be shown.
public void displayLinks(boolean displayLinks) {
this.displayLinks = displayLinks;
}
// Add objects to the include list.
public void includeObjects(Object... objects) {
includeObjects.addAll(Arrays.asList(objects));
filtering = true;
}
// Add objects to the exclude list.
public void excludeObjects(Object... objects) {
excludeObjects.addAll(Arrays.asList(objects));
filtering = true;
}
// Add classes to the include list.
public void includeClasses(Class... clazzes) {
includeClasses.addAll(Arrays.asList(clazzes));
filtering = true;
}
public void includeClassNames(Pattern pattern) {
includeClassNames = pattern;
filtering = true;
}
// Adds classes to the exclude list.
public void excludeClasses(Class... clazzes) {
excludeClasses.addAll(Arrays.asList(clazzes));
filtering = true;
}
public void excludeClassNames(Pattern pattern) {
excludeClassNames = pattern;
filtering = true;
}
// This method adds a new edge to the graph.
public void addEdge(Object head, Object tail) {
addEdge(head, -1, tail, -1, null);
}
public void addEdge(Object head, Object tail, String propertyString) {
addEdge(head, -1, tail, -1, propertyString);
}
public void addEdge(Object head, int headFieldId, Object tail, int tailFieldId) {
addEdge(head, headFieldId, tail, tailFieldId, null);
}
public void addEdge(Object head, int headFieldId, Object tail, int tailFieldId, String propertyString) {
addEdge(getNode(head), headFieldId, getNode(tail), tailFieldId, propertyString);
}
private void addEdge(Node head, int headFieldId, Node tail, int tailFieldId) {
addEdge(head, headFieldId, tail, tailFieldId, null);
}
// Write the graph and close the dot file.
public void close() {
if (dotStream != null) {
writeGraph();
dotStream.close();
dotStream = null;
}
}
//
// Private interface.
//
// Return true if the object is a primitive type.
private boolean isPrimitive(Object object) {
return object.getClass().isPrimitive() || object instanceof Number || object instanceof Boolean || object instanceof String;
}
// Write the graph to the stream.
private void writeGraph() {
dotStream.println("digraph g {");
dotStream.print(" graph ");
graphProperties.writeProperties(dotStream);
dotStream.println(";");
dotStream.print(" node ");
nodeProperties.writeProperties(dotStream);
dotStream.println(";");
dotStream.print(" edge ");
edgeProperties.writeProperties(dotStream);
dotStream.println(";");
for (Node node : nodes) {
dotStream.print(" node" + node.id + " ");
node.writeProperties(dotStream);
dotStream.println(";");
}
for (Edge edge : edges) {
dotStream.print(" node" + edge.head.id + (edge.headFieldId < 0 ? ":f" : ":f" + edge.headFieldId));
dotStream.print(" -> ");
dotStream.print(" node" + edge.tail.id + (edge.tailFieldId < 0 ? ":f" : ":f" + edge.tailFieldId));
dotStream.print(" ");
edge.writeProperties(dotStream);
dotStream.println(";");
}
dotStream.println("}");
}
// Add an object to the node work list.
private Node getNode(Object object) {
// Don't add nulls to graph.
if (object == null) return null;
// Some object types fail mapping.
Node node = null;
try {
node = visited.get(object);
} catch (Throwable ex) {
return null;
}
// If the node is not found add one.
if (node == null) {
node = new Node(nodes.size(), object);
nodes.add(node);
visited.put(object, node);
}
return node;
}
// Determine if a node should be detailed based on object and class filters.
private boolean shouldDetail(Object object) {
if (object == this) return false;
if (!filtering) return true;
if (!includeObjects.isEmpty()) {
return includeObjects.contains(object);
}
if (!includeClasses.isEmpty()) {
for (Class clazz : includeClasses) {
if (clazz.isInstance(object)) return true;
}
return false;
}
if (includeClassNames != null) {
return includeClassNames.matcher(object.getClass().getName()).matches();
}
if (!excludeObjects.isEmpty()) {
if (excludeObjects.contains(object)) return false;
}
if (!excludeClasses.isEmpty()) {
for (Class clazz : excludeClasses) {
if (clazz.isInstance(object)) return false;
}
}
if (excludeClassNames != null) {
return !excludeClassNames.matcher(object.getClass().getName()).matches();
}
return true;
}
// Add a new edge to the graph.
private void addEdge(Node head, int headFieldId, Node tail, int tailFieldId, String propertyString) {
// Exclude null nodes.
if (head == null || tail == null) return;
// Exclude edges to nodes that disn't get displayed.
if (headFieldId > getLimit(head) && head.id >= objectLimit) headFieldId = -1;
if (tailFieldId > getLimit(tail) && tail.id >= objectLimit) tailFieldId = -1;
// Create edge.
Edge edge = new Edge(edges.size(), head, headFieldId, tail, tailFieldId);
// Add properties if present.
if (propertyString != null) edge.addProperties(propertyString);
// Add to edge list.
edges.add(edge);
}
// Return the field/slot limit for a node's object.
private int getLimit(Node node) {
return node.object.getClass().isArray() ? arrayLimit : fieldLimit;
}
// Return a displayable name for the specified class.
private String getClassName(Class clazz) {
String name = clazz.getCanonicalName();
if (name == null) name = clazz.getName();
if (name == null) name = clazz.getSimpleName();
return name;
}
// Add the header information about the object.
private void addHeader(Object object, Class clazz, Node node) {
// Use object if displaying statics.
if (displayStatics && object instanceof Class) clazz = (Class)object;
// Use name as title.
String className = getClassName(clazz);
if (object instanceof Class) {
// Flag Class objects.
className += "(Class)";
}
// Start label for node.
node.addProperty("label", "<f> " + className);
}
// Indicate that the object has more fields.
private void addContinuation(Node node) {
node.extendProperty("label", " | ...");
}
// Return the value of a field.
private Object getValue(Field field, Object object) {
Object value = null;
try {
value = field.get(object);
} catch(Throwable ex) {
}
return value;
}
// Return an array of all the fields in a given class.
private Field[] getFields(Class clazz) {
if (clazz == null) return new Field[0];
if (displayStatics && clazz == Class.class) return new Field[0];
Field[] fields = fieldCache.get(clazz);
if (fields != null) return fields;
Field[] superFields = getFields(clazz.getSuperclass());
Field[] classFields = clazz.getDeclaredFields();
fields = new Field[superFields.length + classFields.length];
System.arraycopy(superFields, 0, fields, 0, superFields.length);
System.arraycopy(classFields, 0, fields, superFields.length, classFields.length);
fieldCache.put(clazz, fields);
return fields;
}
// Add the detail information about the object.
private void addObjectDetail(Object object, Class clazz, Node node) {
Field[] fields = getFields(clazz);
if (displayStatics && fields.length != 0) {
addEdge(node, -1, getNode(clazz), -1);
}
for (int index = 0; index < fields.length && index < fieldLimit; index++) {
Field field = fields[index];
// Ignore static fields.
if ((field.getModifiers() & Modifier.STATIC) != 0) continue;
// Add record row for field.
field.setAccessible(true);
String name = field.getName();
Object value = getValue(field, object);
Format format = new Format(value);
addNodeField(node, name, index, format.string);
// If linking to another object then add edge.
if (displayLinks && !format.isSimple) {
addEdge(node, index, getNode(value), -1);
}
}
// Indicate if object is too big to display.
if (fields.length >= fieldLimit) {
addContinuation(node);
}
}
// Add the detail information about a class.
private void addClassDetail(Object object, Class clazz, Node node) {
Field[] fields = getFields((Class)object);
for (int index = 0; index < fields.length && index < fieldLimit; index++) {
Field field = fields[index];
// Only look at static fields.
if ((field.getModifiers() & Modifier.STATIC) == 0) continue;
// Add record row for field.
field.setAccessible(true);
String name = field.getName();
Object value = getValue(field, object);
Format format = new Format(value);
addNodeField(node, name, index, format.string);
// If linking to another object then add edge.
if (displayLinks && !format.isSimple) {
addEdge(node, index, getNode(value), -1);
}
}
// Indicate if object is too big to display.
if (fields.length >= fieldLimit) {
addContinuation(node);
}
}
// Add the detail information about a collection.
private void addCollectionDetail(Object object, Class clazz, Node node) {
// Convert colection to array and display as array.
Object[] array = ((Collection)object).toArray();
addArrayDetail(array, node);
}
// Add the detail information about a map.
private void addMapDetail(Object object, Class clazz, Node node) {
// Convert map to array and display as array.
Object[] array = ((Map)object).entrySet().toArray();
addArrayDetail(array, node);
}
// Add the detail information about the array.
private void addArrayDetail(Object object, Node node) {
int length = Array.getLength(object);
if (object instanceof char[] || (object instanceof byte[] && allASCII((byte[])object))) {
// If char or displayable byte array then display as string.
Format format = new Format(object);
String string = " | " + format.string;
node.extendProperty("label", string);
length = 0;
} else {
// One line of record per slot in array.
for (int index = 0; index < length && index < arrayLimit; index++) {
// Add record row for slot.
Object value = Array.get(object, index);
Format format = new Format(value);
addNodeField(node, null, index, format.string);
// If linking to another object then add edge.
if (displayLinks && !format.isSimple) {
addEdge(node, index, getNode(value), -1);
}
}
}
// Indicate if object is too big to display.
if (length >= arrayLimit) {
addContinuation(node);
}
}
// Add a field to a node.
private void addNodeField(Node node, String fieldName, int fieldId, String valueString) {
// Start record row.
String labelString = " | ";
// Add report port for edges.
labelString += "<f" + fieldId + "> ";
// Add field name.
if (fieldName != null) labelString += fieldName + ": ";
// Add value.
labelString += valueString;
// Add to record description.
node.extendProperty("label", labelString);
}
// Formats a string value, truncating if needed.
private String formatString(String string, String quote) {
boolean isLong = string.length() > stringLimit;
if (isLong) string = string.substring(0, stringLimit - 1);
string = quote + string + quote;
if (isLong) string += "...";
return string;
}
// Escape a quoted value string for output.
private static String escapeString(String value) {
if (needsEscape(value)) {
final StringBuilder result = new StringBuilder();
result.append('\"');
final StringCharacterIterator iterator = new StringCharacterIterator(value);
for (char ch = iterator.current(); ch != CharacterIterator.DONE; ch = iterator.next()) {
if (ch == '\"' ||
ch == '\'' ||
ch == '{' ||
ch == '}' ||
ch == '[' ||
ch == ']') {
result.append('\\');
result.append(ch);
} else if (ch < ' ' || ch > '~') {
result.append("\\\\u");
String hex = "0000" + Integer.toHexString((int)ch);
hex = hex.substring(hex.length() - 4);
result.append(hex);
} else {
result.append(ch);
}
}
result.append('\"');
value = result.toString();
}
return value;
}
// Return true if the string needs to be quoted.
private static boolean needsEscape(String value) {
if (value.length() > 0 && value.charAt(0) == '\"') return false;
final StringCharacterIterator iterator = new StringCharacterIterator(value);
for (char ch = iterator.current(); ch != CharacterIterator.DONE; ch = iterator.next()) {
if (!Character.isJavaIdentifierPart(ch) && ch != '-') {
return true;
}
}
return false;
}
// Return true if all the characters in the array are ASCII characters.
private boolean allASCII(byte[] array) {
for (byte b : array) {
char ch = (char)b;
if (ch < ' ' && '~' < ch) return false;
}
return false;
}
}