001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.configuration;
029
030import org.opencms.i18n.CmsEncoder;
031import org.opencms.util.CmsStringUtil;
032
033import java.io.FileInputStream;
034import java.io.IOException;
035import java.io.InputStream;
036import java.io.InputStreamReader;
037import java.io.LineNumberReader;
038import java.io.Reader;
039import java.io.UnsupportedEncodingException;
040import java.util.AbstractMap;
041import java.util.ArrayList;
042import java.util.Collection;
043import java.util.Collections;
044import java.util.List;
045import java.util.Map;
046import java.util.Properties;
047import java.util.Set;
048import java.util.StringTokenizer;
049import java.util.TreeMap;
050
051import org.dom4j.Element;
052
053/**
054 * Provides convenient access to configuration parameters.<p>
055 *
056 * Usually the parameters are configured in some sort of String based file,
057 * either in an XML configuration, or in a .property file.
058 * This wrapper allows accessing such String values directly
059 * as <code>int</code>, <code>boolean</code> or other data types, without
060 * worrying about the type conversion.<p>
061 *
062 * It can also read a configuration from a special property file format,
063 * which is explained here:
064 *
065 * <ul>
066 *  <li>
067 *   Each parameter in the file has the syntax <code>key = value</code>
068 *  </li>
069 *  <li>
070 *   The <i>key</i> may use any character but the equal sign '='.
071 *  </li>
072 *  <li>
073 *   <i>value</i> may be separated on different lines if a backslash
074 *   is placed at the end of the line that continues below.
075 *  </li>
076 *  <li>
077 *   If <i>value</i> is a list of strings, each token is separated
078 *   by a comma ','.
079 *  </li>
080 *  <li>
081 *   Commas in each token are escaped placing a backslash right before
082 *   the comma.
083 *  </li>
084 *  <li>
085 *   Backslashes are escaped by using two consecutive backslashes i.e. \\.
086 *   Note: Unlike in regular Java properties files, you don't need to escape Backslashes.
087 *  </li>
088 *  <li>
089 *   If a <i>key</i> is used more than once, the values are appended
090 *   as if they were on the same line separated with commas.
091 *  </li>
092 *  <li>
093 *   Blank lines and lines starting with character '#' are skipped.
094 *  </li>
095 * </ul>
096 *
097 * Here is an example of a valid parameter properties file:<p>
098 *
099 * <pre>
100 *      # lines starting with # are comments
101 *
102 *      # This is the simplest property
103 *      key = value
104 *
105 *      # A long property may be separated on multiple lines
106 *      longvalue = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
107 *                  aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
108 *
109 *      # This is a property with many tokens
110 *      tokens_on_a_line = first token, second token
111 *
112 *      # This sequence generates exactly the same result
113 *      tokens_on_multiple_lines = first token
114 *      tokens_on_multiple_lines = second token
115 *
116 *      # commas may be escaped in tokens
117 *      commas.escaped = Hi\, what'up?
118 * </pre>
119 */
120public class CmsParameterConfiguration extends AbstractMap<String, String> {
121
122    /**
123     * Used to read parameter lines from a property file.<p>
124     *
125     * The lines do not terminate with new-line chars but rather when there is no
126     * backslash sign a the end of the line. This is used to
127     * concatenate multiple lines for readability in the input file.<p>
128     */
129    protected static class ParameterReader extends LineNumberReader {
130
131        /**
132         * Constructor.<p>
133         *
134         * @param reader a reader
135         */
136        public ParameterReader(Reader reader) {
137
138            super(reader);
139        }
140
141        /**
142         * Reads a parameter line.<p>
143         *
144         * @return the parameter line read
145         *
146         * @throws IOException in case of IO errors
147         */
148        public String readParameter() throws IOException {
149
150            StringBuffer buffer = new StringBuffer();
151            String line = readLine();
152            while (line != null) {
153                line = line.trim();
154                if ((line.length() != 0) && (line.charAt(0) != '#')) {
155                    if (endsWithSlash(line)) {
156                        line = line.substring(0, line.length() - 1);
157                        buffer.append(line);
158                    } else {
159                        buffer.append(line);
160                        return buffer.toString(); // normal method end
161                    }
162                }
163                line = readLine();
164            }
165            return null; // EOF reached
166        }
167    }
168
169    /**
170     * This class divides property value into tokens separated by ",".<p>
171     *
172     * Commas in the property value that are wanted
173     * can be escaped using the backslash in front like this "\,".
174     */
175    protected static class ParameterTokenizer extends StringTokenizer {
176
177        /** The property delimiter used while parsing (a comma). */
178        static final String COMMA = ",";
179
180        /**
181         * Constructor.<p>
182         *
183         * @param string the String to break into tokens
184         */
185        public ParameterTokenizer(String string) {
186
187            super(string, COMMA);
188        }
189
190        /**
191         * Returns the next token.<p>
192         *
193         * @return  the next token
194         */
195        @Override
196        public String nextToken() {
197
198            StringBuffer buffer = new StringBuffer();
199
200            while (hasMoreTokens()) {
201                String token = super.nextToken();
202                if (endsWithSlash(token)) {
203                    buffer.append(token.substring(0, token.length() - 1));
204                    buffer.append(COMMA);
205                } else {
206                    buffer.append(token);
207                    break;
208                }
209            }
210
211            return buffer.toString().trim();
212        }
213    }
214
215    /**
216     * An empty, immutable parameter configuration.<p>
217     */
218    public static final CmsParameterConfiguration EMPTY_PARAMETERS = new CmsParameterConfiguration(
219        Collections.<String, String> emptyMap(),
220        Collections.<String, Object> emptyMap());
221
222    /** The parsed map of parameters where the Strings may have become Objects. */
223    private Map<String, Object> m_configurationObjects;
224
225    /** The original map of parameters that contains only String values. */
226    private Map<String, String> m_configurationStrings;
227
228    /**
229     * Creates an empty parameter configuration.<p>
230     */
231    public CmsParameterConfiguration() {
232
233        this(new TreeMap<String, String>(), new TreeMap<String, Object>());
234    }
235
236    /**
237     * Creates a parameter configuration from an input stream.<p>
238     *
239     * @param in the input stream to create the parameter configuration from
240     *
241     * @throws IOException in case of errors loading the parameters from the input stream
242     */
243    public CmsParameterConfiguration(InputStream in)
244    throws IOException {
245
246        this();
247        load(in);
248    }
249
250    /**
251     * Creates a parameter configuration from a Map of Strings.<p>
252     *
253     * @param configuration the map of Strings to create the parameter configuration from
254     */
255    public CmsParameterConfiguration(Map<String, String> configuration) {
256
257        this();
258
259        for (String key : configuration.keySet()) {
260
261            String value = configuration.get(key);
262            add(key, value);
263        }
264    }
265
266    /**
267     * Creates a parameter wrapper by loading the parameters from the specified property file.<p>
268     *
269     * @param file the path of the file to load
270     *
271     * @throws IOException in case of errors loading the parameters from the specified property file
272     */
273    public CmsParameterConfiguration(String file)
274    throws IOException {
275
276        this();
277
278        FileInputStream in = null;
279        try {
280            in = new FileInputStream(file);
281            load(in);
282        } finally {
283            try {
284                if (in != null) {
285                    in.close();
286                }
287            } catch (IOException ex) {
288                // ignore error on close() only
289            }
290        }
291    }
292
293    /**
294     * Creates a parameter configuration from the given maps.<p>
295     *
296     * @param strings the String map
297     * @param objects the object map
298     */
299    private CmsParameterConfiguration(Map<String, String> strings, Map<String, Object> objects) {
300
301        m_configurationStrings = strings;
302        m_configurationObjects = objects;
303    }
304
305    /**
306     * Returns an unmodifiable version of this parameter configuration.<p>
307     *
308     * @param original the configuration to make unmodifiable
309     *
310     * @return an unmodifiable version of this parameter configuration
311     */
312    public static CmsParameterConfiguration unmodifiableVersion(CmsParameterConfiguration original) {
313
314        return new CmsParameterConfiguration(
315            Collections.unmodifiableMap(original.m_configurationStrings),
316            original.m_configurationObjects);
317    }
318
319    /**
320     * Counts the number of successive times 'ch' appears in the
321     * 'line' before the position indicated by the 'index'.<p>
322     *
323     * @param line the line to count
324     * @param index the index position to start
325     * @param ch the character to count
326     *
327     * @return the number of successive times 'ch' appears in the 'line'
328     *      before the position indicated by the 'index'
329     */
330    protected static int countPreceding(String line, int index, char ch) {
331
332        int i;
333        for (i = index - 1; i >= 0; i--) {
334            if (line.charAt(i) != ch) {
335                break;
336            }
337        }
338        return index - 1 - i;
339    }
340
341    /**
342     * Checks if the line ends with odd number of backslashes.<p>
343     *
344     * @param line the line to check
345     *
346     * @return <code>true</code> if the line ends with odd number of backslashes
347     */
348    protected static boolean endsWithSlash(String line) {
349
350        if (!line.endsWith("\\")) {
351            return false;
352        }
353        return ((countPreceding(line, line.length() - 1, '\\') % 2) == 0);
354    }
355
356    /**
357     * Replaces escaped char sequences in the input value.<p>
358     *
359     * @param value the value to unescape
360     *
361     * @return the unescaped String
362     */
363    protected static String unescape(String value) {
364
365        value = CmsStringUtil.substitute(value, "\\,", ",");
366        value = CmsStringUtil.substitute(value, "\\=", "=");
367        value = CmsStringUtil.substitute(value, "\\\\", "\\");
368
369        return value;
370    }
371
372    /**
373     * Add a parameter to this configuration.<p>
374     *
375     * If the parameter already exists then the value will be added
376     * to the existing configuration entry and a List will be created for the values.<p>
377     *
378     * String values separated by a comma "," will NOT be tokenized when this
379     * method is used. To create a List of String values for a parameter, call this method
380     * multiple times with the same parameter name.<p>
381     *
382     * @param key the parameter to add
383     * @param value the value to add
384     */
385    public void add(String key, String value) {
386
387        add(key, value, false);
388    }
389
390    /**
391     * Serializes this parameter configuration for the OpenCms XML configuration.<p>
392     *
393     * For each parameter, a XML node like this<br>
394     * <code>
395     * &lt;param name="theName"&gt;theValue&lt;/param&gt;
396     * </code><br>
397     * is generated and appended to the provided parent node.<p>
398     *
399     * @param parentNode the parent node where the parameter nodes are appended to
400     *
401     * @return the parent node
402     */
403    public Element appendToXml(Element parentNode) {
404
405        return appendToXml(parentNode, null);
406    }
407
408    /**
409     * Serializes this parameter configuration for the OpenCms XML configuration.<p>
410     *
411     * For each parameter, a XML node like this<br>
412     * <code>
413     * &lt;param name="theName"&gt;theValue&lt;/param&gt;
414     * </code><br>
415     * is generated and appended to the provided parent node.<p>
416     *
417     * @param parentNode the parent node where the parameter nodes are appended to
418     * @param parametersToIgnore if not <code>null</code>,
419     *      all parameters in this list are not written to the XML
420     *
421     * @return the parent node
422     */
423    public Element appendToXml(Element parentNode, List<String> parametersToIgnore) {
424
425        for (Map.Entry<String, Object> entry : m_configurationObjects.entrySet()) {
426            String name = entry.getKey();
427            // check if the parameter should be ignored
428            if ((parametersToIgnore == null) || !parametersToIgnore.contains(name)) {
429                // now serialize the parameter name and value
430                Object value = entry.getValue();
431                if (value instanceof List) {
432                    @SuppressWarnings("unchecked")
433                    List<String> values = (List<String>)value;
434                    for (String strValue : values) {
435                        // use the original String as value
436                        Element paramNode = parentNode.addElement(I_CmsXmlConfiguration.N_PARAM);
437                        // set the name attribute
438                        paramNode.addAttribute(I_CmsXmlConfiguration.A_NAME, name);
439                        // set the text of <param> node
440                        paramNode.addText(strValue);
441                    }
442                } else {
443                    // use the original String as value
444                    String strValue = get(name);
445                    Element paramNode = parentNode.addElement(I_CmsXmlConfiguration.N_PARAM);
446                    // set the name attribute
447                    paramNode.addAttribute(I_CmsXmlConfiguration.A_NAME, name);
448                    // set the text of <param> node
449                    paramNode.addText(strValue);
450                }
451            }
452        }
453
454        return parentNode;
455    }
456
457    /**
458     * @see java.util.Map#clear()
459     */
460    @Override
461    public void clear() {
462
463        m_configurationStrings.clear();
464        m_configurationObjects.clear();
465    }
466
467    /**
468     * @see java.util.Map#containsKey(java.lang.Object)
469     */
470    @Override
471    public boolean containsKey(Object key) {
472
473        return m_configurationStrings.containsKey(key);
474    }
475
476    /**
477     * @see java.util.Map#containsValue(java.lang.Object)
478     */
479    @Override
480    public boolean containsValue(Object value) {
481
482        return m_configurationStrings.containsValue(value) || m_configurationObjects.containsValue(value);
483    }
484
485    /**
486     * @see java.util.Map#entrySet()
487     */
488    @Override
489    public Set<java.util.Map.Entry<String, String>> entrySet() {
490
491        return m_configurationStrings.entrySet();
492    }
493
494    /**
495     * Returns the String associated with the given parameter.<p>
496     *
497     * @param key the parameter to look up the value for
498     *
499     * @return the String associated with the given parameter
500     */
501    @Override
502    public String get(Object key) {
503
504        return m_configurationStrings.get(key);
505    }
506
507    /**
508     * Returns the boolean associated with the given parameter,
509     * or the default value in case there is no boolean value for this parameter.<p>
510     *
511     * @param key the parameter to look up the value for
512     * @param defaultValue the default value
513     *
514     * @return the boolean associated with the given parameter,
515     *      or the default value in case there is no boolean value for this parameter
516     */
517    public boolean getBoolean(String key, boolean defaultValue) {
518
519        Object value = m_configurationObjects.get(key);
520
521        if (value instanceof Boolean) {
522            return ((Boolean)value).booleanValue();
523
524        } else if (value instanceof String) {
525            Boolean b = Boolean.valueOf((String)value);
526            m_configurationObjects.put(key, b);
527            return b.booleanValue();
528
529        } else {
530            return defaultValue;
531        }
532    }
533
534    /**
535     * Returns the integer associated with the given parameter,
536     * or the default value in case there is no integer value for this parameter.<p>
537     *
538     * @param key the parameter to look up the value for
539     * @param defaultValue the default value
540     *
541     * @return the integer associated with the given parameter,
542     *      or the default value in case there is no integer value for this parameter
543     */
544    public int getInteger(String key, int defaultValue) {
545
546        Object value = m_configurationObjects.get(key);
547
548        if (value instanceof Integer) {
549            return ((Integer)value).intValue();
550
551        } else if (value instanceof String) {
552            Integer i = new Integer((String)value);
553            m_configurationObjects.put(key, i);
554            return i.intValue();
555
556        } else {
557            return defaultValue;
558        }
559    }
560
561    /**
562     * Returns the List of Strings associated with the given parameter,
563     * or an empty List in case there is no List of Strings for this parameter.<p>
564     *
565     * The list returned is a copy of the internal data of this object, and as
566     * such you may alter it freely.<p>
567     *
568     * @param key the parameter to look up the value for
569     *
570     * @return the List of Strings associated with the given parameter,
571     *      or an empty List in case there is no List of Strings for this parameter
572     */
573    public List<String> getList(String key) {
574
575        return getList(key, null);
576    }
577
578    /**
579     * Returns the List of Strings associated with the given parameter,
580     * or the default value in case there is no List of Strings for this parameter.<p>
581     *
582     * The list returned is a copy of the internal data of this object, and as
583     * such you may alter it freely.<p>
584     *
585     * @param key the parameter to look up the value for
586     * @param defaultValue the default value
587     *
588     * @return the List of Strings associated with the given parameter,
589     *      or the default value in case there is no List of Strings for this parameter
590     */
591    public List<String> getList(String key, List<String> defaultValue) {
592
593        Object value = m_configurationObjects.get(key);
594
595        if (value instanceof List) {
596            @SuppressWarnings("unchecked")
597            List<String> result = (List<String>)value;
598            return new ArrayList<String>(result);
599
600        } else if (value instanceof String) {
601            List<String> values = new ArrayList<String>(1);
602            values.add((String)value);
603            m_configurationObjects.put(key, values);
604            return values;
605
606        } else {
607            if (defaultValue == null) {
608                return new ArrayList<String>();
609            } else {
610                return defaultValue;
611            }
612        }
613    }
614
615    /**
616     * Returns the raw Object associated with the given parameter,
617     * or <code>null</code> in case there is no Object for this parameter.<p>
618     *
619     * @param key the parameter to look up the value for
620     *
621     * @return the raw Object associated with the given parameter,
622     *      or <code>null</code> in case there is no Object for this parameter.<p>
623     */
624    public Object getObject(String key) {
625
626        return m_configurationObjects.get(key);
627    }
628
629    /**
630     * Returns the String associated with the given parameter,
631     * or the given default value in case there is no value for this parameter.<p>
632     *
633     * @param key the parameter to look up the value for
634     * @param defaultValue the default value
635     *
636     * @return the String associated with the given parameter,
637     *      or the given default value in case there is no value for this parameter.<p>
638     */
639    public String getString(String key, String defaultValue) {
640
641        String result = get(key);
642        return result == null ? defaultValue : result;
643    }
644
645    /**
646     * @see java.util.Map#hashCode()
647     */
648    @Override
649    public int hashCode() {
650
651        return m_configurationStrings.hashCode();
652    }
653
654    /**
655     * @see java.util.Map#keySet()
656     */
657    @Override
658    public Set<String> keySet() {
659
660        return m_configurationStrings.keySet();
661    }
662
663    /**
664     * Load the parameters from the given input stream, which must be in property file format.<p>
665     *
666     * @param input the stream to load the input from
667     *
668     * @throws IOException in case of IO errors reading from the stream
669     */
670    public void load(InputStream input) throws IOException {
671
672        ParameterReader reader = null;
673
674        try {
675            reader = new ParameterReader(new InputStreamReader(input, CmsEncoder.ENCODING_ISO_8859_1));
676
677        } catch (UnsupportedEncodingException ex) {
678
679            reader = new ParameterReader(new InputStreamReader(input));
680        }
681
682        while (true) {
683            String line = reader.readParameter();
684            if (line == null) {
685                return; // EOF
686            }
687            int equalSign = line.indexOf('=');
688
689            if (equalSign > 0) {
690                String key = line.substring(0, equalSign).trim();
691                String value = line.substring(equalSign + 1).trim();
692
693                if (CmsStringUtil.isEmptyOrWhitespaceOnly(value)) {
694                    continue;
695                }
696
697                add(key, value, true);
698            }
699        }
700    }
701
702    /**
703     * Set a parameter for this configuration.<p>
704     *
705     * If the parameter already exists then the existing value will be replaced.<p>
706     *
707     * @param key the parameter to set
708     * @param value the value to set
709     *
710     * @return the previous String value from the parameter map
711     */
712    @Override
713    public String put(String key, String value) {
714
715        String result = remove(key);
716        add(key, value, false);
717        return result;
718    }
719
720    /**
721     * Merges this parameter configuration with the provided other parameter configuration.<p>
722     *
723     * The difference form a simple <code>Map&lt;String, String&gt;</code> is that for the parameter
724     * configuration, the values of the keys in both maps are merged and kept in the Object store
725     * as a List.<p>
726     *
727     * As result, <code>this</code> configuration will be altered, the other configuration will
728     * stay unchanged.<p>
729     *
730     * @param other the other parameter configuration to merge this configuration with
731     */
732    @Override
733    public void putAll(Map<? extends String, ? extends String> other) {
734
735        for (String key : other.keySet()) {
736            boolean tokenize = false;
737            if (other instanceof CmsParameterConfiguration) {
738                Object o = ((CmsParameterConfiguration)other).getObject(key);
739                if (o instanceof List) {
740                    tokenize = true;
741                }
742            }
743            add(key, other.get(key), tokenize);
744        }
745    }
746
747    /**
748     * Removes a parameter from this configuration.
749     *
750     * @param key the parameter to remove
751     */
752    @Override
753    public String remove(Object key) {
754
755        String result = m_configurationStrings.remove(key);
756        m_configurationObjects.remove(key);
757        return result;
758    }
759
760    /**
761     * @see java.util.Map#toString()
762     */
763    @Override
764    public String toString() {
765
766        return m_configurationStrings.toString();
767    }
768
769    /**
770     * @see java.util.Map#values()
771     */
772    @Override
773    public Collection<String> values() {
774
775        return m_configurationStrings.values();
776    }
777
778    /**
779     * Add a parameter to this configuration.<p>
780     *
781     * If the parameter already exists then the value will be added
782     * to the existing configuration entry and a List will be created for the values.<p>
783     *
784     * @param key the parameter to add
785     * @param value the value to add
786     * @param tokenize decides if a String value should be tokenized or nor
787     */
788    private void add(String key, String value, boolean tokenize) {
789
790        if (tokenize && (value.indexOf(ParameterTokenizer.COMMA) > 0)) {
791            // token contains commas, so must be split apart then added
792            ParameterTokenizer tokenizer = new ParameterTokenizer(value);
793            while (tokenizer.hasMoreTokens()) {
794                String token = tokenizer.nextToken();
795                addInternal(key, unescape(token));
796            }
797        } else if (tokenize) {
798            addInternal(key, unescape(value));
799        } else {
800            // token contains no commas, so can be simply added
801            addInternal(key, value);
802        }
803    }
804
805    /**
806     * Adds a parameter, parsing the value if required.<p>
807     *
808     * @param key the parameter to add
809     * @param value the value of the parameter
810     */
811    private void addInternal(String key, String value) {
812
813        Object currentObj = m_configurationObjects.get(key);
814        String currentStr = get(key);
815
816        if (currentObj instanceof String) {
817            // one object already in map - convert it to a list
818            List<String> values = new ArrayList<String>(2);
819            values.add(currentStr);
820            values.add(value);
821            m_configurationObjects.put(key, values);
822            m_configurationStrings.put(key, currentStr + ParameterTokenizer.COMMA + value);
823        } else if (currentObj instanceof List) {
824            // already a list - just add the new token
825            @SuppressWarnings("unchecked")
826            List<String> list = (List<String>)currentObj;
827            list.add(value);
828            m_configurationStrings.put(key, currentStr + ParameterTokenizer.COMMA + value);
829        } else {
830            m_configurationObjects.put(key, value);
831            m_configurationStrings.put(key, value);
832        }
833    }
834
835    /**
836     * Creates a new <tt>Properties</tt> object from the existing configuration
837     * extracting all key-value pars whose key are prefixed
838     * with <tt>keyPrefix</tt>. <p>
839     *
840     * For this example config:
841     *
842     * <pre>
843     *      # lines starting with # are comments
844     *      db.pool.default.jdbcDriver=net.bull.javamelody.JdbcDriver
845     *      db.pool.default.connectionProperties.driver=org.gjt.mm.mysql.Driver
846     * </pre>
847     *
848     * <tt>getPrefixedProperties("db.pool.default.connectionProperties")</tt>
849     * will return a <tt>Properties</tt> object with one single entry:
850     * <pre>
851     *      key:"driver", value:"org.gjt.mm.mysql.Driver"
852     * </pre>
853     *
854     * @param keyPrefix prefix to match. If it isn't already, it will be
855     *           terminated with a dot.  If <tt>null</tt>, it will return
856     *           an empty <tt>Properties</tt> instance
857     * @return a new <tt>Properties</tt> object with all the entries from this
858     *          configuration whose keys math the prefix
859     */
860    public Properties getPrefixedProperties(String keyPrefix) {
861
862        Properties props = new Properties();
863        if (null == keyPrefix) {
864            return props;
865        }
866
867        String dotTerminatedKeyPrefix = keyPrefix + (keyPrefix.endsWith(".") ? "" : ".");
868        for (Map.Entry<String, String> e : entrySet()) {
869            String key = e.getKey();
870            if ((null != key) && key.startsWith(dotTerminatedKeyPrefix)) {
871                String subKey = key.substring(dotTerminatedKeyPrefix.length());
872                props.put(subKey, e.getValue());
873            }
874        }
875        return props;
876    }
877}