Coverage Report - org.crosswire.common.util.Ini
 
Classes in this File Line Coverage Branch Coverage Complexity
Ini
0%
0/156
0%
0/98
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  
 }