/* * This file is part of the X10 project (http://x10-lang.org). * * This file is licensed to You under the Eclipse Public License (EPL); * You may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.opensource.org/licenses/eclipse-1.0.php * * (C) Copyright IBM Corporation 2006-2010. */ package x10.config; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Iterator; import java.util.Properties; /** * Base class for the X10 configuration classes. * Contains static methods to load the declared fields of the configuration * object from a file. * * The configuration classes provide mechanisms that allow the user to change * the default configuration values in a simple and systematic manner. * * A typical use of the Configuration is to just read the respective * fields, i.e.: * * <code> * setMinimumPoolSize(config.MINIMUM_POOL_SIZE); * setKeepAliveTime(config.KEEP_ALIVE_TIME); * </code> * * <br> * Clients are expected to add public fields to the configuration * class and initialize them to the respective default values, i.e.: * * <code> * public int MINIMUM_POOL_SIZE = 2; * </code> * * Note that these fields MUST NOT be final. The reason is that * these are just the default values, the Configuration classes * may initialize them to different values based on a configuration * file provided by the user. * * <br> * Users can change these values by providing the name of a configuration * file in the system properties under "x10.configuration". By passing * the option <tt>-Dx10.configuration=myconfig</tt> the file '<tt>myconfig</tt>' * will be read by the constructor of the configuration class. The file is * expected to contain a mapping of field keys to values. The * constructor will then set the respective fields to those * values (using reflection). The format of the configuration file is * described in the JDK documentation for the method * "{@link java.util.Properties.load(InputStream)}". * * <br> * Note that clients should NEVER set the fields (even though they * are public and non-final); after the constructor runs the * value of the Configuration fields should never change. * * <br> * Each configuration (public) field in configuration classes can be * a String, any primitive type, or an array of those. If a configuration * field ARRAY_OPTION is of an array type, the value for ARRAY_OPTION should * be an integer, which declares the size of the array, and its element at * index INDEX can be set using the option name ARRAY_OPTION[INDEX]. * * <br> * To provide a description for option field OPTION, define a private static * final String field OPTION_desc. * * @author Christian Grothoff * @author igor */ public abstract class Configuration { /** * Set a given field or component in a given class to the given value. * @param config the configuration object * @param cls the configuration class * @param key the field key * @param val the given value * @throws OptionError if the argument is invalid * @throws ConfigurationError if there was a problem processing the argument */ protected static void set(Object config, Class<? extends Configuration> cls, String key, String val) throws ConfigurationError, OptionError { assert (key != null); int idx = 0; String fld = null; try { if (key.indexOf('[') > 0) { idx = Integer.parseInt(key.substring(key.indexOf('[')+1,key.indexOf(']'))); fld = key.substring(key.indexOf('.')+1); key = key.substring(0, key.indexOf('[')); } Field f = cls.getField(key); Class<?> t = f.getType(); Object o = config; // TODO: implement arrays as described above // FIXME: do we need support for Object arrays? if (fld != null) { if (t.isArray()) { if (!t.getComponentType().isPrimitive()) { o = Array.get(f.get(config), idx); if (o == null) Array.set(f.get(config), idx, o = t.getComponentType().newInstance()); f = o.getClass().getField(fld); t = f.getType(); } } else throw new OptionError(key + " is not an array"); } if (val == null) { if (t == Boolean.TYPE) val = "true"; else throw new OptionError("Parameter "+key+" expects a value"); } if (t == String.class) { f.set(o, val); } else if (t == Integer.TYPE) { f.setInt(o, Integer.parseInt(val)); } else if (t == Float.TYPE) { f.setFloat(o, Float.parseFloat(val)); } else if (t == Double.TYPE) { f.setDouble(o, Double.parseDouble(val)); } else if (t == Long.TYPE) { f.setLong(o, Long.parseLong(val)); } else if (t == Short.TYPE) { f.setShort(o, Short.parseShort(val)); } else if (t == Byte.TYPE) { f.setByte(o, Byte.parseByte(val)); } else if (t == Character.TYPE) { if (val.length() != 1) throw new OptionError("Parameter "+key+ " expects exactly one character; got '"+val+"'"); f.setChar(o, val.charAt(0)); } else if (t == Boolean.TYPE) { if (val.equalsIgnoreCase("true")) { f.setBoolean(o, true); } else if (val.equalsIgnoreCase("false")) { f.setBoolean(o, false); } else throw new OptionError("Parameter "+key+ " expects a boolean, not '"+val+"'"); } } catch (NoSuchFieldException nsfe) { throw new OptionError("Parameter "+key+" not found"); } catch (InstantiationException ie) { System.err.println("Failed to create object for " + key); throw new ConfigurationError(ie); } catch (IllegalAccessException iae) { System.err.println("Wrong permissions for field " + key + ": " + iae); throw new ConfigurationError(iae); } catch (NumberFormatException z) { throw new OptionError("Parameter "+key+ " expects a number, not '" + val + "'"); } } /** * Get a given field or component in a given class. * @param config the configuration object * @param cls the configuration class * @param key the field key * @return the value of the given component * @throws OptionError if the argument is invalid * @throws ConfigurationError if there was a problem processing the argument */ public static Object get(Object config, Class<? extends Configuration> cls, String key) throws ConfigurationError, OptionError { assert (key != null); int idx = 0; String fld = null; try { if (key.indexOf('[') > 0) { idx = Integer.parseInt(key.substring(key.indexOf('[')+1,key.indexOf(']'))); fld = key.substring(key.indexOf('.')+1); key = key.substring(0, key.indexOf('[')); } Field f = cls.getField(key); Class<?> t = f.getType(); Object o = config; // TODO: implement arrays as described above // FIXME: do we need support for Object arrays? if (fld != null) { if (t.isArray()) { if (!t.getComponentType().isPrimitive()) { o = Array.get(f.get(config), idx); f = o.getClass().getField(fld); t = f.getType(); } } else throw new OptionError(key + " is not an array"); } return f.get(o); } catch (NoSuchFieldException nsfe) { throw new OptionError("Parameter "+key+" not found"); } catch (IllegalAccessException iae) { System.err.println("Wrong permissions for field " + key + ": " + iae); throw new ConfigurationError(iae); } } /** * Obtain the name of the configuration resource used for the current * configuration. * @return null if no resource is given and default values should be used */ public static String getConfigurationResource() { return System.getProperty("x10.configuration"); } /** * Read the configuration from a given resource (if specified) and * initialize the global fields in a given class. * @param config the configuration object * @param cls the configuration class * @param cfg the configuration resource name * * @throws ConfigurationError if unable to process the resource */ public static void readConfiguration(Object config, Class<? extends Configuration> cls, String cfg) throws ConfigurationError { if (cfg == null) return; try { Properties props = new Properties(); InputStream is = cls.getClassLoader().getResourceAsStream(cfg); if (is == null) throw new ConfigurationError("Configuration "+cfg+" not found"); byte[] data = new byte[is.available()]; if (data.length != is.read(data)) throw new ConfigurationError("Cannot read entire file"); String s = new String(data).replace('\\','/'); props.load(new ByteArrayInputStream(s.getBytes())); Iterator<Object> i = props.keySet().iterator(); while (i.hasNext()) { String key = (String) i.next(); String val = props.getProperty(key); try { set(config, cls, key, val); } catch (OptionError e) { System.err.println(e.getMessage()+", ignoring."); } } // end of 'for each configuration directive' } catch (IOException e) { throw new ConfigurationError(e); } } /** * Parse one command line argument into the given configuration class. * This allows the user to specify options also on the command line (in * addition to the configuration file and the defaults). * The argument has to be of the form <code>-FIELD_KEY=value</code>. * @param config the configuration object * @param cls the configuration class * @param arg the current argument * * @throws OptionError if the argument is invalid * @throws ConfigurationError if there was a problem processing the argument */ protected static void parseArgument(Object config, Class<? extends Configuration> cls, String arg) throws OptionError, ConfigurationError { if (arg.length() < 1 || arg.charAt(0) != '-') throw new OptionError("Invalid argument: '"+arg+"'"); int eq = arg.indexOf('='); String optionName; String optionValue = null; if (eq == -1) { optionName = arg.substring(1); } else { optionName = arg.substring(1, eq); optionValue = arg.substring(eq+1); } set(config, cls, optionName, optionValue); } /** * Return a human-readable string representation of a given type. */ private static String typeToString(Class<?> t) { if (t.isPrimitive()) return t.toString(); if (t.isArray()) return typeToString(t.getComponentType())+"[]"; if (t.getPackage() == Package.getPackage("java.lang")) return t.getName().substring("java.lang.".length()); return t.getName(); } /** * Return an array of (option,type,description,default_value) tuples for * the given configuration class. * The options are public static non-final fields, and the descriptions * are private final String fields named OPTION_desc, where OPTION is the * corresponding option field. * TODO: Sort the options? * @param config the configuration object * @param cls the configuration class * @return array of two-element String arrays */ protected static String[][] options(Object config, Class<? extends Configuration> cls) { Field[] flds = cls.getFields(); int num = 0; for (int i = 0; i < flds.length; i++) { Field f = flds[i]; int m = f.getModifiers(); // f is guaranteed to be public if (!Modifier.isStatic(m) && !Modifier.isFinal(m)) num++; } String[][] opts = new String[num][]; int j = 0; for (int i = 0; i < flds.length; i++) { Field f = flds[i]; int m = f.getModifiers(); // f is guaranteed to be public if (Modifier.isStatic(m) || Modifier.isFinal(m)) continue; Class<?> t = f.getType(); String type = typeToString(t); String desc = ""; Object v = null; try { Field d = cls.getDeclaredField(f.getName()+"_desc"); int dm = d.getModifiers(); if (!Modifier.isPrivate(dm) || !Modifier.isStatic(dm) || !Modifier.isFinal(dm)) throw new NoSuchFieldException(); boolean s = d.isAccessible(); d.setAccessible(true); desc = (String) d.get(null) + " "; d.setAccessible(s); } catch (NoSuchFieldException nsfe) { } catch (IllegalAccessException iae) { } try { v = f.get(config); } catch (IllegalAccessException iae) { } if (!t.isPrimitive() && v != null) v = "\"" + v + "\""; opts[j++] = new String[] { f.getName(), type, desc, ""+v }; } return opts; } /** * The error received when attempting to load the configuration from * the specified resource, or null if successful. */ public final ConfigurationError LOAD_ERROR; public Configuration(Class<? extends Configuration> cls) { this(cls, getConfigurationResource()); } public Configuration(Class<? extends Configuration> cls, String cfg) { ConfigurationError loadError = null; try { readConfiguration(this, cls, cfg); } catch (ConfigurationError err) { System.err.println("Failed to read configuration file " + cfg + ": " + err); System.err.println("Using defaults"); loadError = err; } LOAD_ERROR = loadError; } }