import java.util.*; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.Constructor; /* To do: * - toString -- check for its arg being a list or an array? */ /* A base class for ITEC120 (CS1) that provides two things: * - Before introducing objects: a set of utility methods * (mostly, static versions of String methods) * - When first introducing object, extending Object120 will auto-provide * a constructor, equals, toString (and, hashcode). * The constructor takes in one argument per field; * the toString returns a String which looks the same as a constructor call; * the equals does a deep comparison of each fields. * @author Ian Barland * @version 2010.Mar.03 */ abstract class Object120 { /** Return the version number of this library * (since it might get updated through the semester). */ public static String getVersion() { return "class Object120: v1.04 (aether), 2010-Oct-03."; } /** Print version information to the console window. */ public static void printVersion() { System.out.println( Object120.getVersion() ); } //static { Object120.printVersion(); } /** Return a random integer in [0,n). * @param n the top end of the range. * @return a pseudorandom integer in [0,n). * E.g. randomInt(100) might return 0 or 1 or 99, but never 100. * @see Random#nextInt(int) */ public static int randomInt(int n) { return new Random().nextInt(n); /* We should really have just one instance of a Random which we re-use; * however to keep BlueJ's object inspector window free of static fields * the student never mentioned, we avoid this. */ } /** Does a string start with a vowel? (Helpful for determining "a(n) ___".) * @param s Any string. Must be non-null. * @return whether or not `s` has a first letter which is a,e,i,o,u,y (either case). * For example:
   *   startsWithVowel( "ho" ) = false
   *   startsWithVowel( "oh" ) = true
   *   startsWithVowel( "i" )  = true
   *   startsWithVowel( "" )   = false
   *    
*/ private static boolean startsWithVowel( String s ) { return (s != null && s.length() > 0 && "aeiouy".contains(s.substring(0,1).toLowerCase())); } private static boolean hasPrefix( String s, String pre ) { return indexOf(s,pre) == 0; } private static String removePrefix( String s, String pre ) { //assert hasPrefix(s,pre); /* We remove the assertions so that the compiler doesn't generate * the field $assertionsDisabled, which goes on to clutter BlueJ's * object-inspector window with static fields the student never declared. */ return s.substring(pre.length()); } private static String removePrefixIfPresent( String s, String pre ) { return hasPrefix(s,pre) ? removePrefix(s,pre) : s; } /* ======== static versions of Object methods: toString, ... ======== */ /** Are two values equal? Works on any values (non-null). * A static version of Object.equals(Object), * for use in CS1 before we introduce objects. * @param _this the first value to compare. (Must be non-null.) * @param that the second value to compare. (Must be non-null.) * @return true iff `_this` equals `_that`. * For example:
   *    equals( "hello", "howdy" ) = false
   *    equals( "hello", "hello" ) = true
   *    equals( 23, 22+1 ) = true
   * @see Object#equals(Object)
   */
  public static boolean equals( Object _this, Object that ) {
    checkForNull( _this, "equals", "Object" );
    checkForNull(  that, "equals", "Object" );
    return _this.equals(that);
    }
    
        
  /** Convert any value into a String.
   * A static version of Object.toString(), for use in CS1 before we introduce objects.
   * @param _this The value to stringify.  Cannot be null.
   * @return a String representation of `_this`.
   * For example:
   *    toString(43) = "43" 
   *    toString(true) = "true"
   *    
* @see Object#toString() */ public static String toString( Object _this ) { return _this.toString(); } /** Check whether one value is greater, equal, or lesser than another. * A static version of compareTo(Object), for use in CS1 before we introduce objects. * @param _this The first value to compare. * @param that The second value to compare. * @return a * positive number if `_this` is greater than `that`, * zero if `_this` is equal to `that`, * or a negative number if `_this` is less than `that`. * @see Comparable#compareTo(Object) * */ public static int compareTo( Comparable _this, S that ) { checkForNull( _this, "compareTo", "Object" ); String fullTypeName = _this.getClass().toString(); String justTypeName = removePrefix( fullTypeName, "class " ); String naturalTypeName = removePrefixIfPresent( justTypeName, "java.lang." ); checkForNull( that, "compareTo", naturalTypeName + " (or something extending " + naturalTypeName + ")" ); return _this.compareTo(that); /* It seems natural to also write a complementary version * static int compareTo( S _this, Comparable that ) * but then users will commonly get 'error - compareTo is ambiguous, both methods match'. */ } /** Are two Strings equal (up to case, but including punctuation). * A static version of String.equalsIgnoreCase(String), for use in CS1 before we introduce objects. * @param a The first string to compare. Cannot be null. * @param b The second string to compare. Cannot be null. * @return true iff a and b are the same (ignoring case). * For example:
   *   equalsIgnoreCase( "hi", "HI" ) = true
   *   equalsIgnoreCase( "hi", "hi " ) = false
   *   equalsIgnoreCase( "", "" ) = true
   *    
* @see String#equalsIgnoreCase(String) */ public static boolean equalsIgnoreCase( String a, String b ) { checkForNull( a, "equalsIgnoreCase", "String" ); checkForNull( b, "equalsIgnoreCase", "String" ); return a.equalsIgnoreCase(b); } /** Return the number of characters in a string. * A static version of String.length(), for use in CS1 before we introduce objects. * @param _this The string to find the length of. Cannot be null. * @return The number of characters in `_this`. * For example:
   *   length( "hi ho" ) = 5
   *   length( "z" ) = 1
   *   length( "" ) = 0
   *    
* @see String#length() */ public static int length( String _this ) { checkForNull( _this, "length", "String" ); return _this.length(); } /** Return a substring of the given string, from index `from` up to but not including index `to`. * A static version of `String.substring(int,int)`, for use in CS1 before we introduce objects. * @param _this The `String` to take a substring from. Cannot be null. * @param start The index of the first character of the substring. * @param stop The index of the first character after the substring. * @return A String consisting from characters at indices [`start`,`stop`) from `_this`. * @see String#substring(int,int) */ public static String substring( String _this, int from, int to ) { checkForNull( _this, "substring", "String" ); return _this.substring(from,to); } /** Return a substring of the given string from index `from` up through the last character. * A static version of `String.substring(int)`, for use in CS1 before we introduce objects. * @param _this The `String` to take a substring from. Cannot be null. * @param start The index of the first character of the substring. * @return A String consisting from characters at indices [`start`,`length(_this)`) from `_this`. * @see String#substring(int) */ public static String substring( String _this, int from ) { checkForNull( _this, "substring", "String" ); return _this.substring(from); } /** Return where one string is contained inside another. * A static version of `String.indexOf(String)`, for use in CS1 before we introduce objects. * @param _this The `String` to look inside. Cannot be null. * @param target The String to try to find occuring inside `_this`. Cannot be null. * @return The index of `_this` where `target` starts, or -1. * That is: if int i=indexOf(s1,s2), then i==-1 || equals(s2,substring(s1,i,i+length(s2))) * (equivalently, i==-1 || s2.equals(s1.substring(i,i+s2.length()))) * * @see String#indexOf(String) */ public static int indexOf( String _this, String target ) { checkForNull( _this, "indexOf", "String" ); checkForNull( target, "indexOf", "String" ); return _this.indexOf(target); } /** Return a lower-case version of the given string. * A static version of `String.toLowerCase()`, for use in CS1 before we introduce objects. * @param _this The `String` to take a substring from. Cannot be null. * @return A lower-case version of `_this`. * @see String#toLowerCase() */ public static String toLowerCase( String _this ) { checkForNull( _this, "toLowerCase", "String" ); return _this.toLowerCase(); } /** Return an upper-case version of the given string. * A static version of `String.toUpperCase()`, for use in CS1 before we introduce objects. * @param _this The `String` to convert to upper case. Cannot be null. * @return An upper-case version of `_this`. * @see String#toUpperCase() */ public static String toUpperCase( String _this ) { checkForNull( _this, "toUpperCase", "String" ); return _this.toUpperCase(); } /** Convert a String into an int. * @param s A string which is a valid representation of an int; cannot be empty. * @return the int represented by `s`. * For example:
   *    stringToDouble("2") = 2
   *    stringToDouble("007") = 7
   *    
* @throws NumberFormatException if `s` does not represent a valid double. * stringToDouble("2+3") throws NumberFormatException */ public static int stringToInt( String s ) { checkForNull( s, "stringToInt", "String" ); return Integer.parseInt(s); } /** Convert a String to a double. * @param s A string which is a valid representation of a double. * @return the double represented by `s`. * For example:
   *    stringToDouble("43.2") = 43.2
   *    stringToDouble("2") = 2.0
   *    stringToDouble("007") = 7.0
   *    
* @throws NumberFormatException if `s` does not represent a valid double. * For example:
   *    stringToDouble("2+3") -> NumberFormatException
   *    
*/ public static double stringToDouble( String s ) { checkForNull( s, "stringToDouble", "String" ); return Double.parseDouble(s); } /** Convert an int to a String. * @param n The int to convert to a String. * @return a String representing `n`. * For example:
   *    intToString(0) = "0"
   *    intToString(-2) = "-2"
   *    intToString(34) = "34"
   *    
* @see Integer#toString() * @see Integer#toString(int) * @see #toString(Object) */ public static String intToString( int n ) { return Integer.toString(n); } // Redundant, with toString(Object). /** Convert an int to a double. * (Same as casting to an int, but use the regular syntax for calling-a-method.) * @param n the int to convert. * @return n as a double. * For example:
   *    intToDouble(0) = 0.0
   *    intToDouble(-2) = -2.0
   *    intToDouble(2000000000) = 2e9
   *    intToDouble(2000000001) = 2.000000001e9
   *    
*/ public static double intToDouble( int n ) { return new Double(n).intValue(); } /** Convert a double to an int (truncating towards zero). * (Same as casting to an int, but use the regular syntax for calling-a-method.) * @param x The double to convert to an int. * @return The int nearest to x, but between 0 and x. * For example:
   *    doubleToInt(0.0) = 0
   *    doubleToInt(3.1) = 3
   *    doubleToInt(3.9) = 3
   *    doubleToInt(-3.1) = -3.0
   *    doubleToInt(-3.9) = -3.0
   *    
* @see Double#intValue() * @see Math#floor(double) * @see Math#ceil(double) * @see Math#round(double) */ public static int doubleToInt( double x ) { return new Double(x).intValue(); } /** Return character corresponding to a particular the unicode value. * For "ordinary" characters, this is the ascii value ('A'=43,'B'=44','a'=97,...) * @param n The unicode(ascii) value to convert to a char. * Must be valid as a short -- in [0,65536). * @return the character corresponding to the unicode value `n`. * @see http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters */ public static char intToChar( int n ) { return (char)n; } /** Return the unicode value of a character. * For "ordinary" characters, this is the ascii value ('A'=43,'B'=44','a'=97,...) * @param c The character to get the unicode value of. * @return the unicode value of `c`. * @see http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters */ public static int charToInt( char c ) { return (int)c; } /** Convert a String-of-length-1 to a char. * @param s A string of length 1. * @return s as a char. * @see String#charAt(int) */ public static char stringToChar( String s ) { checkForNull( s, "stringToChar", "String" ); if (s.length() != 1) throw new IllegalArgumentException( "Object120.stringToChar: Can only convert strings of length 1; got: \"" + s + "\"." ); return s.charAt(0); } /** Convert a char to a String-of-length-1. * Equivalent to calling Character.toString(c), or Object120.toString(c), or new Character(c).toString(). * @param c The char to convert. * @return c as a String. * @see Character#toString() * @see #toString(Object) */ public static String charToString( char c ) { return Character.toString(c); } /** Select a single character from a string, by index (starting at 0). * A static version of `String.charAt(int)`, for use in CS1 before we introduce objects. * @param _this The `String` to select a character from. Cannot be null. * @param i The index of the character to select from s; 0 <= i < length(s). * @return The `i`th character of `_this`. * @see String#toLowerCase() */ public static char charAt( String _this, int i ) { checkForNull( _this, "charAt", "String" ); return _this.charAt(i); } /** Return whether or not one String contains another. * A static version of `String.contains(CharSequence)`, for use in CS1 before we introduce objects. * @param _this The `String` to look inside of. Cannot be null. * @param target The String to look for in `_this`. * @return whether or not `target` occurs in `_this`. * @see String#contains(CharSequence) * @see #indexOf(String,String) * @see String#indexOf(String) */ public static boolean contains( String _this, CharSequence target ) { checkForNull( _this, "charAt", "String" ); checkForNull( target, "charAt", "String (or CharSequence)" ); return _this.contains(target); } /** Return whether or not a String is the empty string "" (0 letters long). * A static version of `String.isEmpty()`, for use in CS1 before we introduce objects. * @param _this The String to compare to "". Cannot be null. * @return whether or not equals(_this,"") (equivalently, length(_this)==0). * @see String#isEmpty() */ public static boolean isEmpty( String _this ) { checkForNull( _this, "isEmpty", "String" ); return _this.isEmpty(); // Pre-Java1.6: return "".equals(_this); } /* ================================================================ */ /* ======== Constructor, equals, hashcode, toString, ... ======== */ /** Automated constructor: * Initialize each field of the subclass with the provided args, * in the order the fields are declared within the subclass' file. * * Remember that if a use writes their own constructor for a subclass, * this constructor will still get called first with 0 args, * unless the explicitly called `super` with more args. * Alas, we can't check here whether they actually * manage to initialize their fields correctly... */ public Object120( Object... args ) { String subclassName = this.getClass().getName(); // For use in diagnostic messages. List instanceFields = getDeclaredNonstaticFields(this); // TODO: for efficiency, keep a static Map,List> instanceFields. verifySubclass(this,instanceFields); if (args.length == 0) return; // Did they provide the right number of arguments to the constructor? // Note that 0 args provided probably means that the subclass already // was declared (so don't complain in that case). if (args.length != instanceFields.size()) { String msg; msg = "Expected " + instanceFields.size() + " argument" + (instanceFields.size()==1 ? "" : "s") + " to " + subclassName + " constructor, but found " + args.length + " argument" + ((args.length)==1 ? "" : "s") + ": " + Arrays.deepToString(args) + ".\n"; msg += "Try: `new " + subclassName + "("; boolean needComma = false; for (Field f : instanceFields) { msg += (needComma ? ", " : "") + getTypeName(f) + " [" + f.getName() + "]"; needComma = true; } msg += ")`."; throw new IllegalArgumentException( "Object120 super: " + msg); } // Okay, finally -- init each field. int i = 0; for (Field f : instanceFields) { try { f.setAccessible(true); f.set(this, args[i] ); ++i; } catch(IllegalArgumentException e) { throw new RuntimeException( "Object120 super: " + "Can't initialize field " + subclassName + "." + f.getName() + " with value " + args[i].toString() + " (of type " + args[i].getClass().getName() + ").\n" + e.toString() ); } catch(IllegalAccessException e) { throw new RuntimeException( "Object120 super: " + "Security manager doesn't allow accessing fields through reflection. " + "You must write the " + subclassName + " constructor yourself.\n" + e.toString() ); } } } @Override /** A generic hashCode, summing the hash of each field. */ public int hashCode() { int hash = 0; for (Field f : getDeclaredNonstaticFields(this) ) { try { f.setAccessible(true); hash *= Math.pow(2,5)-1; // shuffle the bits (well?). // TODO: 2,5 as named constants. hash += f.get(this).hashCode(); } catch(IllegalAccessException e) { throw new RuntimeException( "Rats, I can't access the field through reflection. " + "Try overriding `hashCode` in " + this.getClass().getName() + "\n" + e.toString() ); } } return hash; /* For performance, we should (if the object is immutable) cache the hash code. * However, so that BlueJ's object-inspector window isn't cluttered with fields * the student never declared, we avoid that. */ } @Override /** A deep equals check: call equals on each field. */ public boolean equals( Object oth ) { if ( this == oth ) { return true; } else if ( oth == null || (this.getClass() != oth.getClass()) ) { return false; } else { for (Field f : getDeclaredNonstaticFields(this)) { try { f.setAccessible(true); boolean thisNull = (f.get(this)==null); boolean thatNull = (f.get(oth )==null); if ( thisNull && thatNull) { /* continue */ } else if ( thisNull && !thatNull) return false; else if (!thisNull && thatNull) return false; else if (! f.get(this).equals(f.get(oth))) return false; else { /* continue */ } } catch(IllegalAccessException e) { throw new RuntimeException( "Rats, I can't access the field through reflection. " + "Try overriding `equals` in " + this.getClass().getName() + "\n" + e.toString() ); } } return true; } } @Override public String toString() { return this.toString(false); } /** Return a String which looks like a constructor call: * @param includeFieldNames If true, include the field names along with their values. * @return a String which looks just like a constructor call * when `includeFieldNames` is false, * or similar to one when `includeFieldNames` is true. * For example:
   *   class Foo extends Object120 { int n; String s }
   *   new Foo(7,"hi").toString(false) = "new Foo( 7, \"hi\" )"
   *   new Foo(8,"ho").toString(true)  = "new Foo( n=8, s=\"ho\" )"
   *   
* Bug: Depending on the Java compiler, the field names might not be * given in the same order they are declared in the class. * (This is because java.lang.reflect doesn't provide access to the declared order; * however, many implementations happen to to use that order.) */ public String toString( boolean includeFieldNames ) { StringBuilder str = new StringBuilder(); str.append("new " + this.getClass().getName() + "( "); boolean needComma = false; // Need to insert a comma before the next field? for (Field f : getDeclaredNonstaticFields(this)) { try { if (needComma) str.append( ", " ); Object theVal = f.get(this); if (includeFieldNames) str.append( f.getName().toString() ).append("="); str.append( quoteMark(theVal.getClass()) ); if (theVal.getClass().isInstance(Object120.class)) { str.append( Object120.class.cast(theVal).toString(includeFieldNames)); } else { str.append( theVal.toString()); } str.append( quoteMark(theVal.getClass()) ); needComma = true; } catch(IllegalAccessException e) { throw new RuntimeException( "Rats, I can't access the field through reflection. " + "Try overriding `" + this.getClass().getName() + "toString`.\n" + e.toString() ); } } str.append( " )" ); return str.toString(); } /* ======== Internal helper methods ======== */ /** Check whether a reference is null, and throw an error if it is. * @param ref The reference to check for being null. * @param methodName The name to put at the start of the (potential) error message. * @param type A string describing `ref`s type, for the (potential) error message. * @throws NullPointerException if `ref`==null. * For example:
   *   checkForNull( "lala", "myMethod", "String" ) -> no effect
   *   checkForNull( null, "myMethod", "String" ) -> throws an exception with
   *     a message like "Object120.myMethod: expected a String but got null."
   *    
*/ private static void checkForNull( Object ref, String methodName, String type ) { if (ref == null) throw new NullPointerException( "Object120." + methodName + ": Expected a" + (startsWithVowel(type) ? "n" : "") + " " + type + " but got null. (Did you forget to initialize a variable or field?)" ); } /** Return the non-static fields in an object's *derived* class. * Intended for classes derived from Object120. * @param obj An object of the class to get the fields from. * @return the non-static fields in an object's *derived* class. */ private static List getDeclaredNonstaticFields( Object obj ) { /* This function should really be cached in a field, for efficiency. * However, to avoid BlueJ's object-inspector window from being cluttered with * fields the student never declared, we re-compute it every time we need it. */ List instanceFields = new ArrayList(); for (Field f : obj.getClass().getDeclaredFields()) { /* N.B. The docs for getDeclaredFields don't explicitly guarantee that two calls * on the same Class will return the fields in the same order. I can't imagine * that an implementation *wouldn't* do that, but perhaps sort it just in case? */ if (!Modifier.isStatic(f.getModifiers())) { instanceFields.add(f); } } return instanceFields; } /** Return a string giving the (non-qualified) type of a Field. (e.g. "String" or "int"). * @param f the Field to get the type of. * @return a string giving the (non-qualified) type of a Field. (e.g. "String" or "int"). */ private static String getTypeName( Field f ) { String[] splinteredName = f.getType().toString().split("\\."); return splinteredName[ splinteredName.length-1 ]; } /** Do some basic checks on the subclass (named appropriately, * subclass named appropriately, etc). * Prints warning messages as appropriate. */ private static void verifySubclass( Object120 obj, List instanceFields ) { String subclassName = obj.getClass().getName(); // Make sure the student capitalized the class' name. if (!Character.isUpperCase( subclassName.charAt(0) )) { System.err.println( "Warning:" + " Class name `" + subclassName + "`" + " should start with upper case, by convention." + " -- Object120 super." ); } for (Field f : instanceFields ) { if (!Character.isLowerCase(f.getName().charAt(0))) { System.err.println( "Object 120 super:" + " Field name `" + f.getName() + "` in class `" + subclassName + "`" + " should start with lower case, by convention." ); } } // We don't currently support X extends Y extends Object120; check for this. if (!obj.getClass().getSuperclass().equals( Object120.class )) { System.err.println( "Warning:" + "class " + subclassName + " should be a *direct* subclass of Object120; " + "Object120 is not (yet) built for extending more than once." ); } if (instanceFields.isEmpty()) { System.err.println( "Warning: class " + subclassName + " has no fields, but you are calling its constructor." + "\n(This is occasionally useful, but not in intro classes.)" + " --Object120."); } else { // Make sure a constructor was defined. Constructor[] cons = obj.getClass().getDeclaredConstructors(); if (cons.length==1 && cons[0].getParameterTypes().length == 0) { System.out.println( cons[0] + " is synth? " + cons[0].isSynthetic() ); System.err.println( "Warning: possible missing constructor for class " + subclassName + "?" + "\n(If you wrote a 0-arg constructor intentionally, ignore this.)" + " --Object120."); } } } /** Return a String to quote a value with: * "\'" for chars, "\"" for strings and similars, "" for all others. * Intended for use with a toString which tries to match Java syntax for literals. * @param c The type of the object to find quotes for. * @return a String to quote a value with. */ private static String quoteMark( Class c ) { if (c.equals(Character.class)) { return "'"; } else if (c.equals(String.class) || c.equals(StringBuffer.class) || c.equals(StringBuilder.class)) { return "\""; } else { return ""; } /* We should really implement this as a (static) Map, * but to avoid cluttering BlueJ's object-inspector window * with static fields that the student never declared, * we avoid that: Map quoteMarks = new HashMap(); quoteMarks.put( Character.class, "'" ); quoteMarks.put( String.class, "\"" ); quoteMarks.put( StringBuffer.class, "\"" ); quoteMarks.put( StringBuilder.class, "\"" ); */ } /* Deficiencies of this class: * Fundamental: when extending Object120: * - final fields won't compile -- not init'd. * - Can't generate setters (only a `set(String fieldName, Object val)`; no good for CS1.) * - doesn't work for 'grandchild classes' of Object120. * To fix/patch: * - in constructor: verify that the parent class is Object120. * * Really, this class should be broken into several others: * Util120 for utility functions, and trio of classes Object,MutableObject,ImmutableObject. * However, for CS1 I only wanted just a single file that Students need to include, not two-four. */ }