1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.configuration;
19
20 import java.io.BufferedReader;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.PrintWriter;
24 import java.io.Reader;
25 import java.io.Writer;
26 import java.net.URL;
27 import java.util.Iterator;
28 import java.util.Set;
29 import java.util.TreeSet;
30
31 /***
32 * <p>
33 * An initialization or ini file is a configuration file tpically found on
34 * Microsoft's Windows operating system and contains data for Windows based
35 * applications.
36 * </p>
37 *
38 * <p>
39 * Although popularized by Windows, ini files can be used on any system or
40 * platform due to the fact that they are merely text files that can easily be
41 * parsed and modified by both humans and computers.
42 * </p>
43 *
44 * <p>
45 * A typcial ini file could look something like:
46 * </p>
47 * <code>
48 * [section1]<br>
49 * ; this is a comment!<br>
50 * var1 = foo<br>
51 * var2 = bar<br>
52 *<br>
53 * [section2]<br>
54 * var1 = doo<br>
55 * </code>
56 *
57 * <p>
58 * The format of ini files is fairly straight forward and is comosed of three
59 * components:<br>
60 * <ul>
61 * <li><b>Sections:</b> Ini files are split into sections, each section
62 * starting with a section declaration. A section declaration starts with a '['
63 * and ends with a ']'. Sections occur on one line only.</li>
64 * <li><b>Parameters:</b> Items in a section are known as parameters.
65 * Parameters have a typical <code>key = value</code> format.</li>
66 * <li><b>Comments:</b> Lines starting with a ';' are assumed to be comments.
67 * </li>
68 * </ul>
69 * </p>
70 *
71 * <p>
72 * There are various implementations of the ini file format by various vendors
73 * which has caused a number of differences to appear. As far as possible this
74 * configuration tries to be lenient and support most of the differences.
75 * </p>
76 *
77 * <p>
78 * Some of the differences supported are as follows:
79 * <ul>
80 * <li><b>Comments:</b> The '#' character is also accepted as a comment
81 * signifier.</li>
82 * <li><b>Key value separtor:</b> The ':' character is also accepted in place
83 * of '=' to separate keys and values in parameters, for example
84 * <code>var1 : foo</code>.</li>
85 * <li><b>Duplicate sections:</b> Typically duplicate sections are not allowed ,
86 * this configuration does however support it. In the event of a duplicate
87 * section, the two section's values are merged.</li>
88 * <li><b>Duplicate parameters:</b> Typically duplicate parameters are only
89 * allowed if they are in two different sections, thus they are local to
90 * sections; this configuration simply merges duplicates; if a section has a
91 * duplicate parameter the values are then added to the key as a list. </li>
92 * </ul>
93 * </p>
94 * <p>
95 * Global parameters are also allowed; any parameters declared before a section
96 * is declared are added to a global section. It is important to note that this
97 * global section does not have a name.
98 * </p>
99 * <p>
100 * In all instances, a parameter's key is prepended with its section name and a
101 * '.' (period). Thus a parameter named "var1" in "section1" will have the key
102 * <code>section1.var1</code> in this configuration. Thus, a section's
103 * parameters can easily be retrieved using the <code>subset</code> method
104 * using the section name as the prefix.
105 * </p>
106 * <p>
107 * <h3>Implementation Details:</h3>
108 * Consider the following ini file:<br>
109 * <code>
110 * default = ok<br>
111 * <br>
112 * [section1]<br>
113 * var1 = foo<br>
114 * var2 = doodle<br>
115 * <br>
116 * [section2]<br>
117 * ; a comment<br>
118 * var1 = baz<br>
119 * var2 = shoodle<br>
120 * bad =<br>
121 * = worse<br>
122 * <br>
123 * [section3]<br>
124 * # another comment<br>
125 * var1 : foo<br>
126 * var2 : bar<br>
127 * var5 : test1<br>
128 * <br>
129 * [section3]<br>
130 * var3 = foo<br>
131 * var4 = bar<br>
132 * var5 = test2<br>
133 * </code>
134 * </p>
135 * <p>
136 * This ini file will be parsed without error. Note:
137 * <ul>
138 * <li>The parameter named "default" is added to the global section, it's value
139 * is accessed simply using <code>getProperty("default")</code>.</li>
140 * <li>Section 1's parameters can be accessed using
141 * <code>getProperty("section1.var1")</code>.</li>
142 * <li>The parameter named "bad" simply adds the parameter with an empty value.
143 * </li>
144 * <li>The empty key with value "= worse" is added using an empty key. This key
145 * is still added to section 2 and the value can be accessed using
146 * <code>getProperty("section2.")</code>, notice the period '.' following the
147 * section name.</li>
148 * <li>Section three uses both '=' and ':' to separate keys and values.</li>
149 * <li>Section 3 has a duplicate key named "var5". The value for this key is
150 * [test1, test2], and is represented as a List.</li>
151 * </ul>
152 * </p>
153 * <p>
154 * The set of sections in this configuration can be retrieved using the
155 * <code>getSections</code> method.
156 * </p>
157 *
158 * @author trevor.miller
159 * @version $Id: INIConfiguration.java 492216 2007-01-03 16:51:24Z oheger $
160 * @since 1.4
161 */
162 public class INIConfiguration extends AbstractFileConfiguration
163 {
164 /***
165 * The characters that signal the start of a comment line.
166 */
167 protected static final String COMMENT_CHARS = "#;";
168
169 /***
170 * The characters used to separate keys from values.
171 */
172 protected static final String SEPARATOR_CHARS = "=:";
173
174 /*** Constant for the used line separator.*/
175 private static final String LINE_SEPARATOR = "\r\n";
176
177 /***
178 * Create a new empty INI Configuration.
179 */
180 public INIConfiguration()
181 {
182 super();
183 }
184
185 /***
186 * Create and load the ini configuration from the given file.
187 *
188 * @param filename The name pr path of the ini file to load.
189 * @throws ConfigurationException If an error occurs while loading the file
190 */
191 public INIConfiguration(String filename) throws ConfigurationException
192 {
193 super(filename);
194 }
195
196 /***
197 * Create and load the ini configuration from the given file.
198 *
199 * @param file The ini file to load.
200 * @throws ConfigurationException If an error occurs while loading the file
201 */
202 public INIConfiguration(File file) throws ConfigurationException
203 {
204 super(file);
205 }
206
207 /***
208 * Create and load the ini configuration from the given url.
209 *
210 * @param url The url of the ini file to load.
211 * @throws ConfigurationException If an error occurs while loading the file
212 */
213 public INIConfiguration(URL url) throws ConfigurationException
214 {
215 super(url);
216 }
217
218 /***
219 * Save the configuration to the specified writer.
220 *
221 * @param writer - The writer to save the configuration to.
222 * @throws ConfigurationException If an error occurs while writing the
223 * configuration
224 */
225 public void save(Writer writer) throws ConfigurationException
226 {
227 PrintWriter pw = new PrintWriter(writer);
228 Iterator iter = this.getSections().iterator();
229 while (iter.hasNext())
230 {
231 String section = (String) iter.next();
232 pw.print("[");
233 pw.print(section);
234 pw.print("]");
235 pw.print(LINE_SEPARATOR);
236
237 Configuration values = this.subset(section);
238 Iterator iterator = values.getKeys();
239 while (iterator.hasNext())
240 {
241 String key = (String) iterator.next();
242 String value = values.getString(key);
243 pw.print(key);
244 pw.print(" = ");
245 pw.print(value);
246 pw.print(LINE_SEPARATOR);
247 }
248
249 pw.print(LINE_SEPARATOR);
250 }
251 }
252
253 /***
254 * Load the configuration from the given reader. Note that the
255 * <code>clear</code> method is not called so the configuration read in
256 * will be merged with the current configuration.
257 *
258 * @param reader The reader to read the configuration from.
259 * @throws ConfigurationException If an error occurs while reading the
260 * configuration
261 */
262 public void load(Reader reader) throws ConfigurationException
263 {
264 try
265 {
266 BufferedReader bufferedReader = new BufferedReader(reader);
267 String line = bufferedReader.readLine();
268 String section = "";
269 while (line != null)
270 {
271 line = line.trim();
272 if (!isCommentLine(line))
273 {
274 if (isSectionLine(line))
275 {
276 section = line.substring(1, line.length() - 1) + ".";
277 }
278 else
279 {
280 String key = "";
281 String value = "";
282 int index = line.indexOf("=");
283 if (index >= 0)
284 {
285 key = section + line.substring(0, index);
286 value = line.substring(index + 1);
287 }
288 else
289 {
290 index = line.indexOf(":");
291 if (index >= 0)
292 {
293 key = section + line.substring(0, index);
294 value = line.substring(index + 1);
295 }
296 else
297 {
298 key = section + line;
299 }
300 }
301 this.addProperty(key.trim(), value.trim());
302 }
303 }
304 line = bufferedReader.readLine();
305 }
306 }
307 catch (IOException ioe)
308 {
309 throw new ConfigurationException(ioe.getMessage());
310 }
311 }
312
313 /***
314 * Determine if the given line is a comment line.
315 *
316 * @param s The line to check.
317 * @return true if the line is empty or starts with one of the comment
318 * characters
319 */
320 protected boolean isCommentLine(String s)
321 {
322 if (s == null)
323 {
324 return false;
325 }
326
327 return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
328 }
329
330 /***
331 * Determine if the given line is a section.
332 *
333 * @param s The line to check.
334 * @return true if the line contains a secion
335 */
336 protected boolean isSectionLine(String s)
337 {
338 if (s == null)
339 {
340 return false;
341 }
342 return s.startsWith("[") && s.endsWith("]");
343 }
344
345 /***
346 * Return a set containing the sections in this ini configuration. Note that
347 * changes to this set do not affect the configuration.
348 *
349 * @return a set containing the sections.
350 */
351 public Set getSections()
352 {
353 Set sections = new TreeSet();
354 Iterator iter = this.getKeys();
355 while (iter.hasNext())
356 {
357 String key = (String) iter.next();
358 int index = key.indexOf(".");
359 if (index >= 0)
360 {
361 sections.add(key.substring(0, index));
362 }
363 }
364 return sections;
365 }
366 }