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