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