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}