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