package edu.mbl.jif.utils.diag; import edu.mbl.jif.utils.diag.TestObject; import java.lang.reflect.*; /** * This class analyses the state of any Object. * Specificly, it uses reflection to query an Object about all it's Fields and their values. * <p> * Since the purpose of the toString method in most classes is simply to return a String * representing the Object's state, <i>this class makes it extremely trivial to write * a toString method</i>. In particular, * <blockquote> * <code> * public String toString() throws IllegalArgumentException, SecurityException {<br> * return StateAnalyser.getState(this);<br> * }<br> * </code> * </blockquote> * is all most classes will need to write. * <p> * There are a few limitations with the above quick toString implementation. * First, see the warnings in the getState method. * Second, the above implementation will be slower than a custom written toString implementation. * (This is rarely a concern, as toString is usually used for diagnostics, but if speed is a factor, * then you will have to custom write toString.) * <p> * Unless otherwise noted, all the methods of this class are multithread safe. * <p> * <b>Warning</b>: the above statement is true only with respect to the operation * of this class itself. A major caveat must be given for any argument * that is a mutable Object. Here, the possibility exists that another Thread * (besides the Thread calling our method) could be simultaneously modifying the Object. * Unpredictable results might occur. * <p> * @see #getState * @author Brent Boyer */ public class StateAnalyzer { // -------------------- constants -------------------- /** * A guesstimate for the number of chars that will be needed to describe * the state of a typical Object. */ protected static final int TYPICAL_STATE_CHAR_SIZE = 256; // -------------------- constructor -------------------- /** This private constructor suppresses the default (public) constructor, ensuring non-instantiability. */ private StateAnalyzer() {} // -------------------- getState -------------------- /** * This method returns a String which describes the target Object (classname and hashcode) * and all of it's declared Fields (names and values). * (A declared field is one that is declared in the source code of a class -- it does not include * Fields defined in any superclasses.) * Additionally, it prints out the current Thread which is executing this method. * <p> * In order to get the values of otherwise inaccessible Fields (e.g. private ones), * this method will call <code>setAccessible(true)</code> on every Field. * When this method is thru with a given Field, however, it will restore the original * accessibility setting. * <p> * <b>Caveats and Warnings</b>: * <ul> * <li> * <i>the formatting of the String returned by this method was designed for * Fields of primitive or String type</i>; if the Field is an Object type which * implements toString and returns a multi-line result, it may not look great * </li> * <li> * this method will not work at all if there are security policies against reflecting * on the target Object and/or getting it's Field names and values * </li> * <li> * <i>there is the danger that infinite callbacks could occur</i> (e.g. if a Field is of a type * that implements toString, and if that type's toString method calls back this method); * even if that does not happen, you could still generate an enormous result * for sufficiently complicated object graphs * </li> * </ul> * <p> * @throws IllegalArgumentException if arg target is null * @throws SecurityException if a SecurityManager forbids some action (e.g. accessing the Fields, or calling a Field's setAccessible method) */ public static final String getState(Object target) throws IllegalArgumentException, SecurityException { if (target == null) { throw new IllegalArgumentException("arg target is null"); } StringBuffer sb = new StringBuffer(TYPICAL_STATE_CHAR_SIZE); appendObjectInfo(target, sb); sb.append(" [\n"); appendFieldsInfo(target.getClass().getDeclaredFields(), target, sb); sb.append("]"); appendThreadInfo(sb); return sb.toString(); } protected static final void appendObjectInfo(Object target, StringBuffer sb) { sb.append(target.getClass().getName()) .append(" (hashcode: ") .append(target.hashCode()) .append(")"); } protected static final void appendFieldsInfo(Field[] fields, Object target, StringBuffer sb) { for (int i = 0; i < fields.length; i++) { boolean originalAccessibility = fields[i].isAccessible(); fields[i].setAccessible(true); // force accessibility for the duration of this method sb.append("\t") .append(fields[i].getName()) .append(": ") .append(extractFieldValue(fields[i], target)) .append("\n"); fields[i].setAccessible(originalAccessibility); // restore original accessibility } } protected static final Object extractFieldValue(Field field, Object target) { try { return field.get(target); } catch (Throwable t) { return handleGetProblem(t); } } /** * Returns a useful diagnostic message. (See the description in the javadocs * for the get method of Field for where I got the descriptions.) * <p> * This method was written genericly, for any invocation of the get method. * However, in this class, other than an ExceptionInInitializerError, * the other Exceptions in the code below should never happen if the appendFieldsInfo * method works as expected. */ protected static final String handleGetProblem(Throwable t) { if (t instanceof IllegalAccessException) { return "<problem: argument is inaccessible>"; } else if (t instanceof IllegalArgumentException) { return "<problem: it appears that the Object supplied to the Field's get method is not an instance of the class or interface corresponding to the Field>"; } else if (t instanceof NullPointerException) { return "<problem: the Object supplied to the Field's get method is null, but the Field is an instance member>"; } else if (t instanceof ExceptionInInitializerError) { return "<problem: the initialization provoked by the get method failed>"; } else { return "<the following unexpected problem happened: "; // + // ThrowableUtil.getTypeAndMessage(t) + ">"; } } protected static final void appendThreadInfo(StringBuffer sb) { sb.append("\n") .append("{Current Thread: ") .append(Thread.currentThread().toString()) .append("}"); } // -------------------- Test (inner class) -------------------- /** * An inner class that consists solely of test code for the parent class. * <p> * Putting all the test code in an inner class rather than a <code>main</code> * method of the parent class has the following benefits: * <ul> * <li>the test code is cleanly separated from the working code</li> * <li>any <code>main</code> in the parent class is now reserved for a true program entry point</li> * <li>all the test code may be easily excluded from the final shippable product * by removing all the Test class files * (e.g. on Windoze, delete all files that end with <i>$Test.class</i>)</li> * </ul> */ class ReflectionTest { public ReflectionTest() { final String SOME_STRING = "yabba dabba do"; boolean someBoolean = true; int someInt = 3; } } public static void main(String[] args) throws Exception { System.out.println( StateAnalyzer.getState(new TestObject()) ); } }