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 * <param name="theName">theValue</param> 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 * <param name="theName">theValue</param> 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<String, String></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}