All Downloads are FREE. Search and download functionalities are using the official Maven repository.

hirondelle.date4j.ToStringUtil Maven / Gradle / Ivy

package hirondelle.date4j;

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 toString method for some common cases.

 

This class is intended only for cases where toString is used in an informal manner (usually for logging and stack traces). It is especially suited for public classes which model domain objects. Here is an example of a return value of the {@link #getText} method :

hirondelle.web4j.model.MyUser {
LoginName: Bob
LoginPassword: ****
EmailAddress: [email protected]
StarRating: 1
FavoriteTheory: Quantum Chromodynamics
SendCard: true
Age: 42
DesiredSalary: 42000
BirthDate: Sat Feb 26 13:45:43 EST 2005
}
 
(Previous versions of this classes used indentation within the braces. That has been removed, since it displays poorly when nesting occurs.)

Here are two more examples, using classes taken from the JDK :

java.util.StringTokenizer {
nextElement: This
hasMoreElements: true
countTokens: 3
nextToken: is
hasMoreTokens: true
}

java.util.ArrayList {
size: 3
toArray: [blah, blah, blah]
isEmpty: false
}
 
There are two use cases for this class. The typical use case is :
  public String toString() {
    return ToStringUtil.getText(this);
  }
 
However, there is a case where this typical style can fail catastrophically : when two objects reference each other, and each has toString implemented as above, then the program will loop indefinitely!

As a remedy for this problem, the following variation is provided :

  public String toString() {
    return ToStringUtil.getTextAvoidCyclicRefs(this, Product.class, "getId");
  }
 
Here, the usual behavior is overridden for any method which returns a Product : instead of calling Product.toString, the return value of Product.getId() is used to textually represent the object. */ final class ToStringUtil { /** Return an informal textual description of an object.

It is highly recommened that the caller not rely on details of the returned String. See class description for examples of return values.

WARNING: If two classes have cyclic references (that is, each has a reference to the other), then infinite looping will result if both call this method! To avoid this problem, use getText for one of the classes, and {@link #getTextAvoidCyclicRefs} for the other class.

The only items which contribute to the result are the class name, and all no-argument public methods which return a value. As well, methods defined by the Object class, and factory methods which return an Object of the native class ("getInstance" methods) do not contribute.

Items are converted to a String simply by calling their toString method, with these exceptions :

  • {@link Util#getArrayAsString(Object)} is used for arrays
  • a method whose name contain the text "password" (not case-sensitive) have their return values hard-coded to "****".

If the method name follows the pattern getXXX, then the word 'get' is removed from the presented result. @param aObject the object for which a toString 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 aSpecialClass, then call aMethodName instead of toString.

If aSpecialClass and aMethodName are null, 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 Object 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 true only if aMethod is public, takes no args, returns a value whose class is not the native class, is not a method of Object. */ 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 list = new ArrayList(); 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 ); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy