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 018package org.apache.commons.configuration; 019 020import java.io.BufferedReader; 021import java.io.File; 022import java.io.IOException; 023import java.io.PrintWriter; 024import java.io.Reader; 025import java.io.Writer; 026import java.net.URL; 027import java.util.Collection; 028import java.util.Iterator; 029import java.util.Set; 030import java.util.TreeSet; 031 032/** 033 * <p> 034 * An initialization or ini file is a configuration file typically found on 035 * Microsoft's Windows operating system and contains data for Windows based 036 * applications. 037 * </p> 038 * 039 * <p> 040 * Although popularized by Windows, ini files can be used on any system or 041 * platform due to the fact that they are merely text files that can easily be 042 * parsed and modified by both humans and computers. 043 * </p> 044 * 045 * <p> 046 * A typical ini file could look something like: 047 * </p> 048 * <pre> 049 * [section1] 050 * ; this is a comment! 051 * var1 = foo 052 * var2 = bar 053 * 054 * [section2] 055 * var1 = doo 056 * </pre> 057 * 058 * <p> 059 * The format of ini files is fairly straight forward and is composed of three 060 * components:<br> 061 * <ul> 062 * <li><b>Sections:</b> Ini files are split into sections, each section 063 * starting with a section declaration. A section declaration starts with a '[' 064 * and ends with a ']'. Sections occur on one line only.</li> 065 * <li><b>Parameters:</b> Items in a section are known as parameters. 066 * Parameters have a typical {@code key = value} format.</li> 067 * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments. 068 * </li> 069 * </ul> 070 * </p> 071 * 072 * <p> 073 * There are various implementations of the ini file format by various vendors 074 * which has caused a number of differences to appear. As far as possible this 075 * configuration tries to be lenient and support most of the differences. 076 * </p> 077 * 078 * <p> 079 * Some of the differences supported are as follows: 080 * <ul> 081 * <li><b>Comments:</b> The '#' character is also accepted as a comment 082 * signifier.</li> 083 * <li><b>Key value separtor:</b> The ':' character is also accepted in place 084 * of '=' to separate keys and values in parameters, for example 085 * {@code var1 : foo}.</li> 086 * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed , 087 * this configuration does however support it. In the event of a duplicate 088 * section, the two section's values are merged.</li> 089 * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only 090 * allowed if they are in two different sections, thus they are local to 091 * sections; this configuration simply merges duplicates; if a section has a 092 * duplicate parameter the values are then added to the key as a list. </li> 093 * </ul> 094 * </p> 095 * <p> 096 * Global parameters are also allowed; any parameters declared before a section 097 * is declared are added to a global section. It is important to note that this 098 * global section does not have a name. 099 * </p> 100 * <p> 101 * In all instances, a parameter's key is prepended with its section name and a 102 * '.' (period). Thus a parameter named "var1" in "section1" will have the key 103 * {@code section1.var1} in this configuration. Thus, a section's 104 * parameters can easily be retrieved using the {@code subset} method 105 * using the section name as the prefix. 106 * </p> 107 * <p> 108 * <h3>Implementation Details:</h3> 109 * Consider the following ini file:<br> 110 * <pre> 111 * default = ok 112 * 113 * [section1] 114 * var1 = foo 115 * var2 = doodle 116 * 117 * [section2] 118 * ; a comment 119 * var1 = baz 120 * var2 = shoodle 121 * bad = 122 * = worse 123 * 124 * [section3] 125 * # another comment 126 * var1 : foo 127 * var2 : bar 128 * var5 : test1 129 * 130 * [section3] 131 * var3 = foo 132 * var4 = bar 133 * var5 = test2 134 * </pre> 135 * </p> 136 * <p> 137 * This ini file will be parsed without error. Note: 138 * <ul> 139 * <li>The parameter named "default" is added to the global section, it's value 140 * is accessed simply using {@code getProperty("default")}.</li> 141 * <li>Section 1's parameters can be accessed using 142 * {@code getProperty("section1.var1")}.</li> 143 * <li>The parameter named "bad" simply adds the parameter with an empty value. 144 * </li> 145 * <li>The empty key with value "= worse" is added using an empty key. This key 146 * is still added to section 2 and the value can be accessed using 147 * {@code getProperty("section2.")}, notice the period '.' following the 148 * section name.</li> 149 * <li>Section three uses both '=' and ':' to separate keys and values.</li> 150 * <li>Section 3 has a duplicate key named "var5". The value for this key is 151 * [test1, test2], and is represented as a List.</li> 152 * </ul> 153 * </p> 154 * <p> 155 * The set of sections in this configuration can be retrieved using the 156 * {@code getSections} method. 157 * </p> 158 * <p> 159 * <em>Note:</em> Configuration objects of this type can be read concurrently 160 * by multiple threads. However if one of these threads modifies the object, 161 * synchronization has to be performed manually. 162 * </p> 163 * 164 * @author Trevor Miller 165 * @version $Id: INIConfiguration.java 1210003 2011-12-03 20:54:46Z oheger $ 166 * @since 1.4 167 * @deprecated This class has been replaced by HierarchicalINIConfiguration, 168 * which provides a superset of the functionality offered by this class. 169 */ 170@Deprecated 171public class INIConfiguration extends AbstractFileConfiguration 172{ 173 /** 174 * The characters that signal the start of a comment line. 175 */ 176 protected static final String COMMENT_CHARS = "#;"; 177 178 /** 179 * The characters used to separate keys from values. 180 */ 181 protected static final String SEPARATOR_CHARS = "=:"; 182 183 /** 184 * Create a new empty INI Configuration. 185 */ 186 public INIConfiguration() 187 { 188 super(); 189 } 190 191 /** 192 * Create and load the ini configuration from the given file. 193 * 194 * @param filename The name pr path of the ini file to load. 195 * @throws ConfigurationException If an error occurs while loading the file 196 */ 197 public INIConfiguration(String filename) throws ConfigurationException 198 { 199 super(filename); 200 } 201 202 /** 203 * Create and load the ini configuration from the given file. 204 * 205 * @param file The ini file to load. 206 * @throws ConfigurationException If an error occurs while loading the file 207 */ 208 public INIConfiguration(File file) throws ConfigurationException 209 { 210 super(file); 211 } 212 213 /** 214 * Create and load the ini configuration from the given url. 215 * 216 * @param url The url of the ini file to load. 217 * @throws ConfigurationException If an error occurs while loading the file 218 */ 219 public INIConfiguration(URL url) throws ConfigurationException 220 { 221 super(url); 222 } 223 224 /** 225 * Save the configuration to the specified writer. 226 * 227 * @param writer - The writer to save the configuration to. 228 * @throws ConfigurationException If an error occurs while writing the 229 * configuration 230 */ 231 public void save(Writer writer) throws ConfigurationException 232 { 233 PrintWriter out = new PrintWriter(writer); 234 Iterator<String> it = getSections().iterator(); 235 while (it.hasNext()) 236 { 237 String section = it.next(); 238 out.print("["); 239 out.print(section); 240 out.print("]"); 241 out.println(); 242 243 Configuration subset = subset(section); 244 Iterator<String> keys = subset.getKeys(); 245 while (keys.hasNext()) 246 { 247 String key = keys.next(); 248 Object value = subset.getProperty(key); 249 if (value instanceof Collection) 250 { 251 Iterator<?> values = ((Collection<?>) value).iterator(); 252 while (values.hasNext()) 253 { 254 value = values.next(); 255 out.print(key); 256 out.print(" = "); 257 out.print(formatValue(value.toString())); 258 out.println(); 259 } 260 } 261 else 262 { 263 out.print(key); 264 out.print(" = "); 265 out.print(formatValue(value.toString())); 266 out.println(); 267 } 268 } 269 270 out.println(); 271 } 272 273 out.flush(); 274 } 275 276 /** 277 * Load the configuration from the given reader. Note that the 278 * {@code clear()} method is not called so the configuration read in 279 * will be merged with the current configuration. 280 * 281 * @param reader The reader to read the configuration from. 282 * @throws ConfigurationException If an error occurs while reading the 283 * configuration 284 */ 285 public void load(Reader reader) throws ConfigurationException 286 { 287 try 288 { 289 BufferedReader bufferedReader = new BufferedReader(reader); 290 String line = bufferedReader.readLine(); 291 String section = ""; 292 while (line != null) 293 { 294 line = line.trim(); 295 if (!isCommentLine(line)) 296 { 297 if (isSectionLine(line)) 298 { 299 section = line.substring(1, line.length() - 1) + "."; 300 } 301 else 302 { 303 String key = ""; 304 String value = ""; 305 int index = line.indexOf("="); 306 if (index >= 0) 307 { 308 key = section + line.substring(0, index); 309 value = parseValue(line.substring(index + 1)); 310 } 311 else 312 { 313 index = line.indexOf(":"); 314 if (index >= 0) 315 { 316 key = section + line.substring(0, index); 317 value = parseValue(line.substring(index + 1)); 318 } 319 else 320 { 321 key = section + line; 322 } 323 } 324 addProperty(key.trim(), value); 325 } 326 } 327 line = bufferedReader.readLine(); 328 } 329 } 330 catch (IOException e) 331 { 332 throw new ConfigurationException("Unable to load the configuration", e); 333 } 334 } 335 336 /** 337 * Parse the value to remove the quotes and ignoring the comment. 338 * Example: 339 * 340 * <pre>"value" ; comment -> value</pre> 341 * 342 * <pre>'value' ; comment -> value</pre> 343 * 344 * @param value 345 */ 346 private String parseValue(String value) 347 { 348 value = value.trim(); 349 350 boolean quoted = value.startsWith("\"") || value.startsWith("'"); 351 boolean stop = false; 352 boolean escape = false; 353 354 char quote = quoted ? value.charAt(0) : 0; 355 356 int i = quoted ? 1 : 0; 357 358 StringBuilder result = new StringBuilder(); 359 while (i < value.length() && !stop) 360 { 361 char c = value.charAt(i); 362 363 if (quoted) 364 { 365 if ('\\' == c && !escape) 366 { 367 escape = true; 368 } 369 else if (!escape && quote == c) 370 { 371 stop = true; 372 } 373 else if (escape && quote == c) 374 { 375 escape = false; 376 result.append(c); 377 } 378 else 379 { 380 if (escape) 381 { 382 escape = false; 383 result.append('\\'); 384 } 385 386 result.append(c); 387 } 388 } 389 else 390 { 391 if (COMMENT_CHARS.indexOf(c) == -1) 392 { 393 result.append(c); 394 } 395 else 396 { 397 stop = true; 398 } 399 } 400 401 i++; 402 } 403 404 String v = result.toString(); 405 if (!quoted) 406 { 407 v = v.trim(); 408 } 409 return v; 410 } 411 412 /** 413 * Add quotes around the specified value if it contains a comment character. 414 */ 415 private String formatValue(String value) 416 { 417 boolean quoted = false; 418 419 for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++) 420 { 421 char c = COMMENT_CHARS.charAt(i); 422 if (value.indexOf(c) != -1) 423 { 424 quoted = true; 425 } 426 } 427 428 if (quoted) 429 { 430 return '"' + value.replaceAll("\"", "\\\\\\\"") + '"'; 431 } 432 else 433 { 434 return value; 435 } 436 } 437 438 /** 439 * Determine if the given line is a comment line. 440 * 441 * @param line The line to check. 442 * @return true if the line is empty or starts with one of the comment 443 * characters 444 */ 445 protected boolean isCommentLine(String line) 446 { 447 if (line == null) 448 { 449 return false; 450 } 451 // blank lines are also treated as comment lines 452 return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0; 453 } 454 455 /** 456 * Determine if the given line is a section. 457 * 458 * @param line The line to check. 459 * @return true if the line contains a secion 460 */ 461 protected boolean isSectionLine(String line) 462 { 463 if (line == null) 464 { 465 return false; 466 } 467 return line.startsWith("[") && line.endsWith("]"); 468 } 469 470 /** 471 * Return a set containing the sections in this ini configuration. Note that 472 * changes to this set do not affect the configuration. 473 * 474 * @return a set containing the sections. 475 */ 476 public Set<String> getSections() 477 { 478 Set<String> sections = new TreeSet<String>(); 479 480 Iterator<String> keys = getKeys(); 481 while (keys.hasNext()) 482 { 483 String key = keys.next(); 484 int index = key.indexOf("."); 485 if (index >= 0) 486 { 487 sections.add(key.substring(0, index)); 488 } 489 } 490 491 return sections; 492 } 493}