001// ***************************************************************************************************************************
002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
003// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
005// * with the License.  You may obtain a copy of the License at                                                              * 
006// *                                                                                                                         *
007// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
008// *                                                                                                                         *
009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
011// * specific language governing permissions and limitations under the License.                                              *
012// ***************************************************************************************************************************
013package org.apache.juneau.config.internal;
014
015import static org.apache.juneau.internal.StringUtils.*;
016import static org.apache.juneau.config.event.ConfigEventType.*;
017
018import java.io.*;
019import java.util.*;
020import java.util.concurrent.*;
021import java.util.concurrent.locks.*;
022
023import org.apache.juneau.*;
024import org.apache.juneau.config.event.*;
025import org.apache.juneau.config.store.*;
026import org.apache.juneau.internal.*;
027
028/**
029 * Represents the parsed contents of a configuration.
030 */
031public class ConfigMap implements ConfigStoreListener {
032
033   private final ConfigStore store;               // The store that created this object.
034   private volatile String contents;        // The original contents of this object.
035   private final String name;               // The name  of this object.
036
037   private final static AsciiSet MOD_CHARS = AsciiSet.create("#$%&*+^@~");
038   
039   // Changes that have been applied since the last load.
040   private final List<ConfigEvent> changes = Collections.synchronizedList(new ArrayList<ConfigEvent>());
041   
042   // Registered listeners listening for changes during saves or reloads.
043   private final Set<ConfigEventListener> listeners = Collections.synchronizedSet(new HashSet<ConfigEventListener>());
044
045   // The parsed entries of this map with all changes applied.
046   final Map<String,ConfigSection> entries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigSection>());
047
048   // The original entries of this map before any changes were applied.
049   final Map<String,ConfigSection> oentries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigSection>());
050   
051   private final ReadWriteLock lock = new ReentrantReadWriteLock();
052   
053   /**
054    * Constructor.
055    * 
056    * @param store The config store.
057    * @param name The config name.
058    * @throws IOException
059    */
060   public ConfigMap(ConfigStore store, String name) throws IOException {
061      this.store = store;
062      this.name = name;
063      load(store.read(name));
064   }
065
066   ConfigMap(String contents) {
067      this.store = null;
068      this.name = null;
069      load(contents);
070   }
071   
072   private ConfigMap load(String contents) {
073      if (contents == null)
074         contents = "";
075      this.contents = contents;
076      
077      entries.clear();
078      oentries.clear();
079      
080      List<String> lines = new LinkedList<>();
081      try (Scanner scanner = new Scanner(contents)) {
082         while (scanner.hasNextLine()) {
083            String line = scanner.nextLine();
084            char c = StringUtils.firstNonWhitespaceChar(line);
085            if (c != 0 || c != '#') {
086               if (c == '[') {
087                  int c2 = StringUtils.lastNonWhitespaceChar(line);
088                  String l = line.trim();
089                  if (c2 != ']' || ! isValidNewSectionName(l.substring(1, l.length()-1)))
090                     throw new ConfigException("Invalid section name found in configuration:  {0}", line) ;
091               }
092            }
093            lines.add(line);
094         }
095      }
096      
097      // Add [blank] section.
098      boolean inserted = false;
099      boolean foundComment = false;
100      for (ListIterator<String> li = lines.listIterator(); li.hasNext();) {
101         String l = li.next();
102         char c = StringUtils.firstNonWhitespaceChar(l);
103         if (c != '#') {
104            if (c == 0 && foundComment) {
105               li.set("[]");
106               inserted = true;
107            }
108            break;
109         } 
110         foundComment = true;
111      }
112      if (! inserted)
113         lines.add(0, "[]");
114      
115      // Collapse any multi-lines.
116      ListIterator<String> li = lines.listIterator(lines.size());
117      String accumulator = null;
118      while (li.hasPrevious()) {
119         String l = li.previous();
120         char c = l.isEmpty() ? 0 : l.charAt(0);
121         if (c == '\t') {
122            c = firstNonWhitespaceChar(l);
123            if (c != '#') {
124               if (accumulator == null)
125                  accumulator = l.substring(1);
126               else
127                  accumulator = l.substring(1) + "\n" + accumulator;
128               li.remove();
129            }
130         } else if (accumulator != null) {
131            li.set(l + "\n" + accumulator);
132            accumulator = null;
133         }
134      }
135      
136      lines = new ArrayList<>(lines);
137      int last = lines.size()-1;
138      int S1 = 1; // Looking for section.
139      int S2 = 2; // Found section, looking for start.
140      int state = S1;
141      
142      List<ConfigSection> sections = new ArrayList<>();
143      
144      for (int i = last; i >= 0; i--) {
145         String l = lines.get(i);
146         char c = StringUtils.firstNonWhitespaceChar(l);
147         
148         if (state == S1) {
149            if (c == '[') {
150               state = S2;
151            }
152         } else {
153            if (c != '#' && (c == '[' || l.indexOf('=') != -1)) {
154               sections.add(new ConfigSection(lines.subList(i+1, last+1)));
155               last = i + 1;// (c == '[' ? i+1 : i);
156               state = (c == '[' ? S2 : S1);
157            }
158         }
159      }
160
161      sections.add(new ConfigSection(lines.subList(0, last+1)));
162      
163      for (int i = sections.size() - 1; i >= 0; i--) {
164         ConfigSection cs = sections.get(i);
165         if (entries.containsKey(cs.name))
166            throw new ConfigException("Duplicate section found in configuration:  [{0}]", cs.name);
167         entries.put(cs.name, cs);
168       }
169
170      oentries.putAll(entries);
171      return this;
172   }
173   
174   
175   //-----------------------------------------------------------------------------------------------------------------
176   // Getters
177   //-----------------------------------------------------------------------------------------------------------------
178   
179   /**
180    * Reads an entry from this map.
181    * 
182    * @param section 
183    *    The section name.
184    *    <br>Must not be <jk>null</jk>.
185    *    <br>Use blank to refer to the default section.
186    * @param key 
187    *    The entry key.
188    *    <br>Must not be <jk>null</jk>.
189    * @return The entry, or <jk>null</jk> if the entry doesn't exist.
190    */
191   public ConfigEntry getEntry(String section, String key) {
192      checkSectionName(section);
193      checkKeyName(key);
194      readLock();
195      try {
196         ConfigSection cs = entries.get(section);
197         return cs == null ? null : cs.entries.get(key);
198      } finally {
199         readUnlock();
200      }
201   }
202
203   /**
204    * Returns the pre-lines on the specified section.
205    * 
206    * <p>
207    * The pre-lines are all lines such as blank lines and comments that preceed a section.
208    * 
209    * @param section 
210    *    The section name.
211    *    <br>Must not be <jk>null</jk>.
212    *    <br>Use blank to refer to the default section.
213    * @return
214    *    An unmodifiable list of lines, or <jk>null</jk> if the section doesn't exist.
215    */
216   public List<String> getPreLines(String section) {
217      checkSectionName(section);
218      readLock();
219      try {
220         ConfigSection cs = entries.get(section);
221         return cs == null ? null : cs.preLines;
222      } finally {
223         readUnlock();
224      }
225   }
226
227   /**
228    * Returns the keys of the entries in the specified section.
229    * 
230    * @return
231    *    An unmodifiable set of keys.
232    */
233   public Set<String> getSections() {
234      return Collections.unmodifiableSet(entries.keySet());
235   }
236
237   /**
238    * Returns the keys of the entries in the specified section.
239    * 
240    * @param section 
241    *    The section name.
242    *    <br>Must not be <jk>null</jk>.
243    *    <br>Use blank to refer to the default section.
244    * @return
245    *    An unmodifiable set of keys, or an empty set if the section doesn't exist.
246    */
247   public Set<String> getKeys(String section) {
248      checkSectionName(section);
249      ConfigSection cs = entries.get(section);
250      return cs == null ? Collections.<String>emptySet() : Collections.unmodifiableSet(cs.entries.keySet());
251   }
252   
253   /**
254    * Returns <jk>true</jk> if this config has the specified section.
255    * 
256    * @param section 
257    *    The section name.
258    *    <br>Must not be <jk>null</jk>.
259    *    <br>Use blank to refer to the default section.
260    * @return <jk>true</jk> if this config has the specified section.
261    */
262   public boolean hasSection(String section) {
263      checkSectionName(section);
264      return entries.get(section) != null;
265   }
266   
267   //-----------------------------------------------------------------------------------------------------------------
268   // Setters
269   //-----------------------------------------------------------------------------------------------------------------
270
271   /**
272    * Adds a new section or replaces the pre-lines on an existing section.
273    * 
274    * @param section 
275    *    The section name.
276    *    <br>Must not be <jk>null</jk>.
277    *    <br>Use blank to refer to the default section.
278    * @param preLines
279    *    The pre-lines on the section.
280    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
281    * @return This object (for method chaining).
282    */
283   public ConfigMap setSection(String section, List<String> preLines) {
284      checkSectionName(section);
285      return applyChange(true, ConfigEvent.setSection(section, preLines));
286   }
287   
288   /**
289    * Adds or overwrites an existing entry.
290    * 
291    * @param section 
292    *    The section name.
293    *    <br>Must not be <jk>null</jk>.
294    *    <br>Use blank to refer to the default section.
295    * @param key 
296    *    The entry key.
297    *    <br>Must not be <jk>null</jk>.
298    * @param value 
299    *    The entry value.
300    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
301    * @param modifiers 
302    *    Optional modifiers.
303    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
304    * @param comment 
305    *    Optional comment.  
306    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
307    * @param preLines 
308    *    Optional pre-lines.
309    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
310    * @return This object (for method chaining).
311    */
312   public ConfigMap setEntry(String section, String key, String value, String modifiers, String comment, List<String> preLines) {
313      checkSectionName(section);
314      checkKeyName(key);
315      if (modifiers != null && ! MOD_CHARS.containsOnly(modifiers))
316         throw new ConfigException("Invalid modifiers: {0}", modifiers);
317      return applyChange(true, ConfigEvent.setEntry(section, key, value, modifiers, comment, preLines));
318   }
319   
320   /**
321    * Removes a section.
322    * 
323    * <p>
324    * This eliminates all entries in the section as well.
325    * 
326    * @param section 
327    *    The section name.
328    *    <br>Must not be <jk>null</jk>.
329    *    <br>Use blank to refer to the default section.
330    * @return This object (for method chaining).
331    */
332   public ConfigMap removeSection(String section) {
333      checkSectionName(section);
334      return applyChange(true, ConfigEvent.removeSection(section));
335   }
336      
337   /**
338    * Removes an entry.
339    * 
340    * @param section 
341    *    The section name.
342    *    <br>Must not be <jk>null</jk>.
343    *    <br>Use blank to refer to the default section.
344    * @param key 
345    *    The entry key.
346    *    <br>Must not be <jk>null</jk>.
347    * @return This object (for method chaining).
348    */
349   public ConfigMap removeEntry(String section, String key) {
350      checkSectionName(section);
351      checkKeyName(key);
352      return applyChange(true, ConfigEvent.removeEntry(section, key));
353   }
354
355   private ConfigMap applyChange(boolean addToChangeList, ConfigEvent ce) {
356      if (ce == null)
357         return this;
358      writeLock();
359      try {
360         String section = ce.getSection();
361         ConfigSection cs = entries.get(section);
362         if (ce.getType() == SET_ENTRY) {
363            if (cs == null) {
364               cs = new ConfigSection(section);
365               entries.put(section, cs);
366            }
367            ConfigEntry oe = cs.entries.get(ce.getKey());
368            if (oe == null)
369               oe = ConfigEntry.NULL;
370            cs.addEntry(
371               ce.getKey(), 
372               ce.getValue() == null ? oe.value : ce.getValue(), 
373               ce.getModifiers() == null ? oe.modifiers : ce.getModifiers(), 
374               ce.getComment() == null ? oe.comment : ce.getComment(), 
375               ce.getPreLines() == null ? oe.preLines : ce.getPreLines()
376            );
377         } else if (ce.getType() == SET_SECTION) {
378            if (cs == null) {
379               cs = new ConfigSection(section);
380               entries.put(section, cs);
381            }
382            if (ce.getPreLines() != null)
383               cs.setPreLines(ce.getPreLines());
384         } else if (ce.getType() == REMOVE_ENTRY) {
385            if (cs != null)
386               cs.entries.remove(ce.getKey());
387         } else if (ce.getType() == REMOVE_SECTION) {
388            if (cs != null)
389               entries.remove(section);
390         }
391         if (addToChangeList)
392            changes.add(ce);
393      } finally {
394         writeUnlock();
395      }
396      return this;   
397   }  
398   
399   /**
400    * Overwrites the contents of the config file.
401    * 
402    * @param contents The new contents of the config file.
403    * @param synchronous Wait until the change has been persisted before returning this map.
404    * @return This object (for method chaining).
405    * @throws IOException
406    * @throws InterruptedException
407    */
408   public ConfigMap load(String contents, boolean synchronous) throws IOException, InterruptedException {
409      
410      if (synchronous) {
411         final CountDownLatch latch = new CountDownLatch(1);
412         ConfigStoreListener l = new ConfigStoreListener() {
413            @Override
414            public void onChange(String contents) {
415               latch.countDown();
416            }
417         };
418         store.register(name, l);
419         store.write(name, null, contents);
420         latch.await(30, TimeUnit.SECONDS);
421         store.unregister(name, l);
422      } else {
423         store.write(name, null, contents);
424      }
425      return this;
426   }
427   
428   //-----------------------------------------------------------------------------------------------------------------
429   // Lifecycle events
430   //-----------------------------------------------------------------------------------------------------------------
431   
432   /**
433    * Persist any changes made to this map and signal all listeners.
434    * 
435    * <p>
436    * If the underlying contents of the file have changed, this will reload it and apply the changes
437    * on top of the modified file.
438    * 
439    * <p>
440    * Subsequent changes made to the underlying file will also be signaled to all listeners.
441    * 
442    * <p>
443    * We try saving the file up to 10 times.
444    * <br>If the file keeps changing on the file system, we throw an exception.
445    * 
446    * @return This object (for method chaining).
447    * @throws IOException
448    */
449   public ConfigMap commit() throws IOException {
450      writeLock();
451      try {
452         String newContents = asString();
453         for (int i = 0; i <= 10; i++) {
454            if (i == 10)
455               throw new ConfigException("Unable to store contents of config to store.");
456            String currentContents = store.write(name, contents, newContents);
457            if (currentContents == null) 
458               break;
459            onChange(currentContents);
460         }
461         this.changes.clear();
462      } finally {
463         writeUnlock();
464      }
465      return this;
466   }
467
468   
469   //-----------------------------------------------------------------------------------------------------------------
470   // Listeners
471   //-----------------------------------------------------------------------------------------------------------------
472   
473   /**
474    * Registers an event listener on this map.
475    * 
476    * @param listener The new listener.
477    * @return This object (for method chaining).
478    */
479   public ConfigMap register(ConfigEventListener listener) {
480      listeners.add(listener);
481      return this;
482   }
483   
484   /**
485    * Unregisters an event listener from this map.
486    * 
487    * @param listener The listener to remove.
488    * @return This object (for method chaining).
489    */
490   public ConfigMap unregister(ConfigEventListener listener) {
491      listeners.remove(listener);
492      return this;
493   }
494   
495   @Override /* ConfigStoreListener */
496   public void onChange(String newContents) {
497      List<ConfigEvent> changes = null;
498      writeLock();
499      try {
500         if (! StringUtils.isEquals(contents, newContents)) {
501            changes = findDiffs(newContents);
502            load(newContents);
503            
504            // Reapply our changes on top of the modifications.
505            for (ConfigEvent ce : this.changes)
506               applyChange(false, ce);
507         }
508      } finally {
509         writeUnlock();
510      }
511      if (changes != null && ! changes.isEmpty())
512         signal(changes);
513   }
514   
515   @Override /* Object */
516   public String toString() {
517      readLock();
518      try {
519         return asString();
520      } finally {
521         readUnlock();
522      }
523   }
524   
525   /**
526    * Returns the values in this config map as a map of maps.
527    * 
528    * <p>
529    * This is considered a snapshot copy of the config map.
530    * 
531    * <p>
532    * The returned map is modifiable, but modifications to the returned map are not reflected in the config map.
533    * 
534    * @return A copy of this config as a map of maps.
535    */
536   public ObjectMap asMap() {
537      ObjectMap m = new ObjectMap();
538      readLock();
539      try {
540         for (ConfigSection cs : entries.values()) {
541            Map<String,String> m2 = new LinkedHashMap<>();
542            for (ConfigEntry ce : cs.entries.values())
543               m2.put(ce.key, ce.value);
544            m.put(cs.name, m2);
545         }
546      } finally {
547         readUnlock();
548      }
549      return m;
550   }
551
552   /**
553    * Serializes this map to the specified writer.
554    * 
555    * @param w The writer to serialize to.
556    * @return The same writer passed in.
557    * @throws IOException
558    */
559   public Writer writeTo(Writer w) throws IOException {
560      readLock();
561      try {
562         for (ConfigSection cs : entries.values())
563            cs.writeTo(w);
564      } finally {
565         readUnlock();
566      }
567      return w;
568   }
569
570   /**
571    * Does a rollback of any changes on this map currently in memory.
572    * 
573    * @return This object (for method chaining).
574    */
575   public ConfigMap rollback() {
576      if (changes.size() > 0) {
577         writeLock();
578         try {
579            changes.clear();
580            load(contents);
581         } finally {
582            writeUnlock();
583         }
584      }
585      return this;
586   }
587
588   
589   //--------------------------------------------------------------------------------
590   // Private methods
591   //--------------------------------------------------------------------------------
592
593   private void readLock() {
594      lock.readLock().lock();
595   }
596
597   private void readUnlock() {
598      lock.readLock().unlock();
599   }
600
601   private void writeLock() {
602      lock.writeLock().lock();
603   }
604
605   private void writeUnlock() {
606      lock.writeLock().unlock();
607   }
608
609   private void checkSectionName(String s) {
610      if (! ("".equals(s) || isValidNewSectionName(s)))
611         throw new IllegalArgumentException("Invalid section name: '" + s + "'");
612   }
613
614   private void checkKeyName(String s) {
615      if (! isValidKeyName(s))
616         throw new IllegalArgumentException("Invalid key name: '" + s + "'");
617   }
618
619   private boolean isValidKeyName(String s) {
620      if (s == null)
621         return false;
622      s = s.trim();
623      if (s.isEmpty())
624         return false;
625      for (int i = 0; i < s.length(); i++) {
626         char c = s.charAt(i);
627         if (c == '/' || c == '\\' || c == '[' || c == ']' || c == '=' || c == '#')
628            return false;
629      }
630      return true;
631   }
632
633   private boolean isValidNewSectionName(String s) {
634      if (s == null)
635         return false;
636      s = s.trim();
637      if (s.isEmpty())
638         return false;
639      for (int i = 0; i < s.length(); i++) {
640         char c = s.charAt(i);
641         if (c == '/' || c == '\\' || c == '[' || c == ']')
642            return false;
643      }
644      return true;
645   }
646
647   private void signal(List<ConfigEvent> changes) {
648      for (ConfigEventListener l : listeners)
649         l.onConfigChange(changes);
650   }
651
652   private List<ConfigEvent> findDiffs(String updatedContents) {
653      List<ConfigEvent> changes = new ArrayList<>();
654      ConfigMap newMap = new ConfigMap(updatedContents);
655      for (ConfigSection ns : newMap.oentries.values()) {
656         ConfigSection s = oentries.get(ns.name);
657         if (s == null) {
658            //changes.add(ConfigEvent.setSection(ns.name, ns.preLines));
659            for (ConfigEntry ne : ns.entries.values()) {
660               changes.add(ConfigEvent.setEntry(ns.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
661            }
662         } else {
663            for (ConfigEntry ne : ns.oentries.values()) {
664               ConfigEntry e = s.oentries.get(ne.key);
665               if (e == null || ! isEquals(e.value, ne.value)) {
666                  changes.add(ConfigEvent.setEntry(s.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
667               }
668            }
669            for (ConfigEntry e : s.oentries.values()) {
670               ConfigEntry ne = ns.oentries.get(e.key);
671               if (ne == null) {
672                  changes.add(ConfigEvent.removeEntry(s.name, e.key));
673               }
674            }
675         }
676      }
677      for (ConfigSection s : oentries.values()) {
678         ConfigSection ns = newMap.oentries.get(s.name);
679         if (ns == null) {
680            //changes.add(ConfigEvent.removeSection(s.name));
681            for (ConfigEntry e : s.oentries.values())
682               changes.add(ConfigEvent.removeEntry(s.name, e.key));
683         }
684      }
685      return changes;
686   }
687
688   // This method should only be called from behind a lock.
689   private String asString() {
690      try {
691         StringWriter sw = new StringWriter();
692         for (ConfigSection cs : entries.values())
693            cs.writeTo(sw);
694         return sw.toString();
695      } catch (IOException e) {
696         throw new RuntimeException(e);  // Not possible.
697      }
698   }
699   
700   
701   //---------------------------------------------------------------------------------------------
702   // ConfigSection
703   //---------------------------------------------------------------------------------------------
704   
705   class ConfigSection {
706
707      final String name;   // The config section name, or blank if the default section.  Never null.
708
709      final List<String> preLines = Collections.synchronizedList(new ArrayList<String>());
710      private final String rawLine;
711      
712      final Map<String,ConfigEntry> oentries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigEntry>());
713      final Map<String,ConfigEntry> entries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigEntry>());
714
715      /**
716       * Constructor.
717       */
718      ConfigSection(String name) {
719         this.name = name;
720         this.rawLine = "[" + name + "]";
721      }
722      
723      /**
724       * Constructor.
725       */
726      ConfigSection(List<String> lines) {
727         
728         String name = null, rawLine = null;
729         
730         int S1 = 1; // Looking for section.
731         int S2 = 2; // Found section, looking for end.
732         int state = S1;
733         int start = 0;
734         
735         for (int i = 0; i < lines.size(); i++) {
736            String l = lines.get(i);
737            char c = StringUtils.firstNonWhitespaceChar(l);
738            if (state == S1) {
739               if (c == '[') {
740                  int i1 = l.indexOf('['), i2 = l.indexOf(']');
741                  name = l.substring(i1+1, i2).trim();
742                  rawLine = l;
743                  state = S2;
744                  start = i+1;
745               } else {
746                  preLines.add(l);
747               }
748            } else {
749               if (c != '#' && l.indexOf('=') != -1) {
750                  ConfigEntry e = new ConfigEntry(l, lines.subList(start, i));
751                  if (entries.containsKey(e.key))
752                     throw new ConfigException("Duplicate entry found in section [{0}] of configuration:  {1}", name, e.key);
753                  entries.put(e.key, e);
754                  start = i+1;
755               } 
756            }
757         }
758         
759         this.name = name;
760         this.rawLine = rawLine;
761         this.oentries.putAll(entries);
762      }
763      
764      ConfigSection addEntry(String key, String value, String modifiers, String comment, List<String> preLines) {
765         ConfigEntry e = new ConfigEntry(key, value, modifiers, comment, preLines);
766         this.entries.put(e.key, e);
767         return this;
768      }
769
770      ConfigSection setPreLines(List<String> preLines) {
771         this.preLines.clear();
772         this.preLines.addAll(preLines);
773         return this;
774      }
775      
776      Writer writeTo(Writer w) throws IOException {
777         for (String s : preLines)
778            w.append(s).append('\n');
779         
780         if (! name.equals(""))
781            w.append(rawLine).append('\n');
782         else {
783            // Need separation between default prelines and first-entry prelines.
784            if (! preLines.isEmpty())
785               w.append('\n');
786         }
787
788         for (ConfigEntry e : entries.values()) 
789            e.writeTo(w);
790         
791         return w;
792      }
793   }
794}