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