diff src/main/gov/nasa/jpf/Config.java @ 0:61d41facf527

initial v8 import (history reset)
author Peter Mehlitz <Peter.C.Mehlitz@nasa.gov>
date Fri, 23 Jan 2015 10:14:01 -0800
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/gov/nasa/jpf/Config.java	Fri Jan 23 10:14:01 2015 -0800
@@ -0,0 +1,2468 @@
+/*
+ * Copyright (C) 2014, United States Government, as represented by the
+ * Administrator of the National Aeronautics and Space Administration.
+ * All rights reserved.
+ *
+ * The Java Pathfinder core (jpf-core) platform is licensed under the
+ * Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ * 
+ *        http://www.apache.org/licenses/LICENSE-2.0. 
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and 
+ * limitations under the License.
+ */
+package gov.nasa.jpf;
+
+
+import gov.nasa.jpf.util.FileUtils;
+import gov.nasa.jpf.util.JPFSiteUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+
+/**
+ * class that encapsulates property-based JPF configuration. This is mainly an
+ * associative array with various typed accessors, and a structured
+ * initialization process. This implementation has the design constraint that it
+ * does not promote symbolic information to concrete types, which means that
+ * frequently accessed data should be promoted and cached in client classes.
+ * This in turn means we assume the data is not going to change at runtime.
+ * Major motivation for this mechanism is to avoid 'Option' classes that have
+ * concrete type fields, and hence are structural bottlenecks, i.e. every
+ * parameterized user extension (Heuristics, Scheduler etc.) require to update
+ * this single class. Note that Config is also not thread safe with respect to
+ * retrieving exceptions that occurred during instantiation
+ *
+ * Another important caveat for both implementation and usage of Config is that
+ * it is supposed to be our master configuration mechanism, i.e. it is also used
+ * to configure other core services like logging. This means that Config
+ * initialization should not depend on these services. Initialization has to
+ * return at all times, recording potential problems for later handling. This is
+ * why we have to keep the Config data model and initialization fairly simple
+ * and robust.
+ *
+ * Except of JPF and Config itself, all JPF classes are loaded by a
+ * Classloader that is constucted by Config (e.g. by collecting jars from
+ * known/configured locations), i.e. we SHOULD NOT rely on any 3rd party
+ * libraries within Config. The class should be as autarkical as possible.
+ *
+ *
+ * PROPERTY SOURCES
+ * ----------------
+ *
+ * (1) one site.properties - this file specifies the location of the jpf-core and
+ * installed extensions, like:
+ *
+ *     jpf-core = /Users/pcmehlitz/projects/jpf/jpf-core
+ *     ...
+ *     jpf-numeric = /Users/pcmehlitz/projects/jpf/jpf-numeric
+ *     ...
+ *     extensions = ${jpf-core}
+ *
+ * Each key/directory that is in site.properties is used to locate a corresponding
+ * project property (jpf.properties) file
+ *
+ * (2) any number of jpf.properties project properties files - each directory
+ * entry in the 'extensions' list is checked for a jpf.properties file, which 
+ * is automatically loaded if found. Project properties mostly contain path 
+ * settings that are used to initialize class loading by the host VM and JPF
+ *
+ * (3) one *.jpf application properties - this specifies all the settings for a
+ * specific JPF run, esp. listener and target/target.args.
+ * app properties can be specified as the sole JPF argument, i.e. instead of
+ * a SUT classname
+ *     ..
+ *     target = x.Y.MySystemUnderTest
+ *     target.args = one,two
+ *     ..
+ *     listener = z.MyListener
+ *
+ * (4) commandline properties - all start with '+', they can override all other props
+ *
+ *
+ * LOOKUP ORDER
+ * ------------
+ *                       property lookup
+ *   property type   :      spec             :  default
+ *   ----------------:-----------------------:----------
+ * |  site           :   +site               : "${user.home}/[.]jpf/site.properties"
+ * |                 :                       :
+ * |  project        :  'extensions' value   : set in site.properties
+ * |                 :                       :
+ * |  app            :   +app                : -
+ * |                 :                       :
+ * v  cmdline        :   +<key>=<val>        : -
+ *
+ * * if there is an explicit spec and the pathname does not exist, throw a
+ * JPFConfigException
+ *
+ * * if the system properties cannot be found, throw a JPFConfigException
+ *
+ *
+ * <2do> need to make NumberFormatException handling consistent - should always
+ * throw an JPFConfigException, not silently returning the default value
+ *
+ */
+
+
+@SuppressWarnings("serial")
+public class Config extends Properties {
+
+  static final char   KEY_PREFIX = '@';
+  public static final String REQUIRES_KEY = "@requires";
+  public static final String INCLUDE_KEY = "@include";
+  public static final String INCLUDE_UNLESS_KEY = "@include_unless";
+  public static final String INCLUDE_IF_KEY = "@include_if";
+  public static final String USING_KEY = "@using";
+
+  static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+  public static final String LIST_SEPARATOR = ",";
+  public static final String PATH_SEPARATOR = ","; // the default for automatic appends
+
+  public static final Class<?>[] CONFIG_ARGTYPES = { Config.class };  
+  public static final Class<?>[] NO_ARGTYPES = new Class<?>[0];
+  public static final Object[] NO_ARGS = new Object[0];
+
+  public static final String TRUE = "true";
+  public static final String FALSE = "false";
+  
+  static final String MAX = "MAX";
+
+  static final String IGNORE_VALUE = "-";
+
+  // maximum number of processes for distributed applications
+  public static int MAX_NUM_PRC = 16;
+
+  // do we want to log the config init
+  public static boolean log = false;
+
+  // bad - a control exception
+  static class MissingRequiredKeyException extends RuntimeException {
+    MissingRequiredKeyException(String details){
+      super(details);
+    }
+  }
+
+  // it seems bad design to keep ClassLoader management in a glorified Properties object,
+  // but a lot of what Config does is to resolve configured types, for which we need
+  // control over the loader that is used for resolution
+  ClassLoader loader = Config.class.getClassLoader();
+    
+  // where did we initialize from
+  ArrayList<Object> sources = new ArrayList<Object>();
+  
+  ArrayList<ConfigChangeListener> changeListeners;
+  
+  // Properties are simple Hashmaps, but we want to maintain the order of entries
+  LinkedList<String> entrySequence = new LinkedList<String>();
+
+  // an [optional] hashmap to keep objects we want to be singletons
+  HashMap<String,Object> singletons;
+  
+  public final Object[] CONFIG_ARGS = { this };
+
+  // the original command line args that were passed into the constructor
+  String[] args;
+  
+  // non-property/option command line args (starting from the first arg that is not prepened by '-','+')
+  String[] freeArgs;
+
+  /**
+   * the standard Config constructor that processes the whole properties stack
+   */
+  public Config (String[] cmdLineArgs)  {
+    args = cmdLineArgs;
+    String[] a = cmdLineArgs.clone(); // we might nullify some of them
+
+    // we need the app properties (*.jpf) pathname upfront because it might define 'site'
+    String appProperties = getAppPropertiesLocation(a);
+
+    //--- the site properties
+    String siteProperties = getSitePropertiesLocation( a, appProperties);
+    if (siteProperties != null){
+      loadProperties( siteProperties);
+    }
+
+    //--- get the project properties from current dir + site configured extensions
+    loadProjectProperties();
+
+    //--- the application properties
+    if (appProperties != null){
+      loadProperties( appProperties);
+    }
+
+    //--- at last, the (rest of the) command line properties
+    loadArgs(a);
+
+    // note that global path collection now happens from initClassLoader(), to
+    // accommodate for deferred project initialization when explicitly setting Config entries
+
+    //printEntries();
+  }
+
+  private Config() {
+    // just interal, for reloading
+  }
+  
+  /**
+   * single source Config constructor (does not process stack)
+   * @param fileName - single properties filename to initialize from 
+   */
+  public Config (String fileName){
+    loadProperties(fileName);
+  }
+
+  public Config (Reader in){
+    try {
+      load(in);
+    } catch (IOException iox){
+      exception("error reading data: " + iox);
+    }
+  }
+  
+  public static void enableLogging (boolean enableLogging){
+    log = enableLogging;
+  }
+
+  public void log (String msg){
+    if (log){ // very simplisitc, but we might do more in the future
+      System.out.println(msg);
+    }
+  }
+
+
+  String getAppPropertiesLocation(String[] args){
+    String path = null;
+
+    path = getPathArg(args, "app");
+    if (path == null){
+      // see if the first free arg is a *.jpf
+      path = getAppArg(args);
+    }
+    
+    put("jpf.app", path);
+
+    return path;
+  }
+
+  String getSitePropertiesLocation(String[] args, String appPropPath){
+    String path = getPathArg(args, "site");
+
+    if (path == null){
+      // look into the app properties
+      // NOTE: we might want to drop this in the future because it constitutes
+      // a cyclic properties file dependency
+      if (appPropPath != null){
+        path = JPFSiteUtils.getMatchFromFile(appPropPath,"site");
+      }
+
+      if (path == null) {
+        File siteProps = JPFSiteUtils.getStandardSiteProperties();
+        if (siteProps != null){
+          path = siteProps.getAbsolutePath();
+        }
+      }
+    }
+    
+    put("jpf.site", path);
+
+    return path;
+  }
+
+
+  // watch out - this does not reset the computed paths!
+  public Config reload() {
+    log("reloading config");
+
+    // just reload all our sources
+    Config newConfig = new Config();
+    for (Object src : sources){
+      if (src instanceof File) {
+        newConfig.loadProperties(((File)src).getPath());
+      } else if (src instanceof URL) {
+        newConfig.loadProperties((URL)src);
+      } else {
+        log("don't know how to reload: " + src);
+      }
+    }
+
+    // now reload command line args on top of that
+    newConfig.loadArgs(args);
+    newConfig.args = args;
+    
+    return newConfig;
+  }
+
+  public String[] getArgs() {
+    return args;
+  }
+
+  /*
+   * note that matching args are expanded and stored here, to avoid any
+   * discrepancy with value expansions (which are order-dependent)
+   */
+  protected String getPathArg (String[] args, String key){
+    int keyLen = key.length();
+
+    for (int i=0; i<args.length; i++){
+      String a = args[i];
+      if (a != null){
+        int len = a.length();
+        if (len > keyLen + 2){
+          if (a.charAt(0) == '+' && a.charAt(keyLen+1) == '='){
+            if (a.substring(1, keyLen+1).equals(key)){
+              String val = expandString(key, a.substring(keyLen+2));
+              args[i] = null; // processed
+              return val;
+            }
+          }
+        }
+      }
+    }
+
+    return null;
+  }
+
+  /*
+   * if the first freeArg is a JPF application property filename, use this
+   * as targetArg and set the "jpf.app" property accordingly
+   */
+  protected String getAppArg (String[] args){
+
+    for (int i=0; i<args.length; i++){
+      String a = args[i];
+      if (a != null && a.length() > 0){
+        switch (a.charAt(0)) {
+          case '+': continue;
+          case '-': continue;
+          default:
+            if (a.endsWith(".jpf")){
+              String val = expandString("jpf.app", a);
+              args[i] = null; // processed
+              return val;
+            }
+        }
+      }
+    }
+
+    return null;
+  }
+
+
+  protected void loadProperties (URL url){
+    log("loading defaults from: " + url);
+
+    InputStream is = null;
+    try {
+      is = url.openStream();
+      load(is);
+      sources.add(url);
+    } catch (IOException iox){
+      log("error in input stream for: " + url + " : " + iox.getMessage());
+    } finally {
+      if (is != null){
+        try {
+          is.close();
+        } catch (IOException iox1){
+          log("error closing input stream for: " + url + " : " + iox1.getMessage());
+        }
+      }
+    }
+  }
+
+  protected void setConfigPathProperties (String fileName){
+    put("config", fileName);
+    int i = fileName.lastIndexOf(File.separatorChar);
+    if (i>=0){
+      put("config_path", fileName.substring(0,i));
+    } else {
+      put("config_path", ".");
+    }
+  }
+
+
+  protected boolean loadProperties (String fileName) {
+    if (fileName != null && fileName.length() > 0) {
+      FileInputStream is = null;
+      try {
+        File f = new File(fileName);
+        if (f.isFile()) {
+          log("loading property file: " + fileName);
+
+          setConfigPathProperties(f.getAbsolutePath());
+          sources.add(f);
+          is = new FileInputStream(f);
+          load(is);
+          return true;
+        } else {
+          throw exception("property file does not exist: " + f.getAbsolutePath());
+        }
+      } catch (MissingRequiredKeyException rkx){
+        // Hmpff - control exception
+        log("missing required key: " + rkx.getMessage() + ", skipping: " + fileName);
+      } catch (IOException iex) {
+        throw exception("error reading properties: " + fileName);
+      } finally {
+        if (is != null){
+          try {
+            is.close();
+          } catch (IOException iox1){
+            log("error closing input stream for file: " + fileName);
+          }
+        }
+      }
+    }
+
+    return false;
+  }
+
+
+  /**
+   * this holds the policy defining in which order we process directories
+   * containing JPF projects (i.e. jpf.properties files)
+   */
+  protected void loadProjectProperties () {
+    // this is the list of directories holding jpf.properties files that
+    // have to be processed in order of entry (increasing priority)
+    LinkedList<File> jpfDirs = new LinkedList<File>();
+
+    // deduce the JPF projects in use (at least jpf-core) from the CL which
+    // defined this class
+    addJPFdirsFromClasspath(jpfDirs);
+
+    // add all the site configured extension dirs (but NOT jpf-core)
+    addJPFdirsFromSiteExtensions(jpfDirs);
+
+    // add the current dir, which has highest priority (this might bump up
+    // a previous entry by reodering it - which includes jpf-core)
+    addCurrentJPFdir(jpfDirs);
+
+    // now load all the jpf.property files we found in these dirs
+    // (later loads can override previous settings)
+    for (File dir : jpfDirs){
+      loadProperties(new File(dir,"jpf.properties").getAbsolutePath());
+    }
+  }
+
+  protected void appendPath (String pathKey, String key, String configPath){
+    String[] paths = getStringArray(key);
+    if (paths != null){
+      for (String e : paths) {
+        if (!e.startsWith("${") || !e.startsWith(File.separator)) {
+          e = configPath + File.separatorChar + e;
+        }
+        append(pathKey, e, PATH_SEPARATOR);
+      }
+    }
+  }
+
+  protected void addJPFdirs (List<File> jpfDirs, File dir){
+    while (dir != null) {
+      File jpfProp = new File(dir, "jpf.properties");
+      if (jpfProp.isFile()) {
+        registerJPFdir(jpfDirs, dir);
+        return;       // we probably don't want recursion here
+      }
+      dir = getParentFile(dir);
+    }
+  }
+
+  /**
+   * add the current dir to the list of JPF components.
+   * Note: this includes the core, so that we maintain the general
+   * principle that the enclosing project takes precedence (imagine the opposite:
+   * if we want to test a certain feature that is overridden by another extension
+   * we don't know about)
+   */
+  protected void addCurrentJPFdir(List<File> jpfDirs){
+    File dir = new File(System.getProperty("user.dir"));
+    while (dir != null) {
+      File jpfProp = new File(dir, "jpf.properties");
+      if (jpfProp.isFile()) {
+        registerJPFdir(jpfDirs, dir);
+        return;
+      }
+      dir = getParentFile(dir);
+    }
+  }
+
+  protected void addJPFdirsFromClasspath(List<File> jpfDirs) {
+    String cp = System.getProperty("java.class.path");
+    String[] cpEntries = cp.split(File.pathSeparator);
+
+    for (String p : cpEntries) {
+      File f = new File(p);
+      File dir = f.isFile() ? getParentFile(f) : f;
+
+      addJPFdirs(jpfDirs, dir);
+    }
+  }
+
+  protected void addJPFdirsFromSiteExtensions (List<File> jpfDirs){
+    String[] extensions = getCompactStringArray("extensions");
+    if (extensions != null){
+      for (String pn : extensions){
+        addJPFdirs( jpfDirs, new File(pn));
+      }
+    }
+  }
+
+  /**
+   * the obvious part is that it only adds to the list if the file is absent
+   * the not-so-obvious part is that it re-orders already present files
+   * to maintain the priority
+   */
+  protected boolean registerJPFdir(List<File> list, File dir){
+    try {
+      dir = dir.getCanonicalFile();
+
+      for (File e : list) {
+        if (e.equals(dir)) {
+          list.remove(e);
+          list.add(e);
+          return false;
+        }
+      }
+    } catch (IOException iox) {
+      throw new JPFConfigException("illegal path spec: " + dir);
+    }
+    
+    list.add(dir);
+    return true;
+  }
+
+  static File root = new File(File.separator);
+
+  protected File getParentFile(File f){
+    if (f == root){
+      return null;
+    } else {
+      File parent = f.getParentFile();
+      if (parent == null){
+        parent = new File(f.getAbsolutePath());
+
+        if (parent.getName().equals(root.getName())) {
+          return root;
+        } else {
+          return parent;
+        }
+      } else {
+        return parent;
+      }
+    }
+  }
+
+
+  /*
+   * argument syntax:
+   *          {'+'<key>['='<val>'] | '-'<driver-arg>} {<free-arg>}
+   *
+   * (1) null cmdLineArgs are ignored
+   * (2) all config cmdLineArgs start with '+'
+   * (3) if '=' is ommitted, a 'true' value is assumed
+   * (4) if <val> is ommitted, a 'null' value is assumed
+   * (5) no spaces around '='
+   * (6) all '-' driver-cmdLineArgs are ignored
+   */
+
+  protected void loadArgs (String[] cmdLineArgs) {
+
+    for (int i=0; i<cmdLineArgs.length; i++){
+      String a = cmdLineArgs[i];
+
+      if (a != null && a.length() > 0){
+        switch (a.charAt(0)){
+          case '+': // Config arg
+            processArg(a.substring(1));
+            break;
+
+          case '-': // driver arg, ignore
+            continue;
+
+          default:  // free (non property/option) cmdLineArgs to follow
+
+            int n = cmdLineArgs.length - i;
+            freeArgs = new String[n];
+            System.arraycopy(cmdLineArgs, i, freeArgs, 0, n);
+
+            return;
+        }
+      }
+    }
+  }
+
+
+  /*
+   * this does not include the '+' prefix, just the 
+   *     <key>[=[<value>]]
+   */
+  protected void processArg (String a) {
+
+    int idx = a.indexOf("=");
+
+    if (idx == 0){
+      throw new JPFConfigException("illegal option: " + a);
+    }
+
+    if (idx > 0) {
+      String key = a.substring(0, idx).trim();
+      String val = a.substring(idx + 1).trim();
+
+      if (val.length() == 0){
+        val = null;
+      }
+
+      setProperty(key, val);
+
+    } else {
+      setProperty(a.trim(), "true");
+    }
+
+  }
+
+
+  /**
+   * replace string constants with global static objects
+   */
+  protected String normalize (String v) {
+    if (v == null){
+      return null; // ? maybe TRUE - check default loading of "key" or "key="
+    }
+
+    // trim leading and trailing blanks (at least Java 1.4.2 does not take care of trailing blanks)
+    v = v.trim();
+    
+    // true/false
+    if ("true".equalsIgnoreCase(v)
+        || "yes".equalsIgnoreCase(v)
+        || "on".equalsIgnoreCase(v)) {
+      v = TRUE;
+    } else if ("false".equalsIgnoreCase(v)
+        || "no".equalsIgnoreCase(v)
+        || "off".equalsIgnoreCase(v)) {
+      v = FALSE;
+    }
+
+    // nil/null
+    if ("nil".equalsIgnoreCase(v) || "null".equalsIgnoreCase(v)){
+      v = null;
+    }
+    
+    return v;
+  }
+
+  
+  // our internal expander
+  // Note that we need to know the key this came from, to handle recursive expansion
+  protected String expandString (String key, String s) {
+    int i, j = 0;
+    if (s == null || s.length() == 0) {
+      return s;
+    }
+
+    while ((i = s.indexOf("${", j)) >= 0) {
+      if ((j = s.indexOf('}', i)) > 0) {
+        String k = s.substring(i + 2, j);
+        String v;
+        
+        if ((key != null) && key.equals(k)) {
+          // that's expanding itself -> use what is there
+          v = getProperty(key);
+        } else {
+          // refers to another key, which is already expanded, so this
+          // can't get recursive (we expand during entry storage)
+          v = getProperty(k);
+        }
+        
+        if (v == null) { // if we don't have it, fall back to system properties
+          v = System.getProperty(k);
+        }
+        
+        if (v != null) {
+          s = s.substring(0, i) + v + s.substring(j + 1, s.length());
+          j = i + v.length();
+        } else {
+          s = s.substring(0, i) + s.substring(j + 1, s.length());
+          j = i;
+        }
+      }
+    }
+
+    return s;    
+  }
+
+
+  boolean loadPropertiesRecursive (String fileName){
+    // save the current values of automatic properties
+    String curConfig = (String)get("config");
+    String curConfigPath = (String)get("config_path");
+
+    File propFile = new File(fileName);
+    if (!propFile.isAbsolute()){
+      propFile = new File(curConfigPath, fileName);
+    }
+    String absPath = propFile.getAbsolutePath();
+
+    if (!propFile.isFile()){
+      throw exception("property file does not exist: " + absPath);
+    }
+
+    boolean ret = loadProperties(absPath);
+
+    // restore the automatic properties
+    super.put("config", curConfig);
+    super.put("config_path", curConfigPath);
+
+    return ret;
+  }
+
+  void includePropertyFile(String key, String value){
+    value = expandString(key, value);
+    if (value != null && value.length() > 0){
+      loadPropertiesRecursive(value);
+    } else {
+      throw exception("@include pathname argument missing");
+    }
+  }
+
+  void includeCondPropertyFile(String key, String value, boolean keyPresent){
+    value = expandString(key, value);
+    if (value != null && value.length() > 0){
+      // check if it's a conditional "@include_unless/if = ?key?pathName"
+      if (value.charAt(0) == '?'){
+        int idx = value.indexOf('?', 1);
+        if (idx > 1){
+          String k = value.substring(1, idx);
+          if (containsKey(k) == keyPresent){
+            String v = value.substring(idx+1);
+            if (v.length() > 0){
+              loadPropertiesRecursive(v);
+            } else {
+              throw exception("@include_unless pathname argument missing (?<key>?<pathName>)");
+            }
+          }
+
+        } else {
+          throw exception("malformed @include_unless argument (?<key>?<pathName>), found: " + value);
+        }
+      } else {
+        throw exception("malformed @include_unless argument (?<key>?<pathName>), found: " + value);
+      }
+    } else {
+      throw exception("@include_unless missing ?<key>?<pathName> argument");
+    }
+  }
+
+
+  void includeProjectPropertyFile (String projectId){
+    String projectPath = getString(projectId);
+    if (projectPath != null){
+      File projectProps = new File(projectPath, "jpf.properties");
+      if (projectProps.isFile()){
+        loadPropertiesRecursive(projectProps.getAbsolutePath());
+
+      } else {
+        throw exception("project properties not found: " + projectProps.getAbsolutePath());
+      }
+
+    } else {
+      throw exception("unknown project id (check site.properties): " + projectId);
+    }
+  }
+
+  // we override this so that we can handle expansion for both key and value
+  // (value expansion can be recursive, i.e. refer to itself)
+  @Override
+  public Object put (Object keyObject, Object valueObject){
+
+    if (keyObject == null){
+      throw exception("no null keys allowed");
+    } else if (!(keyObject instanceof String)){
+      throw exception("only String keys allowed, got: " + keyObject);
+    }
+    if (valueObject != null && !(valueObject instanceof String)){
+      throw exception("only String or null values allowed, got: " + valueObject);
+    }
+
+    String key = (String)keyObject;
+    String value = (String)valueObject;
+
+    if (key.length() == 0){
+      throw exception("no empty keys allowed");
+    }
+
+    if (key.charAt(0) == KEY_PREFIX){
+      processPseudoProperty( key, value);
+      return null; // no value it replaces
+
+    } else {
+      // finally, a real key/value pair to add (or remove) - expand and store
+      String k = expandString(null, key);
+
+      if (!(value == null)) { // add or overwrite entry
+        String v = value;
+
+        if (k.charAt(k.length() - 1) == '+') { // the append hack
+          k = k.substring(0, k.length() - 1);
+          return append(k, v, null);
+
+        } else if (k.charAt(0) == '+') { // the prepend hack
+          k = k.substring(1);
+          return prepend(k, v, null);
+
+        } else { // normal value set
+          v = normalize(expandString(k, v));
+          if (v != null){
+            return setKey(k, v);
+          } else {
+            return removeKey(k);
+          }
+        }
+
+      } else { // setting a null value removes the entry
+        return removeKey(k);
+      }
+    }
+  }
+  
+  protected void processPseudoProperty( String key, String value){
+    if (REQUIRES_KEY.equals(key)) {
+      // shortcircuit loading of property files - used to enforce order
+      // of properties, e.g. to model dependencies
+      for (String reqKey : split(value)) {
+        if (!containsKey(reqKey)) {
+          throw new MissingRequiredKeyException(reqKey);
+        }
+      }
+
+    } else if (INCLUDE_KEY.equals(key)) {
+      includePropertyFile(key, value);
+      
+    } else if (INCLUDE_UNLESS_KEY.equals(key)) {
+      includeCondPropertyFile(key, value, false);
+      
+    } else if (INCLUDE_IF_KEY.equals(key)) {
+      includeCondPropertyFile(key, value, true);
+      
+    } else if (USING_KEY.equals(key)) {
+      // check if corresponding jpf.properties has already been loaded. If yes, skip
+      if (!haveSeenProjectProperty(value)){
+        includeProjectPropertyFile(value);
+      }
+      
+    } else {
+      throw exception("unknown keyword: " + key);
+    }
+  }
+
+  protected boolean haveSeenProjectProperty (String key){
+    String pn = getString(key);
+    if (pn == null){
+      return false;
+    } else {
+      return sources.contains( new File( pn, "jpf.properties"));
+    }
+  }
+  
+  private Object setKey (String k, String v){
+    Object oldValue = put0(k, v);
+    notifyPropertyChangeListeners(k, (String) oldValue, v);
+    return oldValue;
+  }
+
+  private Object removeKey (String k){
+    Object oldValue = super.get(k);
+    remove0(k);
+    notifyPropertyChangeListeners(k, (String) oldValue, null);
+    return oldValue;
+  }
+
+  private Object put0 (String k, Object v){
+    entrySequence.add(k);
+    return super.put(k, v);
+  }
+
+  private Object remove0 (String k){
+    entrySequence.add(k);
+    return super.remove(k);
+  }
+
+  public String prepend (String key, String value, String separator) {
+    String oldValue = getProperty(key);
+    value = normalize( expandString(key, value));
+
+    append0(key, oldValue, value, oldValue, separator);
+
+    return oldValue;
+  }
+
+  public String append (String key, String value, String separator) {
+    String oldValue = getProperty(key);
+    value = normalize( expandString(key, value));
+
+    append0(key, oldValue, oldValue, value, separator);
+
+    return oldValue;
+  }
+
+
+  private void append0 (String key, String oldValue, String a, String b, String separator){
+    String newValue;
+
+    if (a != null){
+      if (b != null) {
+        StringBuilder sb = new StringBuilder(a);
+        if (separator != null) {
+          sb.append(separator);
+        }
+        sb.append(b);
+        newValue = sb.toString();
+
+      } else { // b==null : nothing to append
+        if (oldValue == a){ // using reference compare is intentional here
+          return; // no change
+        } else {
+          newValue = a;
+        }
+      }
+
+    } else { // a==null : nothing to append to
+      if (oldValue == b || b == null){  // using reference compare is intentional here
+        return; // no change
+      } else {
+        newValue = b;
+      }
+    }
+
+    // if we get here, we have a newValue that differs from oldValue
+    put0(key, newValue);
+    notifyPropertyChangeListeners(key, oldValue, newValue);
+  }
+
+  protected String append (String key, String value) {
+    return append(key, value, LIST_SEPARATOR); // append with our standard list separator
+  }
+
+  /**
+   * check if we have a key.index entry. If not, check the non-indexed key. If no
+   * key found return null
+   * This simplifies clients that can have process id indexed properties
+   */
+  public String getIndexableKey (String key, int index){
+    String k = key + '.' + index;
+    if (containsKey(k)){
+      return k;
+    } else {
+      if (containsKey(key)){
+        return key;
+      }
+    }
+    
+    return null; // neither indexed nor non-indexed key in dictionary
+  }
+
+  public void setClassLoader (ClassLoader newLoader){
+    loader = newLoader;
+  }
+
+  public ClassLoader getClassLoader (){
+    return loader;
+  }
+
+  public boolean hasSetClassLoader (){
+    return Config.class.getClassLoader() != loader;
+  }
+
+  public JPFClassLoader initClassLoader (ClassLoader parent) {
+    ArrayList<String> list = new ArrayList<String>();
+
+    // we prefer to call this here automatically instead of allowing
+    // explicit collectGlobalPath() calls because (a) this could not preserve
+    // initial path settings, and (b) setting it *after* the JPFClassLoader got
+    // installed won't work (would have to add URLs explicitly, or would have
+    // to create a new JPFClassLoader, which then conflicts with classes already
+    // defined by the previous one)
+    collectGlobalPaths();
+    if (log){
+      log("collected native_classpath=" + get("native_classpath"));
+      log("collected native_libraries=" + get("native_libraries"));
+    }
+
+
+    String[] cp = getCompactStringArray("native_classpath");
+    cp = FileUtils.expandWildcards(cp);
+    for (String e : cp) {
+      list.add(e);
+    }
+    URL[] urls = FileUtils.getURLs(list);
+
+    String[] nativeLibs = getCompactStringArray("native_libraries");
+
+    JPFClassLoader cl;
+    if (parent instanceof JPFClassLoader){ // no need to create a new one, just initialize
+      cl = (JPFClassLoader)parent;
+      for (URL url : urls){
+        cl.addURL(url);
+      }
+      cl.setNativeLibs(nativeLibs);
+      
+    } else {    
+      cl = new JPFClassLoader( urls, nativeLibs, parent);
+    }
+    
+    loader = cl;
+    return cl;
+  }
+
+  /**
+   * has to be called if 'native_classpath' gets explicitly changed
+   * USE WITH CARE - if this is messed up, it is hard to debug
+   */
+  public void updateClassLoader (){
+    if (loader != null && loader instanceof JPFClassLoader){
+      JPFClassLoader jpfCl = (JPFClassLoader)loader;
+            
+      ArrayList<String> list = new ArrayList<String>();
+      String[] cp = getCompactStringArray("native_classpath");
+      cp = FileUtils.expandWildcards(cp);
+      for (String e : cp) {
+        URL url = FileUtils.getURL(e);
+        jpfCl.addURL(url); // this does not add if already present
+      }
+
+      String[] nativeLibs = getCompactStringArray("native_libraries");
+      jpfCl.setNativeLibs(nativeLibs);
+    }
+  }
+  
+
+  //------------------------------ public methods - the Config API
+
+
+  public String[] getEntrySequence () {
+    // whoever gets this might add/append/remove items, so we have to
+    // avoid ConcurrentModificationExceptions
+    return entrySequence.toArray(new String[entrySequence.size()]);
+  }
+
+  public void addChangeListener (ConfigChangeListener l) {
+    if (changeListeners == null) {
+      changeListeners = new ArrayList<ConfigChangeListener>();
+      changeListeners.add(l);
+    } else {
+      if (!changeListeners.contains(l)) {
+        changeListeners.add(l);
+      }
+    }
+  }
+  
+  public void removeChangeListener (ConfigChangeListener l) {
+    if (changeListeners != null) {
+      changeListeners.remove(l);
+      
+      if (changeListeners.size() == 0) {
+        changeListeners = null;
+      }
+    }
+  }
+  
+  // this shouldn't really be public but only accessible to JPF
+  public void jpfRunTerminated() {
+    if (changeListeners != null) {
+      // note we can't use the standard list iterator here because the sole purpose
+      // of having this notification is to remove the listener from the list during its enumeration
+      // which would give us ConcurrentModificationExceptions
+      ArrayList<ConfigChangeListener> list = (ArrayList<ConfigChangeListener>)changeListeners.clone();
+      for (ConfigChangeListener l : list) {
+        l.jpfRunTerminated(this);
+      }
+    }
+  }
+  
+  public JPFException exception (String msg) {
+    String context = getString("config");
+    if (context != null){
+      msg = "error in " + context + " : " + msg;
+    }
+
+    return new JPFConfigException(msg);
+  }
+
+  public void throwException(String msg) {
+    throw new JPFConfigException(msg);
+  }
+
+  /**
+   * return any command line args that are not options or properties
+   * (this usually contains the application class and arguments)
+   */
+  public String[] getFreeArgs(){
+    return freeArgs;
+  } 
+
+  //--- special keys
+  
+  /*
+   * target and its associated keys (target.args, target.entry) are now
+   * just ordinary key/value pairs and only here as convenience methods
+   * for JPF drivers/shells so that you don't have to remember the key names
+   * 
+   * NOTE - this does only work for a SingleProcessVM, and only has the
+   * desired effect before the JPF object is created
+   */
+  
+  public void setTarget (String clsName) {
+    put("target", clsName);
+  }
+  public String getTarget(){
+    return getString("target");
+  }
+  
+  public void setTargetArgs (String[] args) {
+    StringBuilder sb = new StringBuilder();
+    int i=0;
+    for (String a : args){
+      if (i++ > 0){
+        sb.append(',');
+      }
+      sb.append(a);
+    }
+    put("target.args", sb.toString());
+  }
+  public String[] getTargetArgs(){
+    String[] a = getStringArray("target.args");
+    if (a == null){
+      return new String[0];
+    } else {
+      return a;
+    }
+  }
+  
+  public void setTargetEntry (String mthName) {
+    put("target.entry", mthName);
+  }
+  public String getTargetEntry(){
+    return getString("target.entry");
+  }
+  
+  
+  //----------------------- type specific accessors
+
+  public boolean getBoolean(String key) {
+    String v = getProperty(key);
+    return (v == TRUE);
+  }
+
+  public boolean getBoolean(String key, boolean def) {
+    String v = getProperty(key);
+    if (v != null) {
+      return (v == TRUE);
+    } else {
+      return def;
+    }
+  }
+
+  /**
+   * for a given <baseKey>, check if there are corresponding
+   * values for keys <baseKey>.0 ... <baseKey>.<maxSize>
+   * If a value is found, store it in an array at the respective index
+   *
+   * @param baseKey String with base key without trailing '.'
+   * @param maxSize maximum size of returned value array
+   * @return trimmed array with String values found in dictionary
+   */
+  public String[] getStringEnumeration (String baseKey, int maxSize) {
+    String[] arr = new String[maxSize];
+    int max=-1;
+
+    StringBuilder sb = new StringBuilder(baseKey);
+    sb.append('.');
+    int len = baseKey.length()+1;
+
+    for (int i=0; i<maxSize; i++) {
+      sb.setLength(len);
+      sb.append(i);
+
+      String v = getString(sb.toString());
+      if (v != null) {
+        arr[i] = v;
+        max = i;
+      }
+    }
+
+    if (max >= 0) {
+      max++;
+      if (max < maxSize) {
+        String[] a = new String[max];
+        System.arraycopy(arr,0,a,0,max);
+        return a;
+      } else {
+        return arr;
+      }
+    } else {
+      return null;
+    }
+  }
+
+  public String[] getKeysStartingWith (String prefix){
+    ArrayList<String> list = new ArrayList<String>();
+
+    for (Enumeration e = keys(); e.hasMoreElements(); ){
+      String k = e.nextElement().toString();
+      if (k.startsWith(prefix)){
+        list.add(k);
+      }
+    }
+
+    return list.toArray(new String[list.size()]);
+  }
+
+  public String[] getKeyComponents (String key){
+    return key.split("\\.");
+  }
+
+  public int[] getIntArray (String key) throws JPFConfigException {
+    String v = getProperty(key);
+
+    if (v != null) {
+      String[] sa = split(v);
+      int[] a = new int[sa.length];
+      int i = 0;
+      try {
+        for (; i<sa.length; i++) {
+          String s = sa[i];
+          int val;
+          if (s.startsWith("0x")){
+            val = Integer.parseInt(s.substring(2),16); 
+          } else {
+            val = Integer.parseInt(s);
+          }
+          a[i] = val;
+        }
+        return a;
+      } catch (NumberFormatException nfx) {
+        throw new JPFConfigException("illegal int[] element in '" + key + "' = \"" + sa[i] + '"');
+      }
+    } else {
+      return null;
+    }
+  }
+  public int[] getIntArray (String key, int... defaultValues){
+    int[] val = getIntArray(key);
+    if (val == null){
+      return defaultValues;
+    } else {
+      return val;
+    }
+  }
+
+  public long getDuration (String key, long defValue) {
+    String v = getProperty(key);
+    if (v != null) {
+      long d = 0;
+
+      if (v.indexOf(':') > 0){
+        String[] a = v.split(":");
+        if (a.length > 3){
+          //log.severe("illegal duration: " + key + "=" + v);
+          return defValue;
+        }
+        int m = 1000;
+        for (int i=a.length-1; i>=0; i--, m*=60){
+          try {
+            int n = Integer.parseInt(a[i]);
+            d += m*n;
+          } catch (NumberFormatException nfx) {
+            throw new JPFConfigException("illegal duration element in '" + key + "' = \"" + v + '"');
+          }
+        }
+
+      } else {
+        try {
+          d = Long.parseLong(v);
+        } catch (NumberFormatException nfx) {
+          throw new JPFConfigException("illegal duration element in '" + key + "' = \"" + v + '"');
+        }
+      }
+
+      return d;
+    }
+
+    return defValue;
+  }
+
+  public int getInt(String key) {
+    return getInt(key, 0);
+  }
+
+  public int getInt(String key, int defValue) {
+    String v = getProperty(key);
+    if (v != null) {
+      if (MAX.equals(v)){
+        return Integer.MAX_VALUE;
+      } else {
+        try {
+          return Integer.parseInt(v);
+        } catch (NumberFormatException nfx) {
+          throw new JPFConfigException("illegal int element in '" + key + "' = \"" + v + '"');
+        }
+      }
+    }
+
+    return defValue;
+  }
+
+  public long getLong(String key) {
+    return getLong(key, 0L);
+  }
+
+  public long getLong(String key, long defValue) {
+    String v = getProperty(key);
+    if (v != null) {
+      if (MAX.equals(v)){
+        return Long.MAX_VALUE;
+      } else {
+        try {
+          return Long.parseLong(v);
+        } catch (NumberFormatException nfx) {
+          throw new JPFConfigException("illegal long element in '" + key + "' = \"" + v + '"');
+        }
+      }
+    }
+
+    return defValue;
+  }
+
+  public long[] getLongArray (String key) throws JPFConfigException {
+    String v = getProperty(key);
+
+    if (v != null) {
+      String[] sa = split(v);
+      long[] a = new long[sa.length];
+      int i = 0;
+      try {
+        for (; i<sa.length; i++) {
+          a[i] = Long.parseLong(sa[i]);
+        }
+        return a;
+      } catch (NumberFormatException nfx) {
+        throw new JPFConfigException("illegal long[] element in " + key + " = " + sa[i]);
+      }
+    } else {
+      return null;
+    }
+  }
+
+  public long[] getLongArray (String key, long... defaultValues){
+    long[] val = getLongArray(key);
+    if (val != null){
+      return val;
+    } else {
+      return defaultValues;
+    } 
+  }
+
+  public float getFloat (String key) {
+    return getFloat(key, 0.0f);
+  }
+
+  public float getFloat (String key, float defValue) {
+    String v = getProperty(key);
+    if (v != null) {
+      try {
+        return Float.parseFloat(v);
+      } catch (NumberFormatException nfx) {
+        throw new JPFConfigException("illegal float element in '" + key + "' = \"" + v + '"');
+      }
+    }
+
+    return defValue;
+  }
+  
+  public float[] getFloatArray (String key) throws JPFConfigException {
+    String v = getProperty(key);
+
+    if (v != null) {
+      String[] sa = split(v);
+      float[] a = new float[sa.length];
+      int i = 0;
+      try {
+        for (; i<sa.length; i++) {
+          a[i] = Float.parseFloat(sa[i]);
+        }
+        return a;
+      } catch (NumberFormatException nfx) {
+        throw new JPFConfigException("illegal float[] element in " + key + " = " + sa[i]);
+      }
+    } else {
+      return null;
+    }
+  }
+  public float[] getFloatArray (String key, float... defaultValues){
+    float[] v = getFloatArray( key);
+    if (v != null){
+      return v;
+    } else {
+      return defaultValues;
+    }
+  }
+  
+  
+  public double getDouble (String key) {
+    return getDouble(key, 0.0);
+  }
+
+  public double getDouble (String key, double defValue) {
+    String v = getProperty(key);
+    if (v != null) {
+      try {
+        return Double.parseDouble(v);
+      } catch (NumberFormatException nfx) {
+        throw new JPFConfigException("illegal double element in '" + key + "' = \"" + v + '"');
+      }
+    }
+
+    return defValue;
+  }
+
+  public double[] getDoubleArray (String key) throws JPFConfigException {
+    String v = getProperty(key);
+
+    if (v != null) {
+      String[] sa = split(v);
+      double[] a = new double[sa.length];
+      int i = 0;
+      try {
+        for (; i<sa.length; i++) {
+          a[i] = Double.parseDouble(sa[i]);
+        }
+        return a;
+      } catch (NumberFormatException nfx) {
+        throw new JPFConfigException("illegal double[] element in " + key + " = " + sa[i]);
+      }
+    } else {
+      return null;
+    }
+  }
+  public double[] getDoubleArray (String key, double... defaultValues){
+    double[] v = getDoubleArray( key);
+    if (v != null){
+      return v;
+    } else {
+      return defaultValues;
+    }
+  }
+
+  public <T extends Enum<T>> T getEnum( String key, T[] values, T defValue){
+    String v = getProperty(key);
+
+    if (v != null){
+      for (T t : values){
+        if (v.equalsIgnoreCase(t.name())){
+          return t;
+        }
+      }
+      
+      throw new JPFConfigException("unknown enum value for " + key + " = " + v);
+      
+    } else {
+      return defValue;
+    }
+  }
+
+  public String getString(String key) {
+    return getProperty(key);
+  }
+
+  public String getString(String key, String defValue) {
+    String s = getProperty(key);
+    if (s != null) {
+      return s;
+    } else {
+      return defValue;
+    }
+  }
+
+  /**
+   * return memory size in bytes, or 'defValue' if not in dictionary. Encoding
+   * can have a 'M' or 'k' postfix, values have to be positive integers (decimal
+   * notation)
+   */
+  public long getMemorySize(String key, long defValue) {
+    String v = getProperty(key);
+    long sz = defValue;
+
+    if (v != null) {
+      int n = v.length() - 1;
+      try {
+        char c = v.charAt(n);
+
+        if ((c == 'M') || (c == 'm')) {
+          sz = Long.parseLong(v.substring(0, n)) << 20;
+        } else if ((c == 'K') || (c == 'k')) {
+          sz = Long.parseLong(v.substring(0, n)) << 10;
+        } else {
+          sz = Long.parseLong(v);
+        }
+
+      } catch (NumberFormatException nfx) {
+        throw new JPFConfigException("illegal memory size element in '" + key + "' = \"" + v + '"');
+      }
+    }
+
+    return sz;
+  }
+
+  public HashSet<String> getStringSet(String key){
+    String v = getProperty(key);
+    if (v != null && (v.length() > 0)) {
+      HashSet<String> hs = new HashSet<String>();
+      for (String s : split(v)) {
+        hs.add(s);
+      }
+      return hs;
+    }
+
+    return null;
+    
+  }
+  
+  public HashSet<String> getNonEmptyStringSet(String key){
+    HashSet<String> hs = getStringSet(key);
+    if (hs != null && hs.isEmpty()) {
+      return null;
+    } else {
+      return hs;
+    }
+  }
+    
+  public String[] getStringArray(String key) {
+    String v = getProperty(key);
+    if (v != null && (v.length() > 0)) {
+      return split(v);
+    }
+
+    return null;
+  }
+
+  public String[] getStringArray(String key, char[] delims) {
+    String v = getProperty(key);
+    if (v != null && (v.length() > 0)) {
+      return split(v,delims);
+    }
+
+    return null;
+  }
+
+  public String[] getCompactTrimmedStringArray (String key){
+    String[] a = getStringArray(key);
+
+    if (a != null) {
+      for (int i = 0; i < a.length; i++) {
+        String s = a[i];
+        if (s != null && s.length() > 0) {
+          a[i] = s.trim();
+        }
+      }
+
+      return removeEmptyStrings(a);
+
+    } else {
+      return EMPTY_STRING_ARRAY;
+    }
+  }
+
+  public String[] getCompactStringArray(String key){
+    return removeEmptyStrings(getStringArray(key));
+  }
+
+  
+  public String[] getStringArray(String key, String[] def){
+    String v = getProperty(key);
+    if (v != null && (v.length() > 0)) {
+      return split(v);
+    } else {
+      return def;
+    }
+  }
+
+  public static String[] removeEmptyStrings (String[] a){
+    if (a != null) {
+      int n = 0;
+      for (int i=0; i<a.length; i++){
+        if (a[i].length() > 0){
+          n++;
+        }
+      }
+
+      if (n < a.length){ // we have empty strings in the split
+        String[] r = new String[n];
+        for (int i=0, j=0; i<a.length; i++){
+          if (a[i].length() > 0){
+            r[j++] = a[i];
+            if (j == n){
+              break;
+            }
+          }
+        }
+        return r;
+
+      } else {
+        return a;
+      }
+    }
+
+    return null;
+  }
+
+
+  /**
+   * return an [optional] id part of a property value (all that follows the first '@')
+   */
+  String getIdPart (String key) {
+    String v = getProperty(key);
+    if ((v != null) && (v.length() > 0)) {
+      int i = v.indexOf('@');
+      if (i >= 0){
+        return v.substring(i+1);
+      }
+    }
+
+    return null;
+  }
+
+  public Class<?> asClass (String v) throws JPFConfigException {
+    if ((v != null) && (v.length() > 0)) {
+      v = stripId(v);
+      v = expandClassName(v);
+      try {
+        return loader.loadClass(v);
+      } catch (ClassNotFoundException cfx) {
+        throw new JPFConfigException("class not found " + v + " by classloader: " + loader);
+      } catch (ExceptionInInitializerError ix) {
+        throw new JPFConfigException("class initialization of " + v + " failed: " + ix,
+            ix);
+      }
+    }
+
+    return null;    
+  }
+      
+  public <T> Class<? extends T> getClass(String key, Class<T> type) throws JPFConfigException {
+    Class<?> cls = asClass( getProperty(key));
+    if (cls != null) {
+      if (type.isAssignableFrom(cls)) {
+        return cls.asSubclass(type);
+      } else {
+        throw new JPFConfigException("classname entry for: \"" + key + "\" not of type: " + type.getName());
+      }
+    }
+    return null;
+  }
+  
+    
+  public Class<?> getClass(String key) throws JPFConfigException {
+    return asClass( getProperty(key));
+  }
+  
+  public Class<?> getEssentialClass(String key) throws JPFConfigException {
+    Class<?> cls = getClass(key);
+    if (cls == null) {
+      throw new JPFConfigException("no classname entry for: \"" + key + "\"");
+    }
+
+    return cls;
+  }
+  
+  String stripId (String v) {
+    int i = v.indexOf('@');
+    if (i >= 0) {
+      return v.substring(0,i);
+    } else {
+      return v;
+    }
+  }
+
+  String getId (String v){
+    int i = v.indexOf('@');
+    if (i >= 0) {
+      return v.substring(i+1);
+    } else {
+      return null;
+    }
+  }
+
+  String expandClassName (String clsName) {
+    if (clsName != null && clsName.length() > 0 && clsName.charAt(0) == '.') {
+      return "gov.nasa.jpf" + clsName;
+    } else {
+      return clsName;
+    }
+  }
+
+  
+  public Class<?>[] getClasses(String key) throws JPFConfigException {
+    String[] v = getStringArray(key);
+    if (v != null) {
+      int n = v.length;
+      Class<?>[] a = new Class[n];
+      for (int i = 0; i < n; i++) {
+        String clsName = expandClassName(v[i]);
+        if (clsName != null && clsName.length() > 0){
+          try {
+            clsName = stripId(clsName);
+            a[i] = loader.loadClass(clsName);
+          } catch (ClassNotFoundException cnfx) {
+            throw new JPFConfigException("class not found " + v[i]);
+          } catch (ExceptionInInitializerError ix) {
+            throw new JPFConfigException("class initialization of " + v[i] + " failed: " + ix, ix);
+          }
+        }
+      }
+
+      return a;
+    }
+
+    return null;
+  }
+  
+  /**
+   * this one is used to instantiate objects from a list of keys that share
+   * the same prefix, e.g.
+   * 
+   *  shell.panels = config,site
+   *  shell.panels.site = .shell.panels.SitePanel
+   *  shell.panels.config = .shell.panels.ConfigPanel
+   *  ...
+   * 
+   * note that we specify default class names, not classes, so that the classes
+   * get loaded through our own loader at call time (they might not be visible
+   * to our caller)
+   */
+  public <T> T[] getGroupInstances (String keyPrefix, String keyPostfix, Class<T> type, 
+          String... defaultClsNames) throws JPFConfigException {
+    
+    String[] ids = getCompactTrimmedStringArray(keyPrefix);
+    
+    if (ids.length > 0){
+      keyPrefix = keyPrefix + '.';
+      T[] arr = (T[]) Array.newInstance(type, ids.length);
+      
+      for(int i = 0; i < ids.length; i++){
+        String key = keyPrefix + ids[i];
+        if (keyPostfix != null){
+          key = key + keyPostfix;
+        }
+        arr[i] = getEssentialInstance(key, type);
+      }
+      
+      return arr;
+      
+    } else {
+      T[] arr = (T[]) Array.newInstance(type, defaultClsNames.length);
+              
+      for (int i=0; i<arr.length; i++){
+        arr[i] = getInstance((String)null, defaultClsNames[i], type);
+        if (arr[i] == null){
+          exception("cannot instantiate default type " + defaultClsNames[i]);
+        }
+      }
+      
+      return arr;
+    }
+  }
+  
+  // <2do> - that's kind of kludged together, not very efficient
+  String[] getIds (String key) {
+    String v = getProperty(key);
+
+    if (v != null) {
+      int i = v.indexOf('@');
+      if (i >= 0) { // Ok, we have ids
+        String[] a = split(v);
+        String[] ids = new String[a.length];
+        for (i = 0; i<a.length; i++) {
+          ids[i] = getId(a[i]);
+        }
+        return ids;
+      }
+    }
+
+    return null;
+  }
+
+  public <T> ArrayList<T> getInstances(String key, Class<T> type) throws JPFConfigException {
+
+    Class<?>[] argTypes = { Config.class };
+    Object[] args = { this };
+
+    return getInstances(key,type,argTypes,args);
+  }
+  
+  public <T> ArrayList<T> getInstances(String key, Class<T> type, Class<?>[]argTypes, Object[] args)
+                                                      throws JPFConfigException {
+    Class<?>[] c = getClasses(key);
+
+    if (c != null) {
+      String[] ids = getIds(key);
+
+      ArrayList<T> a = new ArrayList<T>(c.length);
+
+      for (int i = 0; i < c.length; i++) {
+        String id = (ids != null) ? ids[i] : null;
+        T listener = getInstance(key, c[i], type, argTypes, args, id);
+        if (listener != null) {
+          a.add( listener);
+        } else {
+          // should report here
+        }
+      }
+
+      return a;
+      
+    } else {
+      // should report here
+    }
+
+    return null;
+  }
+  
+  public <T> T getInstance(String key, Class<T> type, String defClsName) throws JPFConfigException {
+    Class<?>[] argTypes = CONFIG_ARGTYPES;
+    Object[] args = CONFIG_ARGS;
+
+    Class<?> cls = getClass(key);
+    String id = getIdPart(key);
+
+    if (cls == null) {
+      try {
+        cls = loader.loadClass(defClsName);
+      } catch (ClassNotFoundException cfx) {
+        throw new JPFConfigException("class not found " + defClsName);
+      } catch (ExceptionInInitializerError ix) {
+        throw new JPFConfigException("class initialization of " + defClsName + " failed: " + ix, ix);
+      }
+    }
+    
+    return getInstance(key, cls, type, argTypes, args, id);
+  }
+
+  public <T> T getInstance(String key, Class<T> type) throws JPFConfigException {
+    Class<?>[] argTypes = CONFIG_ARGTYPES;
+    Object[] args = CONFIG_ARGS;
+
+    return getInstance(key, type, argTypes, args);
+  }
+    
+  public <T> T getInstance(String key, Class<T> type, Class<?>[] argTypes,
+                            Object[] args) throws JPFConfigException {
+    Class<?> cls = getClass(key);
+    String id = getIdPart(key);
+
+    if (cls != null) {
+      return getInstance(key, cls, type, argTypes, args, id);
+    } else {
+      return null;
+    }
+  }
+  
+  public <T> T getInstance(String key, Class<T> type, Object arg1, Object arg2)  throws JPFConfigException {
+    Class<?>[] argTypes = new Class<?>[2];
+    argTypes[0] = arg1.getClass();
+    argTypes[1] = arg2.getClass();
+
+    Object[] args = new Object[2];
+    args[0] = arg1;
+    args[1] = arg2;
+
+    return getInstance(key, type, argTypes, args);
+  }
+
+
+  public <T> T getEssentialInstance(String key, Class<T> type) throws JPFConfigException {
+    Class<?>[] argTypes = { Config.class };
+    Object[] args = { this };
+    return getEssentialInstance(key, type, argTypes, args);
+  }
+
+  /**
+   * just a convenience method for ctor calls that take two arguments
+   */
+  public <T> T getEssentialInstance(String key, Class<T> type, Object arg1, Object arg2)  throws JPFConfigException {
+    Class<?>[] argTypes = new Class<?>[2];
+    argTypes[0] = arg1.getClass();
+    argTypes[1] = arg2.getClass();
+
+    Object[] args = new Object[2];
+    args[0] = arg1;
+    args[1] = arg2;
+
+    return getEssentialInstance(key, type, argTypes, args);
+  }
+
+  public <T> T getEssentialInstance(String key, Class<T> type, Class<?>[] argTypes, Object[] args) throws JPFConfigException {
+    Class<?> cls = getEssentialClass(key);
+    String id = getIdPart(key);
+
+    return getInstance(key, cls, type, argTypes, args, id);
+  }
+
+  public <T> T getInstance (String id, String clsName, Class<T> type, Class<?>[] argTypes, Object[] args) throws JPFConfigException {
+    Class<?> cls = asClass(clsName);
+    
+    if (cls != null) {
+      return getInstance(id, cls, type, argTypes, args, id);
+    } else {
+      return null;
+    }
+  }
+  
+  public <T> T getInstance (String id, String clsName, Class<T> type) throws JPFConfigException {
+    Class<?>[] argTypes = CONFIG_ARGTYPES;
+    Object[] args = CONFIG_ARGS;
+
+    Class<?> cls = asClass(clsName);
+    
+    if (cls != null) {
+      return getInstance(id, cls, type, argTypes, args, id);
+    } else {
+      return null;
+    }
+  }
+  
+  /**
+   * this is our private instantiation workhorse - try to instantiate an object of
+   * class 'cls' by using the following ordered set of ctors 1. <cls>(
+   * <argTypes>) 2. <cls>(Config) 3. <cls>() if all of that fails, or there was
+   * a 'type' provided the instantiated object does not comply with, return null
+   */
+  <T> T getInstance(String key, Class<?> cls, Class<T> type, Class<?>[] argTypes,
+                     Object[] args, String id) throws JPFConfigException {
+    Object o = null;
+    Constructor<?> ctor = null;
+
+    if (cls == null) {
+      return null;
+    }
+
+    if (id != null) { // check first if we already have this one instantiated as a singleton
+      if (singletons == null) {
+        singletons = new HashMap<String,Object>();
+      } else {
+        o = type.cast(singletons.get(id));
+      }
+    }
+
+    while (o == null) {
+      try {
+        ctor = cls.getConstructor(argTypes);
+        o = ctor.newInstance(args);
+      } catch (NoSuchMethodException nmx) {
+         
+        if ((argTypes.length > 1) || ((argTypes.length == 1) && (argTypes[0] != Config.class))) {
+          // fallback 1: try a single Config param
+          argTypes = CONFIG_ARGTYPES;
+          args = CONFIG_ARGS;
+
+        } else if (argTypes.length > 0) {
+          // fallback 2: try the default ctor
+          argTypes = NO_ARGTYPES;
+          args = NO_ARGS;
+
+        } else {
+          // Ok, there is no suitable ctor, bail out
+          throw new JPFConfigException(key, cls, "no suitable ctor found");
+        }
+      } catch (IllegalAccessException iacc) {
+        throw new JPFConfigException(key, cls, "\n> ctor not accessible: "
+            + getMethodSignature(ctor));
+      } catch (IllegalArgumentException iarg) {
+        throw new JPFConfigException(key, cls, "\n> illegal constructor arguments: "
+            + getMethodSignature(ctor));
+      } catch (InvocationTargetException ix) {
+        Throwable tx = ix.getTargetException();
+        if (tx instanceof JPFConfigException) {
+          throw new JPFConfigException(tx.getMessage() + "\n> used within \"" + key
+              + "\" instantiation of " + cls);
+        } else {
+          throw new JPFConfigException(key, cls, "\n> exception in "
+              + getMethodSignature(ctor) + ":\n>> " + tx, tx);
+        }
+      } catch (InstantiationException ivt) {
+        throw new JPFConfigException(key, cls,
+            "\n> abstract class cannot be instantiated");
+      } catch (ExceptionInInitializerError eie) {
+        throw new JPFConfigException(key, cls, "\n> static initialization failed:\n>> "
+            + eie.getException(), eie.getException());
+      }
+    }
+
+    // check type
+    if (!type.isInstance(o)) {
+      throw new JPFConfigException(key, cls, "\n> instance not of type: "
+          + type.getName());
+    }
+
+    if (id != null) { // add to singletons (in case it's not already in there)
+      singletons.put(id, o);
+    }
+
+    return type.cast(o); // safe according to above
+  }
+
+  public String getMethodSignature(Constructor<?> ctor) {
+    StringBuilder sb = new StringBuilder(ctor.getName());
+    sb.append('(');
+    Class<?>[] argTypes = ctor.getParameterTypes();
+    for (int i = 0; i < argTypes.length; i++) {
+      if (i > 0) {
+        sb.append(',');
+      }
+      sb.append(argTypes[i].getName());
+    }
+    sb.append(')');
+    return sb.toString();
+  }
+
+  public boolean hasValue(String key) {
+    String v = getProperty(key);
+    return ((v != null) && (v.length() > 0));
+  }
+
+  public boolean hasValueIgnoreCase(String key, String value) {
+    String v = getProperty(key);
+    if (v != null) {
+      return v.equalsIgnoreCase(value);
+    }
+
+    return false;
+  }
+
+  public int getChoiceIndexIgnoreCase(String key, String[] choices) {
+    String v = getProperty(key);
+
+    if ((v != null) && (choices != null)) {
+      for (int i = 0; i < choices.length; i++) {
+        if (v.equalsIgnoreCase(choices[i])) {
+          return i;
+        }
+      }
+    }
+
+    return -1;
+  }
+
+  public URL getURL (String key){
+    String v = getProperty(key);
+    if (v != null) {
+      try {
+        return FileUtils.getURL(v);
+      } catch (Throwable x){
+        throw exception("malformed URL: " + v);
+      }
+    } else {
+      return null;
+    }
+  }
+
+  public File[] getPathArray (String key) {    
+    String v = getProperty(key);
+    if (v != null) {
+      String[] pe = removeEmptyStrings( pathSplit(v));
+      
+      if (pe != null && pe.length > 0) {
+        File[] files = new File[pe.length];
+        for (int i=0; i<files.length; i++) {
+          String path = FileUtils.asPlatformPath(pe[i]);
+          files[i] = new File(path);
+        }
+        return files;
+      }      
+    }
+
+    return new File[0];
+  }
+
+  public File getPath (String key) {
+    String v = getProperty(key);
+    if (v != null) {
+      return new File(FileUtils.asPlatformPath(v));
+    }
+    
+    return null;
+  }
+
+  static final char[] UNIX_PATH_SEPARATORS = {',', ';', ':' };
+  static final char[] WINDOWS_PATH_SEPARATORS = {',', ';' };
+
+  protected String[] pathSplit (String input){
+    if (File.pathSeparatorChar == ':'){
+      return split( input, UNIX_PATH_SEPARATORS);
+    } else {
+      return split( input, WINDOWS_PATH_SEPARATORS);
+    }
+  }
+
+  static final char[] DELIMS = { ',', ';' };
+
+  /**
+   * our own version of split, which handles "`" quoting, and breaks on non-quoted
+   * ',' and ';' chars. We need this so that we can use ';' separated lists in
+   * JPF property files, but still can use quoted ';' if we absolutely have to
+   * specify Java signatures. On the other hand, we can't quote with '\' because
+   * that would make Windows paths even more terrible.
+   * regexes are bad at quoting, and this is more efficient anyways
+   */
+  protected String[] split (String input){
+    return split(input, DELIMS);
+  }
+
+  private boolean isDelim(char[] delim, char c){
+    for (int i=0; i<delim.length; i++){
+      if (c == delim[i]){
+        return true;
+      }
+    }
+    return false;
+  }
+
+  protected String[] split (String input, char[] delim){
+    int n = input.length();
+    ArrayList<String> elements = new ArrayList<String>();
+    boolean quote = false;
+
+    char[] buf = new char[128];
+    int k=0;
+
+    for (int i=0; i<n; i++){
+      char c = input.charAt(i);
+
+      if (!quote) {
+        if (isDelim(delim,c)){ // element separator
+          elements.add( new String(buf, 0, k));
+          k = 0;
+          continue;
+        } else if (c=='`') {
+          quote = true;
+          continue;
+        }
+      }
+
+      if (k >= buf.length){
+        char[] newBuf = new char[buf.length+128];
+        System.arraycopy(buf, 0, newBuf, 0, k);
+        buf = newBuf;
+      }
+      buf[k++] = c;
+      quote = false;
+    }
+
+    if (k>0){
+      elements.add( new String(buf, 0, k));
+    }
+
+    return elements.toArray(new String[elements.size()]);
+  }
+
+  static final String UNINITIALIZED = "uninitialized";
+  // this is where we store the initial values in case we have to recollect
+  String initialNativeClasspath = UNINITIALIZED, 
+          initialClasspath = UNINITIALIZED, 
+          initialSourcepath = UNINITIALIZED, 
+          initialPeerPackages = UNINITIALIZED,
+          initialNativeLibraries = UNINITIALIZED;
+  
+
+  /**
+   * this resets to what was explicitly set in the config files
+   */
+  public void resetGlobalPaths() {
+    if (initialNativeClasspath == UNINITIALIZED){
+      initialNativeClasspath = getString("native_classpath");
+    } else {
+      put0( "native_classpath", initialNativeClasspath);
+    }
+
+    if (initialClasspath == UNINITIALIZED){
+      initialClasspath = getString("classpath");
+    } else {
+      put0( "classpath", initialClasspath);
+    }
+    
+    if (initialSourcepath == UNINITIALIZED){
+      initialSourcepath = getString("sourcepath");
+    } else {
+      put0( "sourcepath", initialSourcepath);
+    }
+
+    if (initialPeerPackages == UNINITIALIZED){
+      initialPeerPackages = getString("peer_packages");
+    } else {
+      put0( "peer_packages", initialPeerPackages);
+    }
+
+    if (initialNativeLibraries == UNINITIALIZED){
+      initialNativeLibraries = getString("native_libraries");
+    } else {
+      put0( "native_libraries", initialNativeLibraries);
+    }
+  }
+
+  /**
+   * collect all the <project>.{native_classpath,classpath,sourcepath,peer_packages,native_libraries}
+   * and append them to the global settings
+   *
+   * NOTE - this is now called from within initClassLoader, which should only happen once and
+   * is the first time we really need the global paths.
+   *
+   * <2do> this is Ok for native_classpath and native_libraries, but we should probably do
+   * classpath, sourcepath and peer_packages separately (they can be collected later)
+   */
+  public void collectGlobalPaths() {
+        
+    // note - this is in the order of entry, i.e. reflects priorities
+    // we have to process this in reverse order so that later entries are prioritized
+    String[] keys = getEntrySequence();
+
+    String nativeLibKey = "." + System.getProperty("os.name") +
+            '.' + System.getProperty("os.arch") + ".native_libraries";
+
+    for (int i = keys.length-1; i>=0; i--){
+      String k = keys[i];
+      if (k.endsWith(".native_classpath")){
+        appendPath("native_classpath", k);
+        
+      } else if (k.endsWith(".classpath")){
+        appendPath("classpath", k);
+        
+      } else if (k.endsWith(".sourcepath")){        
+        appendPath("sourcepath", k);
+        
+      } else if (k.endsWith("peer_packages")){
+        append("peer_packages", getString(k), ",");
+        
+      } else if (k.endsWith(nativeLibKey)){
+        appendPath("native_libraries", k);
+      }
+    }
+  }
+
+  
+  static Pattern absPath = Pattern.compile("(?:[a-zA-Z]:)?[/\\\\].*");
+
+  void appendPath (String pathKey, String key){
+    String projName = key.substring(0, key.indexOf('.'));
+    String pathPrefix = null;
+
+    if (projName.isEmpty()){
+      pathPrefix = new File(".").getAbsolutePath();
+    } else {
+      pathPrefix = getString(projName);
+    }
+
+    if (pathPrefix != null){
+      pathPrefix += '/';
+
+      String[] elements = getCompactStringArray(key);
+      if (elements != null){
+        for (String e : elements) {
+          if (e != null && e.length()>0){
+
+            // if this entry is not an absolute path, or doesn't start with
+            // the project path, prepend the project path
+            if (!(absPath.matcher(e).matches()) && !e.startsWith(pathPrefix)) {
+              e = pathPrefix + e;
+            }
+
+            append(pathKey, e);
+          }
+        }
+      }
+
+    } else {
+      //throw new JPFConfigException("no project path for " + key);
+    }
+  }
+
+
+  //--- our modification interface
+
+  /**
+   * iterate over all keys, if a key starts with the provided keyPrefix, add
+   * this value under the corresponding key suffix. For example:
+   *
+   *  test.report.console.finished = result
+   *
+   *    -> prompotePropertyCategory("test.") ->
+   *
+   *  report.console.finished = result
+   *
+   * if a matching key has an IGNORE_VALUE value ("-"), the entry is *not* promoted
+   * (we need this to override promoted keys)
+   */
+  public void promotePropertyCategory (String keyPrefix){
+    int prefixLen = keyPrefix.length();
+    
+    // HashTable does not support adding elements while iterating over the entrySet 
+    ArrayList<Map.Entry<Object,Object>> promoted = null;
+    
+    for (Map.Entry<Object,Object> e : entrySet()){
+      Object k = e.getKey();
+      if (k instanceof String){
+        String key = (String)k;
+        if (key.startsWith(keyPrefix)){
+          Object v = e.getValue();
+          if (! IGNORE_VALUE.equals(v)){
+            if (promoted == null){
+              promoted = new ArrayList<Map.Entry<Object,Object>>();
+            }
+            promoted.add(e);
+          }
+        }
+      }
+    }
+    
+    if (promoted != null){
+      for (Map.Entry<Object, Object> e : promoted) {
+        String key = (String) e.getKey();
+        key = key.substring(prefixLen);
+
+        put(key, e.getValue());
+      }
+    }
+  }
+
+  
+  @Override
+  public Object setProperty (String key, String newValue) {    
+    Object oldValue = put(key, newValue);    
+    notifyPropertyChangeListeners(key, (String)oldValue, newValue);
+    return oldValue;
+  }
+
+  public void parse (String s) {
+    
+    int i = s.indexOf("=");
+    if (i > 0) {
+      String key, val;
+      
+      if (i > 1 && s.charAt(i-1)=='+') { // append
+        key = s.substring(0, i-1).trim();
+        val = s.substring(i+1); // it's going to be normalized anyways
+        append(key, val);
+        
+      } else { // put
+        key = s.substring(0, i).trim();
+        val = s.substring(i+1);
+        setProperty(key, val);
+      }
+      
+    }
+  }
+  
+  protected void notifyPropertyChangeListeners (String key, String oldValue, String newValue) {
+    if (changeListeners != null) {
+      for (ConfigChangeListener l : changeListeners) {
+        l.propertyChanged(this, key, oldValue, newValue);
+      }
+    }    
+  }
+  
+  public String[] asStringArray (String s){
+    return split(s);
+  }
+  
+  public TreeMap<Object,Object> asOrderedMap() {
+    TreeMap<Object,Object> map = new TreeMap<Object,Object>();
+    map.putAll(this);
+    return map;
+  }
+  
+  //--- various debugging methods
+
+  public void print (PrintWriter pw) {
+    pw.println("----------- Config contents");
+
+    // just how much do you have to do to get a printout with keys in alphabetical order :<
+    TreeSet<String> kset = new TreeSet<String>();
+    for (Enumeration<?> e = propertyNames(); e.hasMoreElements();) {
+      Object k = e.nextElement();
+      if (k instanceof String) {
+        kset.add( (String)k);
+      }
+    }
+
+    for (String key : kset) {
+      String val = getProperty(key);
+      pw.print(key);
+      pw.print(" = ");
+      pw.println(val);
+    }
+
+    pw.flush();
+  }
+
+  public void printSources (PrintWriter pw) {
+    pw.println("----------- Config sources");
+    for (Object src : sources){
+      pw.println(src);
+    }    
+  }
+  
+  public void printEntries() {
+    PrintWriter pw = new PrintWriter(System.out);
+    print(pw);
+  }
+
+  public String getSourceName (Object src){
+    if (src instanceof File){
+      return ((File)src).getAbsolutePath();
+    } else if (src instanceof URL){
+      return ((URL)src).toString();
+    } else {
+      return src.toString();
+    }
+  }
+  
+  public List<Object> getSources() {
+    return sources;
+  }
+  
+  public void printStatus(Logger log) {
+    int idx = 0;
+    
+    for (Object src : sources){
+      if (src instanceof File){
+        log.config("configuration source " + idx++ + " : " + getSourceName(src));
+      }
+    }
+  }
+
+
+}