Allows the user to reflectively inspect an object hierarchy
//package com.ryanm.util.swing; import java.awt.event.MouseEvent; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Collection; import java.util.LinkedList; import javax.swing.JTree; import javax.swing.ToolTipManager; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeWillExpandListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.ExpandVetoException; import javax.swing.tree.TreePath; /** * Allows the user to reflectively inspect an object hierarchy * * @author ryanm */ public class ObjectInspector extends JTree { private boolean showInaccessibleFields = true; private boolean showStaticFields = true; private ObjectNode treeRoot = new ObjectNode( null, true ); private DefaultTreeModel treeModel = new DefaultTreeModel( treeRoot ); private TreeWillExpandListener expansionListener = new TreeWillExpandListener() { @Override public void treeWillCollapse( TreeExpansionEvent event ) throws ExpandVetoException { Object obj = event.getPath().getLastPathComponent(); if( obj instanceof ObjectNode ) { ObjectNode on = ( ObjectNode ) obj; assert !on.root; on.expanded = false; on.refreshValue( on.inspectedObject ); } } @Override public void treeWillExpand( TreeExpansionEvent event ) throws ExpandVetoException { Object obj = event.getPath().getLastPathComponent(); if( obj instanceof ObjectNode ) { ObjectNode on = ( ObjectNode ) obj; on.expanded = true; on.buildChildren(); on.refreshTree( on.inspectedObject ); treeModel.reload( on ); } } }; /** * Builds a new {@link ObjectInspector} * * @param o * The object to inspect * @param showInaccessible * <code>true</code> to display inaccessible fields in * the tree, <code>false</code> to hide them * @param showStatic * <code>true</code> to show static fields, * <code>false</code> to hide them */ public ObjectInspector( Object o, boolean showInaccessible, boolean showStatic ) { setModel( treeModel ); showInaccessibleFields = showInaccessible; showStaticFields = showStatic; setEditable( false ); addTreeWillExpandListener( expansionListener ); treeRoot.refreshTree( o ); ToolTipManager.sharedInstance().registerComponent( this ); } /** * Inspects an object * * @param o * The object to inspect */ public void inspect( Object o ) { treeRoot.refreshTree( o ); } @Override public String getToolTipText( MouseEvent me ) { TreePath pathForLocation = getPathForLocation( me.getX(), me.getY() ); if( pathForLocation != null ) { Object lastPathComponent = pathForLocation.getLastPathComponent(); if( lastPathComponent instanceof ObjectNode ) { ObjectNode on = ( ObjectNode ) lastPathComponent; return on.tooltip; } } return null; } private class ObjectNode extends DefaultMutableTreeNode { private Object inspectedObject = null; private Field inspectedField = null; private final boolean root; private final boolean accessible; private final boolean primitive; private boolean array = false; private boolean childrenBuilt = false; private TreePath path; private final DefaultMutableTreeNode dummyNode = new DefaultMutableTreeNode( "Inspecting..." ); private String tooltip; private boolean expanded = false; private ObjectNode( Object inspectedObject, boolean root ) { this.root = root; this.inspectedObject = inspectedObject; accessible = true; primitive = false; if( root ) { buildChildren(); expanded = true; } } private ObjectNode( Field inspectedField ) { root = false; setUserObject( inspectedField.getType().getSimpleName() + " : " + inspectedField.getName() ); this.inspectedField = inspectedField; primitive = inspectedField.getType().isPrimitive(); boolean a = false; try { inspectedField.setAccessible( true ); a = true; } catch( SecurityException se ) { a = false; } accessible = a; if( !primitive && accessible ) { insert( dummyNode, 0 ); } if( !accessible ) { setUserObject( inspectedField.getName() + " : Inaccessible" ); } tooltip = inspectedField.getType().toString(); } private void refreshTree( Object o ) { if( objectTypeChanged( o ) ) { /* * the object class has changed, we need to change the * tree */ removeAllChildren(); childrenBuilt = false; inspectedObject = o; if( inspectedObject != null ) { array = o.getClass().isArray(); if( !primitive && accessible ) { insert( dummyNode, getChildCount() ); } if( expanded ) { buildChildren(); } } else { childrenBuilt = true; } treeModel.nodeStructureChanged( this ); } else if( array ) { // need to check if the array length has changed int oldCount = getChildCount(); int desiredCount = Array.getLength( o ); // may need to add or remove children while( getChildCount() < desiredCount ) { ObjectNode on = new ObjectNode( null, false ); insert( on, getChildCount() ); } while( getChildCount() > desiredCount ) { remove( getChildCount() - 1 ); } if( oldCount != desiredCount ) { treeModel.nodeStructureChanged( this ); } assert getChildCount() == desiredCount; } inspectedObject = o; if( !root && getChildCount() == 0 ) { expanded = false; } if( expanded && getChildCount() > 0 ) { int index = 0; for( Object child : children ) { assert child != dummyNode; ObjectNode on = ( ObjectNode ) child; if( array ) { on.refreshTree( Array.get( inspectedObject, index ) ); } else if( on.accessible ) { try { on.refreshTree( on.inspectedField.get( inspectedObject ) ); } catch( IllegalArgumentException e ) { e.printStackTrace(); } catch( IllegalAccessException e ) { e.printStackTrace(); } } index++; } } refreshValue( o ); } /** * Updates the value of this node * * @param o */ private void refreshValue( Object o ) { StringBuilder buff = new StringBuilder(); if( inspectedField != null ) { buff.append( inspectedField.getName() ); buff.append( " : " ); buff.append( inspectedField.getType().getSimpleName() ); } else { assert inspectedField == null; if( o != null ) { buff.append( o.getClass().getSimpleName() ); } else { buff.append( "null" ); } } if( primitive ) { buff.append( " : " ); buff.append( o ); } else if( !expanded ) { buff.append( " : " ); buff.append( buildString( o ) ); } setUserObject( buff.toString() ); if( path != null ) { path = new TreePath( getPath() ); } if( path == null ) { path = new TreePath( getPath() ); } treeModel.valueForPathChanged( path, getUserObject() ); if( o != null ) { if( !primitive ) { tooltip = o.getClass().getName(); } else { tooltip = inspectedField.getType().getName(); } } else { tooltip = "null"; } } /** * Determines if the object type has changed * * @param o * the new object * @return <code>true</code> if the tree needs to be changed, * false otherwise */ private boolean objectTypeChanged( Object o ) { if( inspectedObject == null && o == null ) { return false; } else if( inspectedObject == null != ( o == null ) ) { return true; } else if( inspectedObject != null && o != null && !inspectedObject.getClass().equals( o.getClass() ) ) { return true; } return false; } private void buildChildren() { if( !childrenBuilt ) { if( children != null && children.contains( dummyNode ) ) { remove( dummyNode ); } if( inspectedObject != null ) { if( array ) { for( int i = 0; i < Array.getLength( inspectedObject ); i++ ) { ObjectNode on = new ObjectNode( inspectedObject, false ); insert( on, getChildCount() ); } } else { Collection<Field> fields = new LinkedList<Field>(); getFields( fields, inspectedObject.getClass() ); for( Field f : fields ) { ObjectNode on = new ObjectNode( f ); if( ( showInaccessibleFields || on.accessible ) && ( showStaticFields || !Modifier.isStatic( f.getModifiers() ) ) ) { insert( on, getChildCount() ); } } } treeModel.nodeStructureChanged( this ); } else { setUserObject( "null" ); } childrenBuilt = true; } } } /** * Recurses up the inheritance chain and collects all the fields * * @param fields * The collection of fields found so far * @param c * The class to get fields from */ private static void getFields( Collection<Field> fields, Class c ) { for( Field f : c.getDeclaredFields() ) { fields.add( f ); } if( c.getSuperclass() != null ) { getFields( fields, c.getSuperclass() ); } } /** * Attempts to build a nicer looking string than the basic * {@link Object}.toString() * * @param o * The object to build from * @return A descriptive string */ private static String buildString( Object o ) { if( o == null ) { return "null"; } // first see if there is a version of toString more specific // than that supplied by Object... try { Method m = o.getClass().getMethod( "toString" ); if( !m.getDeclaringClass().equals( Object.class ) ) { return o.toString(); } } catch( SecurityException e ) { } catch( NoSuchMethodException e ) { } // then see if it is an array... if( o.getClass().isArray() ) { StringBuilder buff = new StringBuilder( " [ " ); for( int i = 0; i < Array.getLength( o ); i++ ) { /* * this could recurse infinitely, but only if the user is * trying to be malicious, like so - Object[] array = new * Object[ 1 ]; array[ 0 ] = array; - which, I'm sure * we'll agree, is and odd thing to do. I say let the * StackOverflowException catch it. */ buff.append( buildString( Array.get( o, i ) ) ); buff.append( ", " ); } if( Array.getLength( o ) > 0 ) { buff.delete( buff.length() - 2, buff.length() ); } buff.append( " ]" ); return buff.toString(); } return getObjectPosition( o ); } /** * Returns a String of an object's position in memory * * @param o * @return The object's memory position */ private static String getObjectPosition( Object o ) { String s = o.toString(); s = s.substring( s.lastIndexOf( "@" ) ); return s; } }