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 // Skip blank and comment lines 128 if (line.length() == 0) { 129 continue; 130 } 131 if (line.charAt(0) == '#') { 132 continue; 133 } 134 apply(line); 135 } 136 } 137 138 /** 139 * Apply the setting in the given InputStream. 140 * 141 * @param line the command to set confiuration 142 */ 143 public void apply(String line) { 144 assert line != null; 145 146 int eq = line.indexOf('='); 147 if (eq <= 0) { 148 throw new ConfigException("Error, configuration setting must contain \"=\": " + line); 149 } 150 151 String name = line.substring(0, eq).trim(); 152 String value = line.substring(eq + 1).trim(); 153 154 apply(name, value); 155 } 156 157 /** 158 * Set the given setting to the given value. 159 * 160 * @param name the setting to modify 161 * @param value the setting's new value 162 */ 163 public void apply(String name, String value) { 164 assert name != null; 165 assert value != null; 166 167 int dot = name.lastIndexOf('.'); 168 if (dot == -1) { 169 throw new daikon.Daikon.UserError( 170 "Configuration option name must contain a period (.): " + name); 171 } 172 173 @SuppressWarnings("signature") // substring operation 174 @ClassGetName String classname = name.substring(0, dot); 175 String fieldname = name.substring(dot + 1); 176 177 apply(classname, fieldname, value); 178 } 179 180 public void apply(@ClassGetName String classname, String fieldname, String value) { 181 assert classname != null; 182 assert fieldname != null; 183 assert value != null; 184 185 // Use ReflectionPlume version of class.forName so that we can refer to 186 // inner classes using '.' as well as '$' 187 Class<?> clazz; 188 try { 189 clazz = ReflectionPlume.classForName(classname); 190 } catch (ClassNotFoundException e) { 191 throw new ConfigException( 192 String.format( 193 "Configuration option %s=%s attempts to use nonexistent class %s", 194 fieldname, value, classname), 195 e); 196 } catch (LinkageError e) { 197 throw new ConfigException( 198 String.format( 199 "Configuration option %s=%s attempts to use class with linkage error %s", 200 fieldname, value, classname), 201 e); 202 } 203 204 apply(clazz, fieldname, value); 205 } 206 207 public void apply(Class<?> clazz, String fieldname, String value) { 208 assert clazz != null; 209 assert fieldname != null; 210 assert value != null; 211 212 Field field; 213 try { 214 field = clazz.getDeclaredField(PREFIX + fieldname); 215 } catch (SecurityException e) { 216 throw new ConfigException( 217 "Configuration option " + clazz.getName() + "." + fieldname + " is inaccessible"); 218 } catch (NoSuchFieldException e) { 219 throw new ConfigException( 220 "Unknown configuration option " + clazz.getName() + "." + fieldname); 221 } 222 223 apply(field, value); 224 } 225 226 private void apply(Field field, String unparsed) { 227 assert field != null; 228 assert unparsed != null; 229 230 Object value; // typed version of value 231 Class<?> type = field.getType(); 232 233 if (type.equals(Boolean.TYPE)) { 234 if (unparsed.equals("1") || unparsed.equalsIgnoreCase("true")) { 235 value = Boolean.TRUE; 236 } else if (unparsed.equals("0") || unparsed.equalsIgnoreCase("false")) { 237 value = Boolean.FALSE; 238 } else { 239 throw new ConfigException( 240 "Badly formatted boolean argument " 241 + unparsed 242 + " for configuration option " 243 + field.getName()); 244 } 245 } else if (type.equals(Integer.TYPE)) { 246 try { 247 // decode instead of valueOf to handle "0x" and other styles 248 value = Integer.decode(unparsed); 249 } catch (NumberFormatException e) { 250 throw new ConfigException( 251 "Badly formatted argument " 252 + unparsed 253 + " for configuration option " 254 + field.getName()); 255 } 256 } else if (type.equals(Long.TYPE)) { 257 try { 258 // decode instead of valueOf to handle "0x" and other styles 259 value = Long.decode(unparsed); 260 } catch (NumberFormatException e) { 261 throw new ConfigException( 262 "Badly formatted argument " 263 + unparsed 264 + " for configuration option " 265 + field.getName()); 266 } 267 } else if (type.equals(Float.TYPE)) { 268 try { 269 value = Float.valueOf(unparsed); 270 } catch (NumberFormatException e) { 271 throw new ConfigException( 272 "Badly formatted argument " 273 + unparsed 274 + " for configuration option " 275 + field.getName()); 276 } 277 } else if (type.equals(Double.TYPE)) { 278 // assert Double.class == Double.TYPE; 279 try { 280 value = Double.valueOf(unparsed); 281 } catch (NumberFormatException e) { 282 throw new ConfigException( 283 "Badly formatted argument " 284 + unparsed 285 + " for configuration option " 286 + field.getName()); 287 } 288 } else if (type.equals(STRING_CLASS)) { 289 value = unparsed; 290 if ((unparsed.startsWith("\"") && unparsed.endsWith("\"")) 291 || (unparsed.startsWith("'") && unparsed.endsWith("'"))) { 292 value = unparsed.substring(1, unparsed.length() - 1); 293 } 294 value = ((String) value).intern(); 295 // System.out.printf("setting %s to '%s'%n", field, value); 296 } else if ((type.getSuperclass() != null) 297 && type.getSuperclass().getName().equals("java.lang.Enum")) { 298 try { 299 java.lang.reflect.Method valueOf = 300 type.getDeclaredMethod("valueOf", new Class<?>[] {STRING_CLASS}); 301 if (valueOf == null) { 302 // Can't happen, so RuntimeException instead of ConfigException 303 throw new RuntimeException("Didn't find valueOf in " + type); 304 } 305 try { 306 @SuppressWarnings("nullness") // static method, so null first arg is OK: valueOf() 307 Object tmp_value = valueOf.invoke(null, unparsed); 308 value = tmp_value; 309 } catch (IllegalArgumentException e) { 310 throw new ConfigException( 311 "Badly formatted argument " 312 + unparsed 313 + " for configuration option " 314 + field.getName() 315 + ": " 316 + e.getMessage()); 317 } 318 } catch (NoSuchMethodException e) { 319 // Can't happen, so RuntimeException instead of ConfigException 320 throw new RuntimeException(e); 321 } catch (IllegalAccessException e) { 322 // Can't happen, so RuntimeException instead of ConfigException 323 throw new RuntimeException(e); 324 } catch (java.lang.reflect.InvocationTargetException e) { 325 // Can't happen, so RuntimeException instead of ConfigException 326 throw new RuntimeException(e); 327 } 328 } else { 329 throw new ConfigException( 330 "Internal error: Unsupported type " 331 + type.getName() 332 + " for configuration option " 333 + field.toString()); 334 } 335 336 try { 337 setStaticField(field, value); 338 } catch (IllegalAccessException e) { 339 throw new ConfigException("Inaccessible configuration option " + field.toString()); 340 } 341 342 // record the application 343 String classname = field.getDeclaringClass().getName(); 344 String fieldname = field.getName(); 345 assert fieldname.startsWith(PREFIX); // remove the prefix 346 fieldname = fieldname.substring(PREFIX.length()); 347 addRecord(classname, fieldname, unparsed); 348 } 349 350 private void addRecord(String classname, String fieldname, String unparsed) { 351 assert !fieldname.startsWith(PREFIX); // must not have prefix 352 String record = classname + "." + fieldname + " = " + unparsed; 353 statements.add(record); 354 } 355 356 /** 357 * Set a static field to the given value. 358 * 359 * @param field a field; must be static 360 * @param value the value to set the field to 361 * @throws IllegalAccessException if {@code field} is enforcing Java language access control and 362 * the underlying field is either inaccessible or final 363 */ 364 // This method exists to reduce the scope of the warning suppression. 365 @SuppressWarnings({ 366 "nullness:argument", // field is static, so object may be null 367 "interning:argument" // interning is not necessary for how this method is used 368 }) 369 private static void setStaticField(Field field, @Nullable Object value) 370 throws IllegalAccessException { 371 field.set(null, value); 372 } 373}