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 /**
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
171 public 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 }