package rabbitescape.engine.config; import static rabbitescape.engine.util.Util.*; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import rabbitescape.engine.err.RabbitEscapeException; import rabbitescape.engine.util.Util.Function; public class ConfigTools { public static class ConfigParsingError extends RabbitEscapeException { private static final long serialVersionUID = 1L; public String configValue; public int charNumber; } public static class InvalidValueType extends ConfigParsingError { private static final long serialVersionUID = 1L; public final Class<?> expectedClass; public final Class<?> actualClass; public InvalidValueType( Class<?> expectedClass, Class<?> actualClass ) { this.expectedClass = expectedClass; this.actualClass = actualClass; } } public static class InvalidValue extends ConfigParsingError { private static final long serialVersionUID = 1L; public final String value; public final Class<?> clazz; public InvalidValue( String value, Class<?> clazz ) { this.value = value; this.clazz = clazz; } } public static class UnexpectedCharacter extends ConfigParsingError { private static final long serialVersionUID = 1L; public final String expectedChars; public final char actualChar; public UnexpectedCharacter( String expectedChars, char actualChar ) { this.expectedChars = expectedChars; this.actualChar = actualChar; } } public static class UnknownValueType extends RabbitEscapeException { private static final long serialVersionUID = 1L; public final Class<?> clazz; public UnknownValueType( Class<?> clazz ) { this.clazz = clazz; } } public static void setInt( Config config, String key, int value ) { config.set( key, String.valueOf( value ) ); } public static int getInt( Config config, String key ) { return Integer.parseInt( config.get( key ) ); } public static void setBool( Config config, String key, boolean value ) { config.set( key, String.valueOf( value ) ); } public static boolean getBool( Config config, String key ) { return Boolean.parseBoolean( config.get( key ) ); } public static void setString( Config config, String key, String value ) { config.set( key, value ); } public static String getString( Config config, String key ) { return config.get( key ); } // TODO: break into separate class file public static <T> Map<String, T> getMap( Config cfg, String configKey, Class<T> clazz ) { return stringToMap( cfg.get( configKey ), clazz ); } public static <T> Map<String, T> stringToMap( String jsonish, Class<T> clazz ) { final int open_bracket = 0; final int comma_or_close_bracket = 1; final int end = 2; final int key_open_quote_or_close_bracket = 3; final int key_char_or_close_quote = 4; final int colon = 5; final int value_open_quote_or_digit = 6; final int value_char_or_close_quote = 7; final int value_digit_or_comma_or_close_bracket = 8; final int key_open_quote = 9; int mode = open_bracket; StringBuilder key = new StringBuilder(); StringBuilder value = new StringBuilder(); Map<String, T> ret = new TreeMap<String, T>(); int i = 0; for ( char ch : asChars( jsonish ) ) { try { switch ( mode ) { case open_bracket: expect( "{", ch ); mode = key_open_quote_or_close_bracket; break; case key_open_quote: expect( "\"", ch ); mode = key_char_or_close_quote; key.setLength( 0 ); value.setLength( 0 ); break; case key_open_quote_or_close_bracket: expect( "\"}", ch ); if ( ch == '"' ) { mode = key_char_or_close_quote; key.setLength( 0 ); value.setLength( 0 ); } else { mode = end; } break; case key_char_or_close_quote: if ( ch == '"' ) { mode = colon; } else { key.append( ch ); } break; case colon: expect( ":", ch ); mode = value_open_quote_or_digit; break; case value_open_quote_or_digit: expect( "\"0123456789", ch ); if ( ch == '"' ) { expectValueTypeIs( clazz, String.class ); mode = value_char_or_close_quote; } else { expectValueTypeIs( clazz, Integer.class ); mode = value_digit_or_comma_or_close_bracket; value.append( ch ); } break; case value_char_or_close_quote: if ( ch == '"' ) { mode = comma_or_close_bracket; } else { value.append( ch ); } break; case comma_or_close_bracket: expect( ",}", ch ); foundValue( clazz, key, value, ret ); if ( ch == ',' ) { mode = key_open_quote; } else // '}' { mode = end; } break; case value_digit_or_comma_or_close_bracket: if ( ch == ',' ) { foundValue( clazz, key, value, ret ); mode = key_open_quote; } else if ( ch == '}' ) { foundValue( clazz, key, value, ret ); mode = end; } else { expect( "0123456789", ch ); value.append( ch ); } break; case end: expect( "", ch ); // Throw - should have ended! break; } ++i; } catch( ConfigParsingError e ) { e.charNumber = i; e.configValue = jsonish; throw e; } } return ret; } private static <T> void foundValue( Class<T> clazz, StringBuilder key, StringBuilder value, Map<String, T> ret ) { ret.put( key.toString(), makeValue( value, clazz ) ); } private static void expectValueTypeIs( Class<?> expectedClass, Class<?> actualClass ) { if ( expectedClass != actualClass ) { throw new InvalidValueType( expectedClass, actualClass ); } } private static void expect( String exp, char act ) { if ( exp.indexOf( act ) == -1 ) { throw new UnexpectedCharacter( exp, act ); } } @SuppressWarnings( "unchecked" ) private static <T> T makeValue( StringBuilder value, Class<T> clazz ) { String v = value.toString(); if ( clazz == String.class ) { return (T)v; } else if ( clazz == Integer.class ) { try { return (T)new Integer( v ); } catch ( NumberFormatException e ) { throw new InvalidValue( v, clazz ); } } else { throw new UnknownValueType( clazz ); } } public static <T> void setMap( Config cfg, String key, Map<String, T> value ) { cfg.set( key, mapToString( value ) ); } public static <T> String mapToString( Map<String, T> value ) { StringBuilder val = new StringBuilder(); val.append( '{' ); boolean stringVals = true; boolean first = true; List<String> ks = new ArrayList<String>( value.keySet() ); Collections.sort( ks ); for ( String k : ks ) { T rhs = value.get( k ); if ( first ) { first = false; stringVals = rhs instanceof String; } else { val.append( ',' ); } val.append( '"' ); val.append( k ); val.append( "\":" ); if ( stringVals ) val.append( '"' ); val.append( rhs ); if ( stringVals ) val.append( '"' ); } val.append( '}' ); return val.toString(); } /** * Note: does not support negative numbers. */ public static <T> Set<T> getSet( Config cfg, String configKey, Class<T> clazz ) { return stringToSet( cfg.get( configKey ), clazz ); } /** * Note: does not support negative numbers. */ public static <T> void setSet( Config cfg, String configKey, Set<T> value ) { cfg.set( configKey, setToString( new TreeSet<T>( value ) ) ); } public static <T> String setToString( SortedSet<T> ret ) { return "[" + join( ",", map( quoteString( ret ), ret ) ) + "]"; } private static <T> Function<T, String> quoteString( SortedSet<T> set ) { return new Function<T, String>() { @Override public String apply( T t ) { if ( t instanceof String ) { return "\"" + t.toString() + "\""; } else { return t.toString(); } } }; } private static <T> Set<T> stringToSet( String jsonish, Class<T> clazz ) { final int open_square_bracket = 0; final int open_quote_or_digit_or_close_square_bracket = 1; final int char_or_close_quote = 2; final int end = 3; final int digit_or_comma_or_close_square_bracket = 4; final int comma_or_close_square_bracket = 5; final int digit_or_open_quote = 6; int mode = open_square_bracket; boolean foundValue = false; StringBuilder value = new StringBuilder(); Set<T> ret = new TreeSet<T>(); int i = 0; for ( char ch : asChars( jsonish ) ) { try { switch ( mode ) { case open_square_bracket: expect( "[", ch ); mode = open_quote_or_digit_or_close_square_bracket; break; case open_quote_or_digit_or_close_square_bracket: expect( "\"0123456789]", ch ); if ( ch == '"' ) { expectValueTypeIs( clazz, String.class ); foundValue = true; value.setLength( 0 ); mode = char_or_close_quote; } else if ( ch == ']' ) { maybeFoundValue( clazz, value, foundValue, ret ); mode = end; } else { expectValueTypeIs( clazz, Integer.class ); foundValue = true; value.setLength( 0 ); value.append( ch ); mode = digit_or_comma_or_close_square_bracket; } break; case char_or_close_quote: if ( ch == '"' ) { maybeFoundValue( clazz, value, foundValue, ret ); mode = comma_or_close_square_bracket; } else { value.append( ch ); } break; case comma_or_close_square_bracket: expect( ",]", ch ); if ( ch == ',' ) { maybeFoundValue( clazz, value, foundValue, ret ); mode = digit_or_open_quote; } else { mode = end; } break; case digit_or_comma_or_close_square_bracket: expect( "0123456789,]", ch ); if ( ch == ',' ) { maybeFoundValue( clazz, value, foundValue, ret ); mode = digit_or_open_quote; } else if ( ch == ']' ) { maybeFoundValue( clazz, value, foundValue, ret ); mode = end; } else { value.append( ch ); } break; case digit_or_open_quote: expect( "0123456789\"", ch ); if ( ch == '"' ) { expectValueTypeIs( clazz, String.class ); foundValue = true; value.setLength( 0 ); mode = char_or_close_quote; } else { expectValueTypeIs( clazz, Integer.class ); foundValue = true; value.setLength( 0 ); value.append( ch ); mode = digit_or_comma_or_close_square_bracket; } break; case end: expect( "", ch ); // Throw - should have ended! break; } ++i; } catch( ConfigParsingError e ) { e.charNumber = i; e.configValue = jsonish; throw e; } } return ret; } @SuppressWarnings( "unchecked" ) private static <T> void maybeFoundValue( Class<T> clazz, StringBuilder value, boolean foundValue, Set<T> ret ) { if ( foundValue ) { ret.add( makeValue( value, clazz ) ); } } }