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}