Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
Ini |
|
| 2.2972972972972974;2.297 |
1 | /** | |
2 | * Distribution License: | |
3 | * JSword is free software; you can redistribute it and/or modify it under | |
4 | * the terms of the GNU Lesser General Public License, version 2.1 or later | |
5 | * as published by the Free Software Foundation. This program is distributed | |
6 | * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even | |
7 | * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
8 | * See the GNU Lesser General Public License for more details. | |
9 | * | |
10 | * The License is available on the internet at: | |
11 | * http://www.gnu.org/copyleft/lgpl.html | |
12 | * or by writing to: | |
13 | * Free Software Foundation, Inc. | |
14 | * 59 Temple Place - Suite 330 | |
15 | * Boston, MA 02111-1307, USA | |
16 | * | |
17 | * © CrossWire Bible Society, 2015 - 2016 | |
18 | */ | |
19 | package org.crosswire.common.util; | |
20 | ||
21 | import java.io.BufferedReader; | |
22 | import java.io.ByteArrayInputStream; | |
23 | import java.io.File; | |
24 | import java.io.FileInputStream; | |
25 | import java.io.FileOutputStream; | |
26 | import java.io.IOException; | |
27 | import java.io.InputStream; | |
28 | import java.io.InputStreamReader; | |
29 | import java.io.OutputStreamWriter; | |
30 | import java.io.PrintWriter; | |
31 | import java.io.Reader; | |
32 | import java.io.Writer; | |
33 | import java.util.ArrayList; | |
34 | import java.util.Collection; | |
35 | import java.util.Collections; | |
36 | import java.util.List; | |
37 | import java.util.Map; | |
38 | import java.util.TreeMap; | |
39 | ||
40 | import org.slf4j.Logger; | |
41 | import org.slf4j.LoggerFactory; | |
42 | ||
43 | /** | |
44 | * A utility class for loading an INI style, Multimap configuration file. | |
45 | * <p> | |
46 | * SWORD defines a conf as an INI file with one or more sections. | |
47 | * Originally, all modules were described in a single conf, but | |
48 | * now each module has its own conf. | |
49 | * </p> | |
50 | * <p> | |
51 | * SWORD will be using a single conf to hold overrides for many | |
52 | * modules. This is the motivation for this class as opposed to | |
53 | * allowing only a single section as {@link IniSection}. | |
54 | * </p> | |
55 | * <p> | |
56 | * Since the most common use case is for a single section, this | |
57 | * implementation has an API for delegating to the first IniSection. | |
58 | * </p> | |
59 | * | |
60 | * This implementation allows for: | |
61 | * <ul> | |
62 | * <li><strong>Case Insensitive</strong> -- Section names, keys and values are case insensitive.</li> | |
63 | * <li><strong>Comments</strong> -- ; and # preceded only by white space indicate that a line is a comment. | |
64 | * Note: SWORD does not support ; but it is present in some 3rd Party repositories such as IBT.</li> | |
65 | * <li><strong>Multiple Values</strong> -- Each key can have one or more values.</li> | |
66 | * <li><strong>Order</strong> -- Order of sections, keys and values are retained</li> | |
67 | * </ul> | |
68 | * | |
69 | * This implementation does not allow for: | |
70 | * <ul> | |
71 | * <li><strong>Globals</strong> -- (key,value) pairs before the first section.</li> | |
72 | * <li><strong>Quoted Values</strong> -- Values surrounded by "" or ''. | |
73 | * If present they are part of the value.</li> | |
74 | * <li><strong>Retaining comments</strong> -- Comments are ignored.</li> | |
75 | * <li><strong>Comments after content</strong> -- Comments are on lines to themselves.</li> | |
76 | * <li><strong>:</strong> -- as an alternative for =.</li> | |
77 | * <li><strong>nulls</strong> -- null values.</li> | |
78 | * </ul> | |
79 | * | |
80 | * @author DM Smith | |
81 | * @see gnu.lgpl.License The GNU Lesser General Public License for details.<br> | |
82 | */ | |
83 | final class Ini { | |
84 | ||
85 | /** | |
86 | * Create an empty INI Config. | |
87 | */ | |
88 | 0 | Ini() { |
89 | 0 | sectionMap = new TreeMap<String, IniSection>(String.CASE_INSENSITIVE_ORDER); |
90 | 0 | list = new ArrayList(); |
91 | 0 | } |
92 | ||
93 | /** | |
94 | * Start over. | |
95 | */ | |
96 | public void clear() { | |
97 | 0 | sectionMap.clear(); |
98 | 0 | list.clear(); |
99 | 0 | } |
100 | ||
101 | /** | |
102 | * Get the number of sections | |
103 | * | |
104 | * @return the number of known sections | |
105 | */ | |
106 | public int size() { | |
107 | 0 | return sectionMap.size(); |
108 | } | |
109 | ||
110 | /** | |
111 | * Get an unmodifiable collection of the sections in this INI. | |
112 | * | |
113 | * @return the ordered section names | |
114 | */ | |
115 | public List<String> getSections() { | |
116 | 0 | return Collections.unmodifiableList(list); |
117 | } | |
118 | ||
119 | public String getSectionName(int index) { | |
120 | 0 | return list.get(index); |
121 | } | |
122 | ||
123 | /** | |
124 | * Get the name of the first section. | |
125 | * | |
126 | * @return the name of the first section or null if there are no sections | |
127 | * @throws ArrayIndexOutOfBoundsException if there are no sections | |
128 | */ | |
129 | public String getSectionName() { | |
130 | 0 | return size() == 0 ? null : list.get(0); |
131 | } | |
132 | ||
133 | public int getValueSize(String sectionName, String key) { | |
134 | 0 | IniSection section = doGetSection(sectionName); |
135 | 0 | return section == null ? 0 : section.size(key); |
136 | } | |
137 | ||
138 | /** | |
139 | * Get the number of values for a key in the first section | |
140 | * | |
141 | * @param key the key | |
142 | * @return the number of values for a key in the first section | |
143 | */ | |
144 | public int getValueSize(String key) { | |
145 | 0 | IniSection section = getSection(); |
146 | 0 | return section == null ? 0 : section.size(key); |
147 | } | |
148 | ||
149 | /** | |
150 | * Get the value for the key specified by the index and the section. | |
151 | * | |
152 | * @param sectionName the name of the section | |
153 | * @param key the key for the section | |
154 | * @param index the index in the list of values | |
155 | * @return the value at the specified index | |
156 | * @throws ArrayIndexOutOfBoundsException when the index is out of bounds | |
157 | */ | |
158 | public String getValue(String sectionName, String key, int index) { | |
159 | 0 | IniSection section = doGetSection(sectionName); |
160 | 0 | return section == null ? null : section.get(key, index); |
161 | } | |
162 | ||
163 | /** | |
164 | * Get the first value for the key specified by the index and the section. | |
165 | * | |
166 | * @param sectionName the name of the section | |
167 | * @param key the key for the section | |
168 | * @return the value at the specified index | |
169 | * @throws ArrayIndexOutOfBoundsException when the index is out of bounds | |
170 | */ | |
171 | public String getValue(String sectionName, String key) { | |
172 | 0 | IniSection section = doGetSection(sectionName); |
173 | 0 | return section == null ? null : section.get(key, 0); |
174 | } | |
175 | ||
176 | /** | |
177 | * Get the value for the key specified by the index for the first section. | |
178 | * | |
179 | * @param key the key | |
180 | * @param index the index | |
181 | * @return the value at the specified index | |
182 | * @throws ArrayIndexOutOfBoundsException when the index is out of bounds | |
183 | */ | |
184 | public String getValue(String key, int index) { | |
185 | 0 | IniSection section = getSection(); |
186 | 0 | return section == null ? null : section.get(key, index); |
187 | } | |
188 | ||
189 | /** | |
190 | * Get the first value for the key in the first section. | |
191 | * | |
192 | * @param key the key | |
193 | * @return the value at the specified index | |
194 | * @throws ArrayIndexOutOfBoundsException when the index is out of bounds | |
195 | */ | |
196 | public String getValue(String key) { | |
197 | 0 | IniSection section = getSection(); |
198 | 0 | return section == null ? null : section.get(key); |
199 | } | |
200 | ||
201 | /** | |
202 | * Add a key/value pair to a section. | |
203 | * If the section does not exist, it is created. | |
204 | * A null for key or value is not allowed. | |
205 | * An empty string for a key is not allowed. | |
206 | * | |
207 | * @param sectionName the name of the section | |
208 | * @param key the key for the section | |
209 | * @param value the value for the key | |
210 | * @return {@code true} if the element was added or already was present | |
211 | */ | |
212 | public boolean add(String sectionName, String key, String value) { | |
213 | 0 | IniSection section = getOrCreateSection(sectionName); |
214 | 0 | return section.add(key, value); |
215 | } | |
216 | ||
217 | /** | |
218 | * Replace a value for a key. | |
219 | * A null for key or value is not allowed. | |
220 | * An empty string for a key is not allowed. | |
221 | * | |
222 | * @param sectionName the name of the section | |
223 | * @param key the key for the section | |
224 | * @param value the value for the key | |
225 | * @return {@code true} if the element was added or already was present | |
226 | */ | |
227 | public boolean replace(String sectionName, String key, String value) { | |
228 | 0 | IniSection section = getOrCreateSection(sectionName); |
229 | 0 | return section.replace(key, value); |
230 | } | |
231 | ||
232 | /** | |
233 | * Remove the value if present. | |
234 | * If it were the last value for the key, the key is removed. | |
235 | * If it were the last key, the section is removed. | |
236 | * | |
237 | * @param sectionName the name of the section | |
238 | * @param key the key for the section | |
239 | * @param value the value for the key | |
240 | * @return whether the value was present and removed | |
241 | */ | |
242 | public boolean remove(String sectionName, String key, String value) { | |
243 | 0 | IniSection section = sectionMap.get(sectionName); |
244 | 0 | if (section == null) { |
245 | 0 | return false; |
246 | } | |
247 | 0 | boolean changed = section.remove(key, value); |
248 | 0 | if (changed) { |
249 | 0 | if (section.isEmpty()) { |
250 | 0 | sectionMap.remove(sectionName); |
251 | 0 | list.remove(sectionName); |
252 | } | |
253 | } | |
254 | ||
255 | 0 | return changed; |
256 | } | |
257 | ||
258 | /** | |
259 | * Remove the key if present. | |
260 | * If it were the last key for the section, the section is removed. | |
261 | * | |
262 | * @param sectionName the name of the section | |
263 | * @param key the key for the section | |
264 | * @return whether the key was present and removed | |
265 | */ | |
266 | public boolean remove(String sectionName, String key) { | |
267 | 0 | IniSection section = sectionMap.get(sectionName); |
268 | 0 | if (section == null) { |
269 | 0 | return false; |
270 | } | |
271 | 0 | boolean changed = section.remove(key); |
272 | 0 | sectionMap.remove(sectionName); |
273 | 0 | list.remove(sectionName); |
274 | ||
275 | 0 | return changed; |
276 | } | |
277 | ||
278 | // Routines that work on the first section | |
279 | /** | |
280 | * Get the first section. | |
281 | * | |
282 | * @return the first section or null if there are no sections | |
283 | */ | |
284 | public IniSection getSection() { | |
285 | 0 | return size() == 0 ? null : sectionMap.get(list.get(0)); |
286 | } | |
287 | ||
288 | /** | |
289 | * Get the unmodifiable set of keys of the first section. | |
290 | * The set has insertion order. | |
291 | * | |
292 | * @return the keys of the first section | |
293 | */ | |
294 | public Collection<String> getKeys() { | |
295 | 0 | IniSection section = getSection(); |
296 | 0 | return section == null ? null : section.getKeys(); |
297 | } | |
298 | ||
299 | /** | |
300 | * Get the values of a key of the first section. | |
301 | * The collection has insertion order. | |
302 | * Note many keys only have one value. | |
303 | * A key that has no values returns null. | |
304 | * | |
305 | * @param key the key | |
306 | * @return the keyed values or null if the key doesn't exist | |
307 | */ | |
308 | public Collection<String> getValues(String key) { | |
309 | 0 | IniSection section = getSection(); |
310 | 0 | return section == null ? null : section.getValues(key); |
311 | } | |
312 | ||
313 | /** | |
314 | * Add a value for the key. Duplicate values are not allowed. | |
315 | * | |
316 | * @param key the key for the section | |
317 | * @param value the value for the key | |
318 | * @return whether the value was added or is already present. | |
319 | */ | |
320 | public boolean addValue(String key, String value) { | |
321 | 0 | IniSection section = getSection(); |
322 | 0 | return section == null || section.add(key, value); |
323 | } | |
324 | ||
325 | /** | |
326 | * Remove the value if present in the first section. | |
327 | * If it were the last value for the key, the key is removed. | |
328 | * If it were the last key, the section is removed. | |
329 | * | |
330 | * @param key the key for the section | |
331 | * @param value the value for the key | |
332 | * @return whether the value was present and removed | |
333 | */ | |
334 | public boolean removeValue(String key, String value) { | |
335 | 0 | String section = getSectionName(); |
336 | 0 | return section == null || remove(section, key, value); |
337 | } | |
338 | ||
339 | /** | |
340 | * Remove the key if present. | |
341 | * If it were the last key for the section, the section is removed. | |
342 | * | |
343 | * @param key the key for the section | |
344 | * @return whether the key was present and removed | |
345 | */ | |
346 | public boolean removeValue(String key) { | |
347 | 0 | String section = getSectionName(); |
348 | 0 | return section == null || remove(section, key); |
349 | } | |
350 | ||
351 | /** | |
352 | * Replace a value for a key. | |
353 | * A null for key or value is not allowed. | |
354 | * An empty string for a key is not allowed. | |
355 | * | |
356 | * @param key the key for the section | |
357 | * @param value the value for the key | |
358 | * @return {@code true} if the element was added or already was present | |
359 | */ | |
360 | public boolean replaceValue(String key, String value) { | |
361 | 0 | IniSection section = getSection(); |
362 | 0 | return section == null || section.replace(key, value); |
363 | } | |
364 | ||
365 | public void load(InputStream is, String encoding) throws IOException { | |
366 | 0 | Reader in = null; |
367 | try { | |
368 | 0 | in = new InputStreamReader(is, encoding); |
369 | 0 | doLoad(in); |
370 | } finally { | |
371 | 0 | if (in != null) { |
372 | 0 | in.close(); |
373 | 0 | in = null; |
374 | } | |
375 | } | |
376 | 0 | } |
377 | ||
378 | /** | |
379 | * Load the INI from a file using the given encoding. | |
380 | * | |
381 | * @param file the file to load | |
382 | * @param encoding the encoding of the file | |
383 | * @throws IOException | |
384 | */ | |
385 | public void load(File file, String encoding) throws IOException { | |
386 | 0 | InputStream in = null; |
387 | try { | |
388 | 0 | in = new FileInputStream(file); |
389 | 0 | load(in, encoding); |
390 | } finally { | |
391 | 0 | if (in != null) { |
392 | 0 | in.close(); |
393 | 0 | in = null; |
394 | } | |
395 | } | |
396 | 0 | } |
397 | ||
398 | /** | |
399 | * Load the conf from a buffer. This is used to load conf entries from the | |
400 | * mods.d.tar.gz file. | |
401 | * | |
402 | * @param buffer the buffer to load | |
403 | * @param encoding the character encoding for this INI | |
404 | * @throws IOException | |
405 | */ | |
406 | public void load(byte[] buffer, String encoding) throws IOException { | |
407 | 0 | InputStream in = null; |
408 | try { | |
409 | 0 | in = new ByteArrayInputStream(buffer); |
410 | 0 | load(in, encoding); |
411 | } finally { | |
412 | 0 | if (in != null) { |
413 | 0 | in.close(); |
414 | 0 | in = null; |
415 | } | |
416 | } | |
417 | 0 | } |
418 | ||
419 | /** | |
420 | * Save the INI to a file using the given encoding. | |
421 | * | |
422 | * @param file the file to load | |
423 | * @param encoding the encoding of the file | |
424 | * @throws IOException | |
425 | */ | |
426 | public void save(File file, String encoding) throws IOException { | |
427 | 0 | Writer out = null; |
428 | try { | |
429 | 0 | out = new OutputStreamWriter(new FileOutputStream(file), encoding); |
430 | 0 | save(out); |
431 | } finally { | |
432 | 0 | if (out != null) { |
433 | 0 | out.close(); |
434 | 0 | out = null; |
435 | } | |
436 | } | |
437 | 0 | } |
438 | ||
439 | /** | |
440 | * Output the Ini to the given Writer. | |
441 | * | |
442 | * @param out the Writer to which this Ini should be written | |
443 | */ | |
444 | private void save(Writer out) { | |
445 | 0 | PrintWriter writer = null; |
446 | 0 | if (out instanceof PrintWriter) { |
447 | 0 | writer = (PrintWriter) out; |
448 | } else { | |
449 | 0 | writer = new PrintWriter(out); |
450 | } | |
451 | ||
452 | 0 | for (String sectionName : list) { |
453 | 0 | IniSection section = doGetSection(sectionName); |
454 | 0 | section.save(writer); |
455 | 0 | } |
456 | 0 | } |
457 | ||
458 | private IniSection doGetSection(String sectionName) { | |
459 | 0 | return sectionMap.get(sectionName); |
460 | } | |
461 | ||
462 | /** | |
463 | * Get a section, creating it if necessary. | |
464 | * | |
465 | * @param sectionName | |
466 | * @return the found or created section | |
467 | */ | |
468 | private IniSection getOrCreateSection(final String sectionName) { | |
469 | 0 | IniSection section = sectionMap.get(sectionName); |
470 | 0 | if (section == null) { |
471 | 0 | section = new IniSection(sectionName); |
472 | 0 | sectionMap.put(sectionName, section); |
473 | 0 | list.add(sectionName); |
474 | } | |
475 | 0 | return section; |
476 | } | |
477 | ||
478 | private void doLoad(Reader in) throws IOException { | |
479 | 0 | BufferedReader bin = null; |
480 | try { | |
481 | 0 | if (in instanceof BufferedReader) { |
482 | 0 | bin = (BufferedReader) in; |
483 | } else { | |
484 | // Quiet Android from complaining about using the default | |
485 | // BufferReader buffer size. | |
486 | // The actual buffer size is undocumented. So this is a good | |
487 | // idea any way. | |
488 | 0 | bin = new BufferedReader(in, MAX_BUFF_SIZE); |
489 | } | |
490 | ||
491 | 0 | String sectionName = ""; |
492 | 0 | StringBuilder buf = new StringBuilder(); |
493 | while (true) { | |
494 | // Empty out the buffer | |
495 | 0 | buf.setLength(0); |
496 | 0 | String line = advance(bin); |
497 | 0 | if (line == null) { |
498 | 0 | break; |
499 | } | |
500 | ||
501 | 0 | if (isSectionLine(line)) { |
502 | // The conf file contains a leading line of the form [KJV] | |
503 | // This is the acronym by which Sword refers to it. | |
504 | 0 | sectionName = line.substring(1, line.length() - 1); |
505 | 0 | continue; |
506 | } | |
507 | ||
508 | // Is this a key line? | |
509 | 0 | int splitPos = getSplitPos(line); |
510 | 0 | if (splitPos < 0) { |
511 | 0 | LOGGER.warn("Expected to see '=' in [{}]: {}", sectionName, line); |
512 | 0 | continue; |
513 | } | |
514 | ||
515 | 0 | String key = line.substring(0, splitPos).trim(); |
516 | 0 | if (key.length() == 0) { |
517 | 0 | LOGGER.warn("Empty key in [{}]: {}", sectionName, line); |
518 | } | |
519 | 0 | String value = more(bin, line.substring(splitPos + 1).trim()); |
520 | 0 | add(sectionName, key, value); |
521 | 0 | } |
522 | } finally { | |
523 | 0 | if (bin != null) { |
524 | 0 | bin.close(); |
525 | 0 | bin = null; |
526 | } | |
527 | } | |
528 | 0 | } |
529 | ||
530 | /** | |
531 | * Get the next line from the input | |
532 | * | |
533 | * @param bin The reader to get data from | |
534 | * @return the next line or null if there is nothing more | |
535 | * @throws IOException if encountered | |
536 | */ | |
537 | private String advance(BufferedReader bin) throws IOException { | |
538 | // Get the next non-blank, non-comment line | |
539 | 0 | String trimmed = null; |
540 | 0 | for (String line = bin.readLine(); line != null; line = bin.readLine()) { |
541 | // Remove leading and trailing whitespace | |
542 | 0 | trimmed = line.trim(); |
543 | ||
544 | // skip blank and comment lines | |
545 | 0 | if (!isCommentLine(trimmed)) { |
546 | 0 | return trimmed; |
547 | } | |
548 | } | |
549 | 0 | return null; |
550 | } | |
551 | ||
552 | /** | |
553 | * Determine if the given line is a blank or a comment line. | |
554 | * | |
555 | * @param line The line to check. | |
556 | * @return true if the line is empty or starts with one of the comment | |
557 | * characters | |
558 | */ | |
559 | private boolean isCommentLine(final String line) { | |
560 | 0 | if (line == null) { |
561 | 0 | return false; |
562 | } | |
563 | 0 | if (line.length() == 0) { |
564 | 0 | return true; |
565 | } | |
566 | 0 | char firstChar = line.charAt(0); |
567 | 0 | return firstChar == ';' || firstChar == '#'; |
568 | } | |
569 | ||
570 | /** | |
571 | * Is this line a [section]? | |
572 | * | |
573 | * @param line The line to check. | |
574 | * @return true if the line designates a section | |
575 | */ | |
576 | private boolean isSectionLine(final String line) { | |
577 | 0 | return line.charAt(0) == '[' && line.charAt(line.length() - 1) == ']'; |
578 | } | |
579 | ||
580 | /** | |
581 | * Does this line of text represent a key/value pair? | |
582 | * | |
583 | * @param line The line to check. | |
584 | * @return the position of the split position or -1 | |
585 | */ | |
586 | private int getSplitPos(final String line) { | |
587 | 0 | return line.indexOf('='); |
588 | } | |
589 | ||
590 | /** | |
591 | * Get continuation lines, if any. | |
592 | */ | |
593 | private String more(BufferedReader bin, String value) throws IOException { | |
594 | 0 | boolean moreCowBell = false; |
595 | 0 | String line = value; |
596 | 0 | StringBuilder buf = new StringBuilder(); |
597 | ||
598 | do { | |
599 | 0 | moreCowBell = more(line); |
600 | 0 | if (moreCowBell) { |
601 | 0 | line = line.substring(0, line.length() - 1).trim(); |
602 | } | |
603 | 0 | buf.append(line); |
604 | 0 | if (moreCowBell) { |
605 | 0 | buf.append('\n'); |
606 | 0 | line = advance(bin); |
607 | } | |
608 | 0 | } while (moreCowBell && line != null); |
609 | 0 | return buf.toString(); |
610 | } | |
611 | ||
612 | /** | |
613 | * Is there more following this line | |
614 | * | |
615 | * @param line the trimmed string to check | |
616 | * @return whether this line continues | |
617 | */ | |
618 | private static boolean more(final String line) { | |
619 | 0 | int length = line.length(); |
620 | 0 | return length > 0 && line.charAt(length - 1) == '\\'; |
621 | } | |
622 | ||
623 | /** | |
624 | * A map of sections by section names. | |
625 | */ | |
626 | private Map<String, IniSection> sectionMap; | |
627 | ||
628 | /** | |
629 | * Indexed list of sections maintaining insertion order. | |
630 | */ | |
631 | private List<String> list; | |
632 | ||
633 | /** | |
634 | * Buffer size is based on file size but keep it with within reasonable limits | |
635 | */ | |
636 | private static final int MAX_BUFF_SIZE = 8 * 1024; | |
637 | ||
638 | /** | |
639 | * The log stream | |
640 | */ | |
641 | 0 | private static final Logger LOGGER = LoggerFactory.getLogger(Ini.class); |
642 | } |