001package daikon.config; 002 003import java.io.InputStream; 004import java.io.Serializable; 005import java.lang.reflect.Field; 006import java.util.ArrayList; 007import java.util.List; 008import org.checkerframework.checker.nullness.qual.Nullable; 009import org.checkerframework.checker.signature.qual.ClassGetName; 010import org.plumelib.reflection.ReflectionPlume; 011import org.plumelib.util.EntryReader; 012 013/** 014 * This class applies settings from a configuration file that lists variable names and values (see 015 * "example-settings.txt" in this directory for an example). Multiple configuration files can be 016 * read, and the results can be re-written to a new configuration file. 017 * 018 * <p>Important note: classes that have fields set via this Configuration (dkconfig) interface may 019 * not reference daikon.Global in their static initializers, since Global loads the default 020 * configuration, which classloads that class, and we would have a classloading circularity. 021 */ 022public final class Configuration implements Serializable { 023 /** If you add or remove fields, change this number to the current date. */ 024 static final long serialVersionUID = 20020122L; 025 026 // ============================== REPRESENTATION ============================== 027 028 /** The statements that set the configuration. */ 029 @SuppressWarnings("serial") 030 private List<String> statements = new ArrayList<>(); 031 032 // ============================== STATIC COMPONENT ============================== 033 034 static final String PREFIX = "dkconfig_"; 035 036 private static final Class<String> STRING_CLASS; 037 038 static { 039 try { 040 @SuppressWarnings("unchecked") 041 Class<String> STRING_CLASS_tmp = (Class<String>) Class.forName("java.lang.String"); 042 STRING_CLASS = STRING_CLASS_tmp; 043 } catch (Exception e) { 044 throw new RuntimeException(e); 045 } 046 } 047 048 /** 049 * Returns the singleton instance of this class. 050 * 051 * @return the singleton instance of this class 052 */ 053 public static Configuration getInstance() { 054 return instance; 055 } 056 057 /** The singleton instance of this class. */ 058 private static volatile Configuration instance = new Configuration(); 059 060 /** 061 * This used to read a file containing all of the configurable options so that when the options 062 * were saved, they would reflect not only those options specified, but the default values as 063 * well. This would guarantee that changes to the default options would be overridden by the file. 064 * 065 * <p>Unfortunately, that required maintaining a list of all of the configuration variables by 066 * hand. This list quickly became out of date and it seemed that the random results were better 067 * than no attempt at all. The file has thus been removed. If a configuration is changed it only 068 * contains those items specified, not the default values of unspecified options. 069 */ 070 private Configuration() {} 071 072 /** Lets callers differentiate between configuration problems and all others. */ 073 public static class ConfigException extends RuntimeException { 074 public ConfigException(String s, Throwable t) { 075 super(s, t); 076 } 077 078 public ConfigException(String s) { 079 super(s); 080 } 081 082 public ConfigException() { 083 super(); 084 } 085 086 // We are Serializable, so we specify a version to allow changes to 087 // method signatures without breaking serialization. If you add or 088 // remove fields, you should change this number to the current date. 089 static final long serialVersionUID = 20020130L; 090 } 091 092 // ============================== REPLAY ============================== 093 094 public void replay() { 095 // Make a copy of the statements, since apply mutates the list. 096 List<String> copy = new ArrayList<>(statements); 097 for (String statement : copy) { 098 apply(statement); 099 } 100 statements = copy; 101 } 102 103 /** 104 * Take the settings given in the argument and call this.apply(String) for each of them. This 105 * essentially overlaps the settings given in the argument over this (appending them to this in 106 * the process). This method is intended for loading a saved configuration from a file, since 107 * calling this method with the Configuration singleton makes no sense. 108 */ 109 public void overlap(Configuration config) { 110 assert config != null; 111 for (String statement : config.statements) { 112 this.apply(statement); 113 } 114 } 115 116 // ============================== ADT COMPONENT ============================== 117 118 /** 119 * Apply the settings in the given InputStream. 120 * 121 * @param input the commands to set confiuration 122 */ 123 public void apply(InputStream input) { 124 assert input != null; 125 for (String line : new EntryReader(input)) { 126 line = line.trim(); 127 if (line.length() == 0) continue; // skip blank lines 128 if (line.charAt(0) == '#') continue; // skip # comment lines 129 apply(line); 130 } 131 } 132 133 /** 134 * Apply the setting in the given InputStream. 135 * 136 * @param line the command to set confiuration 137 */ 138 public void apply(String line) { 139 assert line != null; 140 141 int eq = line.indexOf('='); 142 if (eq <= 0) { 143 throw new ConfigException("Error, configuration setting must contain \"=\": " + line); 144 } 145 146 String name = line.substring(0, eq).trim(); 147 String value = line.substring(eq + 1).trim(); 148 149 apply(name, value); 150 } 151 152 /** 153 * Set the given setting to the given value. 154 * 155 * @param name the setting to modify 156 * @param value the setting's new value 157 */ 158 public void apply(String name, String value) { 159 assert name != null; 160 assert value != null; 161 162 int dot = name.lastIndexOf('.'); 163 if (dot == -1) { 164 throw new daikon.Daikon.UserError( 165 "Configuration option name must contain a period (.): " + name); 166 } 167 168 @SuppressWarnings("signature") // substring operation 169 @ClassGetName String classname = name.substring(0, dot); 170 String fieldname = name.substring(dot + 1); 171 172 apply(classname, fieldname, value); 173 } 174 175 public void apply(@ClassGetName String classname, String fieldname, String value) { 176 assert classname != null; 177 assert fieldname != null; 178 assert value != null; 179 180 // Use ReflectionPlume version of class.forName so that we can refer to 181 // inner classes using '.' as well as '$' 182 Class<?> clazz; 183 try { 184 clazz = ReflectionPlume.classForName(classname); 185 } catch (ClassNotFoundException e) { 186 throw new ConfigException( 187 String.format( 188 "Configuration option %s=%s attempts to use nonexistent class %s", 189 fieldname, value, classname), 190 e); 191 } catch (LinkageError e) { 192 throw new ConfigException( 193 String.format( 194 "Configuration option %s=%s attempts to use class with linkage error %s", 195 fieldname, value, classname), 196 e); 197 } 198 199 apply(clazz, fieldname, value); 200 } 201 202 public void apply(Class<?> clazz, String fieldname, String value) { 203 assert clazz != null; 204 assert fieldname != null; 205 assert value != null; 206 207 Field field; 208 try { 209 field = clazz.getDeclaredField(PREFIX + fieldname); 210 } catch (SecurityException e) { 211 throw new ConfigException( 212 "Configuration option " + clazz.getName() + "." + fieldname + " is inaccessible"); 213 } catch (NoSuchFieldException e) { 214 throw new ConfigException( 215 "Unknown configuration option " + clazz.getName() + "." + fieldname); 216 } 217 218 apply(field, value); 219 } 220 221 private void apply(Field field, String unparsed) { 222 assert field != null; 223 assert unparsed != null; 224 225 Object value; // typed version of value 226 Class<?> type = field.getType(); 227 228 if (type.equals(Boolean.TYPE)) { 229 if (unparsed.equals("1") || unparsed.equalsIgnoreCase("true")) { 230 value = Boolean.TRUE; 231 } else if (unparsed.equals("0") || unparsed.equalsIgnoreCase("false")) { 232 value = Boolean.FALSE; 233 } else { 234 throw new ConfigException( 235 "Badly formatted boolean argument " 236 + unparsed 237 + " for configuration option " 238 + field.getName()); 239 } 240 } else if (type.equals(Integer.TYPE)) { 241 try { 242 // decode instead of valueOf to handle "0x" and other styles 243 value = Integer.decode(unparsed); 244 } catch (NumberFormatException e) { 245 throw new ConfigException( 246 "Badly formatted argument " 247 + unparsed 248 + " for configuration option " 249 + field.getName()); 250 } 251 } else if (type.equals(Long.TYPE)) { 252 try { 253 // decode instead of valueOf to handle "0x" and other styles 254 value = Long.decode(unparsed); 255 } catch (NumberFormatException e) { 256 throw new ConfigException( 257 "Badly formatted argument " 258 + unparsed 259 + " for configuration option " 260 + field.getName()); 261 } 262 } else if (type.equals(Float.TYPE)) { 263 try { 264 value = Float.valueOf(unparsed); 265 } catch (NumberFormatException e) { 266 throw new ConfigException( 267 "Badly formatted argument " 268 + unparsed 269 + " for configuration option " 270 + field.getName()); 271 } 272 } else if (type.equals(Double.TYPE)) { 273 // assert Double.class == Double.TYPE; 274 try { 275 value = Double.valueOf(unparsed); 276 } catch (NumberFormatException e) { 277 throw new ConfigException( 278 "Badly formatted argument " 279 + unparsed 280 + " for configuration option " 281 + field.getName()); 282 } 283 } else if (type.equals(STRING_CLASS)) { 284 value = unparsed; 285 if ((unparsed.startsWith("\"") && unparsed.endsWith("\"")) 286 || (unparsed.startsWith("'") && unparsed.endsWith("'"))) { 287 value = unparsed.substring(1, unparsed.length() - 1); 288 } 289 value = ((String) value).intern(); 290 // System.out.printf("setting %s to '%s'%n", field, value); 291 } else if ((type.getSuperclass() != null) 292 && type.getSuperclass().getName().equals("java.lang.Enum")) { 293 try { 294 java.lang.reflect.Method valueOf = 295 type.getDeclaredMethod("valueOf", new Class<?>[] {STRING_CLASS}); 296 if (valueOf == null) { 297 // Can't happen, so RuntimeException instead of ConfigException 298 throw new RuntimeException("Didn't find valueOf in " + type); 299 } 300 try { 301 @SuppressWarnings("nullness") // static method, so null first arg is OK: valueOf() 302 Object tmp_value = valueOf.invoke(null, unparsed); 303 value = tmp_value; 304 } catch (IllegalArgumentException e) { 305 throw new ConfigException( 306 "Badly formatted argument " 307 + unparsed 308 + " for configuration option " 309 + field.getName() 310 + ": " 311 + e.getMessage()); 312 } 313 } catch (NoSuchMethodException e) { 314 // Can't happen, so RuntimeException instead of ConfigException 315 throw new RuntimeException(e); 316 } catch (IllegalAccessException e) { 317 // Can't happen, so RuntimeException instead of ConfigException 318 throw new RuntimeException(e); 319 } catch (java.lang.reflect.InvocationTargetException e) { 320 // Can't happen, so RuntimeException instead of ConfigException 321 throw new RuntimeException(e); 322 } 323 } else { 324 throw new ConfigException( 325 "Internal error: Unsupported type " 326 + type.getName() 327 + " for configuration option " 328 + field.toString()); 329 } 330 331 try { 332 setStaticField(field, value); 333 } catch (IllegalAccessException e) { 334 throw new ConfigException("Inaccessible configuration option " + field.toString()); 335 } 336 337 // record the application 338 String classname = field.getDeclaringClass().getName(); 339 String fieldname = field.getName(); 340 assert fieldname.startsWith(PREFIX); // remove the prefix 341 fieldname = fieldname.substring(PREFIX.length()); 342 addRecord(classname, fieldname, unparsed); 343 } 344 345 private void addRecord(String classname, String fieldname, String unparsed) { 346 assert !fieldname.startsWith(PREFIX); // must not have prefix 347 String record = classname + "." + fieldname + " = " + unparsed; 348 statements.add(record); 349 } 350 351 /** 352 * Set a static field to the given value. 353 * 354 * @param field a field; must be static 355 * @param value the value to set the field to 356 * @throws IllegalAccessException if {@code field} is enforcing Java language access control and 357 * the underlying field is either inaccessible or final. 358 */ 359 // This method exists to reduce the scope of the warning suppression. 360 @SuppressWarnings({ 361 "nullness:argument", // field is static, so object may be null 362 "interning:argument" // interning is not necessary for how this method is used 363 }) 364 private static void setStaticField(Field field, @Nullable Object value) 365 throws IllegalAccessException { 366 field.set(null, value); 367 } 368}