package com.elphin.framework.util.date;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
Implements the <tt>toString</tt> method for some common cases.
<P>This class is intended only for cases where <tt>toString</tt> is used in
an informal manner (usually for logging and stack traces). It is especially
suited for <tt>public</tt> classes which model domain objects.
Here is an example of a return value of the {@link #getText} method :
<PRE>
hirondelle.web4j.model.MyUser {
LoginName: Bob
LoginPassword: ****
EmailAddress: bob@blah.com
StarRating: 1
FavoriteTheory: Quantum Chromodynamics
SendCard: true
Age: 42
DesiredSalary: 42000
BirthDate: Sat Feb 26 13:45:43 EST 2005
}
</PRE>
(Previous versions of this classes used indentation within the braces. That has
been removed, since it displays poorly when nesting occurs.)
<P>Here are two more examples, using classes taken from the JDK :
<PRE>
java.util.StringTokenizer {
nextElement: This
hasMoreElements: true
countTokens: 3
nextToken: is
hasMoreTokens: true
}
java.util.ArrayList {
size: 3
toArray: [blah, blah, blah]
isEmpty: false
}
</PRE>
There are two use cases for this class. The typical use case is :
<PRE>
public String toString() {
return ToStringUtil.getText(this);
}
</PRE>
<span class="highlight">However, there is a case where this typical style can
fail catastrophically</span> : when two objects reference each other, and each
has <tt>toString</tt> implemented as above, then the program will loop
indefinitely!
<P>As a remedy for this problem, the following variation is provided :
<PRE>
public String toString() {
return ToStringUtil.getTextAvoidCyclicRefs(this, Product.class, "getId");
}
</PRE>
Here, the usual behavior is overridden for any method
which returns a <tt>Product</tt> : instead of calling <tt>Product.toString</tt>,
the return value of <tt>Product.getId()</tt> is used to textually represent
the object.
*/
final class ToStringUtil {
/**
Return an informal textual description of an object.
<P>It is highly recommened that the caller <em>not</em> rely on details
of the returned <tt>String</tt>. See class description for examples of return
values.
<P><span class="highlight">WARNING</span>: If two classes have cyclic references
(that is, each has a reference to the other), then infinite looping will result
if <em>both</em> call this method! To avoid this problem, use <tt>getText</tt>
for one of the classes, and {@link #getTextAvoidCyclicRefs} for the other class.
<P>The only items which contribute to the result are the class name, and all
no-argument <tt>public</tt> methods which return a value. As well, methods
defined by the <tt>Object</tt> class, and factory methods which return an
<tt>Object</tt> of the native class ("<tt>getInstance</tt>" methods) do not contribute.
<P>Items are converted to a <tt>String</tt> simply by calling their
<tt>toString method</tt>, with these exceptions :
<ul>
<li>{@link Util#getArrayAsString(Object)} is used for arrays
<li>a method whose name contain the text <tt>"password"</tt> (not case-sensitive) have
their return values hard-coded to <tt>"****"</tt>.
</ul>
<P>If the method name follows the pattern <tt>getXXX</tt>, then the word 'get'
is removed from the presented result.
@param aObject the object for which a <tt>toString</tt> result is required.
*/
static String getText(Object aObject) {
return getTextAvoidCyclicRefs(aObject, null, null);
}
/**
As in {@link #getText}, but, for return values which are instances of
<tt>aSpecialClass</tt>, then call <tt>aMethodName</tt> instead of <tt>toString</tt>.
<P> If <tt>aSpecialClass</tt> and <tt>aMethodName</tt> are <tt>null</tt>, then the
behavior is exactly the same as calling {@link #getText}.
*/
static String getTextAvoidCyclicRefs(Object aObject, Class aSpecialClass, String aMethodName) {
StringBuilder result = new StringBuilder();
addStartLine(aObject, result);
Method[] methods = aObject.getClass().getDeclaredMethods();
for(Method method: methods){
if ( isContributingMethod(method, aObject.getClass()) ){
addLineForGetXXXMethod(aObject, method, result, aSpecialClass, aMethodName);
}
}
addEndLine(result);
return result.toString();
}
// PRIVATE //
/*
Names of methods in the <tt>Object</tt> class which are ignored.
*/
private static final String fGET_CLASS = "getClass";
private static final String fCLONE = "clone";
private static final String fHASH_CODE = "hashCode";
private static final String fTO_STRING = "toString";
private static final String fGET = "get";
private static final Object[] fNO_ARGS = new Object[0];
private static final Class[] fNO_PARAMS = new Class[0];
/*
Previous versions of this class indented the data within a block.
That style breaks when one object references another. The indentation
has been removed, but this variable has been retained, since others might
prefer the indentation anyway.
*/
private static final String fINDENT = "";
private static final String fAVOID_CIRCULAR_REFERENCES = "[circular reference]";
private static final Logger fLogger = Util.getLogger(ToStringUtil.class);
private static final String NEW_LINE = System.getProperty("line.separator");
private static Pattern PASSWORD_PATTERN = Pattern.compile("password", Pattern.CASE_INSENSITIVE);
private static String HIDDEN_PASSWORD_VALUE = "****";
//prevent construction by the caller
private ToStringUtil() {
//empty
}
private static void addStartLine(Object aObject, StringBuilder aResult){
aResult.append( aObject.getClass().getName() );
aResult.append(" {");
aResult.append(NEW_LINE);
}
private static void addEndLine(StringBuilder aResult){
aResult.append("}");
aResult.append(NEW_LINE);
}
/**
Return <tt>true</tt> only if <tt>aMethod</tt> is public, takes no args,
returns a value whose class is not the native class, is not a method of
<tt>Object</tt>.
*/
private static boolean isContributingMethod(Method aMethod, Class aNativeClass){
boolean isPublic = Modifier.isPublic( aMethod.getModifiers() );
boolean hasNoArguments = aMethod.getParameterTypes().length == 0;
boolean hasReturnValue = aMethod.getReturnType() != Void.TYPE;
boolean returnsNativeObject = aMethod.getReturnType() == aNativeClass;
boolean isMethodOfObjectClass =
aMethod.getName().equals(fCLONE) ||
aMethod.getName().equals(fGET_CLASS) ||
aMethod.getName().equals(fHASH_CODE) ||
aMethod.getName().equals(fTO_STRING)
;
return
isPublic &&
hasNoArguments &&
hasReturnValue &&
! isMethodOfObjectClass &&
! returnsNativeObject;
}
private static void addLineForGetXXXMethod(
Object aObject,
Method aMethod,
StringBuilder aResult,
Class aCircularRefClass,
String aCircularRefMethodName
){
aResult.append(fINDENT);
aResult.append( getMethodNameMinusGet(aMethod) );
aResult.append(": ");
Object returnValue = getMethodReturnValue(aObject, aMethod);
if ( returnValue != null && returnValue.getClass().isArray() ) {
aResult.append( Util.getArrayAsString(returnValue) );
}
else {
if (aCircularRefClass == null) {
aResult.append( returnValue );
}
else {
if (aCircularRefClass == returnValue.getClass()) {
Method method = getMethodFromName(aCircularRefClass, aCircularRefMethodName);
if ( isContributingMethod(method, aCircularRefClass)){
returnValue = getMethodReturnValue(returnValue, method);
aResult.append( returnValue );
}
else {
aResult.append(fAVOID_CIRCULAR_REFERENCES);
}
}
}
}
aResult.append( NEW_LINE );
}
private static String getMethodNameMinusGet(Method aMethod){
String result = aMethod.getName();
if (result.startsWith(fGET) ) {
result = result.substring(fGET.length());
}
return result;
}
/** Return value is possibly-null. */
private static Object getMethodReturnValue(Object aObject, Method aMethod){
Object result = null;
try {
result = aMethod.invoke(aObject, fNO_ARGS);
}
catch (IllegalAccessException ex){
vomit(aObject, aMethod);
}
catch (InvocationTargetException ex){
vomit(aObject, aMethod);
}
result = dontShowPasswords(result, aMethod);
return result;
}
private static Method getMethodFromName(Class aSpecialClass, String aMethodName){
Method result = null;
try {
result = aSpecialClass.getMethod(aMethodName, fNO_PARAMS);
}
catch ( NoSuchMethodException ex){
vomit(aSpecialClass, aMethodName);
}
return result;
}
private static void vomit(Object aObject, Method aMethod){
fLogger.severe(
"Cannot get return value using reflection. Class: " +
aObject.getClass().getName() +
" Method: " +
aMethod.getName()
);
}
private static void vomit(Class aSpecialClass, String aMethodName){
fLogger.severe(
"Reflection fails to get no-arg method named: " +
Util.quote(aMethodName) +
" for class: " +
aSpecialClass.getName()
);
}
private static Object dontShowPasswords(Object aReturnValue, Method aMethod){
Object result = aReturnValue;
Matcher matcher = PASSWORD_PATTERN.matcher(aMethod.getName());
if ( matcher.find()) {
result = HIDDEN_PASSWORD_VALUE;
}
return result;
}
/*
Two informal classes with cyclic references, used for testing.
*/
private static final class Ping {
public void setPong(Pong aPong){fPong = aPong; }
public Pong getPong(){ return fPong; }
public Integer getId() { return new Integer(123); }
public String getUserPassword(){ return "blah"; }
public String toString() {
return getText(this);
}
private Pong fPong;
}
private static final class Pong {
public void setPing(Ping aPing){ fPing = aPing; }
public Ping getPing() { return fPing; }
public String toString() {
return getTextAvoidCyclicRefs(this, Ping.class, "getId");
//to see the infinite looping, use this instead :
//return getText(this);
}
private Ping fPing;
}
/**
Informal test harness.
*/
public static void main (String... args) {
List<String> list = new ArrayList<String>();
list.add("blah");
list.add("blah");
list.add("blah");
System.out.println( ToStringUtil.getText(list) );
StringTokenizer parser = new StringTokenizer("This is the end.");
System.out.println( ToStringUtil.getText(parser) );
Ping ping = new Ping();
Pong pong = new Pong();
ping.setPong(pong);
pong.setPing(ping);
System.out.println( ping );
System.out.println( pong );
}
}