| Ini.java |
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 Ini() {
89 sectionMap = new TreeMap<String, IniSection>(String.CASE_INSENSITIVE_ORDER);
90 list = new ArrayList();
91 }
92
93 /**
94 * Start over.
95 */
96 public void clear() {
97 sectionMap.clear();
98 list.clear();
99 }
100
101 /**
102 * Get the number of sections
103 *
104 * @return the number of known sections
105 */
106 public int size() {
107 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 return Collections.unmodifiableList(list);
117 }
118
119 public String getSectionName(int index) {
120 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 return size() == 0 ? null : list.get(0);
131 }
132
133 public int getValueSize(String sectionName, String key) {
134 IniSection section = doGetSection(sectionName);
135 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 IniSection section = getSection();
146 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 IniSection section = doGetSection(sectionName);
160 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 IniSection section = doGetSection(sectionName);
173 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 IniSection section = getSection();
186 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 IniSection section = getSection();
198 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 IniSection section = getOrCreateSection(sectionName);
214 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 IniSection section = getOrCreateSection(sectionName);
229 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 IniSection section = sectionMap.get(sectionName);
244 if (section == null) {
245 return false;
246 }
247 boolean changed = section.remove(key, value);
248 if (changed) {
249 if (section.isEmpty()) {
250 sectionMap.remove(sectionName);
251 list.remove(sectionName);
252 }
253 }
254
255 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 IniSection section = sectionMap.get(sectionName);
268 if (section == null) {
269 return false;
270 }
271 boolean changed = section.remove(key);
272 sectionMap.remove(sectionName);
273 list.remove(sectionName);
274
275 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 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 IniSection section = getSection();
296 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 IniSection section = getSection();
310 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 IniSection section = getSection();
322 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 String section = getSectionName();
336 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 String section = getSectionName();
348 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 IniSection section = getSection();
362 return section == null || section.replace(key, value);
363 }
364
365 public void load(InputStream is, String encoding) throws IOException {
366 Reader in = null;
367 try {
368 in = new InputStreamReader(is, encoding);
369 doLoad(in);
370 } finally {
371 if (in != null) {
372 in.close();
373 in = null;
374 }
375 }
376 }
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 InputStream in = null;
387 try {
388 in = new FileInputStream(file);
389 load(in, encoding);
390 } finally {
391 if (in != null) {
392 in.close();
393 in = null;
394 }
395 }
396 }
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 InputStream in = null;
408 try {
409 in = new ByteArrayInputStream(buffer);
410 load(in, encoding);
411 } finally {
412 if (in != null) {
413 in.close();
414 in = null;
415 }
416 }
417 }
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 Writer out = null;
428 try {
429 out = new OutputStreamWriter(new FileOutputStream(file), encoding);
430 save(out);
431 } finally {
432 if (out != null) {
433 out.close();
434 out = null;
435 }
436 }
437 }
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 PrintWriter writer = null;
446 if (out instanceof PrintWriter) {
447 writer = (PrintWriter) out;
448 } else {
449 writer = new PrintWriter(out);
450 }
451
452 for (String sectionName : list) {
453 IniSection section = doGetSection(sectionName);
454 section.save(writer);
455 }
456 }
457
458 private IniSection doGetSection(String sectionName) {
459 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 IniSection section = sectionMap.get(sectionName);
470 if (section == null) {
471 section = new IniSection(sectionName);
472 sectionMap.put(sectionName, section);
473 list.add(sectionName);
474 }
475 return section;
476 }
477
478 private void doLoad(Reader in) throws IOException {
479 BufferedReader bin = null;
480 try {
481 if (in instanceof BufferedReader) {
482 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 bin = new BufferedReader(in, MAX_BUFF_SIZE);
489 }
490
491 String sectionName = "";
492 StringBuilder buf = new StringBuilder();
493 while (true) {
494 // Empty out the buffer
495 buf.setLength(0);
496 String line = advance(bin);
497 if (line == null) {
498 break;
499 }
500
501 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 sectionName = line.substring(1, line.length() - 1);
505 continue;
506 }
507
508 // Is this a key line?
509 int splitPos = getSplitPos(line);
510 if (splitPos < 0) {
511 LOGGER.warn("Expected to see '=' in [{}]: {}", sectionName, line);
512 continue;
513 }
514
515 String key = line.substring(0, splitPos).trim();
516 if (key.length() == 0) {
517 LOGGER.warn("Empty key in [{}]: {}", sectionName, line);
518 }
519 String value = more(bin, line.substring(splitPos + 1).trim());
520 add(sectionName, key, value);
521 }
522 } finally {
523 if (bin != null) {
524 bin.close();
525 bin = null;
526 }
527 }
528 }
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 String trimmed = null;
540 for (String line = bin.readLine(); line != null; line = bin.readLine()) {
541 // Remove leading and trailing whitespace
542 trimmed = line.trim();
543
544 // skip blank and comment lines
545 if (!isCommentLine(trimmed)) {
546 return trimmed;
547 }
548 }
549 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 if (line == null) {
561 return false;
562 }
563 if (line.length() == 0) {
564 return true;
565 }
566 char firstChar = line.charAt(0);
567 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 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 return line.indexOf('=');
588 }
589
590 /**
591 * Get continuation lines, if any.
592 */
593 private String more(BufferedReader bin, String value) throws IOException {
594 boolean moreCowBell = false;
595 String line = value;
596 StringBuilder buf = new StringBuilder();
597
598 do {
599 moreCowBell = more(line);
600 if (moreCowBell) {
601 line = line.substring(0, line.length() - 1).trim();
602 }
603 buf.append(line);
604 if (moreCowBell) {
605 buf.append('\n');
606 line = advance(bin);
607 }
608 } while (moreCowBell && line != null);
609 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 int length = line.length();
620 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 private static final Logger LOGGER = LoggerFactory.getLogger(Ini.class);
642 }
643