001    // copied from commons-collections-3.1
002    /*
003     *  Copyright 2001-2004 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    package gov.lbl.dsd.sea.nio.util;
018    
019    import java.io.File;
020    import java.io.FileInputStream;
021    import java.io.IOException;
022    import java.io.InputStream;
023    import java.io.InputStreamReader;
024    import java.io.LineNumberReader;
025    import java.io.OutputStream;
026    import java.io.PrintWriter;
027    import java.io.Reader;
028    import java.io.UnsupportedEncodingException;
029    import java.util.ArrayList;
030    import java.util.Enumeration;
031    import java.util.Hashtable;
032    import java.util.Iterator;
033    import java.util.List;
034    import java.util.NoSuchElementException;
035    import java.util.Properties;
036    import java.util.StringTokenizer;
037    import java.util.Vector;
038    
039    /**
040     * This class extends normal Java properties by adding the possibility
041     * to use the same key many times concatenating the value strings
042     * instead of overwriting them.
043     * <p>
044     * <b>Please consider using the <code>PropertiesConfiguration</code> class in
045     * Commons-Configuration as soon as it is released.</b>
046     * <p>
047     * The Extended Properties syntax is explained here:
048     *
049     * <ul>
050     *  <li>
051     *   Each property has the syntax <code>key = value</code>
052     *  </li>
053     *  <li>
054     *   The <i>key</i> may use any character but the equal sign '='.
055     *  </li>
056     *  <li>
057     *   <i>value</i> may be separated on different lines if a backslash
058     *   is placed at the end of the line that continues below.
059     *  </li>
060     *  <li>
061     *   If <i>value</i> is a list of strings, each token is separated
062     *   by a comma ','.
063     *  </li>
064     *  <li>
065     *   Commas in each token are escaped placing a backslash right before
066     *   the comma.
067     *  </li>
068     *  <li>
069     *   Backslashes are escaped by using two consecutive backslashes i.e. \\
070     *  </li>
071     *  <li>
072     *   If a <i>key</i> is used more than once, the values are appended
073     *   like if they were on the same line separated with commas.
074     *  </li>
075     *  <li>
076     *   Blank lines and lines starting with character '#' are skipped.
077     *  </li>
078     *  <li>
079     *   If a property is named "include" (or whatever is defined by
080     *   setInclude() and getInclude() and the value of that property is
081     *   the full path to a file on disk, that file will be included into
082     *   the ConfigurationsRepository. You can also pull in files relative
083     *   to the parent configuration file. So if you have something
084     *   like the following:
085     *
086     *   include = additional.properties
087     *
088     *   Then "additional.properties" is expected to be in the same
089     *   directory as the parent configuration file.
090     * 
091     *   Duplicate name values will be replaced, so be careful.
092     *
093     *  </li>
094     * </ul>
095     *
096     * <p>Here is an example of a valid extended properties file:
097     *
098     * <p><pre>
099     *      # lines starting with # are comments
100     *
101     *      # This is the simplest property
102     *      key = value
103     *
104     *      # A long property may be separated on multiple lines
105     *      longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
106     *                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
107     *
108     *      # This is a property with many tokens
109     *      tokens_on_a_line = first token, second token
110     *
111     *      # This sequence generates exactly the same result
112     *      tokens_on_multiple_lines = first token
113     *      tokens_on_multiple_lines = second token
114     *
115     *      # commas may be escaped in tokens
116     *      commas.escaped = Hi\, what'up?
117     * </pre>
118     *
119     * <p><b>NOTE</b>: this class has <b>not</b> been written for
120     * performance nor low memory usage.  In fact, it's way slower than it
121     * could be and generates too much memory garbage.  But since
122     * performance is not an issue during intialization (and there is not
123     * much time to improve it), I wrote it this way.  If you don't like
124     * it, go ahead and tune it up!
125     *
126     * @since Commons Collections 1.0
127     * @version $Revision: 1.1 $ $Date: 2004/07/21 20:54:43 $
128     * 
129     * @author <a href="mailto:stefano@apache.org">Stefano Mazzocchi</a>
130     * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
131     * @author <a href="mailto:daveb@miceda-data">Dave Bryson</a>
132     * @author <a href="mailto:jvanzyl@periapt.com">Jason van Zyl</a>
133     * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
134     * @author <a href="mailto:leon@opticode.co.za">Leon Messerschmidt</a>
135     * @author <a href="mailto:kjohnson@transparent.com">Kent Johnson</a>
136     * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
137     * @author <a href="mailto:ipriha@surfeu.fi">Ilkka Priha</a>
138     * @author Janek Bogucki
139     * @author Mohan Kishore
140     * @author Stephen Colebourne
141     */
142    public class ExtendedProperties extends Hashtable {
143        
144        /**
145         * Default configurations repository.
146         */
147        private ExtendedProperties defaults;
148    
149        /**
150         * The file connected to this repository (holding comments and
151         * such).
152         *
153         * @serial
154         */
155        protected String file;
156    
157        /**
158         * Base path of the configuration file used to create
159         * this ExtendedProperties object.
160         */
161        protected String basePath;
162    
163        /**
164         * File separator.
165         */
166        protected String fileSeparator = System.getProperty("file.separator");
167    
168        /**
169         * Has this configuration been intialized.
170         */
171        protected boolean isInitialized = false;
172    
173        /**
174         * This is the name of the property that can point to other
175         * properties file for including other properties files.
176         */
177        protected static String include = "include";
178    
179        /**
180         * These are the keys in the order they listed
181         * in the configuration file. This is useful when
182         * you wish to perform operations with configuration
183         * information in a particular order.
184         */
185        protected ArrayList keysAsListed = new ArrayList();
186    
187        protected final static String START_TOKEN="${";
188        protected final static String END_TOKEN="}";
189    
190    
191        /**
192         * Interpolate key names to handle ${key} stuff
193         *
194         * @param base string to interpolate
195         * @return returns the key name with the ${key} substituted
196         */
197        protected String interpolate(String base) {
198            // COPIED from [configuration] 2003-12-29
199            return (interpolateHelper(base, null));
200        }
201    
202        /**
203         * Recursive handler for multiple levels of interpolation.
204         *
205         * When called the first time, priorVariables should be null.
206         *
207         * @param base string with the ${key} variables
208         * @param priorVariables serves two purposes: to allow checking for
209         * loops, and creating a meaningful exception message should a loop
210         * occur.  It's 0'th element will be set to the value of base from
211         * the first call.  All subsequent interpolated variables are added
212         * afterward.
213         *
214         * @return the string with the interpolation taken care of
215         */
216        protected String interpolateHelper(String base, List priorVariables) {
217            // COPIED from [configuration] 2003-12-29
218            if (base == null) {
219                return null;
220            }
221    
222            // on the first call initialize priorVariables
223            // and add base as the first element
224            if (priorVariables == null) {
225                priorVariables = new ArrayList();
226                priorVariables.add(base);
227            }
228    
229            int begin = -1;
230            int end = -1;
231            int prec = 0 - END_TOKEN.length();
232            String variable = null;
233            StringBuffer result = new StringBuffer();
234    
235            // FIXME: we should probably allow the escaping of the start token
236            while (((begin = base.indexOf(START_TOKEN, prec + END_TOKEN.length())) > -1)
237                && ((end = base.indexOf(END_TOKEN, begin)) > -1)) {
238                result.append(base.substring(prec + END_TOKEN.length(), begin));
239                variable = base.substring(begin + START_TOKEN.length(), end);
240    
241                // if we've got a loop, create a useful exception message and throw
242                if (priorVariables.contains(variable)) {
243                    String initialBase = priorVariables.remove(0).toString();
244                    priorVariables.add(variable);
245                    StringBuffer priorVariableSb = new StringBuffer();
246    
247                    // create a nice trace of interpolated variables like so:
248                    // var1->var2->var3
249                    for (Iterator it = priorVariables.iterator(); it.hasNext();) {
250                        priorVariableSb.append(it.next());
251                        if (it.hasNext()) {
252                            priorVariableSb.append("->");
253                        }
254                    }
255    
256                    throw new IllegalStateException(
257                        "infinite loop in property interpolation of " + initialBase + ": " + priorVariableSb.toString());
258                }
259                // otherwise, add this variable to the interpolation list.
260                else {
261                    priorVariables.add(variable);
262                }
263    
264                //QUESTION: getProperty or getPropertyDirect
265                Object value = getProperty(variable);
266                if (value != null) {
267                    result.append(interpolateHelper(value.toString(), priorVariables));
268    
269                    // pop the interpolated variable off the stack
270                    // this maintains priorVariables correctness for
271                    // properties with multiple interpolations, e.g.
272                    // prop.name=${some.other.prop1}/blahblah/${some.other.prop2}
273                    priorVariables.remove(priorVariables.size() - 1);
274                } else if (defaults != null && defaults.getString(variable, null) != null) {
275                    result.append(defaults.getString(variable));
276                } else {
277                    //variable not defined - so put it back in the value
278                    result.append(START_TOKEN).append(variable).append(END_TOKEN);
279                }
280                prec = end;
281            }
282            result.append(base.substring(prec + END_TOKEN.length(), base.length()));
283    
284            return result.toString();
285        }
286        
287        /**
288         * Inserts a backslash before every comma and backslash. 
289         */
290        private static String escape(String s) {
291            StringBuffer buf = new StringBuffer(s);
292            for (int i = 0; i < buf.length(); i++) {
293                char c = buf.charAt(i);
294                if (c == ',' || c == '\\') {
295                    buf.insert(i, '\\');
296                    i++;
297                }
298            }
299            return buf.toString();
300        }
301        
302        /**
303         * Removes a backslash from every pair of backslashes. 
304         */
305        private static String unescape(String s) {
306            StringBuffer buf = new StringBuffer(s);
307            for (int i = 0; i < buf.length() - 1; i++) {
308                char c1 = buf.charAt(i);
309                char c2 = buf.charAt(i + 1);
310                if (c1 == '\\' && c2 == '\\') {
311                    buf.deleteCharAt(i);
312                }
313            }
314            return buf.toString();
315        }
316    
317        /**
318         * Counts the number of successive times 'ch' appears in the
319         * 'line' before the position indicated by the 'index'.
320         */
321        private static int countPreceding(String line, int index, char ch) {
322            int i;
323            for (i = index - 1; i >= 0; i--) {
324                if (line.charAt(i) != ch) {
325                    break;
326                }
327            }
328            return index - 1 - i;
329        }
330    
331        /**
332         * Checks if the line ends with odd number of backslashes 
333         */
334        private static boolean endsWithSlash(String line) {
335            if (!line.endsWith("\\")) {
336                return false;
337            }
338            return (countPreceding(line, line.length() - 1, '\\') % 2 == 0);
339        }
340    
341        /**
342         * This class is used to read properties lines.  These lines do
343         * not terminate with new-line chars but rather when there is no
344         * backslash sign a the end of the line.  This is used to
345         * concatenate multiple lines for readability.
346         */
347        static class PropertiesReader extends LineNumberReader {
348            /**
349             * Constructor.
350             *
351             * @param reader A Reader.
352             */
353            public PropertiesReader(Reader reader) {
354                super(reader);
355            }
356    
357            /**
358             * Read a property.
359             *
360             * @return a String property
361             * @throws IOException if there is difficulty reading the source.
362             */
363            public String readProperty() throws IOException {
364                StringBuffer buffer = new StringBuffer();
365    
366                try {
367                    while (true) {
368                        String line = readLine().trim();
369                        if ((line.length() != 0) && (line.charAt(0) != '#')) {
370                            if (endsWithSlash(line)) {
371                                line = line.substring(0, line.length() - 1);
372                                buffer.append(line);
373                            } else {
374                                buffer.append(line);
375                                break;
376                            }
377                        }
378                    }
379                } catch (NullPointerException ex) {
380                    return null;
381                }
382    
383                return buffer.toString();
384            }
385        }
386    
387        /**
388         * This class divides into tokens a property value.  Token
389         * separator is "," but commas into the property value are escaped
390         * using the backslash in front.
391         */
392        static class PropertiesTokenizer extends StringTokenizer {
393            /**
394             * The property delimiter used while parsing (a comma).
395             */
396            static final String DELIMITER = ",";
397    
398            /**
399             * Constructor.
400             *
401             * @param string A String.
402             */
403            public PropertiesTokenizer(String string) {
404                super(string, DELIMITER);
405            }
406    
407            /**
408             * Check whether the object has more tokens.
409             *
410             * @return True if the object has more tokens.
411             */
412            public boolean hasMoreTokens() {
413                return super.hasMoreTokens();
414            }
415    
416            /**
417             * Get next token.
418             *
419             * @return A String.
420             */
421            public String nextToken() {
422                StringBuffer buffer = new StringBuffer();
423    
424                while (hasMoreTokens()) {
425                    String token = super.nextToken();
426                    if (endsWithSlash(token)) {
427                        buffer.append(token.substring(0, token.length() - 1));
428                        buffer.append(DELIMITER);
429                    } else {
430                        buffer.append(token);
431                        break;
432                    }
433                }
434    
435                return buffer.toString().trim();
436            }
437        }
438    
439        /**
440         * Creates an empty extended properties object.
441         */
442        public ExtendedProperties() {
443            super();
444        }
445    
446        /**
447         * Creates and loads the extended properties from the specified file.
448         *
449         * @param file  the filename to load
450         * @throws IOException if a file error occurs
451         */
452        public ExtendedProperties(String file) throws IOException {
453            this(file, null);
454        }
455    
456        /**
457         * Creates and loads the extended properties from the specified file.
458         *
459         * @param file  the filename to load
460         * @param defaultFile  a second filename to load default values from
461         * @throws IOException if a file error occurs
462         */
463        public ExtendedProperties(String file, String defaultFile) throws IOException {
464            this.file = file;
465    
466            basePath = new File(file).getAbsolutePath();
467            basePath = basePath.substring(0, basePath.lastIndexOf(fileSeparator) + 1);
468    
469            FileInputStream in = null;
470            try {
471                in = new FileInputStream(file);
472                this.load(in);
473            } finally {
474                try {
475                    if (in != null) {
476                        in.close();
477                    }
478                } catch (IOException ex) {}
479            }
480    
481            if (defaultFile != null) {
482                defaults = new ExtendedProperties(defaultFile);
483            }
484        }
485    
486        /**
487         * Indicate to client code whether property
488         * resources have been initialized or not.
489         */
490        public boolean isInitialized() {
491            return isInitialized;
492        }
493    
494        /**
495         * Gets the property value for including other properties files.
496         * By default it is "include".
497         *
498         * @return A String.
499         */
500        public String getInclude() {
501            return include;
502        }
503    
504        /**
505         * Sets the property value for including other properties files.
506         * By default it is "include".
507         *
508         * @param inc A String.
509         */
510        public void setInclude(String inc) {
511            include = inc;
512        }
513    
514        /**
515         * Load the properties from the given input stream.
516         *
517         * @param input  the InputStream to load from
518         * @throws IOException if an IO error occurs
519         */
520        public void load(InputStream input) throws IOException {
521            load(input, null);
522        }
523    
524        /**
525         * Load the properties from the given input stream
526         * and using the specified encoding.
527         *
528         * @param input  the InputStream to load from
529         * @param enc  the encoding to use
530         * @throws IOException if an IO error occurs
531         */
532        public synchronized void load(InputStream input, String enc) throws IOException {
533            PropertiesReader reader = null;
534            if (enc != null) {
535                try {
536                    reader = new PropertiesReader(new InputStreamReader(input, enc));
537                    
538                } catch (UnsupportedEncodingException ex) {
539                    // Another try coming up....
540                }
541            }
542            
543            if (reader == null) {
544                try {
545                    reader = new PropertiesReader(new InputStreamReader(input, "8859_1"));
546                    
547                } catch (UnsupportedEncodingException ex) {
548                    // ISO8859-1 support is required on java platforms but....
549                    // If it's not supported, use the system default encoding
550                    reader = new PropertiesReader(new InputStreamReader(input));
551                }
552            }
553    
554            try {
555                while (true) {
556                    String line = reader.readProperty();
557                    int equalSign = line.indexOf('=');
558    
559                    if (equalSign > 0) {
560                        String key = line.substring(0, equalSign).trim();
561                        String value = line.substring(equalSign + 1).trim();
562    
563                        // Configure produces lines like this ... just ignore them
564                        if ("".equals(value)) {
565                            continue;
566                        }
567    
568                        if (getInclude() != null && key.equalsIgnoreCase(getInclude())) {
569                            // Recursively load properties files.
570                            File file = null;
571    
572                            if (value.startsWith(fileSeparator)) {
573                                // We have an absolute path so we'll use this
574                                file = new File(value);
575                                
576                            } else {
577                                // We have a relative path, and we have two 
578                                // possible forms here. If we have the "./" form
579                                // then just strip that off first before continuing.
580                                if (value.startsWith("." + fileSeparator)) {
581                                    value = value.substring(2);
582                                }
583    
584                                file = new File(basePath + value);
585                            }
586    
587                            if (file != null && file.exists() && file.canRead()) {
588                                load(new FileInputStream(file));
589                            }
590                        } else {
591                            addProperty(key, value);
592                        }
593                    }
594                }
595            } catch (NullPointerException ex) {
596                // Should happen only when EOF is reached.
597                return;
598            } finally {
599                // Loading is initializing
600                isInitialized = true;
601            }
602        }
603    
604        /**
605         * Gets a property from the configuration.
606         *
607         * @param key property to retrieve
608         * @return value as object. Will return user value if exists,
609         *        if not then default value if exists, otherwise null
610         */
611        public Object getProperty(String key) {
612            // first, try to get from the 'user value' store
613            Object obj = this.get(key);
614    
615            if (obj == null) {
616                // if there isn't a value there, get it from the
617                // defaults if we have them
618                if (defaults != null) {
619                    obj = defaults.get(key);
620                }
621            }
622    
623            return obj;
624        }
625        
626        /**
627         * Add a property to the configuration. If it already
628         * exists then the value stated here will be added
629         * to the configuration entry. For example, if
630         *
631         * <code>resource.loader = file</code>
632         *
633         * is already present in the configuration and you
634         *
635         * <code>addProperty("resource.loader", "classpath")</code>
636         *
637         * Then you will end up with a Vector like the
638         * following:
639         *
640         * <code>["file", "classpath"]</code>
641         *
642         * @param key  the key to add
643         * @param value  the value to add
644         */
645        public void addProperty(String key, Object value) {
646            if (value instanceof String) {
647                String str = (String) value;
648                if (str.indexOf(PropertiesTokenizer.DELIMITER) > 0) {
649                    // token contains commas, so must be split apart then added
650                    PropertiesTokenizer tokenizer = new PropertiesTokenizer(str);
651                    while (tokenizer.hasMoreTokens()) {
652                        String token = tokenizer.nextToken();
653                        addPropertyInternal(key, unescape(token));
654                    }
655                } else {
656                    // token contains no commas, so can be simply added
657                    addPropertyInternal(key, unescape(str));
658                }
659            } else {
660                addPropertyInternal(key, value);
661            }
662    
663            // Adding a property connotes initialization
664            isInitialized = true;
665        }
666    
667        /**
668         * Adds a key/value pair to the map.  This routine does
669         * no magic morphing.  It ensures the keylist is maintained
670         *
671         * @param key  the key to store at
672         * @param value  the decoded object to store
673         */
674        private void addPropertyDirect(String key, Object value) {
675            // safety check
676            if (!containsKey(key)) {
677                keysAsListed.add(key);
678            }
679            put(key, value);
680        }
681    
682        /**
683         * Adds a decoded property to the map w/o checking for commas - used
684         * internally when a property has been broken up into
685         * strings that could contain escaped commas to prevent
686         * the inadvertent vectorization.
687         * <p>
688         * Thanks to Leon Messerschmidt for this one.
689         *
690         * @param key  the key to store at
691         * @param value  the decoded object to store
692         */
693        private void addPropertyInternal(String key, Object value) {
694            Object current = this.get(key);
695    
696            if (current instanceof String) {
697                // one object already in map - convert it to a vector
698                Vector v = new Vector(2);
699                v.addElement(current);
700                v.addElement(value);
701                put(key, v);
702                
703            } else if (current instanceof Vector) {
704                // already a vector - just add the new token
705                ((Vector) current).addElement(value);
706                
707            } else {
708                // brand new key - store in keysAsListed to retain order
709                if (!containsKey(key)) {
710                    keysAsListed.add(key);
711                }
712                put(key, value);
713            }
714        }
715    
716        /**
717         * Set a property, this will replace any previously
718         * set values. Set values is implicitly a call
719         * to clearProperty(key), addProperty(key,value).
720         *
721         * @param key  the key to set
722         * @param value  the value to set
723         */
724        public void setProperty(String key, Object value) {
725            clearProperty(key);
726            addProperty(key, value);
727        }
728        
729        /**
730         * Save the properties to the given output stream.
731         * <p>
732         * The stream is not closed, but it is flushed.
733         *
734         * @param output  an OutputStream, may be null
735         * @param header  a textual comment to act as a file header
736         * @throws IOException if an IO error occurs
737         */
738        public synchronized void save(OutputStream output, String header) {
739            if (output == null) {
740                return;
741            }
742            PrintWriter theWrtr = new PrintWriter(output);
743            if (header != null) {
744                theWrtr.println(header);
745            }
746            
747            Enumeration theKeys = keys();
748            while (theKeys.hasMoreElements()) {
749                String key = (String) theKeys.nextElement();
750                Object value = get(key);
751                if (value != null) {
752                    if (value instanceof String) {
753                        StringBuffer currentOutput = new StringBuffer();
754                        currentOutput.append(key);
755                        currentOutput.append("=");
756                        currentOutput.append(escape((String) value));
757                        theWrtr.println(currentOutput.toString());
758                        
759                    } else if (value instanceof Vector) {
760                        Vector values = (Vector) value;
761                        Enumeration valuesEnum = values.elements();
762                        while (valuesEnum.hasMoreElements()) {
763                            String currentElement = (String) valuesEnum.nextElement();
764                            StringBuffer currentOutput = new StringBuffer();
765                            currentOutput.append(key);
766                            currentOutput.append("=");
767                            currentOutput.append(escape(currentElement));
768                            theWrtr.println(currentOutput.toString());
769                        }
770                    }
771                }
772                theWrtr.println();
773                theWrtr.flush();
774            }
775        }
776    
777        /**
778         * Combines an existing Hashtable with this Hashtable.
779         * <p>
780         * Warning: It will overwrite previous entries without warning.
781         *
782         * @param props  the properties to combine
783         */
784        public void combine(ExtendedProperties props) {
785            for (Iterator it = props.getKeys(); it.hasNext();) {
786                String key = (String) it.next();
787                setProperty(key, props.get(key));
788            }
789        }
790        
791        /**
792         * Clear a property in the configuration.
793         *
794         * @param key  the property key to remove along with corresponding value
795         */
796        public void clearProperty(String key) {
797            if (containsKey(key)) {
798                // we also need to rebuild the keysAsListed or else
799                // things get *very* confusing
800                for (int i = 0; i < keysAsListed.size(); i++) {
801                    if (( keysAsListed.get(i)).equals(key)) {
802                        keysAsListed.remove(i);
803                        break;
804                    }
805                }
806                remove(key);
807            }
808        }
809    
810        /**
811         * Get the list of the keys contained in the configuration
812         * repository.
813         *
814         * @return an Iterator over the keys
815         */
816        public Iterator getKeys() {
817            return keysAsListed.iterator();
818        }
819    
820        /**
821         * Get the list of the keys contained in the configuration
822         * repository that match the specified prefix.
823         *
824         * @param prefix  the prefix to match
825         * @return an Iterator of keys that match the prefix
826         */
827        public Iterator getKeys(String prefix) {
828            Iterator keys = getKeys();
829            ArrayList matchingKeys = new ArrayList();
830    
831            while (keys.hasNext()) {
832                Object key = keys.next();
833    
834                if (key instanceof String && ((String) key).startsWith(prefix)) {
835                    matchingKeys.add(key);
836                }
837            }
838            return matchingKeys.iterator();
839        }
840    
841        /**
842         * Create an ExtendedProperties object that is a subset
843         * of this one. Take into account duplicate keys
844         * by using the setProperty() in ExtendedProperties.
845         *
846         * @param prefix  the prefix to get a subset for
847         * @return a new independent ExtendedProperties
848         */
849        public ExtendedProperties subset(String prefix) {
850            ExtendedProperties c = new ExtendedProperties();
851            Iterator keys = getKeys();
852            boolean validSubset = false;
853    
854            while (keys.hasNext()) {
855                Object key = keys.next();
856    
857                if (key instanceof String && ((String) key).startsWith(prefix)) {
858                    if (!validSubset) {
859                        validSubset = true;
860                    }
861    
862                    /*
863                     * Check to make sure that c.subset(prefix) doesn't
864                     * blow up when there is only a single property
865                     * with the key prefix. This is not a useful
866                     * subset but it is a valid subset.
867                     */
868                    String newKey = null;
869                    if (((String) key).length() == prefix.length()) {
870                        newKey = prefix;
871                    } else {
872                        newKey = ((String) key).substring(prefix.length() + 1);
873                    }
874    
875                    /*
876                     *  use addPropertyDirect() - this will plug the data as 
877                     *  is into the Map, but will also do the right thing
878                     *  re key accounting
879                     */
880                    c.addPropertyDirect(newKey, get(key));
881                }
882            }
883    
884            if (validSubset) {
885                return c;
886            } else {
887                return null;
888            }
889        }
890    
891        /**
892         * Display the configuration for debugging purposes to System.out.
893         */
894        public void display() {
895            Iterator i = getKeys();
896    
897            while (i.hasNext()) {
898                String key = (String) i.next();
899                Object value = get(key);
900                System.out.println(key + " => " + value);
901            }
902        }
903    
904        /**
905         * Get a string associated with the given configuration key.
906         *
907         * @param key The configuration key.
908         * @return The associated string.
909         * @throws ClassCastException is thrown if the key maps to an
910         * object that is not a String.
911         */
912        public String getString(String key) {
913            return getString(key, null);
914        }
915    
916        /**
917         * Get a string associated with the given configuration key.
918         *
919         * @param key The configuration key.
920         * @param defaultValue The default value.
921         * @return The associated string if key is found,
922         * default value otherwise.
923         * @throws ClassCastException is thrown if the key maps to an
924         * object that is not a String.
925         */
926        public String getString(String key, String defaultValue) {
927            Object value = get(key);
928    
929            if (value instanceof String) {
930                return interpolate((String) value);
931                
932            } else if (value == null) {
933                if (defaults != null) {
934                    return interpolate(defaults.getString(key, defaultValue));
935                } else {
936                    return interpolate(defaultValue);
937                }
938            } else if (value instanceof Vector) {
939                return interpolate((String) ((Vector) value).get(0));
940            } else {
941                throw new ClassCastException('\'' + key + "' doesn't map to a String object");
942            }
943        }
944    
945        /**
946         * Get a list of properties associated with the given
947         * configuration key.
948         *
949         * @param key The configuration key.
950         * @return The associated properties if key is found.
951         * @throws ClassCastException is thrown if the key maps to an
952         * object that is not a String/Vector.
953         * @throws IllegalArgumentException if one of the tokens is
954         * malformed (does not contain an equals sign).
955         */
956        public Properties getProperties(String key) {
957            return getProperties(key, new Properties());
958        }
959    
960        /**
961         * Get a list of properties associated with the given
962         * configuration key.
963         *
964         * @param key The configuration key.
965         * @return The associated properties if key is found.
966         * @throws ClassCastException is thrown if the key maps to an
967         * object that is not a String/Vector.
968         * @throws IllegalArgumentException if one of the tokens is
969         * malformed (does not contain an equals sign).
970         */
971        public Properties getProperties(String key, Properties defaults) {
972            /*
973             * Grab an array of the tokens for this key.
974             */
975            String[] tokens = getStringArray(key);
976    
977            // Each token is of the form 'key=value'.
978            Properties props = new Properties(defaults);
979            for (int i = 0; i < tokens.length; i++) {
980                String token = tokens[i];
981                int equalSign = token.indexOf('=');
982                if (equalSign > 0) {
983                    String pkey = token.substring(0, equalSign).trim();
984                    String pvalue = token.substring(equalSign + 1).trim();
985                    props.put(pkey, pvalue);
986                } else {
987                    throw new IllegalArgumentException('\'' + token + "' does not contain " + "an equals sign");
988                }
989            }
990            return props;
991        }
992    
993        /**
994         * Get an array of strings associated with the given configuration
995         * key.
996         *
997         * @param key The configuration key.
998         * @return The associated string array if key is found.
999         * @throws ClassCastException is thrown if the key maps to an
1000         * object that is not a String/Vector.
1001         */
1002        public String[] getStringArray(String key) {
1003            Object value = get(key);
1004    
1005            // What's your vector, Victor?
1006            Vector vector;
1007            if (value instanceof String) {
1008                vector = new Vector(1);
1009                vector.addElement(value);
1010                
1011            } else if (value instanceof Vector) {
1012                vector = (Vector) value;
1013                
1014            } else if (value == null) {
1015                if (defaults != null) {
1016                    return defaults.getStringArray(key);
1017                } else {
1018                    return new String[0];
1019                }
1020            } else {
1021                throw new ClassCastException('\'' + key + "' doesn't map to a String/Vector object");
1022            }
1023    
1024            String[] tokens = new String[vector.size()];
1025            for (int i = 0; i < tokens.length; i++) {
1026                tokens[i] = (String) vector.elementAt(i);
1027            }
1028    
1029            return tokens;
1030        }
1031    
1032        /**
1033         * Get a Vector of strings associated with the given configuration
1034         * key.
1035         *
1036         * @param key The configuration key.
1037         * @return The associated Vector.
1038         * @throws ClassCastException is thrown if the key maps to an
1039         * object that is not a Vector.
1040         */
1041        public Vector getVector(String key) {
1042            return getVector(key, null);
1043        }
1044    
1045        /**
1046         * Get a Vector of strings associated with the given configuration
1047         * key.
1048         *
1049         * @param key The configuration key.
1050         * @param defaultValue The default value.
1051         * @return The associated Vector.
1052         * @throws ClassCastException is thrown if the key maps to an
1053         * object that is not a Vector.
1054         */
1055        public Vector getVector(String key, Vector defaultValue) {
1056            Object value = get(key);
1057    
1058            if (value instanceof Vector) {
1059                return (Vector) value;
1060                
1061            } else if (value instanceof String) {
1062                Vector v = new Vector(1);
1063                v.addElement(value);
1064                put(key, v);
1065                return v;
1066                
1067            } else if (value == null) {
1068                if (defaults != null) {
1069                    return defaults.getVector(key, defaultValue);
1070                } else {
1071                    return ((defaultValue == null) ? new Vector() : defaultValue);
1072                }
1073            } else {
1074                throw new ClassCastException('\'' + key + "' doesn't map to a Vector object");
1075            }
1076        }
1077    
1078        /**
1079         * Get a boolean associated with the given configuration key.
1080         *
1081         * @param key The configuration key.
1082         * @return The associated boolean.
1083         * @throws NoSuchElementException is thrown if the key doesn't
1084         * map to an existing object.
1085         * @throws ClassCastException is thrown if the key maps to an
1086         * object that is not a Boolean.
1087         */
1088        public boolean getBoolean(String key) {
1089            Boolean b = getBoolean(key, null);
1090            if (b != null) {
1091                return b.booleanValue();
1092            } else {
1093                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1094            }
1095        }
1096    
1097        /**
1098         * Get a boolean associated with the given configuration key.
1099         *
1100         * @param key The configuration key.
1101         * @param defaultValue The default value.
1102         * @return The associated boolean.
1103         * @throws ClassCastException is thrown if the key maps to an
1104         * object that is not a Boolean.
1105         */
1106        public boolean getBoolean(String key, boolean defaultValue) {
1107            return getBoolean(key, new Boolean(defaultValue)).booleanValue();
1108        }
1109    
1110        /**
1111         * Get a boolean associated with the given configuration key.
1112         *
1113         * @param key The configuration key.
1114         * @param defaultValue The default value.
1115         * @return The associated boolean if key is found and has valid
1116         * format, default value otherwise.
1117         * @throws ClassCastException is thrown if the key maps to an
1118         * object that is not a Boolean.
1119         */
1120        public Boolean getBoolean(String key, Boolean defaultValue) {
1121    
1122            Object value = get(key);
1123    
1124            if (value instanceof Boolean) {
1125                return (Boolean) value;
1126                
1127            } else if (value instanceof String) {
1128                String s = testBoolean((String) value);
1129                Boolean b = new Boolean(s);
1130                put(key, b);
1131                return b;
1132                
1133            } else if (value == null) {
1134                if (defaults != null) {
1135                    return defaults.getBoolean(key, defaultValue);
1136                } else {
1137                    return defaultValue;
1138                }
1139            } else {
1140                throw new ClassCastException('\'' + key + "' doesn't map to a Boolean object");
1141            }
1142        }
1143    
1144        /**
1145         * Test whether the string represent by value maps to a boolean
1146         * value or not. We will allow <code>true</code>, <code>on</code>,
1147         * and <code>yes</code> for a <code>true</code> boolean value, and
1148         * <code>false</code>, <code>off</code>, and <code>no</code> for
1149         * <code>false</code> boolean values.  Case of value to test for
1150         * boolean status is ignored.
1151         *
1152         * @param value  the value to test for boolean state
1153         * @return <code>true</code> or <code>false</code> if the supplied
1154         * text maps to a boolean value, or <code>null</code> otherwise.
1155         */
1156        public String testBoolean(String value) {
1157            String s = value.toLowerCase();
1158    
1159            if (s.equals("true") || s.equals("on") || s.equals("yes")) {
1160                return "true";
1161            } else if (s.equals("false") || s.equals("off") || s.equals("no")) {
1162                return "false";
1163            } else {
1164                return null;
1165            }
1166        }
1167    
1168        /**
1169         * Get a byte associated with the given configuration key.
1170         *
1171         * @param key The configuration key.
1172         * @return The associated byte.
1173         * @throws NoSuchElementException is thrown if the key doesn't
1174         * map to an existing object.
1175         * @throws ClassCastException is thrown if the key maps to an
1176         * object that is not a Byte.
1177         * @throws NumberFormatException is thrown if the value mapped
1178         * by the key has not a valid number format.
1179         */
1180        public byte getByte(String key) {
1181            Byte b = getByte(key, null);
1182            if (b != null) {
1183                return b.byteValue();
1184            } else {
1185                throw new NoSuchElementException('\'' + key + " doesn't map to an existing object");
1186            }
1187        }
1188    
1189        /**
1190         * Get a byte associated with the given configuration key.
1191         *
1192         * @param key The configuration key.
1193         * @param defaultValue The default value.
1194         * @return The associated byte.
1195         * @throws ClassCastException is thrown if the key maps to an
1196         * object that is not a Byte.
1197         * @throws NumberFormatException is thrown if the value mapped
1198         * by the key has not a valid number format.
1199         */
1200        public byte getByte(String key, byte defaultValue) {
1201            return getByte(key, new Byte(defaultValue)).byteValue();
1202        }
1203    
1204        /**
1205         * Get a byte associated with the given configuration key.
1206         *
1207         * @param key The configuration key.
1208         * @param defaultValue The default value.
1209         * @return The associated byte if key is found and has valid
1210         * format, default value otherwise.
1211         * @throws ClassCastException is thrown if the key maps to an
1212         * object that is not a Byte.
1213         * @throws NumberFormatException is thrown if the value mapped
1214         * by the key has not a valid number format.
1215         */
1216        public Byte getByte(String key, Byte defaultValue) {
1217            Object value = get(key);
1218    
1219            if (value instanceof Byte) {
1220                return (Byte) value;
1221                
1222            } else if (value instanceof String) {
1223                Byte b = new Byte((String) value);
1224                put(key, b);
1225                return b;
1226                
1227            } else if (value == null) {
1228                if (defaults != null) {
1229                    return defaults.getByte(key, defaultValue);
1230                } else {
1231                    return defaultValue;
1232                }
1233            } else {
1234                throw new ClassCastException('\'' + key + "' doesn't map to a Byte object");
1235            }
1236        }
1237    
1238        /**
1239         * Get a short associated with the given configuration key.
1240         *
1241         * @param key The configuration key.
1242         * @return The associated short.
1243         * @throws NoSuchElementException is thrown if the key doesn't
1244         * map to an existing object.
1245         * @throws ClassCastException is thrown if the key maps to an
1246         * object that is not a Short.
1247         * @throws NumberFormatException is thrown if the value mapped
1248         * by the key has not a valid number format.
1249         */
1250        public short getShort(String key) {
1251            Short s = getShort(key, null);
1252            if (s != null) {
1253                return s.shortValue();
1254            } else {
1255                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1256            }
1257        }
1258    
1259        /**
1260         * Get a short associated with the given configuration key.
1261         *
1262         * @param key The configuration key.
1263         * @param defaultValue The default value.
1264         * @return The associated short.
1265         * @throws ClassCastException is thrown if the key maps to an
1266         * object that is not a Short.
1267         * @throws NumberFormatException is thrown if the value mapped
1268         * by the key has not a valid number format.
1269         */
1270        public short getShort(String key, short defaultValue) {
1271            return getShort(key, new Short(defaultValue)).shortValue();
1272        }
1273    
1274        /**
1275         * Get a short associated with the given configuration key.
1276         *
1277         * @param key The configuration key.
1278         * @param defaultValue The default value.
1279         * @return The associated short if key is found and has valid
1280         * format, default value otherwise.
1281         * @throws ClassCastException is thrown if the key maps to an
1282         * object that is not a Short.
1283         * @throws NumberFormatException is thrown if the value mapped
1284         * by the key has not a valid number format.
1285         */
1286        public Short getShort(String key, Short defaultValue) {
1287            Object value = get(key);
1288    
1289            if (value instanceof Short) {
1290                return (Short) value;
1291                
1292            } else if (value instanceof String) {
1293                Short s = new Short((String) value);
1294                put(key, s);
1295                return s;
1296                
1297            } else if (value == null) {
1298                if (defaults != null) {
1299                    return defaults.getShort(key, defaultValue);
1300                } else {
1301                    return defaultValue;
1302                }
1303            } else {
1304                throw new ClassCastException('\'' + key + "' doesn't map to a Short object");
1305            }
1306        }
1307    
1308        /**
1309         * The purpose of this method is to get the configuration resource
1310         * with the given name as an integer.
1311         *
1312         * @param name The resource name.
1313         * @return The value of the resource as an integer.
1314         */
1315        public int getInt(String name) {
1316            return getInteger(name);
1317        }
1318    
1319        /**
1320         * The purpose of this method is to get the configuration resource
1321         * with the given name as an integer, or a default value.
1322         *
1323         * @param name The resource name
1324         * @param def The default value of the resource.
1325         * @return The value of the resource as an integer.
1326         */
1327        public int getInt(String name, int def) {
1328            return getInteger(name, def);
1329        }
1330    
1331        /**
1332         * Get a int associated with the given configuration key.
1333         *
1334         * @param key The configuration key.
1335         * @return The associated int.
1336         * @throws NoSuchElementException is thrown if the key doesn't
1337         * map to an existing object.
1338         * @throws ClassCastException is thrown if the key maps to an
1339         * object that is not a Integer.
1340         * @throws NumberFormatException is thrown if the value mapped
1341         * by the key has not a valid number format.
1342         */
1343        public int getInteger(String key) {
1344            Integer i = getInteger(key, null);
1345            if (i != null) {
1346                return i.intValue();
1347            } else {
1348                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1349            }
1350        }
1351    
1352        /**
1353         * Get a int associated with the given configuration key.
1354         *
1355         * @param key The configuration key.
1356         * @param defaultValue The default value.
1357         * @return The associated int.
1358         * @throws ClassCastException is thrown if the key maps to an
1359         * object that is not a Integer.
1360         * @throws NumberFormatException is thrown if the value mapped
1361         * by the key has not a valid number format.
1362         */
1363        public int getInteger(String key, int defaultValue) {
1364            Integer i = getInteger(key, null);
1365    
1366            if (i == null) {
1367                return defaultValue;
1368            }
1369            return i.intValue();
1370        }
1371    
1372        /**
1373         * Get a int associated with the given configuration key.
1374         *
1375         * @param key The configuration key.
1376         * @param defaultValue The default value.
1377         * @return The associated int if key is found and has valid
1378         * format, default value otherwise.
1379         * @throws ClassCastException is thrown if the key maps to an
1380         * object that is not a Integer.
1381         * @throws NumberFormatException is thrown if the value mapped
1382         * by the key has not a valid number format.
1383         */
1384        public Integer getInteger(String key, Integer defaultValue) {
1385            Object value = get(key);
1386    
1387            if (value instanceof Integer) {
1388                return (Integer) value;
1389                
1390            } else if (value instanceof String) {
1391                Integer i = new Integer((String) value);
1392                put(key, i);
1393                return i;
1394                
1395            } else if (value == null) {
1396                if (defaults != null) {
1397                    return defaults.getInteger(key, defaultValue);
1398                } else {
1399                    return defaultValue;
1400                }
1401            } else {
1402                throw new ClassCastException('\'' + key + "' doesn't map to a Integer object");
1403            }
1404        }
1405    
1406        /**
1407         * Get a long associated with the given configuration key.
1408         *
1409         * @param key The configuration key.
1410         * @return The associated long.
1411         * @throws NoSuchElementException is thrown if the key doesn't
1412         * map to an existing object.
1413         * @throws ClassCastException is thrown if the key maps to an
1414         * object that is not a Long.
1415         * @throws NumberFormatException is thrown if the value mapped
1416         * by the key has not a valid number format.
1417         */
1418        public long getLong(String key) {
1419            Long l = getLong(key, null);
1420            if (l != null) {
1421                return l.longValue();
1422            } else {
1423                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1424            }
1425        }
1426    
1427        /**
1428         * Get a long associated with the given configuration key.
1429         *
1430         * @param key The configuration key.
1431         * @param defaultValue The default value.
1432         * @return The associated long.
1433         * @throws ClassCastException is thrown if the key maps to an
1434         * object that is not a Long.
1435         * @throws NumberFormatException is thrown if the value mapped
1436         * by the key has not a valid number format.
1437         */
1438        public long getLong(String key, long defaultValue) {
1439            return getLong(key, new Long(defaultValue)).longValue();
1440        }
1441    
1442        /**
1443         * Get a long associated with the given configuration key.
1444         *
1445         * @param key The configuration key.
1446         * @param defaultValue The default value.
1447         * @return The associated long if key is found and has valid
1448         * format, default value otherwise.
1449         * @throws ClassCastException is thrown if the key maps to an
1450         * object that is not a Long.
1451         * @throws NumberFormatException is thrown if the value mapped
1452         * by the key has not a valid number format.
1453         */
1454        public Long getLong(String key, Long defaultValue) {
1455            Object value = get(key);
1456    
1457            if (value instanceof Long) {
1458                return (Long) value;
1459                
1460            } else if (value instanceof String) {
1461                Long l = new Long((String) value);
1462                put(key, l);
1463                return l;
1464                
1465            } else if (value == null) {
1466                if (defaults != null) {
1467                    return defaults.getLong(key, defaultValue);
1468                } else {
1469                    return defaultValue;
1470                }
1471            } else {
1472                throw new ClassCastException('\'' + key + "' doesn't map to a Long object");
1473            }
1474        }
1475    
1476        /**
1477         * Get a float associated with the given configuration key.
1478         *
1479         * @param key The configuration key.
1480         * @return The associated float.
1481         * @throws NoSuchElementException is thrown if the key doesn't
1482         * map to an existing object.
1483         * @throws ClassCastException is thrown if the key maps to an
1484         * object that is not a Float.
1485         * @throws NumberFormatException is thrown if the value mapped
1486         * by the key has not a valid number format.
1487         */
1488        public float getFloat(String key) {
1489            Float f = getFloat(key, null);
1490            if (f != null) {
1491                return f.floatValue();
1492            } else {
1493                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1494            }
1495        }
1496    
1497        /**
1498         * Get a float associated with the given configuration key.
1499         *
1500         * @param key The configuration key.
1501         * @param defaultValue The default value.
1502         * @return The associated float.
1503         * @throws ClassCastException is thrown if the key maps to an
1504         * object that is not a Float.
1505         * @throws NumberFormatException is thrown if the value mapped
1506         * by the key has not a valid number format.
1507         */
1508        public float getFloat(String key, float defaultValue) {
1509            return getFloat(key, new Float(defaultValue)).floatValue();
1510        }
1511    
1512        /**
1513         * Get a float associated with the given configuration key.
1514         *
1515         * @param key The configuration key.
1516         * @param defaultValue The default value.
1517         * @return The associated float if key is found and has valid
1518         * format, default value otherwise.
1519         * @throws ClassCastException is thrown if the key maps to an
1520         * object that is not a Float.
1521         * @throws NumberFormatException is thrown if the value mapped
1522         * by the key has not a valid number format.
1523         */
1524        public Float getFloat(String key, Float defaultValue) {
1525            Object value = get(key);
1526    
1527            if (value instanceof Float) {
1528                return (Float) value;
1529                
1530            } else if (value instanceof String) {
1531                Float f = new Float((String) value);
1532                put(key, f);
1533                return f;
1534                
1535            } else if (value == null) {
1536                if (defaults != null) {
1537                    return defaults.getFloat(key, defaultValue);
1538                } else {
1539                    return defaultValue;
1540                }
1541            } else {
1542                throw new ClassCastException('\'' + key + "' doesn't map to a Float object");
1543            }
1544        }
1545    
1546        /**
1547         * Get a double associated with the given configuration key.
1548         *
1549         * @param key The configuration key.
1550         * @return The associated double.
1551         * @throws NoSuchElementException is thrown if the key doesn't
1552         * map to an existing object.
1553         * @throws ClassCastException is thrown if the key maps to an
1554         * object that is not a Double.
1555         * @throws NumberFormatException is thrown if the value mapped
1556         * by the key has not a valid number format.
1557         */
1558        public double getDouble(String key) {
1559            Double d = getDouble(key, null);
1560            if (d != null) {
1561                return d.doubleValue();
1562            } else {
1563                throw new NoSuchElementException('\'' + key + "' doesn't map to an existing object");
1564            }
1565        }
1566    
1567        /**
1568         * Get a double associated with the given configuration key.
1569         *
1570         * @param key The configuration key.
1571         * @param defaultValue The default value.
1572         * @return The associated double.
1573         * @throws ClassCastException is thrown if the key maps to an
1574         * object that is not a Double.
1575         * @throws NumberFormatException is thrown if the value mapped
1576         * by the key has not a valid number format.
1577         */
1578        public double getDouble(String key, double defaultValue) {
1579            return getDouble(key, new Double(defaultValue)).doubleValue();
1580        }
1581    
1582        /**
1583         * Get a double associated with the given configuration key.
1584         *
1585         * @param key The configuration key.
1586         * @param defaultValue The default value.
1587         * @return The associated double if key is found and has valid
1588         * format, default value otherwise.
1589         * @throws ClassCastException is thrown if the key maps to an
1590         * object that is not a Double.
1591         * @throws NumberFormatException is thrown if the value mapped
1592         * by the key has not a valid number format.
1593         */
1594        public Double getDouble(String key, Double defaultValue) {
1595            Object value = get(key);
1596    
1597            if (value instanceof Double) {
1598                return (Double) value;
1599                
1600            } else if (value instanceof String) {
1601                Double d = new Double((String) value);
1602                put(key, d);
1603                return d;
1604                
1605            } else if (value == null) {
1606                if (defaults != null) {
1607                    return defaults.getDouble(key, defaultValue);
1608                } else {
1609                    return defaultValue;
1610                }
1611            } else {
1612                throw new ClassCastException('\'' + key + "' doesn't map to a Double object");
1613            }
1614        }
1615    
1616        /**
1617         * Convert a standard properties class into a configuration class.
1618         *
1619         * @param props  the properties object to convert
1620         * @return new ExtendedProperties created from props
1621         */
1622        public static ExtendedProperties convertProperties(Properties props) {
1623            ExtendedProperties c = new ExtendedProperties();
1624    
1625            for (Enumeration e = props.keys(); e.hasMoreElements();) {
1626                String s = (String) e.nextElement();
1627                c.setProperty(s, props.getProperty(s));
1628            }
1629    
1630            return c;
1631        }
1632        
1633    }