Coverage Report - org.crosswire.common.util.IniSection
 
Classes in this File Line Coverage Branch Coverage Complexity
IniSection
0%
0/217
0%
0/122
2.548
 
 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.HashMap;
 37  
 import java.util.Iterator;
 38  
 import java.util.List;
 39  
 import java.util.Map;
 40  
 
 41  
 /**
 42  
  * A utility class for a section of an INI style configuration file.
 43  
  * Keys and values are maintained in insertion order. A key may have more than one value.
 44  
  * <p>
 45  
  * SWORD defines a conf as an INI file with one or more sections.
 46  
  * Originally, all modules were described in a single conf, but
 47  
  * now each module has its own conf.
 48  
  * </p>
 49  
  * <p>
 50  
  * SWORD will be using a single conf to hold overrides for many
 51  
  * modules. This is the motivation for this class as opposed to
 52  
  * allowing only a single section as {@link IniSection}.
 53  
  * </p>
 54  
  * <p>
 55  
  * Since the most common use case is for a single section, this
 56  
  * implementation has an API for delegating to the first IniSection.
 57  
  * </p>
 58  
  * 
 59  
  * This implementation allows for:
 60  
  * <ul>
 61  
  * <li><strong>Case Insensitive</strong> -- Section names, keys and values are case insensitive.</li>
 62  
  * <li><strong>Comments</strong> -- ; and # preceded only by white space indicate that a line is a comment.
 63  
  *              Note: SWORD does not support ; but it is present in some 3rd Party repositories such as IBT.</li>
 64  
  * <li><strong>Multiple Values</strong> -- Each key can have one or more values.</li>
 65  
  * <li><strong>Order</strong> -- Order of sections, keys and values are retained</li>
 66  
  * </ul>
 67  
  * 
 68  
  * This implementation does not allow for:
 69  
  * <ul>
 70  
  * <li><strong>Globals</strong> -- (key,value) pairs before the first section.</li>
 71  
  * <li><strong>Quoted Values</strong> -- Values surrounded by "" or ''.
 72  
  *              If present they are part of the value.</li>
 73  
  * <li><strong>Retaining comments</strong> -- Comments are ignored.</li>
 74  
  * <li><strong>Comments after content</strong> -- Comments are on lines to themselves.</li>
 75  
  * <li><strong>:</strong> -- as an alternative for =.</li>
 76  
  * <li><strong>nulls</strong> -- null values.</li>
 77  
  * </ul>
 78  
  *
 79  
 
 80  
  * @author DM Smith
 81  
  * @see gnu.lgpl.License The GNU Lesser General Public License for details.<br>
 82  
  */
 83  0
 public final class IniSection implements Iterable {
 84  
 
 85  
     /**
 86  
      * Create an empty INI config without a name.
 87  
      */
 88  
     public IniSection() {
 89  0
         this((String) null);
 90  0
     }
 91  
     /**
 92  
      * Create an empty INI Config.
 93  
      * @param name the section name
 94  
      */
 95  0
     public IniSection(String name) {
 96  0
         this.name = name;
 97  0
         section = new HashMap<String, List<String>>();
 98  0
         warnings = new StringBuilder();
 99  0
     }
 100  
 
 101  
     /**
 102  
      * Copy constructor
 103  
      * 
 104  
      * @param config the config to copy
 105  
      */
 106  0
     public IniSection(IniSection config) {
 107  0
         this.name = config.getName();
 108  0
         section = new HashMap<String, List<String>>();
 109  0
         for (String key : config.getKeys()) {
 110  0
             for (String value : config.getValues(key)) {
 111  0
                 add(key, value);
 112  
             }
 113  
         }
 114  0
     }
 115  
     /**
 116  
      * Start over.
 117  
      */
 118  
     public void clear() {
 119  0
         section.clear();
 120  0
         warnings.setLength(0);
 121  0
         warnings.trimToSize();
 122  0
         report = "";
 123  0
     }
 124  
 
 125  
     /**
 126  
      * Set the name of this INI config.
 127  
      * 
 128  
      * @param name
 129  
      */
 130  
     public void setName(String name) {
 131  0
         this.name = name;
 132  0
     }
 133  
 
 134  
     /**
 135  
      * Get the [name] of this section
 136  
      *
 137  
      * @return the name
 138  
      */
 139  
     public String getName() {
 140  0
         return name;
 141  
     }
 142  
 
 143  
     /**
 144  
      * Get the number of keys in this section.
 145  
      * 
 146  
      * @return the count
 147  
      */
 148  
     public int size() {
 149  0
         return section.size();
 150  
     }
 151  
 
 152  
     /**
 153  
      * Get the number of values for a key.
 154  
      * 
 155  
      * @param key the key
 156  
      * @return the number of values for a key or 0 if the key does not exist.
 157  
      */
 158  
     public int size(String key) {
 159  0
         Collection<String> values = section.get(key);
 160  0
         return values == null ? 0 : values.size();
 161  
     }
 162  
 
 163  
     /**
 164  
      * Determine whether this section has any keys
 165  
      *
 166  
      * @return {@code true} if this section is empty
 167  
      */
 168  
     public boolean isEmpty() {
 169  0
         return section.isEmpty();
 170  
     }
 171  
 
 172  
     public Iterator iterator() {
 173  0
         return section.keySet().iterator();
 174  
     }
 175  
     /**
 176  
      * Get the unmodifiable unordered list of keys.
 177  
      *
 178  
      * @return the set of keys
 179  
      */
 180  
     public Collection<String> getKeys() {
 181  0
         return Collections.unmodifiableSet(section.keySet());
 182  
     }
 183  
 
 184  
     /**
 185  
      * Returns {@code true} if the IniSection contains any values for the specified key.
 186  
      *
 187  
      * @param key key to search for in IniSection
 188  
      * @return {@code true} if the key exists
 189  
      */
 190  
     public boolean containsKey(String key) {
 191  0
         return section.containsKey(key);
 192  
     }
 193  
 
 194  
     /**
 195  
      * Returns {@code true} if the IniSection contains the specified value for any key.
 196  
      *
 197  
      * @param value value to search for in IniSection
 198  
      * @return {@code true} if the value exists.
 199  
      */
 200  
     public boolean containsValue(String value) {
 201  0
         for (Collection<String> collection : section.values()) {
 202  0
             if (collection.contains(value)) {
 203  0
                 return true;
 204  
             }
 205  
         }
 206  0
         return false;
 207  
     }
 208  
 
 209  
     /**
 210  
      * Returns {@code true} if the IniSection contains the specified value for the given key.
 211  
      *
 212  
      * @param key the key for the section
 213  
      * @param value value to search for in IniSection
 214  
      * @return {@code true} if the value exists.
 215  
      */
 216  
     public boolean containsValue(String key, String value) {
 217  0
         Collection<String> values = section.get(key);
 218  0
         return values != null && values.contains(value);
 219  
     }
 220  
 
 221  
     /**
 222  
      * Add a value for the key. Duplicate values are not allowed.
 223  
      *
 224  
      * @param key the key for the section
 225  
      * @param value the value for the key
 226  
      * @return whether the value was added or is already present.
 227  
      */
 228  
     public boolean add(String key, String value) {
 229  0
         if (!allowed(key, value)) {
 230  0
             return false;
 231  
         }
 232  
 
 233  0
         Collection<String> values = getOrCreateValues(key);
 234  0
         if (values.contains(value)) {
 235  0
             warnings.append("Duplicate value: ").append(key).append(" = ").append(value).append('\n');
 236  0
             return true;
 237  
         }
 238  0
         return values.add(value);
 239  
     }
 240  
 
 241  
     /**
 242  
      * Get the unmodifiable collection of values of a key.
 243  
      * The collection has insertion order.
 244  
      * Note many keys only have one value.
 245  
      * A key that has no values returns null.
 246  
      *
 247  
      * @param key the key
 248  
      * @return the keyed values or null if the key doesn't exist
 249  
      */
 250  
     public Collection<String> getValues(String key) {
 251  0
         if (section.containsKey(key)) {
 252  0
             return Collections.unmodifiableCollection(section.get(key));
 253  
         }
 254  0
         return null;
 255  
     }
 256  
 
 257  
     /**
 258  
      * Get the value for the key specified by the index.
 259  
      * 
 260  
      * @param key the key
 261  
      * @param index the index
 262  
      * @return the value at the specified index
 263  
      * @throws ArrayIndexOutOfBoundsException when the index is out of bounds
 264  
      */
 265  
     public String get(String key, int index) {
 266  0
         List<String> values = section.get(key);
 267  0
         return values == null ? null : values.get(index);
 268  
     }
 269  
 
 270  
     /**
 271  
      * Get the first value for the key.
 272  
      * 
 273  
      * @param key the key
 274  
      * @return the value at the specified index or null
 275  
      */
 276  
     public String get(String key) {
 277  0
         List<String> values = section.get(key);
 278  0
         return values == null ? null : values.get(0);
 279  
     }
 280  
 
 281  
     public String get(String key, String defaultValue) {
 282  0
         List<String> values = section.get(key);
 283  0
         return values == null ? defaultValue : values.get(0);
 284  
     }
 285  
 
 286  
     /**
 287  
      * Remove the value if present.
 288  
      * If it were the last value for the key, the key is removed.
 289  
      * 
 290  
      * @param key the key for the section
 291  
      * @param value the value for the key
 292  
      * @return whether the value was present and removed
 293  
      */
 294  
     public boolean remove(String key, String value) {
 295  0
         Collection<String> values = section.get(key);
 296  0
         if (values == null) {
 297  0
             return false;
 298  
         }
 299  
 
 300  0
         boolean changed = values.remove(value);
 301  0
         if (changed) {
 302  0
             if (values.isEmpty()) {
 303  0
                 section.remove(key);
 304  
             }
 305  
         }
 306  
 
 307  0
         return changed;
 308  
     }
 309  
 
 310  
     /**
 311  
      * Remove the key and all its values, if present.
 312  
      * 
 313  
      * @param key the key for the section
 314  
      * @return whether the key was present and removed
 315  
      */
 316  
     public boolean remove(String key) {
 317  0
         Collection<String> values = section.get(key);
 318  0
         if (values == null) {
 319  0
             return false;
 320  
         }
 321  0
         section.remove(key);
 322  0
         return true;
 323  
     }
 324  
 
 325  
     /**
 326  
      * Replace the value(s) for the key with a new value.
 327  
      *
 328  
      * @param key the key for the section
 329  
      * @param value the value for the key
 330  
      * @return whether the replace happened
 331  
      */
 332  
     public boolean replace(String key, String value) {
 333  0
         if (!allowed(key, value)) {
 334  0
             return false;
 335  
         }
 336  
 
 337  0
         Collection<String> values = getOrCreateValues(key);
 338  0
         values.clear();
 339  0
         return values.add(value);
 340  
     }
 341  
 
 342  
     /**
 343  
      * Load the INI from an InputStream using the given encoding.
 344  
      *
 345  
      * @param is the InputStream to read from
 346  
      * @param encoding the encoding of the file
 347  
      * @throws IOException
 348  
      */
 349  
     public void load(InputStream is, String encoding) throws IOException {
 350  0
         load(is, encoding, null);
 351  0
     }
 352  
 
 353  
     /**
 354  
      * Load the INI from an InputStream using the given encoding. Filter keys as specified.
 355  
      *
 356  
      * @param is the InputStream to read from
 357  
      * @param encoding the encoding of the file
 358  
      * @param filter the filter, possibly null, for the desired keys
 359  
      * @throws IOException
 360  
      */
 361  
     public void load(InputStream is, String encoding, Filter<String> filter) throws IOException {
 362  0
         Reader in = null;
 363  
         try {
 364  0
             in = new InputStreamReader(is, encoding);
 365  0
             doLoad(in, filter);
 366  
         } finally {
 367  0
             if (in != null) {
 368  0
                 in.close();
 369  0
                 in = null;
 370  
             }
 371  
         }
 372  0
     }
 373  
 
 374  
     /**
 375  
      * Load the INI from a file using the given encoding.
 376  
      *
 377  
      * @param file the file to load
 378  
      * @param encoding the encoding of the file
 379  
      * @throws IOException
 380  
      */
 381  
     public void load(File file, String encoding) throws IOException {
 382  0
         load(file, encoding, null);
 383  0
     }
 384  
 
 385  
     /**
 386  
      * Load the INI from a file using the given encoding. Filter keys as specified.
 387  
      *
 388  
      * @param file the file to load
 389  
      * @param encoding the encoding of the file
 390  
      * @param filter the filter, possibly null, for the desired keys
 391  
      * @throws IOException
 392  
      */
 393  
     public void load(File file, String encoding, Filter<String> filter) throws IOException {
 394  0
         this.configFile = file;
 395  0
         this.charset = encoding;
 396  0
         InputStream in = null;
 397  
         try {
 398  0
             in = new FileInputStream(file);
 399  0
             load(in, encoding, filter);
 400  
         } finally {
 401  0
             if (in != null) {
 402  0
                 in.close();
 403  0
                 in = null;
 404  
             }
 405  
         }
 406  0
     }
 407  
 
 408  
     /**
 409  
      * Load the conf from a buffer. This is used to load conf entries from the
 410  
      * mods.d.tar.gz file.
 411  
      *
 412  
      * @param buffer the buffer to load
 413  
      * @param encoding the character encoding of this INI
 414  
      * @throws IOException
 415  
      */
 416  
     public void load(byte[] buffer, String encoding) throws IOException {
 417  0
         load(buffer, encoding, null);
 418  0
     }
 419  
 
 420  
     /**
 421  
      * Load the conf from a buffer. Filter keys as specified.
 422  
      * This is used to load conf entries from the mods.d.tar.gz file.
 423  
      *
 424  
      * @param buffer the buffer to load
 425  
      * @param encoding the character encoding of this INI
 426  
      * @param filter the filter, possibly null, for the desired keys
 427  
      * @throws IOException
 428  
      */
 429  
     public void load(byte[] buffer, String encoding, Filter<String> filter) throws IOException {
 430  0
         InputStream in = null;
 431  
         try {
 432  0
             in = new ByteArrayInputStream(buffer);
 433  0
             load(in, encoding, filter);
 434  
         } finally {
 435  0
             if (in != null) {
 436  0
                 in.close();
 437  0
                 in = null;
 438  
             }
 439  
         }
 440  0
     }
 441  
 
 442  
     /**
 443  
      * Save this INI to the file from which it was loaded.
 444  
      * @throws IOException
 445  
      */
 446  
     public void save() throws IOException {
 447  0
         assert configFile != null;
 448  0
         assert charset != null;
 449  0
         if (configFile != null && charset != null) {
 450  0
             save(configFile, charset);
 451  
         }
 452  0
     }
 453  
 
 454  
     /**
 455  
      * Save the INI to a file using the given encoding.
 456  
      *
 457  
      * @param file the file to load
 458  
      * @param encoding the encoding of the file
 459  
      * @throws IOException
 460  
      */
 461  
     public void save(File file, String encoding) throws IOException {
 462  0
         this.configFile = file;
 463  0
         this.charset = encoding;
 464  0
         Writer out = null;
 465  
         try {
 466  0
             out = new OutputStreamWriter(new FileOutputStream(file), encoding);
 467  0
             save(out);
 468  
         } finally {
 469  0
             if (out != null) {
 470  0
                 out.close();
 471  0
                 out = null;
 472  
             }
 473  
         }
 474  0
     }
 475  
 
 476  
     /**
 477  
      * Output this section using the print writer. The section ends with a blank line.
 478  
      * The items are output in insertion order.
 479  
      * 
 480  
      * @param out the output stream
 481  
      */
 482  
     public void save(Writer out) {
 483  0
         PrintWriter writer = null;
 484  0
         if (out instanceof PrintWriter) {
 485  0
             writer = (PrintWriter) out;
 486  
         } else {
 487  0
             writer = new PrintWriter(out);
 488  
         }
 489  
 
 490  0
         writer.print("[");
 491  0
         writer.print(name);
 492  0
         writer.print("]");
 493  0
         writer.println();
 494  
 
 495  0
         boolean first = true;
 496  0
         Iterator<String> keys = section.keySet().iterator();
 497  0
         while (keys.hasNext()) {
 498  0
             String key = keys.next();
 499  0
             Collection<String> values = section.get(key);
 500  0
             Iterator<String> iter = values.iterator();
 501  
             String value;
 502  0
             while (iter.hasNext()) {
 503  0
                 if (!first) {
 504  0
                     writer.println();
 505  0
                     first = false;
 506  
                 }
 507  0
                 value = iter.next();
 508  0
                 writer.print(key);
 509  0
                 writer.print(" = ");
 510  0
                 writer.print(format(value));
 511  0
                 writer.println();
 512  
             }
 513  0
         }
 514  
 
 515  0
         writer.flush();
 516  0
     }
 517  
 
 518  
     /**
 519  
      * Obtain a report of issues with this IniSection. It only reports once per load.
 520  
      * 
 521  
      * @return the report with one issue per line or an empty string if there are no issues
 522  
      */
 523  
     public String report() {
 524  0
         String str = report;
 525  0
         report = "";
 526  0
         return str;
 527  
     }
 528  
 
 529  
     /**
 530  
      * A helper to format the output of the content as expected
 531  
      * @param value the value to be formatted
 532  
      * @return the transformed value
 533  
      */
 534  
     private String format(final String value) {
 535  
         // Find continuations and replace newlines with a ' \'
 536  
         // Indenting the next line
 537  
         // Note: if the quoting of values is allowed this may need to be revisited.
 538  0
         return value.replaceAll("\n", " \\\\\n\t");
 539  
     }
 540  
 
 541  
     private Collection<String> getOrCreateValues(final String key) {
 542  0
         List<String> values = section.get(key);
 543  0
         if (values == null) {
 544  0
             values = new ArrayList<String>();
 545  0
             section.put(key, values);
 546  
         }
 547  0
         return values;
 548  
     }
 549  
 
 550  
     private void doLoad(Reader in, Filter<String> filter) throws IOException {
 551  0
         BufferedReader bin = null;
 552  
         try {
 553  0
             if (in instanceof BufferedReader) {
 554  0
                 bin = (BufferedReader) in;
 555  
             } else {
 556  
                 // Quiet Android from complaining about using the default
 557  
                 // BufferReader buffer size.
 558  
                 // The actual buffer size is undocumented. So this is a good
 559  
                 // idea any way.
 560  0
                 bin = new BufferedReader(in, MAX_BUFF_SIZE);
 561  
             }
 562  
 
 563  
             while (true) {
 564  0
                 String line = advance(bin);
 565  0
                 if (line == null) {
 566  0
                     break;
 567  
                 }
 568  
 
 569  0
                 if (isSectionLine(line)) {
 570  
                     // The conf file contains a leading line of the form [KJV]
 571  
                     // This is the acronym by which Sword refers to it.
 572  0
                     name = line.substring(1, line.length() - 1);
 573  0
                     continue;
 574  
                 }
 575  
 
 576  
                 // Is this a key line?
 577  0
                 int splitPos = getSplitPos(line);
 578  0
                 if (splitPos < 0) {
 579  0
                     warnings.append("Skipping: Expected to see '=' in: ").append(line).append('\n');
 580  0
                     continue;
 581  
                 }
 582  
 
 583  0
                 String key = line.substring(0, splitPos).trim();
 584  0
                 String value = more(bin, line.substring(splitPos + 1).trim());
 585  0
                 if (filter == null || filter.test(key)) {
 586  0
                     add(key, value);
 587  
                 }
 588  0
             }
 589  0
             report = warnings.toString();
 590  0
             warnings.setLength(0);
 591  0
             warnings.trimToSize();
 592  
         } finally {
 593  0
             if (bin != null) {
 594  0
                 bin.close();
 595  0
                 bin = null;
 596  
             }
 597  
         }
 598  0
     }
 599  
 
 600  
     /**
 601  
      * Get the next line from the input
 602  
      *
 603  
      * @param bin The reader to get data from
 604  
      * @return the next line or null if there is nothing more
 605  
      * @throws IOException if encountered
 606  
      */
 607  
     private String advance(BufferedReader bin) throws IOException {
 608  
         // Get the next non-blank, non-comment line
 609  0
         String trimmed = null;
 610  0
         for (String line = bin.readLine(); line != null; line = bin.readLine()) {
 611  
             // Remove leading and trailing whitespace
 612  0
             trimmed = line.trim();
 613  
 
 614  
             // skip blank and comment lines
 615  0
             if (!isCommentLine(trimmed)) {
 616  0
                 return trimmed;
 617  
             }
 618  
         }
 619  0
         return null;
 620  
     }
 621  
 
 622  
     /**
 623  
      * Determine if the given line is a blank or a comment line.
 624  
      *
 625  
      * @param line The line to check.
 626  
      * @return true if the line is empty or starts with one of the comment
 627  
      *         characters
 628  
      */
 629  
     private boolean isCommentLine(final String line) {
 630  0
         if (line == null) {
 631  0
             return false;
 632  
         }
 633  0
         if (line.length() == 0) {
 634  0
             return true;
 635  
         }
 636  0
         char firstChar = line.charAt(0);
 637  0
         return firstChar == ';' || firstChar == '#';
 638  
     }
 639  
 
 640  
     /**
 641  
      * Is this line a [section]?
 642  
      *
 643  
      * @param line The line to check.
 644  
      * @return true if the line designates a section
 645  
      */
 646  
     private boolean isSectionLine(final String line) {
 647  0
         return line.charAt(0) == '[' && line.charAt(line.length() - 1) == ']';
 648  
     }
 649  
 
 650  
     /**
 651  
      * Does this line of text represent a key/value pair?
 652  
      * 
 653  
      * @param line The line to check.
 654  
      * @return the position of the split position or -1
 655  
      */
 656  
     private int getSplitPos(final String line) {
 657  0
         return line.indexOf('=');
 658  
     }
 659  
 
 660  
     /**
 661  
      * Get continuation lines, if any.
 662  
      */
 663  
     private String more(BufferedReader bin, String value) throws IOException {
 664  0
         boolean moreCowBell = false;
 665  0
         String line = value;
 666  0
         StringBuilder buf = new StringBuilder();
 667  
 
 668  
         do {
 669  0
             moreCowBell = more(line);
 670  0
             if (moreCowBell) {
 671  0
                 line = line.substring(0, line.length() - 1).trim();
 672  
             }
 673  0
             buf.append(line);
 674  0
             if (moreCowBell) {
 675  0
                 buf.append('\n');
 676  0
                 line = advance(bin);
 677  
                 // Is this new line a potential key line?
 678  
                 // It cannot both continue the prior
 679  
                 // and also be a key line.
 680  0
                 int splitPos = getSplitPos(line);
 681  0
                 if (splitPos >= 0) {
 682  0
                     warnings.append("Possible trailing continuation on previous line. Found: ").append(line).append('\n');
 683  
                 }
 684  
             }
 685  0
         } while (moreCowBell && line != null);
 686  0
         String cowBell = buf.toString();
 687  0
         buf = null;
 688  0
         line = null;
 689  0
         return cowBell;
 690  
     }
 691  
 
 692  
     /**
 693  
      * Is there more following this line
 694  
      *
 695  
      * @param line the trimmed string to check
 696  
      * @return whether this line continues
 697  
      */
 698  
     private static boolean more(final String line) {
 699  0
         int length = line.length();
 700  0
         return length > 0 && line.charAt(length - 1) == '\\';
 701  
     }
 702  
 
 703  
     private boolean allowed(String key, String value) {
 704  0
         if (key == null || key.length() == 0 || value == null) {
 705  0
             if (key == null) {
 706  0
                 warnings.append("Null keys not allowed: ").append(" = ").append(value).append('\n');
 707  0
             } else if (key.length() == 0) {
 708  0
                 warnings.append("Empty keys not allowed: ").append(" = ").append(value).append('\n');
 709  
             }
 710  0
             if (value == null) {
 711  0
                 warnings.append("Null values are not allowed: ").append(key).append(" = ").append('\n');
 712  
             }
 713  0
             return false;
 714  
         }
 715  0
         return true;
 716  
     }
 717  
 
 718  
     /**
 719  
      * The name of the section.
 720  
      */
 721  
     private String name;
 722  
 
 723  
     /**
 724  
      * A map of values by key names.
 725  
      */
 726  
     private Map<String, List<String>> section;
 727  
 
 728  
     private File configFile;
 729  
 
 730  
     private String charset;
 731  
 
 732  
     private StringBuilder warnings;
 733  
 
 734  
     private String report;
 735  
 
 736  
     /**
 737  
      * Buffer size is based on file size but keep it with within reasonable limits
 738  
      */
 739  
     private static final int MAX_BUFF_SIZE = 2 * 1024;
 740  
 }