Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
HierarchicalINIConfiguration |
|
| 4.173913043478261;4,174 |
1 | /* | |
2 | * Licensed to the Apache Software Foundation (ASF) under one or more | |
3 | * contributor license agreements. See the NOTICE file distributed with | |
4 | * this work for additional information regarding copyright ownership. | |
5 | * The ASF licenses this file to You under the Apache License, Version 2.0 | |
6 | * (the "License"); you may not use this file except in compliance with | |
7 | * the License. You may obtain a copy of the License at | |
8 | * | |
9 | * http://www.apache.org/licenses/LICENSE-2.0 | |
10 | * | |
11 | * Unless required by applicable law or agreed to in writing, software | |
12 | * distributed under the License is distributed on an "AS IS" BASIS, | |
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | * See the License for the specific language governing permissions and | |
15 | * limitations under the License. | |
16 | */ | |
17 | package org.apache.commons.configuration; | |
18 | ||
19 | import java.io.BufferedReader; | |
20 | import java.io.File; | |
21 | import java.io.IOException; | |
22 | import java.io.PrintWriter; | |
23 | import java.io.Reader; | |
24 | import java.io.Writer; | |
25 | import java.net.URL; | |
26 | import java.util.Collection; | |
27 | import java.util.Collections; | |
28 | import java.util.Iterator; | |
29 | import java.util.LinkedHashSet; | |
30 | import java.util.List; | |
31 | import java.util.Set; | |
32 | ||
33 | import org.apache.commons.configuration.tree.ConfigurationNode; | |
34 | import org.apache.commons.configuration.tree.ViewNode; | |
35 | ||
36 | /** | |
37 | * <p> | |
38 | * A specialized hierarchical configuration implementation for parsing ini | |
39 | * files. | |
40 | * </p> | |
41 | * <p> | |
42 | * An initialization or ini file is a configuration file typically found on | |
43 | * Microsoft's Windows operating system and contains data for Windows based | |
44 | * applications. | |
45 | * </p> | |
46 | * <p> | |
47 | * Although popularized by Windows, ini files can be used on any system or | |
48 | * platform due to the fact that they are merely text files that can easily be | |
49 | * parsed and modified by both humans and computers. | |
50 | * </p> | |
51 | * <p> | |
52 | * A typical ini file could look something like: | |
53 | * </p> | |
54 | * <pre> | |
55 | * [section1] | |
56 | * ; this is a comment! | |
57 | * var1 = foo | |
58 | * var2 = bar | |
59 | * | |
60 | * [section2] | |
61 | * var1 = doo | |
62 | * </pre> | |
63 | * <p> | |
64 | * The format of ini files is fairly straight forward and is composed of three | |
65 | * components:<br> | |
66 | * <ul> | |
67 | * <li><b>Sections:</b> Ini files are split into sections, each section starting | |
68 | * with a section declaration. A section declaration starts with a '[' and ends | |
69 | * with a ']'. Sections occur on one line only.</li> | |
70 | * <li><b>Parameters:</b> Items in a section are known as parameters. Parameters | |
71 | * have a typical {@code key = value} format.</li> | |
72 | * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.</li> | |
73 | * </ul> | |
74 | * </p> | |
75 | * <p> | |
76 | * There are various implementations of the ini file format by various vendors | |
77 | * which has caused a number of differences to appear. As far as possible this | |
78 | * configuration tries to be lenient and support most of the differences. | |
79 | * </p> | |
80 | * <p> | |
81 | * Some of the differences supported are as follows: | |
82 | * <ul> | |
83 | * <li><b>Comments:</b> The '#' character is also accepted as a comment | |
84 | * signifier.</li> | |
85 | * <li><b>Key value separator:</b> The ':' character is also accepted in place of | |
86 | * '=' to separate keys and values in parameters, for example | |
87 | * {@code var1 : foo}.</li> | |
88 | * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed, | |
89 | * this configuration does however support this feature. In the event of a duplicate | |
90 | * section, the two section's values are merged so that there is only a single | |
91 | * section. <strong>Note</strong>: This also affects the internal data of the | |
92 | * configuration. If it is saved, only a single section is written!</li> | |
93 | * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only | |
94 | * allowed if they are in two different sections, thus they are local to | |
95 | * sections; this configuration simply merges duplicates; if a section has a | |
96 | * duplicate parameter the values are then added to the key as a list.</li> | |
97 | * </ul> | |
98 | * </p> | |
99 | * <p> | |
100 | * Global parameters are also allowed; any parameters declared before a section | |
101 | * is declared are added to a global section. It is important to note that this | |
102 | * global section does not have a name. | |
103 | * </p> | |
104 | * <p> | |
105 | * In all instances, a parameter's key is prepended with its section name and a | |
106 | * '.' (period). Thus a parameter named "var1" in "section1" will have the key | |
107 | * {@code section1.var1} in this configuration. (This is the default | |
108 | * behavior. Because this is a hierarchical configuration you can change this by | |
109 | * setting a different {@link org.apache.commons.configuration.tree.ExpressionEngine}.) | |
110 | * </p> | |
111 | * <p> | |
112 | * <h3>Implementation Details:</h3> Consider the following ini file:<br> | |
113 | * <pre> | |
114 | * default = ok | |
115 | * | |
116 | * [section1] | |
117 | * var1 = foo | |
118 | * var2 = doodle | |
119 | * | |
120 | * [section2] | |
121 | * ; a comment | |
122 | * var1 = baz | |
123 | * var2 = shoodle | |
124 | * bad = | |
125 | * = worse | |
126 | * | |
127 | * [section3] | |
128 | * # another comment | |
129 | * var1 : foo | |
130 | * var2 : bar | |
131 | * var5 : test1 | |
132 | * | |
133 | * [section3] | |
134 | * var3 = foo | |
135 | * var4 = bar | |
136 | * var5 = test2 | |
137 | * | |
138 | * [sectionSeparators] | |
139 | * passwd : abc=def | |
140 | * a:b = "value" | |
141 | * </pre> | |
142 | * </p> | |
143 | * <p> | |
144 | * This ini file will be parsed without error. Note: | |
145 | * <ul> | |
146 | * <li>The parameter named "default" is added to the global section, it's value | |
147 | * is accessed simply using {@code getProperty("default")}.</li> | |
148 | * <li>Section 1's parameters can be accessed using | |
149 | * {@code getProperty("section1.var1")}.</li> | |
150 | * <li>The parameter named "bad" simply adds the parameter with an empty value.</li> | |
151 | * <li>The empty key with value "= worse" is added using a key consisting of a | |
152 | * single space character. This key is still added to section 2 and the value | |
153 | * can be accessed using {@code getProperty("section2. ")}, notice the | |
154 | * period '.' and the space following the section name.</li> | |
155 | * <li>Section three uses both '=' and ':' to separate keys and values.</li> | |
156 | * <li>Section 3 has a duplicate key named "var5". The value for this key is | |
157 | * [test1, test2], and is represented as a List.</li> | |
158 | * <li>The section called <em>sectionSeparators</em> demonstrates how the | |
159 | * configuration deals with multiple occurrences of separator characters. Per | |
160 | * default the first separator character in a line is detected and used to | |
161 | * split the key from the value. Therefore the first property definition in this | |
162 | * section has the key {@code passwd} and the value {@code abc=def}. | |
163 | * This default behavior can be changed by using quotes. If there is a separator | |
164 | * character before the first quote character (ignoring whitespace), this | |
165 | * character is used as separator. Thus the second property definition in the | |
166 | * section has the key {@code a:b} and the value {@code value}.</li> | |
167 | * </ul> | |
168 | * </p> | |
169 | * <p> | |
170 | * Internally, this configuration maps the content of the represented ini file | |
171 | * to its node structure in the following way: | |
172 | * <ul> | |
173 | * <li>Sections are represented by direct child nodes of the root node.</li> | |
174 | * <li>For the content of a section, corresponding nodes are created as children | |
175 | * of the section node.</li> | |
176 | * </ul> | |
177 | * This explains how the keys for the properties can be constructed. You can | |
178 | * also use other methods of {@link HierarchicalConfiguration} for querying or | |
179 | * manipulating the hierarchy of configuration nodes, for instance the | |
180 | * {@code configurationAt()} method for obtaining the data of a specific | |
181 | * section. However, be careful that the storage scheme described above is not | |
182 | * violated (e.g. by adding multiple levels of nodes or inserting duplicate | |
183 | * section nodes). Otherwise, the special methods for ini configurations may not | |
184 | * work correctly! | |
185 | * </p> | |
186 | * <p> | |
187 | * The set of sections in this configuration can be retrieved using the | |
188 | * {@code getSections()} method. For obtaining a | |
189 | * {@code SubnodeConfiguration} with the content of a specific section the | |
190 | * {@code getSection()} method can be used. | |
191 | * </p> | |
192 | * <p> | |
193 | * <em>Note:</em> Configuration objects of this type can be read concurrently by | |
194 | * multiple threads. However if one of these threads modifies the object, | |
195 | * synchronization has to be performed manually. | |
196 | * </p> | |
197 | * | |
198 | * @author <a | |
199 | * href="http://commons.apache.org/configuration/team-list.html">Commons | |
200 | * Configuration team</a> | |
201 | * @version $Id: HierarchicalINIConfiguration.java 1234362 2012-01-21 16:59:48Z oheger $ | |
202 | * @since 1.6 | |
203 | */ | |
204 | public class HierarchicalINIConfiguration extends | |
205 | AbstractHierarchicalFileConfiguration | |
206 | { | |
207 | /** | |
208 | * The characters that signal the start of a comment line. | |
209 | */ | |
210 | protected static final String COMMENT_CHARS = "#;"; | |
211 | ||
212 | /** | |
213 | * The characters used to separate keys from values. | |
214 | */ | |
215 | protected static final String SEPARATOR_CHARS = "=:"; | |
216 | ||
217 | /** | |
218 | * The serial version UID. | |
219 | */ | |
220 | private static final long serialVersionUID = 2548006161386850670L; | |
221 | ||
222 | /** | |
223 | * Constant for the line separator. | |
224 | */ | |
225 | 1 | private static final String LINE_SEPARATOR = System.getProperty("line.separator"); |
226 | ||
227 | /** | |
228 | * The characters used for quoting values. | |
229 | */ | |
230 | private static final String QUOTE_CHARACTERS = "\"'"; | |
231 | ||
232 | /** | |
233 | * The line continuation character. | |
234 | */ | |
235 | private static final String LINE_CONT = "\\"; | |
236 | ||
237 | /** | |
238 | * Create a new empty INI Configuration. | |
239 | */ | |
240 | public HierarchicalINIConfiguration() | |
241 | { | |
242 | 55 | super(); |
243 | 55 | } |
244 | ||
245 | /** | |
246 | * Create and load the ini configuration from the given file. | |
247 | * | |
248 | * @param filename The name pr path of the ini file to load. | |
249 | * @throws ConfigurationException If an error occurs while loading the file | |
250 | */ | |
251 | public HierarchicalINIConfiguration(String filename) | |
252 | throws ConfigurationException | |
253 | { | |
254 | 1 | super(filename); |
255 | 1 | } |
256 | ||
257 | /** | |
258 | * Create and load the ini configuration from the given file. | |
259 | * | |
260 | * @param file The ini file to load. | |
261 | * @throws ConfigurationException If an error occurs while loading the file | |
262 | */ | |
263 | public HierarchicalINIConfiguration(File file) | |
264 | throws ConfigurationException | |
265 | { | |
266 | 1 | super(file); |
267 | 1 | } |
268 | ||
269 | /** | |
270 | * Create and load the ini configuration from the given url. | |
271 | * | |
272 | * @param url The url of the ini file to load. | |
273 | * @throws ConfigurationException If an error occurs while loading the file | |
274 | */ | |
275 | public HierarchicalINIConfiguration(URL url) throws ConfigurationException | |
276 | { | |
277 | 1 | super(url); |
278 | 1 | } |
279 | ||
280 | /** | |
281 | * Save the configuration to the specified writer. | |
282 | * | |
283 | * @param writer - The writer to save the configuration to. | |
284 | * @throws ConfigurationException If an error occurs while writing the | |
285 | * configuration | |
286 | */ | |
287 | public void save(Writer writer) throws ConfigurationException | |
288 | { | |
289 | 8 | PrintWriter out = new PrintWriter(writer); |
290 | 8 | Iterator<String> it = getSections().iterator(); |
291 | 24 | while (it.hasNext()) |
292 | { | |
293 | 16 | String section = it.next(); |
294 | Configuration subset; | |
295 | 16 | if (section != null) |
296 | { | |
297 | 14 | out.print("["); |
298 | 14 | out.print(section); |
299 | 14 | out.print("]"); |
300 | 14 | out.println(); |
301 | 14 | subset = createSubnodeConfiguration(getSectionNode(section)); |
302 | } | |
303 | else | |
304 | { | |
305 | 2 | subset = getSection(null); |
306 | } | |
307 | ||
308 | 16 | Iterator<String> keys = subset.getKeys(); |
309 | 46 | while (keys.hasNext()) |
310 | { | |
311 | 30 | String key = keys.next(); |
312 | 30 | Object value = subset.getProperty(key); |
313 | 30 | if (value instanceof Collection) |
314 | { | |
315 | 3 | Iterator<?> values = ((Collection<?>) value).iterator(); |
316 | 9 | while (values.hasNext()) |
317 | { | |
318 | 6 | value = values.next(); |
319 | 6 | out.print(key); |
320 | 6 | out.print(" = "); |
321 | 6 | out.print(formatValue(value.toString())); |
322 | 6 | out.println(); |
323 | } | |
324 | 3 | } |
325 | else | |
326 | { | |
327 | 27 | out.print(key); |
328 | 27 | out.print(" = "); |
329 | 27 | out.print(formatValue(value.toString())); |
330 | 27 | out.println(); |
331 | } | |
332 | 30 | } |
333 | ||
334 | 16 | out.println(); |
335 | 16 | } |
336 | ||
337 | 8 | out.flush(); |
338 | 8 | } |
339 | ||
340 | /** | |
341 | * Load the configuration from the given reader. Note that the | |
342 | * {@code clear()} method is not called so the configuration read in will | |
343 | * be merged with the current configuration. | |
344 | * | |
345 | * @param reader The reader to read the configuration from. | |
346 | * @throws ConfigurationException If an error occurs while reading the | |
347 | * configuration | |
348 | */ | |
349 | public void load(Reader reader) throws ConfigurationException | |
350 | { | |
351 | try | |
352 | { | |
353 | 51 | BufferedReader bufferedReader = new BufferedReader(reader); |
354 | 51 | ConfigurationNode sectionNode = getRootNode(); |
355 | ||
356 | 51 | String line = bufferedReader.readLine(); |
357 | 539 | while (line != null) |
358 | { | |
359 | 488 | line = line.trim(); |
360 | 488 | if (!isCommentLine(line)) |
361 | { | |
362 | 386 | if (isSectionLine(line)) |
363 | { | |
364 | 86 | String section = line.substring(1, line.length() - 1); |
365 | 86 | sectionNode = getSectionNode(section); |
366 | 86 | } |
367 | ||
368 | else | |
369 | { | |
370 | 300 | String key = ""; |
371 | 300 | String value = ""; |
372 | 300 | int index = findSeparator(line); |
373 | 300 | if (index >= 0) |
374 | { | |
375 | 300 | key = line.substring(0, index); |
376 | 300 | value = parseValue(line.substring(index + 1), bufferedReader); |
377 | } | |
378 | else | |
379 | { | |
380 | 0 | key = line; |
381 | } | |
382 | 300 | key = key.trim(); |
383 | 300 | if (key.length() < 1) |
384 | { | |
385 | // use space for properties with no key | |
386 | 1 | key = " "; |
387 | } | |
388 | 300 | createValueNodes(sectionNode, key, value); |
389 | } | |
390 | } | |
391 | ||
392 | 488 | line = bufferedReader.readLine(); |
393 | } | |
394 | } | |
395 | 0 | catch (IOException e) |
396 | { | |
397 | 0 | throw new ConfigurationException( |
398 | "Unable to load the configuration", e); | |
399 | 51 | } |
400 | 51 | } |
401 | ||
402 | /** | |
403 | * Creates the node(s) for the given key value-pair. If delimiter parsing is | |
404 | * enabled, the value string is split if possible, and for each single value | |
405 | * a node is created. Otherwise only a single node is added to the section. | |
406 | * | |
407 | * @param sectionNode the section node new nodes have to be added | |
408 | * @param key the key | |
409 | * @param value the value string | |
410 | */ | |
411 | private void createValueNodes(ConfigurationNode sectionNode, String key, | |
412 | String value) | |
413 | { | |
414 | Collection<String> values; | |
415 | 300 | if (isDelimiterParsingDisabled()) |
416 | { | |
417 | 1 | values = Collections.singleton(value); |
418 | } | |
419 | else | |
420 | { | |
421 | 299 | values = PropertyConverter.split(value, getListDelimiter(), false); |
422 | } | |
423 | ||
424 | 300 | for (String v : values) |
425 | { | |
426 | 302 | ConfigurationNode node = createNode(key); |
427 | 302 | node.setValue(v); |
428 | 302 | sectionNode.addChild(node); |
429 | 302 | } |
430 | 300 | } |
431 | ||
432 | /** | |
433 | * Parse the value to remove the quotes and ignoring the comment. Example: | |
434 | * | |
435 | * <pre> | |
436 | * "value" ; comment -> value | |
437 | * </pre> | |
438 | * | |
439 | * <pre> | |
440 | * 'value' ; comment -> value | |
441 | * </pre> | |
442 | * Note that a comment character is only recognized if there is at least one | |
443 | * whitespace character before it. So it can appear in the property value, | |
444 | * e.g.: | |
445 | * <pre> | |
446 | * C:\\Windows;C:\\Windows\\system32 | |
447 | * </pre> | |
448 | * | |
449 | * @param val the value to be parsed | |
450 | * @param reader the reader (needed if multiple lines have to be read) | |
451 | * @throws IOException if an IO error occurs | |
452 | */ | |
453 | private static String parseValue(String val, BufferedReader reader) throws IOException | |
454 | { | |
455 | 300 | StringBuilder propertyValue = new StringBuilder(); |
456 | boolean lineContinues; | |
457 | 300 | String value = val.trim(); |
458 | ||
459 | do | |
460 | { | |
461 | 349 | boolean quoted = value.startsWith("\"") || value.startsWith("'"); |
462 | 349 | boolean stop = false; |
463 | 349 | boolean escape = false; |
464 | ||
465 | 349 | char quote = quoted ? value.charAt(0) : 0; |
466 | ||
467 | 349 | int i = quoted ? 1 : 0; |
468 | ||
469 | 349 | StringBuilder result = new StringBuilder(); |
470 | 349 | char lastChar = 0; |
471 | 2839 | while (i < value.length() && !stop) |
472 | { | |
473 | 2490 | char c = value.charAt(i); |
474 | ||
475 | 2490 | if (quoted) |
476 | { | |
477 | 799 | if ('\\' == c && !escape) |
478 | { | |
479 | 45 | escape = true; |
480 | } | |
481 | 754 | else if (!escape && quote == c) |
482 | { | |
483 | 68 | stop = true; |
484 | } | |
485 | 686 | else if (escape && quote == c) |
486 | { | |
487 | 36 | escape = false; |
488 | 36 | result.append(c); |
489 | } | |
490 | else | |
491 | { | |
492 | 650 | if (escape) |
493 | { | |
494 | 9 | escape = false; |
495 | 9 | result.append('\\'); |
496 | } | |
497 | ||
498 | 650 | result.append(c); |
499 | } | |
500 | } | |
501 | else | |
502 | { | |
503 | 1691 | if (isCommentChar(c) && Character.isWhitespace(lastChar)) |
504 | { | |
505 | 18 | stop = true; |
506 | } | |
507 | else | |
508 | { | |
509 | 1673 | result.append(c); |
510 | } | |
511 | } | |
512 | ||
513 | 2490 | i++; |
514 | 2490 | lastChar = c; |
515 | 2490 | } |
516 | ||
517 | 349 | String v = result.toString(); |
518 | 349 | if (!quoted) |
519 | { | |
520 | 281 | v = v.trim(); |
521 | 281 | lineContinues = lineContinues(v); |
522 | 281 | if (lineContinues) |
523 | { | |
524 | // remove trailing "\" | |
525 | 42 | v = v.substring(0, v.length() - 1).trim(); |
526 | } | |
527 | } | |
528 | else | |
529 | { | |
530 | 68 | lineContinues = lineContinues(value, i); |
531 | } | |
532 | 349 | propertyValue.append(v); |
533 | ||
534 | 349 | if (lineContinues) |
535 | { | |
536 | 56 | propertyValue.append(LINE_SEPARATOR); |
537 | 56 | value = reader.readLine(); |
538 | } | |
539 | 349 | } while (lineContinues && value != null); |
540 | ||
541 | 300 | return propertyValue.toString(); |
542 | } | |
543 | ||
544 | /** | |
545 | * Tests whether the specified string contains a line continuation marker. | |
546 | * | |
547 | * @param line the string to check | |
548 | * @return a flag whether this line continues | |
549 | */ | |
550 | private static boolean lineContinues(String line) | |
551 | { | |
552 | 349 | String s = line.trim(); |
553 | 349 | return s.equals(LINE_CONT) |
554 | || (s.length() > 2 && s.endsWith(LINE_CONT) && Character | |
555 | .isWhitespace(s.charAt(s.length() - 2))); | |
556 | } | |
557 | ||
558 | /** | |
559 | * Tests whether the specified string contains a line continuation marker | |
560 | * after the specified position. This method parses the string to remove a | |
561 | * comment that might be present. Then it checks whether a line continuation | |
562 | * marker can be found at the end. | |
563 | * | |
564 | * @param line the line to check | |
565 | * @param pos the start position | |
566 | * @return a flag whether this line continues | |
567 | */ | |
568 | private static boolean lineContinues(String line, int pos) | |
569 | { | |
570 | String s; | |
571 | ||
572 | 68 | if (pos >= line.length()) |
573 | { | |
574 | 35 | s = line; |
575 | } | |
576 | else | |
577 | { | |
578 | 33 | int end = pos; |
579 | 87 | while (end < line.length() && !isCommentChar(line.charAt(end))) |
580 | { | |
581 | 54 | end++; |
582 | } | |
583 | 33 | s = line.substring(pos, end); |
584 | } | |
585 | ||
586 | 68 | return lineContinues(s); |
587 | } | |
588 | ||
589 | /** | |
590 | * Tests whether the specified character is a comment character. | |
591 | * | |
592 | * @param c the character | |
593 | * @return a flag whether this character starts a comment | |
594 | */ | |
595 | private static boolean isCommentChar(char c) | |
596 | { | |
597 | 1771 | return COMMENT_CHARS.indexOf(c) >= 0; |
598 | } | |
599 | ||
600 | /** | |
601 | * Tries to find the index of the separator character in the given string. | |
602 | * This method checks for the presence of separator characters in the given | |
603 | * string. If multiple characters are found, the first one is assumed to be | |
604 | * the correct separator. If there are quoting characters, they are taken | |
605 | * into account, too. | |
606 | * | |
607 | * @param line the line to be checked | |
608 | * @return the index of the separator character or -1 if none is found | |
609 | */ | |
610 | private static int findSeparator(String line) | |
611 | { | |
612 | 300 | int index = |
613 | findSeparatorBeforeQuote(line, | |
614 | findFirstOccurrence(line, QUOTE_CHARACTERS)); | |
615 | 300 | if (index < 0) |
616 | { | |
617 | 239 | index = findFirstOccurrence(line, SEPARATOR_CHARS); |
618 | } | |
619 | 300 | return index; |
620 | } | |
621 | ||
622 | /** | |
623 | * Checks for the occurrence of the specified separators in the given line. | |
624 | * The index of the first separator is returned. | |
625 | * | |
626 | * @param line the line to be investigated | |
627 | * @param separators a string with the separator characters to look for | |
628 | * @return the lowest index of a separator character or -1 if no separator | |
629 | * is found | |
630 | */ | |
631 | private static int findFirstOccurrence(String line, String separators) | |
632 | { | |
633 | 539 | int index = -1; |
634 | ||
635 | 1617 | for (int i = 0; i < separators.length(); i++) |
636 | { | |
637 | 1078 | char sep = separators.charAt(i); |
638 | 1078 | int pos = line.indexOf(sep); |
639 | 1078 | if (pos >= 0) |
640 | { | |
641 | 324 | if (index < 0 || pos < index) |
642 | { | |
643 | 315 | index = pos; |
644 | } | |
645 | } | |
646 | } | |
647 | ||
648 | 539 | return index; |
649 | } | |
650 | ||
651 | /** | |
652 | * Searches for a separator character directly before a quoting character. | |
653 | * If the first non-whitespace character before a quote character is a | |
654 | * separator, it is considered the "real" separator in this line - even if | |
655 | * there are other separators before. | |
656 | * | |
657 | * @param line the line to be investigated | |
658 | * @param quoteIndex the index of the quote character | |
659 | * @return the index of the separator before the quote or < 0 if there is | |
660 | * none | |
661 | */ | |
662 | private static int findSeparatorBeforeQuote(String line, int quoteIndex) | |
663 | { | |
664 | 300 | int index = quoteIndex - 1; |
665 | 361 | while (index >= 0 && Character.isWhitespace(line.charAt(index))) |
666 | { | |
667 | 61 | index--; |
668 | } | |
669 | ||
670 | 300 | if (index >= 0 && SEPARATOR_CHARS.indexOf(line.charAt(index)) < 0) |
671 | { | |
672 | 0 | index = -1; |
673 | } | |
674 | ||
675 | 300 | return index; |
676 | } | |
677 | ||
678 | /** | |
679 | * Add quotes around the specified value if it contains a comment character. | |
680 | */ | |
681 | private String formatValue(String value) | |
682 | { | |
683 | 33 | boolean quoted = false; |
684 | ||
685 | 99 | for (int i = 0; i < COMMENT_CHARS.length() && !quoted; i++) |
686 | { | |
687 | 66 | char c = COMMENT_CHARS.charAt(i); |
688 | 66 | if (value.indexOf(c) != -1) |
689 | { | |
690 | 1 | quoted = true; |
691 | } | |
692 | } | |
693 | ||
694 | 33 | if (quoted) |
695 | { | |
696 | 1 | return '"' + value.replaceAll("\"", "\\\\\\\"") + '"'; |
697 | } | |
698 | else | |
699 | { | |
700 | 32 | return value; |
701 | } | |
702 | } | |
703 | ||
704 | /** | |
705 | * Determine if the given line is a comment line. | |
706 | * | |
707 | * @param line The line to check. | |
708 | * @return true if the line is empty or starts with one of the comment | |
709 | * characters | |
710 | */ | |
711 | protected boolean isCommentLine(String line) | |
712 | { | |
713 | 492 | if (line == null) |
714 | { | |
715 | 1 | return false; |
716 | } | |
717 | // blank lines are also treated as comment lines | |
718 | 491 | return line.length() < 1 || COMMENT_CHARS.indexOf(line.charAt(0)) >= 0; |
719 | } | |
720 | ||
721 | /** | |
722 | * Determine if the given line is a section. | |
723 | * | |
724 | * @param line The line to check. | |
725 | * @return true if the line contains a section | |
726 | */ | |
727 | protected boolean isSectionLine(String line) | |
728 | { | |
729 | 389 | if (line == null) |
730 | { | |
731 | 1 | return false; |
732 | } | |
733 | 388 | return line.startsWith("[") && line.endsWith("]"); |
734 | } | |
735 | ||
736 | /** | |
737 | * Return a set containing the sections in this ini configuration. Note that | |
738 | * changes to this set do not affect the configuration. | |
739 | * | |
740 | * @return a set containing the sections. | |
741 | */ | |
742 | public Set<String> getSections() | |
743 | { | |
744 | 19 | Set<String> sections = new LinkedHashSet<String>(); |
745 | 19 | boolean globalSection = false; |
746 | 19 | boolean inSection = false; |
747 | ||
748 | 19 | for (ConfigurationNode node : getRootNode().getChildren()) |
749 | { | |
750 | 48 | if (isSectionNode(node)) |
751 | { | |
752 | 42 | inSection = true; |
753 | 42 | sections.add(node.getName()); |
754 | } | |
755 | else | |
756 | { | |
757 | 6 | if (!inSection && !globalSection) |
758 | { | |
759 | 5 | globalSection = true; |
760 | 5 | sections.add(null); |
761 | } | |
762 | } | |
763 | } | |
764 | ||
765 | 19 | return sections; |
766 | } | |
767 | ||
768 | /** | |
769 | * Returns a configuration with the content of the specified section. This | |
770 | * provides an easy way of working with a single section only. The way this | |
771 | * configuration is structured internally, this method is very similar to | |
772 | * calling {@link HierarchicalConfiguration#configurationAt(String)} with | |
773 | * the name of the section in question. There are the following differences | |
774 | * however: | |
775 | * <ul> | |
776 | * <li>This method never throws an exception. If the section does not exist, | |
777 | * it is created now. The configuration returned in this case is empty.</li> | |
778 | * <li>If section is contained multiple times in the configuration, the | |
779 | * configuration returned by this method is initialized with the first | |
780 | * occurrence of the section. (This can only happen if | |
781 | * {@code addProperty()} has been used in a way that does not conform | |
782 | * to the storage scheme used by {@code HierarchicalINIConfiguration}. | |
783 | * If used correctly, there will not be duplicate sections.)</li> | |
784 | * <li>There is special support for the global section: Passing in | |
785 | * <b>null</b> as section name returns a configuration with the content of | |
786 | * the global section (which may also be empty).</li> | |
787 | * </ul> | |
788 | * | |
789 | * @param name the name of the section in question; <b>null</b> represents | |
790 | * the global section | |
791 | * @return a configuration containing only the properties of the specified | |
792 | * section | |
793 | */ | |
794 | public SubnodeConfiguration getSection(String name) | |
795 | { | |
796 | 2512 | if (name == null) |
797 | { | |
798 | 2504 | return getGlobalSection(); |
799 | } | |
800 | ||
801 | else | |
802 | { | |
803 | try | |
804 | { | |
805 | 8 | return configurationAt(name); |
806 | } | |
807 | 3 | catch (IllegalArgumentException iex) |
808 | { | |
809 | // the passed in key does not map to exactly one node | |
810 | // obtain the node for the section, create it on demand | |
811 | 3 | return new SubnodeConfiguration(this, getSectionNode(name)); |
812 | } | |
813 | } | |
814 | } | |
815 | ||
816 | /** | |
817 | * Obtains the node representing the specified section. This method is | |
818 | * called while the configuration is loaded. If a node for this section | |
819 | * already exists, it is returned. Otherwise a new node is created. | |
820 | * | |
821 | * @param sectionName the name of the section | |
822 | * @return the node for this section | |
823 | */ | |
824 | private ConfigurationNode getSectionNode(String sectionName) | |
825 | { | |
826 | 103 | List<ConfigurationNode> nodes = getRootNode().getChildren(sectionName); |
827 | 103 | if (!nodes.isEmpty()) |
828 | { | |
829 | 17 | return nodes.get(0); |
830 | } | |
831 | ||
832 | 86 | ConfigurationNode node = createNode(sectionName); |
833 | 86 | markSectionNode(node); |
834 | 86 | getRootNode().addChild(node); |
835 | 86 | return node; |
836 | } | |
837 | ||
838 | /** | |
839 | * Creates a sub configuration for the global section of the represented INI | |
840 | * configuration. | |
841 | * | |
842 | * @return the sub configuration for the global section | |
843 | */ | |
844 | private SubnodeConfiguration getGlobalSection() | |
845 | { | |
846 | 2504 | ViewNode parent = new ViewNode(); |
847 | ||
848 | 2504 | for (ConfigurationNode node : getRootNode().getChildren()) |
849 | { | |
850 | 10012 | if (!isSectionNode(node)) |
851 | { | |
852 | 2503 | synchronized (node) |
853 | { | |
854 | 2503 | parent.addChild(node); |
855 | 2503 | } |
856 | } | |
857 | } | |
858 | ||
859 | 2504 | return createSubnodeConfiguration(parent); |
860 | } | |
861 | ||
862 | /** | |
863 | * Marks a configuration node as a section node. This means that this node | |
864 | * represents a section header. This implementation uses the node's | |
865 | * reference property to store a flag. | |
866 | * | |
867 | * @param node the node to be marked | |
868 | */ | |
869 | private static void markSectionNode(ConfigurationNode node) | |
870 | { | |
871 | 86 | node.setReference(Boolean.TRUE); |
872 | 86 | } |
873 | ||
874 | /** | |
875 | * Checks whether the specified configuration node represents a section. | |
876 | * | |
877 | * @param node the node in question | |
878 | * @return a flag whether this node represents a section | |
879 | */ | |
880 | private static boolean isSectionNode(ConfigurationNode node) | |
881 | { | |
882 | 10060 | return node.getReference() != null || node.getChildrenCount() > 0; |
883 | } | |
884 | } |