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