001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.commons.configuration; 018 019 import java.io.BufferedReader; 020 import java.io.File; 021 import java.io.IOException; 022 import java.io.PrintWriter; 023 import java.io.Reader; 024 import java.io.Writer; 025 import java.net.URL; 026 import java.util.Collection; 027 import java.util.Iterator; 028 import java.util.List; 029 import java.util.Set; 030 031 import org.apache.commons.collections.set.ListOrderedSet; 032 import org.apache.commons.configuration.tree.ConfigurationNode; 033 import org.apache.commons.configuration.tree.DefaultConfigurationNode; 034 import org.apache.commons.configuration.tree.ViewNode; 035 import org.apache.commons.lang.StringUtils; 036 037 /** 038 * <p> 039 * A specialized hierarchical configuration implementation for parsing ini 040 * files. 041 * </p> 042 * <p> 043 * An initialization or ini file is a configuration file typically found on 044 * Microsoft's Windows operating system and contains data for Windows based 045 * applications. 046 * </p> 047 * <p> 048 * Although popularized by Windows, ini files can be used on any system or 049 * platform due to the fact that they are merely text files that can easily be 050 * parsed and modified by both humans and computers. 051 * </p> 052 * <p> 053 * A typcial ini file could look something like: 054 * </p> 055 * <code> 056 * [section1]<br> 057 * ; this is a comment!<br> 058 * var1 = foo<br> 059 * var2 = bar<br> 060 * <br> 061 * [section2]<br> 062 * var1 = doo<br> 063 * </code> 064 * <p> 065 * The format of ini files is fairly straight forward and is composed of three 066 * components:<br> 067 * <ul> 068 * <li><b>Sections:</b> Ini files are split into sections, each section starting 069 * with a section declaration. A section declaration starts with a '[' and ends 070 * with a ']'. Sections occur on one line only.</li> 071 * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters 072 * have a typical <code>key = value</code> format.</li> 073 * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li> 074 * </ul> 075 * </p> 076 * <p> 077 * There are various implementations of the ini file format by various vendors 078 * which has caused a number of differences to appear. As far as possible this 079 * configuration tries to be lenient and support most of the differences. 080 * </p> 081 * <p> 082 * Some of the differences supported are as follows: 083 * <ul> 084 * <li><b>Comments:</b> The '#' character is also accepted as a comment 085 * signifier.</li> 086 * <li><b>Key value separtor:</b> The ':' character is also accepted in place of 087 * '=' to separate keys and values in parameters, for example 088 * <code>var1 : foo</code>.</li> 089 * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed, 090 * this configuration does however support it. In the event of a duplicate 091 * section, the two section's values are merged.</li> 092 * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only 093 * allowed if they are in two different sections, thus they are local to 094 * sections; this configuration simply merges duplicates; if a section has a 095 * duplicate parameter the values are then added to the key as a list.</li> 096 * </ul> 097 * </p> 098 * <p> 099 * Global parameters are also allowed; any parameters declared before a section 100 * is declared are added to a global section. It is important to note that this 101 * global section does not have a name. 102 * </p> 103 * <p> 104 * In all instances, a parameter's key is prepended with its section name and a 105 * '.' (period). Thus a parameter named "var1" in "section1" will have the key 106 * <code>section1.var1</code> in this configuration. (This is the default 107 * behavior. Because this is a hierarchical configuration you can change this by 108 * setting a different {@link org.apache.commons.configuration.tree.ExpressionEngine}.) 109 * </p> 110 * <p> 111 * <h3>Implementation Details:</h3> Consider the following ini file:<br> 112 * <code> 113 * default = ok<br> 114 * <br> 115 * [section1]<br> 116 * var1 = foo<br> 117 * var2 = doodle<br> 118 * <br> 119 * [section2]<br> 120 * ; a comment<br> 121 * var1 = baz<br> 122 * var2 = shoodle<br> 123 * bad =<br> 124 * = worse<br> 125 * <br> 126 * [section3]<br> 127 * # another comment<br> 128 * var1 : foo<br> 129 * var2 : bar<br> 130 * var5 : test1<br> 131 * <br> 132 * [section3]<br> 133 * var3 = foo<br> 134 * var4 = bar<br> 135 * var5 = test2<br> 136 * </code> 137 * </p> 138 * <p> 139 * This ini file will be parsed without error. Note: 140 * <ul> 141 * <li>The parameter named "default" is added to the global section, it's value 142 * is accessed simply using <code>getProperty("default")</code>.</li> 143 * <li>Section 1's parameters can be accessed using 144 * <code>getProperty("section1.var1")</code>.</li> 145 * <li>The parameter named "bad" simply adds the parameter with an empty value.</li> 146 * <li>The empty key with value "= worse" is added using a key consisting of a 147 * single space character. This key is still added to section 2 and the value 148 * can be accessed using <code>getProperty("section2. ")</code>, notice the 149 * period '.' and the space following the section name.</li> 150 * <li>Section three uses both '=' and ':' to separate keys and values.</li> 151 * <li>Section 3 has a duplicate key named "var5". The value for this key is 152 * [test1, test2], and is represented as a List.</li> 153 * </ul> 154 * </p> 155 * <p> 156 * Internally, this configuration maps the content of the represented ini file 157 * to its node structure in the following way: 158 * <ul> 159 * <li>Sections are represented by direct child nodes of the root node.</li> 160 * <li>For the content of a section, corresponding nodes are created as children 161 * of the section node.</li> 162 * </ul> 163 * This explains how the keys for the properties can be constructed. You can 164 * also use other methods of {@link HierarchicalConfiguration} for querying or 165 * manipulating the hierarchy of configuration nodes, for instance the 166 * <code>configurationAt()</code> method for obtaining the data of a specific 167 * section. 168 * </p> 169 * <p> 170 * The set of sections in this configuration can be retrieved using the 171 * <code>getSections()</code> method. For obtaining a 172 * <code>SubnodeConfiguration</code> with the content of a specific section the 173 * <code>getSection()</code> method can be used. 174 * </p> 175 * <p> 176 * <em>Note:</em> Configuration objects of this type can be read concurrently by 177 * multiple threads. However if one of these threads modifies the object, 178 * synchronization has to be performed manually. 179 * </p> 180 * 181 * @author <a 182 * href="http://commons.apache.org/configuration/team-list.html">Commons 183 * Configuration team</a> 184 * @version $Id: HierarchicalINIConfiguration.java 720295 2008-11-24 21:29:42Z oheger $ 185 * @since 1.6 186 */ 187 public class HierarchicalINIConfiguration extends 188 AbstractHierarchicalFileConfiguration 189 { 190 /** 191 * The characters that signal the start of a comment line. 192 */ 193 protected static final String COMMENT_CHARS = "#;"; 194 195 /** 196 * The characters used to separate keys from values. 197 */ 198 protected static final String SEPARATOR_CHARS = "=:"; 199 200 /** 201 * The serial version UID. 202 */ 203 private static final long serialVersionUID = 2548006161386850670L; 204 205 /** 206 * Constant for the line separator. 207 */ 208 private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 209 210 /** 211 * The line continuation character. 212 */ 213 private static final String LINE_CONT = "\\"; 214 215 /** 216 * Create a new empty INI Configuration. 217 */ 218 public HierarchicalINIConfiguration() 219 { 220 super(); 221 } 222 223 /** 224 * Create and load the ini configuration from the given file. 225 * 226 * @param filename The name pr path of the ini file to load. 227 * @throws ConfigurationException If an error occurs while loading the file 228 */ 229 public HierarchicalINIConfiguration(String filename) 230 throws ConfigurationException 231 { 232 super(filename); 233 } 234 235 /** 236 * Create and load the ini configuration from the given file. 237 * 238 * @param file The ini file to load. 239 * @throws ConfigurationException If an error occurs while loading the file 240 */ 241 public HierarchicalINIConfiguration(File file) 242 throws ConfigurationException 243 { 244 super(file); 245 } 246 247 /** 248 * Create and load the ini configuration from the given url. 249 * 250 * @param url The url of the ini file to load. 251 * @throws ConfigurationException If an error occurs while loading the file 252 */ 253 public HierarchicalINIConfiguration(URL url) throws ConfigurationException 254 { 255 super(url); 256 } 257 258 /** 259 * Save the configuration to the specified writer. 260 * 261 * @param writer - The writer to save the configuration to. 262 * @throws ConfigurationException If an error occurs while writing the 263 * configuration 264 */ 265 public void save(Writer writer) throws ConfigurationException 266 { 267 PrintWriter out = new PrintWriter(writer); 268 Iterator it = getSections().iterator(); 269 while (it.hasNext()) 270 { 271 String section = (String) it.next(); 272 if (section != null) 273 { 274 out.print("["); 275 out.print(section); 276 out.print("]"); 277 out.println(); 278 } 279 280 Configuration subset = getSection(section); 281 Iterator keys = subset.getKeys(); 282 while (keys.hasNext()) 283 { 284 String key = (String) keys.next(); 285 Object value = subset.getProperty(key); 286 if (value instanceof Collection) 287 { 288 Iterator values = ((Collection) value).iterator(); 289 while (values.hasNext()) 290 { 291 value = (Object) values.next(); 292 out.print(key); 293 out.print(" = "); 294 out.print(formatValue(value.toString())); 295 out.println(); 296 } 297 } 298 else 299 { 300 out.print(key); 301 out.print(" = "); 302 out.print(formatValue(value.toString())); 303 out.println(); 304 } 305 } 306 307 out.println(); 308 } 309 310 out.flush(); 311 } 312 313 /** 314 * Load the configuration from the given reader. Note that the 315 * <code>clear</code> method is not called so the configuration read in will 316 * be merged with the current configuration. 317 * 318 * @param reader The reader to read the configuration from. 319 * @throws ConfigurationException If an error occurs while reading the 320 * configuration 321 */ 322 public void load(Reader reader) throws ConfigurationException 323 { 324 try 325 { 326 BufferedReader bufferedReader = new BufferedReader(reader); 327 ConfigurationNode sectionNode = getRootNode(); 328 329 String line = bufferedReader.readLine(); 330 while (line != null) 331 { 332 line = line.trim(); 333 if (!isCommentLine(line)) 334 { 335 if (isSectionLine(line)) 336 { 337 String section = line.substring(1, line.length() - 1); 338 sectionNode = getSectionNode(section); 339 } 340 341 else 342 { 343 String key = ""; 344 String value = ""; 345 int index = line.indexOf("="); 346 if (index >= 0) 347 { 348 key = line.substring(0, index); 349 value = parseValue(line.substring(index + 1), bufferedReader); 350 } 351 else 352 { 353 index = line.indexOf(":"); 354 if (index >= 0) 355 { 356 key = line.substring(0, index); 357 value = parseValue(line.substring(index + 1), bufferedReader); 358 } 359 else 360 { 361 key = line; 362 } 363 } 364 key = key.trim(); 365 if (key.length() < 1) 366 { 367 // use space for properties with no key 368 key = " "; 369 } 370 ConfigurationNode node = createNode(key); 371 node.setValue(value); 372 sectionNode.addChild(node); 373 } 374 } 375 376 line = bufferedReader.readLine(); 377 } 378 } 379 catch (IOException e) 380 { 381 throw new ConfigurationException( 382 "Unable to load the configuration", e); 383 } 384 } 385 386 /** 387 * Parse the value to remove the quotes and ignoring the comment. Example: 388 * 389 * <pre> 390 * "value" ; comment -> value 391 * </pre> 392 * 393 * <pre> 394 * 'value' ; comment -> value 395 * </pre> 396 * 397 * @param val the value to be parsed 398 * @param reader the reader (needed if multiple lines have to be read) 399 * @throws IOException if an IO error occurs 400 */ 401 private static String parseValue(String val, BufferedReader reader) throws IOException 402 { 403 StringBuffer propertyValue = new StringBuffer(); 404 boolean lineContinues; 405 String value = val.trim(); 406 407 do 408 { 409 boolean quoted = value.startsWith("\"") || value.startsWith("'"); 410 boolean stop = false; 411 boolean escape = false; 412 413 char quote = quoted ? value.charAt(0) : 0; 414 415 int i = quoted ? 1 : 0; 416 417 StringBuffer result = new StringBuffer(); 418 while (i < value.length() && !stop) 419 { 420 char c = value.charAt(i); 421 422 if (quoted) 423 { 424 if ('\\' == c && !escape) 425 { 426 escape = true; 427 } 428 else if (!escape && quote == c) 429 { 430 stop = true; 431 } 432 else if (escape && quote == c) 433 { 434 escape = false; 435 result.append(c); 436 } 437 else 438 { 439 if (escape) 440 { 441 escape = false; 442 result.append('\\'); 443 } 444 445 result.append(c); 446 } 447 } 448 else 449 { 450 if (!isCommentChar(c)) 451 { 452 result.append(c); 453 } 454 else 455 { 456 stop = true; 457 } 458 } 459 460 i++; 461 } 462 463 String v = result.toString(); 464 if (!quoted) 465 { 466 v = v.trim(); 467 lineContinues = lineContinues(v); 468 if (lineContinues) 469 { 470 // remove trailing "\" 471 v = v.substring(0, v.length() - 1).trim(); 472 } 473 } 474 else 475 { 476 lineContinues = lineContinues(value, i); 477 } 478 propertyValue.append(v); 479 480 if (lineContinues) 481 { 482 propertyValue.append(LINE_SEPARATOR); 483 value = reader.readLine(); 484 } 485 } while (lineContinues && value != null); 486 487 return propertyValue.toString(); 488 } 489 490 /** 491 * Tests whether the specified string contains a line continuation marker. 492 * 493 * @param line the string to check 494 * @return a flag whether this line continues 495 */ 496 private static boolean lineContinues(String line) 497 { 498 String s = line.trim(); 499 return s.equals(LINE_CONT) 500 || (s.length() > 2 && s.endsWith(LINE_CONT) && Character 501 .isWhitespace(s.charAt(s.length() - 2))); 502 } 503 504 /** 505 * Tests whether the specified string contains a line continuation marker 506 * after the specified position. This method parses the string to remove a 507 * comment that might be present. Then it checks whether a line continuation 508 * marker can be found at the end. 509 * 510 * @param line the line to check 511 * @param pos the start position 512 * @return a flag whether this line continues 513 */ 514 private static boolean lineContinues(String line, int pos) 515 { 516 String s; 517 518 if (pos >= line.length()) 519 { 520 s = line; 521 } 522 else 523 { 524 int end = pos; 525 while (end < line.length() && !isCommentChar(line.charAt(end))) 526 { 527 end++; 528 } 529 s = line.substring(pos, end); 530 } 531 532 return lineContinues(s); 533 } 534 535 /** 536 * Tests whether the specified character is a comment character. 537 * 538 * @param c the character 539 * @return a flag whether this character starts a comment 540 */ 541 private static boolean isCommentChar(char c) 542 { 543 return COMMENT_CHARS.indexOf(c) >= 0; 544 } 545 546 /** 547 * Add quotes around the specified value if it contains a comment character. 548 */ 549 private String formatValue(String value) 550 { 551 boolean quoted = false; 552 553 for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++) 554 { 555 char c = COMMENT_CHARS.charAt(i); 556 if (value.indexOf(c) != -1) 557 { 558 quoted = true; 559 } 560 } 561 562 if (quoted) 563 { 564 return '"' + StringUtils.replace(value, "\"", "\\\"") + '"'; 565 } 566 else 567 { 568 return value; 569 } 570 } 571 572 /** 573 * Determine if the given line is a comment line. 574 * 575 * @param line The line to check. 576 * @return true if the line is empty or starts with one of the comment 577 * characters 578 */ 579 protected boolean isCommentLine(String line) 580 { 581 if (line == null) 582 { 583 return false; 584 } 585 // blank lines are also treated as comment lines 586 return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0; 587 } 588 589 /** 590 * Determine if the given line is a section. 591 * 592 * @param line The line to check. 593 * @return true if the line contains a secion 594 */ 595 protected boolean isSectionLine(String line) 596 { 597 if (line == null) 598 { 599 return false; 600 } 601 return line.startsWith("[") && line.endsWith("]"); 602 } 603 604 /** 605 * Return a set containing the sections in this ini configuration. Note that 606 * changes to this set do not affect the configuration. 607 * 608 * @return a set containing the sections. 609 */ 610 public Set getSections() 611 { 612 Set sections = new ListOrderedSet(); 613 boolean globalSection = false; 614 615 for (Iterator it = getRootNode().getChildren().iterator(); it.hasNext();) 616 { 617 ConfigurationNode node = (ConfigurationNode) it.next(); 618 if (isSectionNode(node)) 619 { 620 if (globalSection) 621 { 622 sections.add(null); 623 globalSection = false; 624 } 625 sections.add(node.getName()); 626 } 627 else 628 { 629 globalSection = true; 630 } 631 } 632 633 return sections; 634 } 635 636 /** 637 * Returns a configuration with the content of the specified section. This 638 * provides an easy way of working with a single section only. The way this 639 * configuration is structured internally, this method is very similar to 640 * calling 641 * <code>{@link HierarchicalConfiguration#configurationAt(String)}</code> 642 * with the name of the section in question. There are the following 643 * differences however: 644 * <ul> 645 * <li>This method never throws an exception. If the section does not exist, 646 * an empty configuration is returned.</li> 647 * <li>There is special support for the global section: Passing in 648 * <b>null</b> as section name returns a configuration with the content of 649 * the global section (which may also be empty).</li> 650 * </ul> 651 * 652 * @param name the name of the section in question; <b>null</b> represents 653 * the global section 654 * @return a configuration containing only the properties of the specified 655 * section 656 */ 657 public SubnodeConfiguration getSection(String name) 658 { 659 if (name == null) 660 { 661 return getGlobalSection(); 662 } 663 664 else 665 { 666 try 667 { 668 return configurationAt(name); 669 } 670 catch (IllegalArgumentException iex) 671 { 672 // the passed in key does not map to exactly one node 673 // return an empty configuration 674 return new SubnodeConfiguration(this, 675 new DefaultConfigurationNode()); 676 } 677 } 678 } 679 680 /** 681 * Obtains the node representing the specified section. This method is 682 * called while the configuration is loaded. If a node for this section 683 * already exists, it is returned. Otherwise a new node is created. 684 * 685 * @param sectionName the name of the section 686 * @return the node for this section 687 */ 688 private ConfigurationNode getSectionNode(String sectionName) 689 { 690 List nodes = getRootNode().getChildren(sectionName); 691 if (!nodes.isEmpty()) 692 { 693 return (ConfigurationNode) nodes.get(0); 694 } 695 696 ConfigurationNode node = createNode(sectionName); 697 markSectionNode(node); 698 getRootNode().addChild(node); 699 return node; 700 } 701 702 /** 703 * Creates a sub configuration for the global section of the represented INI 704 * configuration. 705 * 706 * @return the sub configuration for the global section 707 */ 708 private SubnodeConfiguration getGlobalSection() 709 { 710 ViewNode parent = new ViewNode(); 711 712 for (Iterator it = getRootNode().getChildren().iterator(); it.hasNext();) 713 { 714 ConfigurationNode node = (ConfigurationNode) it.next(); 715 if (!isSectionNode(node)) 716 { 717 parent.addChild(node); 718 } 719 } 720 721 return createSubnodeConfiguration(parent); 722 } 723 724 /** 725 * Marks a configuration node as a section node. This means that this node 726 * represents a section header. This implementation uses the node's 727 * reference property to store a flag. 728 * 729 * @param node the node to be marked 730 */ 731 private static void markSectionNode(ConfigurationNode node) 732 { 733 node.setReference(Boolean.TRUE); 734 } 735 736 /** 737 * Checks whether the specified configuration node represents a section. 738 * 739 * @param node the node in question 740 * @return a flag whether this node represents a section 741 */ 742 private static boolean isSectionNode(ConfigurationNode node) 743 { 744 return node.getReference() != null || node.getChildrenCount() > 0; 745 } 746 }