package clearcut; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.HashMap; import java.lang.reflect.Constructor; /** Injects dependencies */ public class Injector { private Ini ini; private static int MAX_DIRS_UP = 5; public static Injector INJECTOR = new Injector(); public Injector () { load(); } /** Find the app.ini file in this folder or above and load properties */ private synchronized void load() { try { File file = new File( "." ); String path = file.getAbsolutePath(); if( ! path.equals( File.separator ) ) { int dirsUp = 0; while ( ! (path == null) && ! path.equals( File.separator ) && ! readAppIniFile( path ) && ++ dirsUp < MAX_DIRS_UP ) { file = new File( path ); path = chop( path ); } } } catch( Exception x ) { x.printStackTrace(); } } public String iniPath() { return this.ini.path(); } private String chop( String path ) { if( path.equals( File.separator ) ) return null; int pos = path.lastIndexOf( File.separatorChar ); if( pos == -1 ) return null; return path.substring( 0, pos ); } private void setProperties( String path ) throws IniException { if( this.ini == null ) { this.ini = new Ini( path ); } } private boolean readAppIniFile( String dir ) throws IniException { String path = dir + File.separator + Ini.app(); File file = new File( path ); if ( ! file.exists() ) return false; else setProperties( path ); return true; } @SuppressWarnings("unchecked") public String fessUp() { String ret = null; for( String key : this.ini.keySet() ) { Object value = this.ini.get( key ); for( String[] entry : (List<String[]>)value ) ret+=( "\n"+key+ ": " + ((String[])entry)[0]+"="+((String[])entry)[1] ); } return ret; } /** Returns all the key-value pairs in a given section, eg. my_name=Jim in section [profile] */ @SuppressWarnings("unchecked") public List < String [ ] > properties( String section ) throws InjectionException { Object obj = this.ini.get(section); if( obj == null ) return null; if( obj instanceof List ) return ( List <String []> ) obj; else throw new InjectionException( "Section "+section+" contains something which is not a list of key-value pairs"); } /** Keys in a section can have multiple entries, eg. logfile:a.log logfile:b.log */ public List<String> properties( String section, String key ) throws InjectionException { List < String > list = new ArrayList< String > (); List < String [] > properties = properties( section ); for( String [] value : properties ) if( value == null || value.length != 2 ) throw new InjectionException( "Problem initializing ["+section+"] section from "+Ini.app()+": not all entries in this section are key/value pairs" ); else if( value[0].equals( key ) ) list.add( value[1] ); return list; } /** A key-value pair under a section, eg. realMember=example.biz.Member under [injection] */ public String property( String section, String key ) throws InjectionException { List < String [] > properties = properties( section ); for( String [] value : properties ) if( value == null || value.length != 2 ) throw new InjectionException( "Problem initializing ["+section+"] section from "+Ini.app()+": not all entries in this section are key/value pairs" ); else if( value[0].equals( key ) ) return value[1]; return null; } /** 'NO SECTION' properties are those at the top of the app.ini file above any 'sections' */ public String property( String key ) throws InjectionException { return property(Ini.NO_SECTION, key); } /** This method is the core of Injector. It returns objects created using constructors from app.ini. */ public Object implement( String section, String key ) throws InjectionException { if( key == null || key.trim().length() < 1 ) throw new InjectionException( "Needs to be called with a word as its parameter, eg. a_person" ); List <String []> properties = properties( section ); if(properties == null || properties.size() < 1) throw new InjectionException( "Section ["+section+"] was not found in "+Ini.app() ); String constructor = property( section, key ); if( constructor == null || constructor.length() < 1 ) throw new InjectionException( "Valid "+key+" implementation not found in " + Ini.app() ); Object [] parameters = null; String className = new String( constructor ); boolean hasParameters = constructor.indexOf ('(')>-1; if( hasParameters ) { className = constructor.substring(0, constructor.indexOf( '(' )); parameters = parameters( constructor ); if( parameters.length < 1 ) hasParameters = false; } Class [] classes = new Class[0]; className = className.trim(); try { Class CLASS = Class.forName ( className ); if( CLASS == null ) throw new InjectionException( ""+key+" implementation "+constructor+" not found" ); if( ! hasParameters ) // Hope the thing has a default constructor return CLASS.newInstance(); Constructor[] constructors = CLASS.getConstructors(); for( Constructor con : constructors ) { classes = con.getParameterTypes(); if( classes.length == parameters.length ) { int matches = 0; for( int i = 0; i < classes.length; i ++ ) { Class cla$$ = classes[ i ]; Object obj = parameters[ i ]; try { parameters[ matches ++ ] = cast( section, cla$$, obj ); } catch( InjectionCastException e ) { // Don't use this constructor, try another... } } // Same no. of params, same(ish) types... let's try it if( matches == parameters.length ) { try { return con.newInstance( parameters ); } catch( IllegalArgumentException i ) { // Oh well, we tried... try another constructor } } } } } // Indirection -> recursion. Is that a useful comment or what? catch( ClassNotFoundException c ) { return implement ( className ); } catch( Exception x ) { throw new InjectionException( x ); } throw new InjectionException ( "Constructor not found for "+constructor+" from "+Ini.app() ); } public Object implement( String key ) throws InjectionException { return implement( "injection", key ); } /** object is the thing you are trying to stick into a parameter list, castInto is the type of the thing you are trying to stuff it into, and I can't remember what section is */ private Object cast( String section, Class castInto, Object object ) throws InjectionException { Class castFrom = object.getClass(); String str = object.toString(); if( castInto == castFrom ) return object; if( StringBuffer.class == castFrom && str.equals( "null" ) ) return null; if( numeric( castInto, object )) { Boolean numeric = Numeric( str ); if( numeric == null ) return Integer.parseInt( str ); else return Double.parseDouble( str ); } try { if( StringBuffer.class == castFrom ) { // StringBuffer means something special - see parameters() try { Object ca$t = implement( str ); // implement calls cast calls implement return castInto.cast( ca$t ); } catch( InjectionException e ) { str = property( section, str ); if( str == null ) throw e; else return str; } } else return castInto.cast( object ); } catch( ClassCastException x ) { throw new InjectionCastException( "Class cast exception casting a " +castFrom.getName() + " into a "+ castInto.getName() ); } } private boolean numeric( Class one, Object other ) { Class two = other.getClass(); return( ( one == int.class || one == Integer.class || one == double.class || one == Double.class || one == float.class || one == Float.class || one == long.class || one == Long.class || one == short.class || one == Short.class || one == byte.class || one == Byte.class || one == char.class || one == Character.class || one == BigInteger.class || one == BigDecimal.class ) && ( (two == String.class && (Numeric( (String) other ) == null || Numeric( (String) other ).booleanValue() == true) ) || ( two == int.class || two == Integer.class || two == double.class || two == Double.class || two == float.class || two == Float.class || two == long.class || two == Long.class || two == short.class || two == Short.class || two == byte.class || two == Byte.class || two == char.class || two == Character.class || two == BigInteger.class || two == BigDecimal.class ) ) ); } /** Returns null if parameter is an integer, true if it is floating point, and false otherwise */ private Boolean Numeric( String num ) { try { Long.parseLong( num ); return null; } catch( NumberFormatException e ) { try { Double.parseDouble( num ); return new Boolean(true); } catch( NumberFormatException x ) { return new Boolean(false); } } } public static char DOUBLEQUOTE = '"'; public static char QUOTE = '\''; public static char UNDERSCORE = '_'; public static char COMMA = ','; /** Reads the parameters out of a constructor line from app.ini, eg. 'org.foo.Bar("$", 42)' */ private Object [] parameters( String constructor ) throws InjectionException { try { List <Object> params = new ArrayList <Object> (); int pos = constructor.indexOf( '(' ); int end = constructor.indexOf( ')' ); if( pos < 0 || end < 0 || end < pos + 1 ) throw new InjectionException( constructor + " from "+Ini.app()+" does not have a nice pair of parentheses" ); StringBuffer currentWord = new StringBuffer(); boolean inDoubleQuote = false; StringBuffer currentNumber = new StringBuffer(); boolean inNumber = false; StringBuffer currentSomethingElse = new StringBuffer(); boolean inQuote = false; while( pos ++ < constructor.length() - 1 ) { char ch = constructor.charAt( pos ); if( ! inDoubleQuote && ! inQuote ) { if( ch == ')' ) { if( currentWord.length() > 0 ) params.add( currentWord.toString() ); if( currentNumber.length() > 0 ) { String number = currentNumber.toString(); if( number.indexOf( '.' ) > -1 ) params.add( Double.parseDouble( number )); else params.add( Long.parseLong( number )); } if( currentSomethingElse.length() > 0 ) { String somethingElse = currentSomethingElse.toString(); if( somethingElse.equals( "true" ) || somethingElse.equals( "false" ) ) params.add( new Boolean( somethingElse ) ); else params.add( currentSomethingElse ); } return params.toArray(); } if( ch == DOUBLEQUOTE ) { inDoubleQuote = true; continue; } // "O'REILLY" if( ch == QUOTE ) { inQuote = true; continue; } if( ! inNumber &&( ! Character.isLetterOrDigit( ch ) && ch != UNDERSCORE ) ) { if( currentSomethingElse.length() > 0 ) { String somethingElse = currentSomethingElse.toString(); if( somethingElse.equals( "true" ) || somethingElse.equals( "false" ) ) params.add( new Boolean( somethingElse ) ); else params.add( currentSomethingElse ); currentSomethingElse = new StringBuffer(); continue; } } if( Character.isWhitespace( ch ) || ch == COMMA ) { if( inNumber ) { String number = currentNumber.toString(); if( number.indexOf( "." ) > -1 ) params.add( Double.parseDouble( number )); else params.add( Long.parseLong( number )); currentNumber = new StringBuffer(); inNumber = false; } continue; } } if( inDoubleQuote && ch == DOUBLEQUOTE ) { params.add( currentWord.toString() ); currentWord = new StringBuffer(); inDoubleQuote = false; continue; } if( inQuote && ch == QUOTE ) { params.add( currentWord.toString() ); currentWord = new StringBuffer(); inQuote = false; continue; } if( ! inDoubleQuote && ! inQuote && currentSomethingElse.length() < 1 && "-1234567890.".indexOf( ch ) > -1 ) { if( inNumber && ch == '-' && currentNumber.indexOf( "-" ) > -1 ) throw new InjectionException( "Minus sign inside number in " + constructor + " in " + Ini.app() ); if( inNumber && ch == '.' && currentNumber.indexOf( "." ) > -1 ) throw new InjectionException( "Dot inside number in " + constructor + " in " + Ini.app() ); // TODO: make locale-friendly inNumber = true; } if( (inDoubleQuote && ch != DOUBLEQUOTE) || (inQuote && ch != QUOTE) ) currentWord.append( ch ); else if( inNumber ) currentNumber.append( ch ); else currentSomethingElse.append( ch ); } if( inNumber ) throw new InjectionException( "Unterminated constructor "+constructor+" in "+Ini.app() ); if( inQuote ) throw new InjectionException( "Unterminated quote in "+constructor+" in "+Ini.app() ); if( inDoubleQuote ) throw new InjectionException( "Unterminated double quote in "+constructor+" in "+Ini.app() ); String err = "Error - parameters() should have returned before this point.\nCurrent parameters: "; for(Object obj:params) err += obj.getClass().getName()+" " +obj.toString(); throw new InjectionException( err ); } catch( Exception e ) { throw new InjectionException( constructor, e ); } } }