Fixing a few bugs in the statistics printout.
[jpf-core.git] / src / main / gov / nasa / jpf / Config.java
1 /*
2  * Copyright (C) 2014, United States Government, as represented by the
3  * Administrator of the National Aeronautics and Space Administration.
4  * All rights reserved.
5  *
6  * The Java Pathfinder core (jpf-core) platform is licensed under the
7  * Apache License, Version 2.0 (the "License"); you may not use this file except
8  * in compliance with the License. You may obtain a copy of the License at
9  * 
10  *        http://www.apache.org/licenses/LICENSE-2.0. 
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and 
16  * limitations under the License.
17  */
18 package gov.nasa.jpf;
19
20
21 import gov.nasa.jpf.util.FileUtils;
22 import gov.nasa.jpf.util.JPFSiteUtils;
23
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.IOException;
27 import java.io.InputStream;
28 import java.io.PrintWriter;
29 import java.io.Reader;
30 import java.lang.reflect.Array;
31 import java.lang.reflect.Constructor;
32 import java.lang.reflect.InvocationTargetException;
33 import java.net.URL;
34 import java.util.ArrayList;
35 import java.util.Enumeration;
36 import java.util.HashMap;
37 import java.util.HashSet;
38 import java.util.LinkedList;
39 import java.util.List;
40 import java.util.Map;
41 import java.util.Properties;
42 import java.util.TreeMap;
43 import java.util.TreeSet;
44 import java.util.logging.Logger;
45 import java.util.regex.Pattern;
46
47
48 /**
49  * class that encapsulates property-based JPF configuration. This is mainly an
50  * associative array with various typed accessors, and a structured
51  * initialization process. This implementation has the design constraint that it
52  * does not promote symbolic information to concrete types, which means that
53  * frequently accessed data should be promoted and cached in client classes.
54  * This in turn means we assume the data is not going to change at runtime.
55  * Major motivation for this mechanism is to avoid 'Option' classes that have
56  * concrete type fields, and hence are structural bottlenecks, i.e. every
57  * parameterized user extension (Heuristics, Scheduler etc.) require to update
58  * this single class. Note that Config is also not thread safe with respect to
59  * retrieving exceptions that occurred during instantiation
60  *
61  * Another important caveat for both implementation and usage of Config is that
62  * it is supposed to be our master configuration mechanism, i.e. it is also used
63  * to configure other core services like logging. This means that Config
64  * initialization should not depend on these services. Initialization has to
65  * return at all times, recording potential problems for later handling. This is
66  * why we have to keep the Config data model and initialization fairly simple
67  * and robust.
68  *
69  * Except of JPF and Config itself, all JPF classes are loaded by a
70  * Classloader that is constucted by Config (e.g. by collecting jars from
71  * known/configured locations), i.e. we SHOULD NOT rely on any 3rd party
72  * libraries within Config. The class should be as autarkical as possible.
73  *
74  *
75  * PROPERTY SOURCES
76  * ----------------
77  *
78  * (1) one site.properties - this file specifies the location of the jpf-core and
79  * installed extensions, like:
80  *
81  *     jpf-core = /Users/pcmehlitz/projects/jpf/jpf-core
82  *     ...
83  *     jpf-numeric = /Users/pcmehlitz/projects/jpf/jpf-numeric
84  *     ...
85  *     extensions = ${jpf-core}
86  *
87  * Each key/directory that is in site.properties is used to locate a corresponding
88  * project property (jpf.properties) file
89  *
90  * (2) any number of jpf.properties project properties files - each directory
91  * entry in the 'extensions' list is checked for a jpf.properties file, which 
92  * is automatically loaded if found. Project properties mostly contain path 
93  * settings that are used to initialize class loading by the host VM and JPF
94  *
95  * (3) one *.jpf application properties - this specifies all the settings for a
96  * specific JPF run, esp. listener and target/target.args.
97  * app properties can be specified as the sole JPF argument, i.e. instead of
98  * a SUT classname
99  *     ..
100  *     target = x.Y.MySystemUnderTest
101  *     target.args = one,two
102  *     ..
103  *     listener = z.MyListener
104  *
105  * (4) commandline properties - all start with '+', they can override all other props
106  *
107  *
108  * LOOKUP ORDER
109  * ------------
110  *                       property lookup
111  *   property type   :      spec             :  default
112  *   ----------------:-----------------------:----------
113  * |  site           :   +site               : "${user.home}/[.]jpf/site.properties"
114  * |                 :                       :
115  * |  project        :  'extensions' value   : set in site.properties
116  * |                 :                       :
117  * |  app            :   +app                : -
118  * |                 :                       :
119  * v  cmdline        :   +<key>=<val>        : -
120  *
121  * * if there is an explicit spec and the pathname does not exist, throw a
122  * JPFConfigException
123  *
124  * * if the system properties cannot be found, throw a JPFConfigException
125  *
126  *
127  * <2do> need to make NumberFormatException handling consistent - should always
128  * throw an JPFConfigException, not silently returning the default value
129  *
130  */
131
132
133 @SuppressWarnings("serial")
134 public class Config extends Properties {
135
136   static final char   KEY_PREFIX = '@';
137   public static final String REQUIRES_KEY = "@requires";
138   public static final String INCLUDE_KEY = "@include";
139   public static final String INCLUDE_UNLESS_KEY = "@include_unless";
140   public static final String INCLUDE_IF_KEY = "@include_if";
141   public static final String USING_KEY = "@using";
142
143   static final String[] EMPTY_STRING_ARRAY = new String[0];
144
145   public static final String LIST_SEPARATOR = ",";
146   public static final String PATH_SEPARATOR = ","; // the default for automatic appends
147
148   public static final Class<?>[] CONFIG_ARGTYPES = { Config.class };  
149   public static final Class<?>[] NO_ARGTYPES = new Class<?>[0];
150   public static final Object[] NO_ARGS = new Object[0];
151
152   public static final String TRUE = "true";
153   public static final String FALSE = "false";
154   
155   static final String MAX = "MAX";
156
157   static final String IGNORE_VALUE = "-";
158
159   // maximum number of processes for distributed applications
160   public static int MAX_NUM_PRC = 16;
161
162   // do we want to log the config init
163   public static boolean log = false;
164
165   // bad - a control exception
166   static class MissingRequiredKeyException extends RuntimeException {
167     MissingRequiredKeyException(String details){
168       super(details);
169     }
170   }
171
172   // it seems bad design to keep ClassLoader management in a glorified Properties object,
173   // but a lot of what Config does is to resolve configured types, for which we need
174   // control over the loader that is used for resolution
175   ClassLoader loader = Config.class.getClassLoader();
176     
177   // where did we initialize from
178   ArrayList<Object> sources = new ArrayList<Object>();
179   
180   ArrayList<ConfigChangeListener> changeListeners;
181   
182   // Properties are simple Hashmaps, but we want to maintain the order of entries
183   LinkedList<String> entrySequence = new LinkedList<String>();
184
185   // an [optional] hashmap to keep objects we want to be singletons
186   HashMap<String,Object> singletons;
187   
188   public final Object[] CONFIG_ARGS = { this };
189
190   // the original command line args that were passed into the constructor
191   String[] args;
192   
193   // non-property/option command line args (starting from the first arg that is not prepened by '-','+')
194   String[] freeArgs;
195
196   /**
197    * the standard Config constructor that processes the whole properties stack
198    */
199   public Config (String[] cmdLineArgs)  {
200     args = cmdLineArgs;
201     String[] a = cmdLineArgs.clone(); // we might nullify some of them
202
203     // we need the app properties (*.jpf) pathname upfront because it might define 'site'
204     String appProperties = getAppPropertiesLocation(a);
205
206     //--- the site properties
207     String siteProperties = getSitePropertiesLocation( a, appProperties);
208     if (siteProperties != null){
209       loadProperties( siteProperties);
210     }
211
212     //--- get the project properties from current dir + site configured extensions
213     loadProjectProperties();
214
215     //--- the application properties
216     if (appProperties != null){
217       loadProperties( appProperties);
218     }
219
220     //--- at last, the (rest of the) command line properties
221     loadArgs(a);
222
223     // note that global path collection now happens from initClassLoader(), to
224     // accommodate for deferred project initialization when explicitly setting Config entries
225
226     //printEntries();
227   }
228
229   private Config() {
230     // just interal, for reloading
231   }
232   
233   /**
234    * single source Config constructor (does not process stack)
235    * @param fileName - single properties filename to initialize from 
236    */
237   public Config (String fileName){
238     loadProperties(fileName);
239   }
240
241   public Config (Reader in){
242     try {
243       load(in);
244     } catch (IOException iox){
245       exception("error reading data: " + iox);
246     }
247   }
248   
249   public static void enableLogging (boolean enableLogging){
250     log = enableLogging;
251   }
252
253   public void log (String msg){
254     if (log){ // very simplisitc, but we might do more in the future
255       System.out.println(msg);
256     }
257   }
258
259
260   String getAppPropertiesLocation(String[] args){
261     String path = null;
262
263     path = getPathArg(args, "app");
264     if (path == null){
265       // see if the first free arg is a *.jpf
266       path = getAppArg(args);
267     }
268     
269     put("jpf.app", path);
270
271     return path;
272   }
273
274   String getSitePropertiesLocation(String[] args, String appPropPath){
275     String path = getPathArg(args, "site");
276
277     if (path == null){
278       // look into the app properties
279       // NOTE: we might want to drop this in the future because it constitutes
280       // a cyclic properties file dependency
281       if (appPropPath != null){
282         path = JPFSiteUtils.getMatchFromFile(appPropPath,"site");
283       }
284
285       if (path == null) {
286         File siteProps = JPFSiteUtils.getStandardSiteProperties();
287         if (siteProps != null){
288           path = siteProps.getAbsolutePath();
289         }
290       }
291     }
292     
293     put("jpf.site", path);
294
295     return path;
296   }
297
298
299   // watch out - this does not reset the computed paths!
300   public Config reload() {
301     log("reloading config");
302
303     // just reload all our sources
304     Config newConfig = new Config();
305     for (Object src : sources){
306       if (src instanceof File) {
307         newConfig.loadProperties(((File)src).getPath());
308       } else if (src instanceof URL) {
309         newConfig.loadProperties((URL)src);
310       } else {
311         log("don't know how to reload: " + src);
312       }
313     }
314
315     // now reload command line args on top of that
316     newConfig.loadArgs(args);
317     newConfig.args = args;
318     
319     return newConfig;
320   }
321
322   public String[] getArgs() {
323     return args;
324   }
325
326   /*
327    * note that matching args are expanded and stored here, to avoid any
328    * discrepancy with value expansions (which are order-dependent)
329    */
330   protected String getPathArg (String[] args, String key){
331     int keyLen = key.length();
332
333     for (int i=0; i<args.length; i++){
334       String a = args[i];
335       if (a != null){
336         int len = a.length();
337         if (len > keyLen + 2){
338           if (a.charAt(0) == '+' && a.charAt(keyLen+1) == '='){
339             if (a.substring(1, keyLen+1).equals(key)){
340               String val = expandString(key, a.substring(keyLen+2));
341               args[i] = null; // processed
342               return val;
343             }
344           }
345         }
346       }
347     }
348
349     return null;
350   }
351
352   /*
353    * if the first freeArg is a JPF application property filename, use this
354    * as targetArg and set the "jpf.app" property accordingly
355    */
356   protected String getAppArg (String[] args){
357
358     for (int i=0; i<args.length; i++){
359       String a = args[i];
360       if (a != null && a.length() > 0){
361         switch (a.charAt(0)) {
362           case '+': continue;
363           case '-': continue;
364           default:
365             if (a.endsWith(".jpf")){
366               String val = expandString("jpf.app", a);
367               args[i] = null; // processed
368               return val;
369             }
370         }
371       }
372     }
373
374     return null;
375   }
376
377
378   protected void loadProperties (URL url){
379     log("loading defaults from: " + url);
380
381     InputStream is = null;
382     try {
383       is = url.openStream();
384       load(is);
385       sources.add(url);
386     } catch (IOException iox){
387       log("error in input stream for: " + url + " : " + iox.getMessage());
388     } finally {
389       if (is != null){
390         try {
391           is.close();
392         } catch (IOException iox1){
393           log("error closing input stream for: " + url + " : " + iox1.getMessage());
394         }
395       }
396     }
397   }
398
399   protected void setConfigPathProperties (String fileName){
400     put("config", fileName);
401     int i = fileName.lastIndexOf(File.separatorChar);
402     if (i>=0){
403       put("config_path", fileName.substring(0,i));
404     } else {
405       put("config_path", ".");
406     }
407   }
408
409
410   protected boolean loadProperties (String fileName) {
411     if (fileName != null && fileName.length() > 0) {
412       FileInputStream is = null;
413       try {
414         File f = new File(fileName);
415         if (f.isFile()) {
416           log("loading property file: " + fileName);
417
418           setConfigPathProperties(f.getAbsolutePath());
419           sources.add(f);
420           is = new FileInputStream(f);
421           load(is);
422           return true;
423         } else {
424           throw exception("property file does not exist: " + f.getAbsolutePath());
425         }
426       } catch (MissingRequiredKeyException rkx){
427         // Hmpff - control exception
428         log("missing required key: " + rkx.getMessage() + ", skipping: " + fileName);
429       } catch (IOException iex) {
430         throw exception("error reading properties: " + fileName);
431       } finally {
432         if (is != null){
433           try {
434             is.close();
435           } catch (IOException iox1){
436             log("error closing input stream for file: " + fileName);
437           }
438         }
439       }
440     }
441
442     return false;
443   }
444
445
446   /**
447    * this holds the policy defining in which order we process directories
448    * containing JPF projects (i.e. jpf.properties files)
449    */
450   protected void loadProjectProperties () {
451     // this is the list of directories holding jpf.properties files that
452     // have to be processed in order of entry (increasing priority)
453     LinkedList<File> jpfDirs = new LinkedList<File>();
454
455     // deduce the JPF projects in use (at least jpf-core) from the CL which
456     // defined this class
457     addJPFdirsFromClasspath(jpfDirs);
458
459     // add all the site configured extension dirs (but NOT jpf-core)
460     addJPFdirsFromSiteExtensions(jpfDirs);
461
462     // add the current dir, which has highest priority (this might bump up
463     // a previous entry by reodering it - which includes jpf-core)
464     addCurrentJPFdir(jpfDirs);
465
466     // now load all the jpf.property files we found in these dirs
467     // (later loads can override previous settings)
468     for (File dir : jpfDirs){
469       loadProperties(new File(dir,"jpf.properties").getAbsolutePath());
470     }
471   }
472
473   protected void appendPath (String pathKey, String key, String configPath){
474     String[] paths = getStringArray(key);
475     if (paths != null){
476       for (String e : paths) {
477         if (!e.startsWith("${") || !e.startsWith(File.separator)) {
478           e = configPath + File.separatorChar + e;
479         }
480         append(pathKey, e, PATH_SEPARATOR);
481       }
482     }
483   }
484
485   protected void addJPFdirs (List<File> jpfDirs, File dir){
486     while (dir != null) {
487       File jpfProp = new File(dir, "jpf.properties");
488       if (jpfProp.isFile()) {
489         registerJPFdir(jpfDirs, dir);
490         return;       // we probably don't want recursion here
491       }
492       dir = getParentFile(dir);
493     }
494   }
495
496   /**
497    * add the current dir to the list of JPF components.
498    * Note: this includes the core, so that we maintain the general
499    * principle that the enclosing project takes precedence (imagine the opposite:
500    * if we want to test a certain feature that is overridden by another extension
501    * we don't know about)
502    */
503   protected void addCurrentJPFdir(List<File> jpfDirs){
504     File dir = new File(System.getProperty("user.dir"));
505     while (dir != null) {
506       File jpfProp = new File(dir, "jpf.properties");
507       if (jpfProp.isFile()) {
508         registerJPFdir(jpfDirs, dir);
509         return;
510       }
511       dir = getParentFile(dir);
512     }
513   }
514
515   protected void addJPFdirsFromClasspath(List<File> jpfDirs) {
516     String cp = System.getProperty("java.class.path");
517     String[] cpEntries = cp.split(File.pathSeparator);
518
519     for (String p : cpEntries) {
520       File f = new File(p);
521       File dir = f.isFile() ? getParentFile(f) : f;
522
523       addJPFdirs(jpfDirs, dir);
524     }
525   }
526
527   protected void addJPFdirsFromSiteExtensions (List<File> jpfDirs){
528     String[] extensions = getCompactStringArray("extensions");
529     if (extensions != null){
530       for (String pn : extensions){
531         addJPFdirs( jpfDirs, new File(pn));
532       }
533     }
534   }
535
536   /**
537    * the obvious part is that it only adds to the list if the file is absent
538    * the not-so-obvious part is that it re-orders already present files
539    * to maintain the priority
540    */
541   protected boolean registerJPFdir(List<File> list, File dir){
542     try {
543       dir = dir.getCanonicalFile();
544
545       for (File e : list) {
546         if (e.equals(dir)) {
547           list.remove(e);
548           list.add(e);
549           return false;
550         }
551       }
552     } catch (IOException iox) {
553       throw new JPFConfigException("illegal path spec: " + dir);
554     }
555     
556     list.add(dir);
557     return true;
558   }
559
560   static File root = new File(File.separator);
561
562   protected File getParentFile(File f){
563     if (f == root){
564       return null;
565     } else {
566       File parent = f.getParentFile();
567       if (parent == null){
568         parent = new File(f.getAbsolutePath());
569
570         if (parent.getName().equals(root.getName())) {
571           return root;
572         } else {
573           return parent;
574         }
575       } else {
576         return parent;
577       }
578     }
579   }
580
581
582   /*
583    * argument syntax:
584    *          {'+'<key>['='<val>'] | '-'<driver-arg>} {<free-arg>}
585    *
586    * (1) null cmdLineArgs are ignored
587    * (2) all config cmdLineArgs start with '+'
588    * (3) if '=' is ommitted, a 'true' value is assumed
589    * (4) if <val> is ommitted, a 'null' value is assumed
590    * (5) no spaces around '='
591    * (6) all '-' driver-cmdLineArgs are ignored
592    */
593
594   protected void loadArgs (String[] cmdLineArgs) {
595
596     for (int i=0; i<cmdLineArgs.length; i++){
597       String a = cmdLineArgs[i];
598
599       if (a != null && a.length() > 0){
600         switch (a.charAt(0)){
601           case '+': // Config arg
602             processArg(a.substring(1));
603             break;
604
605           case '-': // driver arg, ignore
606             continue;
607
608           default:  // free (non property/option) cmdLineArgs to follow
609
610             int n = cmdLineArgs.length - i;
611             freeArgs = new String[n];
612             System.arraycopy(cmdLineArgs, i, freeArgs, 0, n);
613
614             return;
615         }
616       }
617     }
618   }
619
620
621   /*
622    * this does not include the '+' prefix, just the 
623    *     <key>[=[<value>]]
624    */
625   protected void processArg (String a) {
626
627     int idx = a.indexOf("=");
628
629     if (idx == 0){
630       throw new JPFConfigException("illegal option: " + a);
631     }
632
633     if (idx > 0) {
634       String key = a.substring(0, idx).trim();
635       String val = a.substring(idx + 1).trim();
636
637       if (val.length() == 0){
638         val = null;
639       }
640
641       setProperty(key, val);
642
643     } else {
644       setProperty(a.trim(), "true");
645     }
646
647   }
648
649
650   /**
651    * replace string constants with global static objects
652    */
653   protected String normalize (String v) {
654     if (v == null){
655       return null; // ? maybe TRUE - check default loading of "key" or "key="
656     }
657
658     // trim leading and trailing blanks (at least Java 1.4.2 does not take care of trailing blanks)
659     v = v.trim();
660     
661     // true/false
662     if ("true".equalsIgnoreCase(v)
663         || "yes".equalsIgnoreCase(v)
664         || "on".equalsIgnoreCase(v)) {
665       v = TRUE;
666     } else if ("false".equalsIgnoreCase(v)
667         || "no".equalsIgnoreCase(v)
668         || "off".equalsIgnoreCase(v)) {
669       v = FALSE;
670     }
671
672     // nil/null
673     if ("nil".equalsIgnoreCase(v) || "null".equalsIgnoreCase(v)){
674       v = null;
675     }
676     
677     return v;
678   }
679
680   
681   // our internal expander
682   // Note that we need to know the key this came from, to handle recursive expansion
683   protected String expandString (String key, String s) {
684     int i, j = 0;
685     if (s == null || s.length() == 0) {
686       return s;
687     }
688
689     while ((i = s.indexOf("${", j)) >= 0) {
690       if ((j = s.indexOf('}', i)) > 0) {
691         String k = s.substring(i + 2, j);
692         String v;
693         
694         if ((key != null) && key.equals(k)) {
695           // that's expanding itself -> use what is there
696           v = getProperty(key);
697         } else {
698           // refers to another key, which is already expanded, so this
699           // can't get recursive (we expand during entry storage)
700           v = getProperty(k);
701         }
702         
703         if (v == null) { // if we don't have it, fall back to system properties
704           v = System.getProperty(k);
705         }
706         
707         if (v != null) {
708           s = s.substring(0, i) + v + s.substring(j + 1, s.length());
709           j = i + v.length();
710         } else {
711           s = s.substring(0, i) + s.substring(j + 1, s.length());
712           j = i;
713         }
714       }
715     }
716
717     return s;    
718   }
719
720
721   boolean loadPropertiesRecursive (String fileName){
722     // save the current values of automatic properties
723     String curConfig = (String)get("config");
724     String curConfigPath = (String)get("config_path");
725
726     File propFile = new File(fileName);
727     if (!propFile.isAbsolute()){
728       propFile = new File(curConfigPath, fileName);
729     }
730     String absPath = propFile.getAbsolutePath();
731
732     if (!propFile.isFile()){
733       throw exception("property file does not exist: " + absPath);
734     }
735
736     boolean ret = loadProperties(absPath);
737
738     // restore the automatic properties
739     super.put("config", curConfig);
740     super.put("config_path", curConfigPath);
741
742     return ret;
743   }
744
745   void includePropertyFile(String key, String value){
746     value = expandString(key, value);
747     if (value != null && value.length() > 0){
748       loadPropertiesRecursive(value);
749     } else {
750       throw exception("@include pathname argument missing");
751     }
752   }
753
754   void includeCondPropertyFile(String key, String value, boolean keyPresent){
755     value = expandString(key, value);
756     if (value != null && value.length() > 0){
757       // check if it's a conditional "@include_unless/if = ?key?pathName"
758       if (value.charAt(0) == '?'){
759         int idx = value.indexOf('?', 1);
760         if (idx > 1){
761           String k = value.substring(1, idx);
762           if (containsKey(k) == keyPresent){
763             String v = value.substring(idx+1);
764             if (v.length() > 0){
765               loadPropertiesRecursive(v);
766             } else {
767               throw exception("@include_unless pathname argument missing (?<key>?<pathName>)");
768             }
769           }
770
771         } else {
772           throw exception("malformed @include_unless argument (?<key>?<pathName>), found: " + value);
773         }
774       } else {
775         throw exception("malformed @include_unless argument (?<key>?<pathName>), found: " + value);
776       }
777     } else {
778       throw exception("@include_unless missing ?<key>?<pathName> argument");
779     }
780   }
781
782
783   void includeProjectPropertyFile (String projectId){
784     String projectPath = getString(projectId);
785     if (projectPath != null){
786       File projectProps = new File(projectPath, "jpf.properties");
787       if (projectProps.isFile()){
788         loadPropertiesRecursive(projectProps.getAbsolutePath());
789
790       } else {
791         throw exception("project properties not found: " + projectProps.getAbsolutePath());
792       }
793
794     } else {
795       throw exception("unknown project id (check site.properties): " + projectId);
796     }
797   }
798
799   // we override this so that we can handle expansion for both key and value
800   // (value expansion can be recursive, i.e. refer to itself)
801   @Override
802   public Object put (Object keyObject, Object valueObject){
803
804     if (keyObject == null){
805       throw exception("no null keys allowed");
806     } else if (!(keyObject instanceof String)){
807       throw exception("only String keys allowed, got: " + keyObject);
808     }
809     if (valueObject != null && !(valueObject instanceof String)){
810       throw exception("only String or null values allowed, got: " + valueObject);
811     }
812
813     String key = (String)keyObject;
814     String value = (String)valueObject;
815
816     if (key.length() == 0){
817       throw exception("no empty keys allowed");
818     }
819
820     if (key.charAt(0) == KEY_PREFIX){
821       processPseudoProperty( key, value);
822       return null; // no value it replaces
823
824     } else {
825       // finally, a real key/value pair to add (or remove) - expand and store
826       String k = expandString(null, key);
827
828       if (!(value == null)) { // add or overwrite entry
829         String v = value;
830
831         if (k.charAt(k.length() - 1) == '+') { // the append hack
832           k = k.substring(0, k.length() - 1);
833           return append(k, v, null);
834
835         } else if (k.charAt(0) == '+') { // the prepend hack
836           k = k.substring(1);
837           return prepend(k, v, null);
838
839         } else { // normal value set
840           v = normalize(expandString(k, v));
841           if (v != null){
842             return setKey(k, v);
843           } else {
844             return removeKey(k);
845           }
846         }
847
848       } else { // setting a null value removes the entry
849         return removeKey(k);
850       }
851     }
852   }
853   
854   protected void processPseudoProperty( String key, String value){
855     if (REQUIRES_KEY.equals(key)) {
856       // shortcircuit loading of property files - used to enforce order
857       // of properties, e.g. to model dependencies
858       for (String reqKey : split(value)) {
859         if (!containsKey(reqKey)) {
860           throw new MissingRequiredKeyException(reqKey);
861         }
862       }
863
864     } else if (INCLUDE_KEY.equals(key)) {
865       includePropertyFile(key, value);
866       
867     } else if (INCLUDE_UNLESS_KEY.equals(key)) {
868       includeCondPropertyFile(key, value, false);
869       
870     } else if (INCLUDE_IF_KEY.equals(key)) {
871       includeCondPropertyFile(key, value, true);
872       
873     } else if (USING_KEY.equals(key)) {
874       // check if corresponding jpf.properties has already been loaded. If yes, skip
875       if (!haveSeenProjectProperty(value)){
876         includeProjectPropertyFile(value);
877       }
878       
879     } else {
880       throw exception("unknown keyword: " + key);
881     }
882   }
883
884   protected boolean haveSeenProjectProperty (String key){
885     String pn = getString(key);
886     if (pn == null){
887       return false;
888     } else {
889       return sources.contains( new File( pn, "jpf.properties"));
890     }
891   }
892   
893   private Object setKey (String k, String v){
894     Object oldValue = put0(k, v);
895     notifyPropertyChangeListeners(k, (String) oldValue, v);
896     return oldValue;
897   }
898
899   private Object removeKey (String k){
900     Object oldValue = super.get(k);
901     remove0(k);
902     notifyPropertyChangeListeners(k, (String) oldValue, null);
903     return oldValue;
904   }
905
906   private Object put0 (String k, Object v){
907     entrySequence.add(k);
908     return super.put(k, v);
909   }
910
911   private Object remove0 (String k){
912     entrySequence.add(k);
913     return super.remove(k);
914   }
915
916   public String prepend (String key, String value, String separator) {
917     String oldValue = getProperty(key);
918     value = normalize( expandString(key, value));
919
920     append0(key, oldValue, value, oldValue, separator);
921
922     return oldValue;
923   }
924
925   public String append (String key, String value, String separator) {
926     String oldValue = getProperty(key);
927     value = normalize( expandString(key, value));
928
929     append0(key, oldValue, oldValue, value, separator);
930
931     return oldValue;
932   }
933
934
935   private void append0 (String key, String oldValue, String a, String b, String separator){
936     String newValue;
937
938     if (a != null){
939       if (b != null) {
940         StringBuilder sb = new StringBuilder(a);
941         if (separator != null) {
942           sb.append(separator);
943         }
944         sb.append(b);
945         newValue = sb.toString();
946
947       } else { // b==null : nothing to append
948         if (oldValue == a){ // using reference compare is intentional here
949           return; // no change
950         } else {
951           newValue = a;
952         }
953       }
954
955     } else { // a==null : nothing to append to
956       if (oldValue == b || b == null){  // using reference compare is intentional here
957         return; // no change
958       } else {
959         newValue = b;
960       }
961     }
962
963     // if we get here, we have a newValue that differs from oldValue
964     put0(key, newValue);
965     notifyPropertyChangeListeners(key, oldValue, newValue);
966   }
967
968   protected String append (String key, String value) {
969     return append(key, value, LIST_SEPARATOR); // append with our standard list separator
970   }
971
972   /**
973    * check if we have a key.index entry. If not, check the non-indexed key. If no
974    * key found return null
975    * This simplifies clients that can have process id indexed properties
976    */
977   public String getIndexableKey (String key, int index){
978     String k = key + '.' + index;
979     if (containsKey(k)){
980       return k;
981     } else {
982       if (containsKey(key)){
983         return key;
984       }
985     }
986     
987     return null; // neither indexed nor non-indexed key in dictionary
988   }
989
990   public void setClassLoader (ClassLoader newLoader){
991     loader = newLoader;
992   }
993
994   public ClassLoader getClassLoader (){
995     return loader;
996   }
997
998   public boolean hasSetClassLoader (){
999     return Config.class.getClassLoader() != loader;
1000   }
1001
1002   public JPFClassLoader initClassLoader (ClassLoader parent) {
1003     ArrayList<String> list = new ArrayList<String>();
1004
1005     // we prefer to call this here automatically instead of allowing
1006     // explicit collectGlobalPath() calls because (a) this could not preserve
1007     // initial path settings, and (b) setting it *after* the JPFClassLoader got
1008     // installed won't work (would have to add URLs explicitly, or would have
1009     // to create a new JPFClassLoader, which then conflicts with classes already
1010     // defined by the previous one)
1011     collectGlobalPaths();
1012     if (log){
1013       log("collected native_classpath=" + get("native_classpath"));
1014       log("collected native_libraries=" + get("native_libraries"));
1015     }
1016
1017
1018     String[] cp = getCompactStringArray("native_classpath");
1019     cp = FileUtils.expandWildcards(cp);
1020     for (String e : cp) {
1021       list.add(e);
1022     }
1023     URL[] urls = FileUtils.getURLs(list);
1024
1025     String[] nativeLibs = getCompactStringArray("native_libraries");
1026
1027     JPFClassLoader cl;
1028     if (parent instanceof JPFClassLoader){ // no need to create a new one, just initialize
1029       cl = (JPFClassLoader)parent;
1030       for (URL url : urls){
1031         cl.addURL(url);
1032       }
1033       cl.setNativeLibs(nativeLibs);
1034       
1035     } else {    
1036       cl = new JPFClassLoader( urls, nativeLibs, parent);
1037     }
1038     
1039     loader = cl;
1040     return cl;
1041   }
1042
1043   /**
1044    * has to be called if 'native_classpath' gets explicitly changed
1045    * USE WITH CARE - if this is messed up, it is hard to debug
1046    */
1047   public void updateClassLoader (){
1048     if (loader != null && loader instanceof JPFClassLoader){
1049       JPFClassLoader jpfCl = (JPFClassLoader)loader;
1050             
1051       ArrayList<String> list = new ArrayList<String>();
1052       String[] cp = getCompactStringArray("native_classpath");
1053       cp = FileUtils.expandWildcards(cp);
1054       for (String e : cp) {
1055         URL url = FileUtils.getURL(e);
1056         jpfCl.addURL(url); // this does not add if already present
1057       }
1058
1059       String[] nativeLibs = getCompactStringArray("native_libraries");
1060       jpfCl.setNativeLibs(nativeLibs);
1061     }
1062   }
1063   
1064
1065   //------------------------------ public methods - the Config API
1066
1067
1068   public String[] getEntrySequence () {
1069     // whoever gets this might add/append/remove items, so we have to
1070     // avoid ConcurrentModificationExceptions
1071     return entrySequence.toArray(new String[entrySequence.size()]);
1072   }
1073
1074   public void addChangeListener (ConfigChangeListener l) {
1075     if (changeListeners == null) {
1076       changeListeners = new ArrayList<ConfigChangeListener>();
1077       changeListeners.add(l);
1078     } else {
1079       if (!changeListeners.contains(l)) {
1080         changeListeners.add(l);
1081       }
1082     }
1083   }
1084   
1085   public void removeChangeListener (ConfigChangeListener l) {
1086     if (changeListeners != null) {
1087       changeListeners.remove(l);
1088       
1089       if (changeListeners.size() == 0) {
1090         changeListeners = null;
1091       }
1092     }
1093   }
1094   
1095   // this shouldn't really be public but only accessible to JPF
1096   public void jpfRunTerminated() {
1097     if (changeListeners != null) {
1098       // note we can't use the standard list iterator here because the sole purpose
1099       // of having this notification is to remove the listener from the list during its enumeration
1100       // which would give us ConcurrentModificationExceptions
1101       ArrayList<ConfigChangeListener> list = (ArrayList<ConfigChangeListener>)changeListeners.clone();
1102       for (ConfigChangeListener l : list) {
1103         l.jpfRunTerminated(this);
1104       }
1105     }
1106   }
1107   
1108   public JPFException exception (String msg) {
1109     String context = getString("config");
1110     if (context != null){
1111       msg = "error in " + context + " : " + msg;
1112     }
1113
1114     return new JPFConfigException(msg);
1115   }
1116
1117   public void throwException(String msg) {
1118     throw new JPFConfigException(msg);
1119   }
1120
1121   /**
1122    * return any command line args that are not options or properties
1123    * (this usually contains the application class and arguments)
1124    */
1125   public String[] getFreeArgs(){
1126     return freeArgs;
1127   } 
1128
1129   //--- special keys
1130   
1131   /*
1132    * target and its associated keys (target.args, target.entry) are now
1133    * just ordinary key/value pairs and only here as convenience methods
1134    * for JPF drivers/shells so that you don't have to remember the key names
1135    * 
1136    * NOTE - this does only work for a SingleProcessVM, and only has the
1137    * desired effect before the JPF object is created
1138    */
1139   
1140   public void setTarget (String clsName) {
1141     put("target", clsName);
1142   }
1143   public String getTarget(){
1144     return getString("target");
1145   }
1146   
1147   public void setTargetArgs (String[] args) {
1148     StringBuilder sb = new StringBuilder();
1149     int i=0;
1150     for (String a : args){
1151       if (i++ > 0){
1152         sb.append(',');
1153       }
1154       sb.append(a);
1155     }
1156     put("target.args", sb.toString());
1157   }
1158   public String[] getTargetArgs(){
1159     String[] a = getStringArray("target.args");
1160     if (a == null){
1161       return new String[0];
1162     } else {
1163       return a;
1164     }
1165   }
1166   
1167   public void setTargetEntry (String mthName) {
1168     put("target.entry", mthName);
1169   }
1170   public String getTargetEntry(){
1171     return getString("target.entry");
1172   }
1173   
1174   
1175   //----------------------- type specific accessors
1176
1177   public boolean getBoolean(String key) {
1178     String v = getProperty(key);
1179     return (v == TRUE);
1180   }
1181
1182   public boolean getBoolean(String key, boolean def) {
1183     String v = getProperty(key);
1184     if (v != null) {
1185       return (v == TRUE);
1186     } else {
1187       return def;
1188     }
1189   }
1190
1191   /**
1192    * for a given <baseKey>, check if there are corresponding
1193    * values for keys <baseKey>.0 ... <baseKey>.<maxSize>
1194    * If a value is found, store it in an array at the respective index
1195    *
1196    * @param baseKey String with base key without trailing '.'
1197    * @param maxSize maximum size of returned value array
1198    * @return trimmed array with String values found in dictionary
1199    */
1200   public String[] getStringEnumeration (String baseKey, int maxSize) {
1201     String[] arr = new String[maxSize];
1202     int max=-1;
1203
1204     StringBuilder sb = new StringBuilder(baseKey);
1205     sb.append('.');
1206     int len = baseKey.length()+1;
1207
1208     for (int i=0; i<maxSize; i++) {
1209       sb.setLength(len);
1210       sb.append(i);
1211
1212       String v = getString(sb.toString());
1213       if (v != null) {
1214         arr[i] = v;
1215         max = i;
1216       }
1217     }
1218
1219     if (max >= 0) {
1220       max++;
1221       if (max < maxSize) {
1222         String[] a = new String[max];
1223         System.arraycopy(arr,0,a,0,max);
1224         return a;
1225       } else {
1226         return arr;
1227       }
1228     } else {
1229       return null;
1230     }
1231   }
1232
1233   public String[] getKeysStartingWith (String prefix){
1234     ArrayList<String> list = new ArrayList<String>();
1235
1236     for (Enumeration e = keys(); e.hasMoreElements(); ){
1237       String k = e.nextElement().toString();
1238       if (k.startsWith(prefix)){
1239         list.add(k);
1240       }
1241     }
1242
1243     return list.toArray(new String[list.size()]);
1244   }
1245
1246   public String[] getKeyComponents (String key){
1247     return key.split("\\.");
1248   }
1249
1250   public int[] getIntArray (String key) throws JPFConfigException {
1251     String v = getProperty(key);
1252
1253     if (v != null) {
1254       String[] sa = split(v);
1255       int[] a = new int[sa.length];
1256       int i = 0;
1257       try {
1258         for (; i<sa.length; i++) {
1259           String s = sa[i];
1260           int val;
1261           if (s.startsWith("0x")){
1262             val = Integer.parseInt(s.substring(2),16); 
1263           } else {
1264             val = Integer.parseInt(s);
1265           }
1266           a[i] = val;
1267         }
1268         return a;
1269       } catch (NumberFormatException nfx) {
1270         throw new JPFConfigException("illegal int[] element in '" + key + "' = \"" + sa[i] + '"');
1271       }
1272     } else {
1273       return null;
1274     }
1275   }
1276   public int[] getIntArray (String key, int... defaultValues){
1277     int[] val = getIntArray(key);
1278     if (val == null){
1279       return defaultValues;
1280     } else {
1281       return val;
1282     }
1283   }
1284
1285   public long getDuration (String key, long defValue) {
1286     String v = getProperty(key);
1287     if (v != null) {
1288       long d = 0;
1289
1290       if (v.indexOf(':') > 0){
1291         String[] a = v.split(":");
1292         if (a.length > 3){
1293           //log.severe("illegal duration: " + key + "=" + v);
1294           return defValue;
1295         }
1296         int m = 1000;
1297         for (int i=a.length-1; i>=0; i--, m*=60){
1298           try {
1299             int n = Integer.parseInt(a[i]);
1300             d += m*n;
1301           } catch (NumberFormatException nfx) {
1302             throw new JPFConfigException("illegal duration element in '" + key + "' = \"" + v + '"');
1303           }
1304         }
1305
1306       } else {
1307         try {
1308           d = Long.parseLong(v);
1309         } catch (NumberFormatException nfx) {
1310           throw new JPFConfigException("illegal duration element in '" + key + "' = \"" + v + '"');
1311         }
1312       }
1313
1314       return d;
1315     }
1316
1317     return defValue;
1318   }
1319
1320   public int getInt(String key) {
1321     return getInt(key, 0);
1322   }
1323
1324   public int getInt(String key, int defValue) {
1325     String v = getProperty(key);
1326     if (v != null) {
1327       if (MAX.equals(v)){
1328         return Integer.MAX_VALUE;
1329       } else {
1330         try {
1331           return Integer.parseInt(v);
1332         } catch (NumberFormatException nfx) {
1333           throw new JPFConfigException("illegal int element in '" + key + "' = \"" + v + '"');
1334         }
1335       }
1336     }
1337
1338     return defValue;
1339   }
1340
1341   public long getLong(String key) {
1342     return getLong(key, 0L);
1343   }
1344
1345   public long getLong(String key, long defValue) {
1346     String v = getProperty(key);
1347     if (v != null) {
1348       if (MAX.equals(v)){
1349         return Long.MAX_VALUE;
1350       } else {
1351         try {
1352           return Long.parseLong(v);
1353         } catch (NumberFormatException nfx) {
1354           throw new JPFConfigException("illegal long element in '" + key + "' = \"" + v + '"');
1355         }
1356       }
1357     }
1358
1359     return defValue;
1360   }
1361
1362   public long[] getLongArray (String key) throws JPFConfigException {
1363     String v = getProperty(key);
1364
1365     if (v != null) {
1366       String[] sa = split(v);
1367       long[] a = new long[sa.length];
1368       int i = 0;
1369       try {
1370         for (; i<sa.length; i++) {
1371           a[i] = Long.parseLong(sa[i]);
1372         }
1373         return a;
1374       } catch (NumberFormatException nfx) {
1375         throw new JPFConfigException("illegal long[] element in " + key + " = " + sa[i]);
1376       }
1377     } else {
1378       return null;
1379     }
1380   }
1381
1382   public long[] getLongArray (String key, long... defaultValues){
1383     long[] val = getLongArray(key);
1384     if (val != null){
1385       return val;
1386     } else {
1387       return defaultValues;
1388     } 
1389   }
1390
1391   public float getFloat (String key) {
1392     return getFloat(key, 0.0f);
1393   }
1394
1395   public float getFloat (String key, float defValue) {
1396     String v = getProperty(key);
1397     if (v != null) {
1398       try {
1399         return Float.parseFloat(v);
1400       } catch (NumberFormatException nfx) {
1401         throw new JPFConfigException("illegal float element in '" + key + "' = \"" + v + '"');
1402       }
1403     }
1404
1405     return defValue;
1406   }
1407   
1408   public float[] getFloatArray (String key) throws JPFConfigException {
1409     String v = getProperty(key);
1410
1411     if (v != null) {
1412       String[] sa = split(v);
1413       float[] a = new float[sa.length];
1414       int i = 0;
1415       try {
1416         for (; i<sa.length; i++) {
1417           a[i] = Float.parseFloat(sa[i]);
1418         }
1419         return a;
1420       } catch (NumberFormatException nfx) {
1421         throw new JPFConfigException("illegal float[] element in " + key + " = " + sa[i]);
1422       }
1423     } else {
1424       return null;
1425     }
1426   }
1427   public float[] getFloatArray (String key, float... defaultValues){
1428     float[] v = getFloatArray( key);
1429     if (v != null){
1430       return v;
1431     } else {
1432       return defaultValues;
1433     }
1434   }
1435   
1436   
1437   public double getDouble (String key) {
1438     return getDouble(key, 0.0);
1439   }
1440
1441   public double getDouble (String key, double defValue) {
1442     String v = getProperty(key);
1443     if (v != null) {
1444       try {
1445         return Double.parseDouble(v);
1446       } catch (NumberFormatException nfx) {
1447         throw new JPFConfigException("illegal double element in '" + key + "' = \"" + v + '"');
1448       }
1449     }
1450
1451     return defValue;
1452   }
1453
1454   public double[] getDoubleArray (String key) throws JPFConfigException {
1455     String v = getProperty(key);
1456
1457     if (v != null) {
1458       String[] sa = split(v);
1459       double[] a = new double[sa.length];
1460       int i = 0;
1461       try {
1462         for (; i<sa.length; i++) {
1463           a[i] = Double.parseDouble(sa[i]);
1464         }
1465         return a;
1466       } catch (NumberFormatException nfx) {
1467         throw new JPFConfigException("illegal double[] element in " + key + " = " + sa[i]);
1468       }
1469     } else {
1470       return null;
1471     }
1472   }
1473   public double[] getDoubleArray (String key, double... defaultValues){
1474     double[] v = getDoubleArray( key);
1475     if (v != null){
1476       return v;
1477     } else {
1478       return defaultValues;
1479     }
1480   }
1481
1482   public <T extends Enum<T>> T getEnum( String key, T[] values, T defValue){
1483     String v = getProperty(key);
1484
1485     if (v != null){
1486       for (T t : values){
1487         if (v.equalsIgnoreCase(t.name())){
1488           return t;
1489         }
1490       }
1491       
1492       throw new JPFConfigException("unknown enum value for " + key + " = " + v);
1493       
1494     } else {
1495       return defValue;
1496     }
1497   }
1498
1499   public String getString(String key) {
1500     return getProperty(key);
1501   }
1502
1503   public String getString(String key, String defValue) {
1504     String s = getProperty(key);
1505     if (s != null) {
1506       return s;
1507     } else {
1508       return defValue;
1509     }
1510   }
1511
1512   /**
1513    * return memory size in bytes, or 'defValue' if not in dictionary. Encoding
1514    * can have a 'M' or 'k' postfix, values have to be positive integers (decimal
1515    * notation)
1516    */
1517   public long getMemorySize(String key, long defValue) {
1518     String v = getProperty(key);
1519     long sz = defValue;
1520
1521     if (v != null) {
1522       int n = v.length() - 1;
1523       try {
1524         char c = v.charAt(n);
1525
1526         if ((c == 'M') || (c == 'm')) {
1527           sz = Long.parseLong(v.substring(0, n)) << 20;
1528         } else if ((c == 'K') || (c == 'k')) {
1529           sz = Long.parseLong(v.substring(0, n)) << 10;
1530         } else {
1531           sz = Long.parseLong(v);
1532         }
1533
1534       } catch (NumberFormatException nfx) {
1535         throw new JPFConfigException("illegal memory size element in '" + key + "' = \"" + v + '"');
1536       }
1537     }
1538
1539     return sz;
1540   }
1541
1542   public HashSet<String> getStringSet(String key){
1543     String v = getProperty(key);
1544     if (v != null && (v.length() > 0)) {
1545       HashSet<String> hs = new HashSet<String>();
1546       for (String s : split(v)) {
1547         hs.add(s);
1548       }
1549       return hs;
1550     }
1551
1552     return null;
1553     
1554   }
1555   
1556   public HashSet<String> getNonEmptyStringSet(String key){
1557     HashSet<String> hs = getStringSet(key);
1558     if (hs != null && hs.isEmpty()) {
1559       return null;
1560     } else {
1561       return hs;
1562     }
1563   }
1564     
1565   public String[] getStringArray(String key) {
1566     String v = getProperty(key);
1567     if (v != null && (v.length() > 0)) {
1568       return split(v);
1569     }
1570
1571     return null;
1572   }
1573
1574   public String[] getStringArray(String key, char[] delims) {
1575     String v = getProperty(key);
1576     if (v != null && (v.length() > 0)) {
1577       return split(v,delims);
1578     }
1579
1580     return null;
1581   }
1582
1583   public String[] getCompactTrimmedStringArray (String key){
1584     String[] a = getStringArray(key);
1585
1586     if (a != null) {
1587       for (int i = 0; i < a.length; i++) {
1588         String s = a[i];
1589         if (s != null && s.length() > 0) {
1590           a[i] = s.trim();
1591         }
1592       }
1593
1594       return removeEmptyStrings(a);
1595
1596     } else {
1597       return EMPTY_STRING_ARRAY;
1598     }
1599   }
1600
1601   public String[] getCompactStringArray(String key){
1602     return removeEmptyStrings(getStringArray(key));
1603   }
1604
1605   
1606   public String[] getStringArray(String key, String[] def){
1607     String v = getProperty(key);
1608     if (v != null && (v.length() > 0)) {
1609       return split(v);
1610     } else {
1611       return def;
1612     }
1613   }
1614
1615   public static String[] removeEmptyStrings (String[] a){
1616     if (a != null) {
1617       int n = 0;
1618       for (int i=0; i<a.length; i++){
1619         if (a[i].length() > 0){
1620           n++;
1621         }
1622       }
1623
1624       if (n < a.length){ // we have empty strings in the split
1625         String[] r = new String[n];
1626         for (int i=0, j=0; i<a.length; i++){
1627           if (a[i].length() > 0){
1628             r[j++] = a[i];
1629             if (j == n){
1630               break;
1631             }
1632           }
1633         }
1634         return r;
1635
1636       } else {
1637         return a;
1638       }
1639     }
1640
1641     return null;
1642   }
1643
1644
1645   /**
1646    * return an [optional] id part of a property value (all that follows the first '@')
1647    */
1648   String getIdPart (String key) {
1649     String v = getProperty(key);
1650     if ((v != null) && (v.length() > 0)) {
1651       int i = v.indexOf('@');
1652       if (i >= 0){
1653         return v.substring(i+1);
1654       }
1655     }
1656
1657     return null;
1658   }
1659
1660   public Class<?> asClass (String v) throws JPFConfigException {
1661     if ((v != null) && (v.length() > 0)) {
1662       v = stripId(v);
1663       v = expandClassName(v);
1664       try {
1665         return loader.loadClass(v);
1666       } catch (ClassNotFoundException cfx) {
1667         throw new JPFConfigException("class not found " + v + " by classloader: " + loader);
1668       } catch (ExceptionInInitializerError ix) {
1669         throw new JPFConfigException("class initialization of " + v + " failed: " + ix,
1670             ix);
1671       }
1672     }
1673
1674     return null;    
1675   }
1676       
1677   public <T> Class<? extends T> getClass(String key, Class<T> type) throws JPFConfigException {
1678     Class<?> cls = asClass( getProperty(key));
1679     if (cls != null) {
1680       if (type.isAssignableFrom(cls)) {
1681         return cls.asSubclass(type);
1682       } else {
1683         throw new JPFConfigException("classname entry for: \"" + key + "\" not of type: " + type.getName());
1684       }
1685     }
1686     return null;
1687   }
1688   
1689     
1690   public Class<?> getClass(String key) throws JPFConfigException {
1691     return asClass( getProperty(key));
1692   }
1693   
1694   public Class<?> getEssentialClass(String key) throws JPFConfigException {
1695     Class<?> cls = getClass(key);
1696     if (cls == null) {
1697       throw new JPFConfigException("no classname entry for: \"" + key + "\"");
1698     }
1699
1700     return cls;
1701   }
1702   
1703   String stripId (String v) {
1704     int i = v.indexOf('@');
1705     if (i >= 0) {
1706       return v.substring(0,i);
1707     } else {
1708       return v;
1709     }
1710   }
1711
1712   String getId (String v){
1713     int i = v.indexOf('@');
1714     if (i >= 0) {
1715       return v.substring(i+1);
1716     } else {
1717       return null;
1718     }
1719   }
1720
1721   String expandClassName (String clsName) {
1722     if (clsName != null && clsName.length() > 0 && clsName.charAt(0) == '.') {
1723       return "gov.nasa.jpf" + clsName;
1724     } else {
1725       return clsName;
1726     }
1727   }
1728
1729   
1730   public Class<?>[] getClasses(String key) throws JPFConfigException {
1731     String[] v = getStringArray(key);
1732     if (v != null) {
1733       int n = v.length;
1734       Class<?>[] a = new Class[n];
1735       for (int i = 0; i < n; i++) {
1736         String clsName = expandClassName(v[i]);
1737         if (clsName != null && clsName.length() > 0){
1738           try {
1739             clsName = stripId(clsName);
1740             a[i] = loader.loadClass(clsName);
1741           } catch (ClassNotFoundException cnfx) {
1742             throw new JPFConfigException("class not found " + v[i]);
1743           } catch (ExceptionInInitializerError ix) {
1744             throw new JPFConfigException("class initialization of " + v[i] + " failed: " + ix, ix);
1745           }
1746         }
1747       }
1748
1749       return a;
1750     }
1751
1752     return null;
1753   }
1754   
1755   /**
1756    * this one is used to instantiate objects from a list of keys that share
1757    * the same prefix, e.g.
1758    * 
1759    *  shell.panels = config,site
1760    *  shell.panels.site = .shell.panels.SitePanel
1761    *  shell.panels.config = .shell.panels.ConfigPanel
1762    *  ...
1763    * 
1764    * note that we specify default class names, not classes, so that the classes
1765    * get loaded through our own loader at call time (they might not be visible
1766    * to our caller)
1767    */
1768   public <T> T[] getGroupInstances (String keyPrefix, String keyPostfix, Class<T> type, 
1769           String... defaultClsNames) throws JPFConfigException {
1770     
1771     String[] ids = getCompactTrimmedStringArray(keyPrefix);
1772     
1773     if (ids.length > 0){
1774       keyPrefix = keyPrefix + '.';
1775       T[] arr = (T[]) Array.newInstance(type, ids.length);
1776       
1777       for(int i = 0; i < ids.length; i++){
1778         String key = keyPrefix + ids[i];
1779         if (keyPostfix != null){
1780           key = key + keyPostfix;
1781         }
1782         arr[i] = getEssentialInstance(key, type);
1783       }
1784       
1785       return arr;
1786       
1787     } else {
1788       T[] arr = (T[]) Array.newInstance(type, defaultClsNames.length);
1789               
1790       for (int i=0; i<arr.length; i++){
1791         arr[i] = getInstance((String)null, defaultClsNames[i], type);
1792         if (arr[i] == null){
1793           exception("cannot instantiate default type " + defaultClsNames[i]);
1794         }
1795       }
1796       
1797       return arr;
1798     }
1799   }
1800   
1801   // <2do> - that's kind of kludged together, not very efficient
1802   String[] getIds (String key) {
1803     String v = getProperty(key);
1804
1805     if (v != null) {
1806       int i = v.indexOf('@');
1807       if (i >= 0) { // Ok, we have ids
1808         String[] a = split(v);
1809         String[] ids = new String[a.length];
1810         for (i = 0; i<a.length; i++) {
1811           ids[i] = getId(a[i]);
1812         }
1813         return ids;
1814       }
1815     }
1816
1817     return null;
1818   }
1819
1820   public <T> ArrayList<T> getInstances(String key, Class<T> type) throws JPFConfigException {
1821
1822     Class<?>[] argTypes = { Config.class };
1823     Object[] args = { this };
1824
1825     return getInstances(key,type,argTypes,args);
1826   }
1827   
1828   public <T> ArrayList<T> getInstances(String key, Class<T> type, Class<?>[]argTypes, Object[] args)
1829                                                       throws JPFConfigException {
1830     Class<?>[] c = getClasses(key);
1831
1832     if (c != null) {
1833       String[] ids = getIds(key);
1834
1835       ArrayList<T> a = new ArrayList<T>(c.length);
1836
1837       for (int i = 0; i < c.length; i++) {
1838         String id = (ids != null) ? ids[i] : null;
1839         T listener = getInstance(key, c[i], type, argTypes, args, id);
1840         if (listener != null) {
1841           a.add( listener);
1842         } else {
1843           // should report here
1844         }
1845       }
1846
1847       return a;
1848       
1849     } else {
1850       // should report here
1851     }
1852
1853     return null;
1854   }
1855   
1856   public <T> T getInstance(String key, Class<T> type, String defClsName) throws JPFConfigException {
1857     Class<?>[] argTypes = CONFIG_ARGTYPES;
1858     Object[] args = CONFIG_ARGS;
1859
1860     Class<?> cls = getClass(key);
1861     String id = getIdPart(key);
1862
1863     if (cls == null) {
1864       try {
1865         cls = loader.loadClass(defClsName);
1866       } catch (ClassNotFoundException cfx) {
1867         throw new JPFConfigException("class not found " + defClsName);
1868       } catch (ExceptionInInitializerError ix) {
1869         throw new JPFConfigException("class initialization of " + defClsName + " failed: " + ix, ix);
1870       }
1871     }
1872     
1873     return getInstance(key, cls, type, argTypes, args, id);
1874   }
1875
1876   public <T> T getInstance(String key, Class<T> type) throws JPFConfigException {
1877     Class<?>[] argTypes = CONFIG_ARGTYPES;
1878     Object[] args = CONFIG_ARGS;
1879
1880     return getInstance(key, type, argTypes, args);
1881   }
1882     
1883   public <T> T getInstance(String key, Class<T> type, Class<?>[] argTypes,
1884                             Object[] args) throws JPFConfigException {
1885     Class<?> cls = getClass(key);
1886     String id = getIdPart(key);
1887
1888     if (cls != null) {
1889       return getInstance(key, cls, type, argTypes, args, id);
1890     } else {
1891       return null;
1892     }
1893   }
1894   
1895   public <T> T getInstance(String key, Class<T> type, Object arg1, Object arg2)  throws JPFConfigException {
1896     Class<?>[] argTypes = new Class<?>[2];
1897     argTypes[0] = arg1.getClass();
1898     argTypes[1] = arg2.getClass();
1899
1900     Object[] args = new Object[2];
1901     args[0] = arg1;
1902     args[1] = arg2;
1903
1904     return getInstance(key, type, argTypes, args);
1905   }
1906
1907
1908   public <T> T getEssentialInstance(String key, Class<T> type) throws JPFConfigException {
1909     Class<?>[] argTypes = { Config.class };
1910     Object[] args = { this };
1911     return getEssentialInstance(key, type, argTypes, args);
1912   }
1913
1914   /**
1915    * just a convenience method for ctor calls that take two arguments
1916    */
1917   public <T> T getEssentialInstance(String key, Class<T> type, Object arg1, Object arg2)  throws JPFConfigException {
1918     Class<?>[] argTypes = new Class<?>[2];
1919     argTypes[0] = arg1.getClass();
1920     argTypes[1] = arg2.getClass();
1921
1922     Object[] args = new Object[2];
1923     args[0] = arg1;
1924     args[1] = arg2;
1925
1926     return getEssentialInstance(key, type, argTypes, args);
1927   }
1928
1929   public <T> T getEssentialInstance(String key, Class<T> type, Class<?>[] argTypes, Object[] args) throws JPFConfigException {
1930     Class<?> cls = getEssentialClass(key);
1931     String id = getIdPart(key);
1932
1933     return getInstance(key, cls, type, argTypes, args, id);
1934   }
1935
1936   public <T> T getInstance (String id, String clsName, Class<T> type, Class<?>[] argTypes, Object[] args) throws JPFConfigException {
1937     Class<?> cls = asClass(clsName);
1938     
1939     if (cls != null) {
1940       return getInstance(id, cls, type, argTypes, args, id);
1941     } else {
1942       return null;
1943     }
1944   }
1945   
1946   public <T> T getInstance (String id, String clsName, Class<T> type) throws JPFConfigException {
1947     Class<?>[] argTypes = CONFIG_ARGTYPES;
1948     Object[] args = CONFIG_ARGS;
1949
1950     Class<?> cls = asClass(clsName);
1951     
1952     if (cls != null) {
1953       return getInstance(id, cls, type, argTypes, args, id);
1954     } else {
1955       return null;
1956     }
1957   }
1958   
1959   /**
1960    * this is our private instantiation workhorse - try to instantiate an object of
1961    * class 'cls' by using the following ordered set of ctors 1. <cls>(
1962    * <argTypes>) 2. <cls>(Config) 3. <cls>() if all of that fails, or there was
1963    * a 'type' provided the instantiated object does not comply with, return null
1964    */
1965   <T> T getInstance(String key, Class<?> cls, Class<T> type, Class<?>[] argTypes,
1966                      Object[] args, String id) throws JPFConfigException {
1967     Object o = null;
1968     Constructor<?> ctor = null;
1969
1970     if (cls == null) {
1971       return null;
1972     }
1973
1974     if (id != null) { // check first if we already have this one instantiated as a singleton
1975       if (singletons == null) {
1976         singletons = new HashMap<String,Object>();
1977       } else {
1978         o = type.cast(singletons.get(id));
1979       }
1980     }
1981
1982     while (o == null) {
1983       try {
1984         ctor = cls.getConstructor(argTypes);
1985         o = ctor.newInstance(args);
1986       } catch (NoSuchMethodException nmx) {
1987          
1988         if ((argTypes.length > 1) || ((argTypes.length == 1) && (argTypes[0] != Config.class))) {
1989           // fallback 1: try a single Config param
1990           argTypes = CONFIG_ARGTYPES;
1991           args = CONFIG_ARGS;
1992
1993         } else if (argTypes.length > 0) {
1994           // fallback 2: try the default ctor
1995           argTypes = NO_ARGTYPES;
1996           args = NO_ARGS;
1997
1998         } else {
1999           // Ok, there is no suitable ctor, bail out
2000           throw new JPFConfigException(key, cls, "no suitable ctor found");
2001         }
2002       } catch (IllegalAccessException iacc) {
2003         throw new JPFConfigException(key, cls, "\n> ctor not accessible: "
2004             + getMethodSignature(ctor));
2005       } catch (IllegalArgumentException iarg) {
2006         throw new JPFConfigException(key, cls, "\n> illegal constructor arguments: "
2007             + getMethodSignature(ctor));
2008       } catch (InvocationTargetException ix) {
2009         Throwable tx = ix.getTargetException();
2010         if (tx instanceof JPFConfigException) {
2011           throw new JPFConfigException(tx.getMessage() + "\n> used within \"" + key
2012               + "\" instantiation of " + cls);
2013         } else {
2014           throw new JPFConfigException(key, cls, "\n> exception in "
2015               + getMethodSignature(ctor) + ":\n>> " + tx, tx);
2016         }
2017       } catch (InstantiationException ivt) {
2018         throw new JPFConfigException(key, cls,
2019             "\n> abstract class cannot be instantiated");
2020       } catch (ExceptionInInitializerError eie) {
2021         throw new JPFConfigException(key, cls, "\n> static initialization failed:\n>> "
2022             + eie.getException(), eie.getException());
2023       }
2024     }
2025
2026     // check type
2027     if (!type.isInstance(o)) {
2028       throw new JPFConfigException(key, cls, "\n> instance not of type: "
2029           + type.getName());
2030     }
2031
2032     if (id != null) { // add to singletons (in case it's not already in there)
2033       singletons.put(id, o);
2034     }
2035
2036     return type.cast(o); // safe according to above
2037   }
2038
2039   public String getMethodSignature(Constructor<?> ctor) {
2040     StringBuilder sb = new StringBuilder(ctor.getName());
2041     sb.append('(');
2042     Class<?>[] argTypes = ctor.getParameterTypes();
2043     for (int i = 0; i < argTypes.length; i++) {
2044       if (i > 0) {
2045         sb.append(',');
2046       }
2047       sb.append(argTypes[i].getName());
2048     }
2049     sb.append(')');
2050     return sb.toString();
2051   }
2052
2053   public boolean hasValue(String key) {
2054     String v = getProperty(key);
2055     return ((v != null) && (v.length() > 0));
2056   }
2057
2058   public boolean hasValueIgnoreCase(String key, String value) {
2059     String v = getProperty(key);
2060     if (v != null) {
2061       return v.equalsIgnoreCase(value);
2062     }
2063
2064     return false;
2065   }
2066
2067   public int getChoiceIndexIgnoreCase(String key, String[] choices) {
2068     String v = getProperty(key);
2069
2070     if ((v != null) && (choices != null)) {
2071       for (int i = 0; i < choices.length; i++) {
2072         if (v.equalsIgnoreCase(choices[i])) {
2073           return i;
2074         }
2075       }
2076     }
2077
2078     return -1;
2079   }
2080
2081   public URL getURL (String key){
2082     String v = getProperty(key);
2083     if (v != null) {
2084       try {
2085         return FileUtils.getURL(v);
2086       } catch (Throwable x){
2087         throw exception("malformed URL: " + v);
2088       }
2089     } else {
2090       return null;
2091     }
2092   }
2093
2094   public File[] getPathArray (String key) {    
2095     String v = getProperty(key);
2096     if (v != null) {
2097       String[] pe = removeEmptyStrings( pathSplit(v));
2098       
2099       if (pe != null && pe.length > 0) {
2100         File[] files = new File[pe.length];
2101         for (int i=0; i<files.length; i++) {
2102           String path = FileUtils.asPlatformPath(pe[i]);
2103           files[i] = new File(path);
2104         }
2105         return files;
2106       }      
2107     }
2108
2109     return new File[0];
2110   }
2111
2112   public File getPath (String key) {
2113     String v = getProperty(key);
2114     if (v != null) {
2115       return new File(FileUtils.asPlatformPath(v));
2116     }
2117     
2118     return null;
2119   }
2120
2121   static final char[] UNIX_PATH_SEPARATORS = {',', ';', ':' };
2122   static final char[] WINDOWS_PATH_SEPARATORS = {',', ';' };
2123
2124   protected String[] pathSplit (String input){
2125     if (File.pathSeparatorChar == ':'){
2126       return split( input, UNIX_PATH_SEPARATORS);
2127     } else {
2128       return split( input, WINDOWS_PATH_SEPARATORS);
2129     }
2130   }
2131
2132   static final char[] DELIMS = { ',', ';' };
2133
2134   /**
2135    * our own version of split, which handles "`" quoting, and breaks on non-quoted
2136    * ',' and ';' chars. We need this so that we can use ';' separated lists in
2137    * JPF property files, but still can use quoted ';' if we absolutely have to
2138    * specify Java signatures. On the other hand, we can't quote with '\' because
2139    * that would make Windows paths even more terrible.
2140    * regexes are bad at quoting, and this is more efficient anyways
2141    */
2142   protected String[] split (String input){
2143     return split(input, DELIMS);
2144   }
2145
2146   private boolean isDelim(char[] delim, char c){
2147     for (int i=0; i<delim.length; i++){
2148       if (c == delim[i]){
2149         return true;
2150       }
2151     }
2152     return false;
2153   }
2154
2155   protected String[] split (String input, char[] delim){
2156     int n = input.length();
2157     ArrayList<String> elements = new ArrayList<String>();
2158     boolean quote = false;
2159
2160     char[] buf = new char[128];
2161     int k=0;
2162
2163     for (int i=0; i<n; i++){
2164       char c = input.charAt(i);
2165
2166       if (!quote) {
2167         if (isDelim(delim,c)){ // element separator
2168           elements.add( new String(buf, 0, k));
2169           k = 0;
2170           continue;
2171         } else if (c=='`') {
2172           quote = true;
2173           continue;
2174         }
2175       }
2176
2177       if (k >= buf.length){
2178         char[] newBuf = new char[buf.length+128];
2179         System.arraycopy(buf, 0, newBuf, 0, k);
2180         buf = newBuf;
2181       }
2182       buf[k++] = c;
2183       quote = false;
2184     }
2185
2186     if (k>0){
2187       elements.add( new String(buf, 0, k));
2188     }
2189
2190     return elements.toArray(new String[elements.size()]);
2191   }
2192
2193   static final String UNINITIALIZED = "uninitialized";
2194   // this is where we store the initial values in case we have to recollect
2195   String initialNativeClasspath = UNINITIALIZED, 
2196           initialClasspath = UNINITIALIZED, 
2197           initialSourcepath = UNINITIALIZED, 
2198           initialPeerPackages = UNINITIALIZED,
2199           initialNativeLibraries = UNINITIALIZED;
2200   
2201
2202   /**
2203    * this resets to what was explicitly set in the config files
2204    */
2205   public void resetGlobalPaths() {
2206     if (initialNativeClasspath == UNINITIALIZED){
2207       initialNativeClasspath = getString("native_classpath");
2208     } else {
2209       put0( "native_classpath", initialNativeClasspath);
2210     }
2211
2212     if (initialClasspath == UNINITIALIZED){
2213       initialClasspath = getString("classpath");
2214     } else {
2215       put0( "classpath", initialClasspath);
2216     }
2217     
2218     if (initialSourcepath == UNINITIALIZED){
2219       initialSourcepath = getString("sourcepath");
2220     } else {
2221       put0( "sourcepath", initialSourcepath);
2222     }
2223
2224     if (initialPeerPackages == UNINITIALIZED){
2225       initialPeerPackages = getString("peer_packages");
2226     } else {
2227       put0( "peer_packages", initialPeerPackages);
2228     }
2229
2230     if (initialNativeLibraries == UNINITIALIZED){
2231       initialNativeLibraries = getString("native_libraries");
2232     } else {
2233       put0( "native_libraries", initialNativeLibraries);
2234     }
2235   }
2236
2237   /**
2238    * collect all the <project>.{native_classpath,classpath,sourcepath,peer_packages,native_libraries}
2239    * and append them to the global settings
2240    *
2241    * NOTE - this is now called from within initClassLoader, which should only happen once and
2242    * is the first time we really need the global paths.
2243    *
2244    * <2do> this is Ok for native_classpath and native_libraries, but we should probably do
2245    * classpath, sourcepath and peer_packages separately (they can be collected later)
2246    */
2247   public void collectGlobalPaths() {
2248         
2249     // note - this is in the order of entry, i.e. reflects priorities
2250     // we have to process this in reverse order so that later entries are prioritized
2251     String[] keys = getEntrySequence();
2252
2253     String nativeLibKey = "." + System.getProperty("os.name") +
2254             '.' + System.getProperty("os.arch") + ".native_libraries";
2255
2256     for (int i = keys.length-1; i>=0; i--){
2257       String k = keys[i];
2258       if (k.endsWith(".native_classpath")){
2259         appendPath("native_classpath", k);
2260         
2261       } else if (k.endsWith(".classpath")){
2262         appendPath("classpath", k);
2263         
2264       } else if (k.endsWith(".sourcepath")){        
2265         appendPath("sourcepath", k);
2266         
2267       } else if (k.endsWith("peer_packages")){
2268         append("peer_packages", getString(k), ",");
2269         
2270       } else if (k.endsWith(nativeLibKey)){
2271         appendPath("native_libraries", k);
2272       }
2273     }
2274   }
2275
2276   
2277   static Pattern absPath = Pattern.compile("(?:[a-zA-Z]:)?[/\\\\].*");
2278
2279   void appendPath (String pathKey, String key){
2280     String projName = key.substring(0, key.indexOf('.'));
2281     String pathPrefix = null;
2282
2283     if (projName.isEmpty()){
2284       pathPrefix = new File(".").getAbsolutePath();
2285     } else {
2286       pathPrefix = getString(projName);
2287     }
2288
2289     if (pathPrefix != null){
2290       pathPrefix += '/';
2291
2292       String[] elements = getCompactStringArray(key);
2293       if (elements != null){
2294         for (String e : elements) {
2295           if (e != null && e.length()>0){
2296
2297             // if this entry is not an absolute path, or doesn't start with
2298             // the project path, prepend the project path
2299             if (!(absPath.matcher(e).matches()) && !e.startsWith(pathPrefix)) {
2300               e = pathPrefix + e;
2301             }
2302
2303             append(pathKey, e);
2304           }
2305         }
2306       }
2307
2308     } else {
2309       //throw new JPFConfigException("no project path for " + key);
2310     }
2311   }
2312
2313
2314   //--- our modification interface
2315
2316   /**
2317    * iterate over all keys, if a key starts with the provided keyPrefix, add
2318    * this value under the corresponding key suffix. For example:
2319    *
2320    *  test.report.console.finished = result
2321    *
2322    *    -> prompotePropertyCategory("test.") ->
2323    *
2324    *  report.console.finished = result
2325    *
2326    * if a matching key has an IGNORE_VALUE value ("-"), the entry is *not* promoted
2327    * (we need this to override promoted keys)
2328    */
2329   public void promotePropertyCategory (String keyPrefix){
2330     int prefixLen = keyPrefix.length();
2331     
2332     // HashTable does not support adding elements while iterating over the entrySet 
2333     ArrayList<Map.Entry<Object,Object>> promoted = null;
2334     
2335     for (Map.Entry<Object,Object> e : entrySet()){
2336       Object k = e.getKey();
2337       if (k instanceof String){
2338         String key = (String)k;
2339         if (key.startsWith(keyPrefix)){
2340           Object v = e.getValue();
2341           if (! IGNORE_VALUE.equals(v)){
2342             if (promoted == null){
2343               promoted = new ArrayList<Map.Entry<Object,Object>>();
2344             }
2345             promoted.add(e);
2346           }
2347         }
2348       }
2349     }
2350     
2351     if (promoted != null){
2352       for (Map.Entry<Object, Object> e : promoted) {
2353         String key = (String) e.getKey();
2354         key = key.substring(prefixLen);
2355
2356         put(key, e.getValue());
2357       }
2358     }
2359   }
2360
2361   
2362   @Override
2363   public Object setProperty (String key, String newValue) {    
2364     Object oldValue = put(key, newValue);    
2365     notifyPropertyChangeListeners(key, (String)oldValue, newValue);
2366     return oldValue;
2367   }
2368
2369   public void parse (String s) {
2370     
2371     int i = s.indexOf("=");
2372     if (i > 0) {
2373       String key, val;
2374       
2375       if (i > 1 && s.charAt(i-1)=='+') { // append
2376         key = s.substring(0, i-1).trim();
2377         val = s.substring(i+1); // it's going to be normalized anyways
2378         append(key, val);
2379         
2380       } else { // put
2381         key = s.substring(0, i).trim();
2382         val = s.substring(i+1);
2383         setProperty(key, val);
2384       }
2385       
2386     }
2387   }
2388   
2389   protected void notifyPropertyChangeListeners (String key, String oldValue, String newValue) {
2390     if (changeListeners != null) {
2391       for (ConfigChangeListener l : changeListeners) {
2392         l.propertyChanged(this, key, oldValue, newValue);
2393       }
2394     }    
2395   }
2396   
2397   public String[] asStringArray (String s){
2398     return split(s);
2399   }
2400   
2401   public TreeMap<Object,Object> asOrderedMap() {
2402     TreeMap<Object,Object> map = new TreeMap<Object,Object>();
2403     map.putAll(this);
2404     return map;
2405   }
2406   
2407   //--- various debugging methods
2408
2409   public void print (PrintWriter pw) {
2410     pw.println("----------- Config contents");
2411
2412     // just how much do you have to do to get a printout with keys in alphabetical order :<
2413     TreeSet<String> kset = new TreeSet<String>();
2414     for (Enumeration<?> e = propertyNames(); e.hasMoreElements();) {
2415       Object k = e.nextElement();
2416       if (k instanceof String) {
2417         kset.add( (String)k);
2418       }
2419     }
2420
2421     for (String key : kset) {
2422       String val = getProperty(key);
2423       pw.print(key);
2424       pw.print(" = ");
2425       pw.println(val);
2426     }
2427
2428     pw.flush();
2429   }
2430
2431   public void printSources (PrintWriter pw) {
2432     pw.println("----------- Config sources");
2433     for (Object src : sources){
2434       pw.println(src);
2435     }    
2436   }
2437   
2438   public void printEntries() {
2439     PrintWriter pw = new PrintWriter(System.out);
2440     print(pw);
2441   }
2442
2443   public String getSourceName (Object src){
2444     if (src instanceof File){
2445       return ((File)src).getAbsolutePath();
2446     } else if (src instanceof URL){
2447       return ((URL)src).toString();
2448     } else {
2449       return src.toString();
2450     }
2451   }
2452   
2453   public List<Object> getSources() {
2454     return sources;
2455   }
2456   
2457   public void printStatus(Logger log) {
2458     int idx = 0;
2459     
2460     for (Object src : sources){
2461       if (src instanceof File){
2462         log.config("configuration source " + idx++ + " : " + getSourceName(src));
2463       }
2464     }
2465   }
2466
2467
2468 }