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.CollectionUtils.*;
016import static org.apache.juneau.common.internal.StringUtils.*;
017import static org.apache.juneau.common.internal.ThrowableUtils.*;
018import static org.apache.juneau.config.event.ConfigEventType.*;
019
020import java.io.*;
021import java.util.*;
022import java.util.concurrent.*;
023
024import org.apache.juneau.*;
025import org.apache.juneau.collections.*;
026import org.apache.juneau.common.internal.*;
027import org.apache.juneau.config.event.*;
028import org.apache.juneau.config.store.*;
029import org.apache.juneau.internal.*;
030
031/**
032 * Represents the parsed contents of a configuration.
033 */
034public class ConfigMap implements ConfigStoreListener {
035
036   private final ConfigStore store;         // The store that created this object.
037   private volatile String contents;        // The original contents of this object.
038   final String name;                       // The name  of this object.
039
040   // Changes that have been applied since the last load.
041   private final List<ConfigEvent> changes = synced(new ConfigEvents());
042
043   // Registered listeners listening for changes during saves or reloads.
044   private final Set<ConfigEventListener> listeners = synced(set());
045
046   // The parsed entries of this map with all changes applied.
047   final Map<String,ConfigSection> entries = synced(map());
048
049   // The original entries of this map before any changes were applied.
050   final Map<String,ConfigSection> oentries = synced(map());
051
052   // Import statements in this config.
053   final List<Import> imports = new CopyOnWriteArrayList<>();
054
055   private final SimpleReadWriteLock lock = new SimpleReadWriteLock();
056
057   /**
058    * Constructor.
059    *
060    * @param store The config store.
061    * @param name The config name.
062    * @throws IOException Thrown by underlying stream.
063    */
064   public ConfigMap(ConfigStore store, String name) throws IOException {
065      this.store = store;
066      this.name = name;
067      load(store.read(name));
068   }
069
070   ConfigMap(ConfigStore store, String name, String contents) throws IOException {
071      this.store = store;
072      this.name = name;
073      load(contents);
074   }
075
076   private ConfigMap load(String contents) throws IOException {
077      if (contents == null)
078         contents = "";
079      this.contents = contents;
080
081      entries.clear();
082      oentries.clear();
083      imports.forEach(Import::unregisterAll);
084      imports.clear();
085
086      Map<String,ConfigMap> imports = map();
087
088      List<String> lines = linkedList();
089      try (Scanner scanner = new Scanner(contents)) {
090         while (scanner.hasNextLine()) {
091            String line = scanner.nextLine();
092            char c = firstChar(line);
093            int c2 = StringUtils.lastNonWhitespaceChar(line);
094            if (c == '[') {
095               String l = line.trim();
096               if (c2 != ']' || ! isValidNewSectionName(l.substring(1, l.length()-1)))
097                  throw new ConfigException("Invalid section name found in configuration:  {0}", line);
098            } else if (c == '<') {
099               String l = line.trim();
100               int i = l.indexOf('>');
101               if (i != -1) {
102                  String l2 = l.substring(1, i);
103                  if (! isValidConfigName(l2))
104                     throw new ConfigException("Invalid import config name found in configuration:  {0}", line);
105                  String l3 = l.substring(i+1);
106                  if (! (isEmpty(l3) || firstChar(l3) == '#'))
107                     throw new ConfigException("Invalid import config name found in configuration:  {0}", line);
108                  String importName = l2.trim();
109                  try {
110                     if (! imports.containsKey(importName))
111                        imports.put(importName, store.getMap(importName));
112                  } catch (StackOverflowError e) {
113                     throw new IOException("Import loop detected in configuration '"+name+"'->'"+importName+"'");
114                  }
115               }
116            }
117            lines.add(line);
118         }
119      }
120
121      List<Import> irl = list(imports.size());
122      forEachReverse(listFrom(imports.values()), x -> irl.add(new Import(x).register(listeners)));
123      this.imports.addAll(irl);
124
125      // Add [blank] section.
126      boolean inserted = false;
127      boolean foundComment = false;
128      for (ListIterator<String> li = lines.listIterator(); li.hasNext();) {
129         String l = li.next();
130         char c = firstNonWhitespaceChar(l);
131         if (c != '#') {
132            if (c == 0 && foundComment) {
133               li.set("[]");
134               inserted = true;
135            }
136            break;
137         }
138         foundComment = true;
139      }
140      if (! inserted)
141         lines.add(0, "[]");
142
143      // Collapse any multi-lines.
144      ListIterator<String> li = lines.listIterator(lines.size());
145      String accumulator = null;
146      while (li.hasPrevious()) {
147         String l = li.previous();
148         char c = firstChar(l);
149         if (c == '\t') {
150            c = firstNonWhitespaceChar(l);
151            if (c != '#') {
152               if (accumulator == null)
153                  accumulator = l.substring(1);
154               else
155                  accumulator = l.substring(1) + "\n" + accumulator;
156               li.remove();
157            }
158         } else if (accumulator != null) {
159            li.set(l + "\n" + accumulator);
160            accumulator = null;
161         }
162      }
163
164      lines = copyOf(lines);
165      int last = lines.size()-1;
166      int S1 = 1; // Looking for section.
167      int S2 = 2; // Found section, looking for start.
168      int state = S1;
169
170      List<ConfigSection> sections = list();
171
172      for (int i = last; i >= 0; i--) {
173         String l = lines.get(i);
174         char c = firstChar(l);
175
176         if (state == S1) {
177            if (c == '[') {
178               state = S2;
179            }
180         } else {
181            if (c != '#' && (c == '[' || l.indexOf('=') != -1)) {
182               sections.add(new ConfigSection(lines.subList(i+1, last+1)));
183               last = i + 1;// (c == '[' ? i+1 : i);
184               state = (c == '[' ? S2 : S1);
185            }
186         }
187      }
188
189      sections.add(new ConfigSection(lines.subList(0, last+1)));
190
191      for (int i = sections.size() - 1; i >= 0; i--) {
192         ConfigSection cs = sections.get(i);
193         if (entries.containsKey(cs.name))
194            throw new ConfigException("Duplicate section found in configuration:  [{0}]", cs.name);
195         entries.put(cs.name, cs);
196       }
197
198      oentries.putAll(entries);
199      return this;
200   }
201
202   //-----------------------------------------------------------------------------------------------------------------
203   // Getters
204   //-----------------------------------------------------------------------------------------------------------------
205
206   /**
207    * Reads an entry from this map.
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    * @param key
214    *    The entry key.
215    *    <br>Must not be <jk>null</jk>.
216    * @return The entry, or <jk>null</jk> if the entry doesn't exist.
217    */
218   public ConfigMapEntry getEntry(String section, String key) {
219      checkSectionName(section);
220      checkKeyName(key);
221      try (SimpleLock x = lock.read()) {
222         ConfigSection cs = entries.get(section);
223         ConfigMapEntry ce = cs == null ? null : cs.entries.get(key);
224
225         if (ce == null)
226            ce = imports.stream().map(y -> y.getConfigMap().getEntry(section, key)).filter(y -> y != null).findFirst().orElse(null);
227
228         return ce;
229      }
230   }
231
232   /**
233    * Returns the pre-lines on the specified section.
234    *
235    * <p>
236    * The pre-lines are all lines such as blank lines and comments that preceed a section.
237    *
238    * @param section
239    *    The section name.
240    *    <br>Must not be <jk>null</jk>.
241    *    <br>Use blank to refer to the default section.
242    * @return
243    *    An unmodifiable list of lines, or <jk>null</jk> if the section doesn't exist.
244    */
245   public List<String> getPreLines(String section) {
246      checkSectionName(section);
247      try (SimpleLock x = lock.read()) {
248         ConfigSection cs = entries.get(section);
249         return cs == null ? null : cs.preLines;
250      }
251   }
252
253   /**
254    * Returns the keys of the entries in the specified section.
255    *
256    * @return
257    *    An unmodifiable set of keys.
258    */
259   public Set<String> getSections() {
260      Set<String> s = imports.isEmpty() ? entries.keySet() : set();
261      if (! imports.isEmpty()) {
262         imports.forEach(x -> s.addAll(x.getConfigMap().getSections()));
263         s.addAll(entries.keySet());
264      }
265      return unmodifiable(s);
266   }
267
268   /**
269    * Returns the keys of the entries in the specified section.
270    *
271    * @param section
272    *    The section name.
273    *    <br>Must not be <jk>null</jk>.
274    *    <br>Use blank to refer to the default section.
275    * @return
276    *    An unmodifiable set of keys, or an empty set if the section doesn't exist.
277    */
278   public Set<String> getKeys(String section) {
279      checkSectionName(section);
280      ConfigSection cs = entries.get(section);
281      Set<String> s = imports.isEmpty() && cs != null ? cs.entries.keySet() : set();
282      if (! imports.isEmpty()) {
283         imports.forEach(x -> s.addAll(x.getConfigMap().getKeys(section)));
284         if (cs != null)
285            s.addAll(cs.entries.keySet());
286      }
287      return unmodifiable(s);
288   }
289
290   /**
291    * Returns <jk>true</jk> if this config has the specified section.
292    *
293    * @param section
294    *    The section name.
295    *    <br>Must not be <jk>null</jk>.
296    *    <br>Use blank to refer to the default section.
297    * @return <jk>true</jk> if this config has the specified section.
298    */
299   public boolean hasSection(String section) {
300      checkSectionName(section);
301      return entries.get(section) != null || imports.stream().anyMatch(x -> x.getConfigMap().hasSection(section));
302   }
303
304   //-----------------------------------------------------------------------------------------------------------------
305   // Setters
306   //-----------------------------------------------------------------------------------------------------------------
307
308   /**
309    * Adds a new section or replaces the pre-lines on an existing section.
310    *
311    * @param section
312    *    The section name.
313    *    <br>Must not be <jk>null</jk>.
314    *    <br>Use blank to refer to the default section.
315    * @param preLines
316    *    The pre-lines on the section.
317    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
318    * @return This object.
319    */
320   public ConfigMap setSection(String section, List<String> preLines) {
321      checkSectionName(section);
322      return applyChange(true, ConfigEvent.setSection(name, section, preLines));
323   }
324
325   /**
326    * Adds or overwrites an existing entry.
327    *
328    * @param section
329    *    The section name.
330    *    <br>Must not be <jk>null</jk>.
331    *    <br>Use blank to refer to the default section.
332    * @param key
333    *    The entry key.
334    *    <br>Must not be <jk>null</jk>.
335    * @param value
336    *    The entry value.
337    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
338    * @param modifiers
339    *    Optional modifiers.
340    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
341    * @param comment
342    *    Optional comment.
343    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
344    * @param preLines
345    *    Optional pre-lines.
346    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
347    * @return This object.
348    */
349   public ConfigMap setEntry(String section, String key, String value, String modifiers, String comment, List<String> preLines) {
350      checkSectionName(section);
351      checkKeyName(key);
352      return applyChange(true, ConfigEvent.setEntry(name, section, key, value, modifiers, comment, preLines));
353   }
354
355
356   /**
357    * Not implemented.
358    *
359    * @param section
360    *    The section name where to place the import statement.
361    *    <br>Must not be <jk>null</jk>.
362    *    <br>Use blank for the default section.
363    * @param importName
364    *    The import name.
365    *    <br>Must not be <jk>null</jk>.
366    * @param preLines
367    *    Optional comment and blank lines to add immediately before the import statement.
368    *    <br>If <jk>null</jk>, previous pre-lines will not be replaced.
369    * @return This object.
370    */
371   public ConfigMap setImport(String section, String importName, List<String> preLines) {
372      throw new UnsupportedOperationException("Not implemented.");
373   }
374
375   /**
376    * Removes a section.
377    *
378    * <p>
379    * This eliminates all entries in the section as well.
380    *
381    * @param section
382    *    The section name.
383    *    <br>Must not be <jk>null</jk>.
384    *    <br>Use blank to refer to the default section.
385    * @return This object.
386    */
387   public ConfigMap removeSection(String section) {
388      checkSectionName(section);
389      return applyChange(true, ConfigEvent.removeSection(name, section));
390   }
391
392   /**
393    * Removes an entry.
394    *
395    * @param section
396    *    The section name.
397    *    <br>Must not be <jk>null</jk>.
398    *    <br>Use blank to refer to the default section.
399    * @param key
400    *    The entry key.
401    *    <br>Must not be <jk>null</jk>.
402    * @return This object.
403    */
404   public ConfigMap removeEntry(String section, String key) {
405      checkSectionName(section);
406      checkKeyName(key);
407      return applyChange(true, ConfigEvent.removeEntry(name, section, key));
408   }
409
410   /**
411    * Not implemented.
412    *
413    * @param section
414    *    The section name where to place the import statement.
415    *    <br>Must not be <jk>null</jk>.
416    *    <br>Use blank for the default section.
417    * @param importName
418    *    The import name.
419    *    <br>Must not be <jk>null</jk>.
420    * @return This object.
421    */
422   public ConfigMap removeImport(String section, String importName) {
423      throw new UnsupportedOperationException("Not implemented.");
424   }
425
426   private ConfigMap applyChange(boolean addToChangeList, ConfigEvent ce) {
427      if (ce == null)
428         return this;
429      try (SimpleLock x = lock.write()) {
430         String section = ce.getSection();
431         ConfigSection cs = entries.get(section);
432         if (ce.getType() == SET_ENTRY) {
433            if (cs == null) {
434               cs = new ConfigSection(section);
435               entries.put(section, cs);
436            }
437            ConfigMapEntry oe = cs.entries.get(ce.getKey());
438            if (oe == null)
439               oe = ConfigMapEntry.NULL;
440            cs.addEntry(
441               ce.getKey(),
442               ce.getValue() == null ? oe.value : ce.getValue(),
443               ce.getModifiers() == null ? oe.modifiers : ce.getModifiers(),
444               ce.getComment() == null ? oe.comment : ce.getComment(),
445               ce.getPreLines() == null ? oe.preLines : ce.getPreLines()
446            );
447         } else if (ce.getType() == SET_SECTION) {
448            if (cs == null) {
449               cs = new ConfigSection(section);
450               entries.put(section, cs);
451            }
452            if (ce.getPreLines() != null)
453               cs.setPreLines(ce.getPreLines());
454         } else if (ce.getType() == REMOVE_ENTRY) {
455            if (cs != null)
456               cs.entries.remove(ce.getKey());
457         } else if (ce.getType() == REMOVE_SECTION) {
458            if (cs != null)
459               entries.remove(section);
460         }
461         if (addToChangeList)
462            changes.add(ce);
463      }
464      return this;
465   }
466
467   /**
468    * Overwrites the contents of the config file.
469    *
470    * @param contents The new contents of the config file.
471    * @param synchronous Wait until the change has been persisted before returning this map.
472    * @return This object.
473    * @throws IOException Thrown by underlying stream.
474    * @throws InterruptedException Thread was interrupted.
475    */
476   public ConfigMap load(String contents, boolean synchronous) throws IOException, InterruptedException {
477
478      if (synchronous) {
479         final CountDownLatch latch = new CountDownLatch(1);
480         ConfigStoreListener listener = contents1 -> latch.countDown();
481         store.register(name, listener);
482         store.write(name, null, contents);
483         latch.await(30, TimeUnit.SECONDS);
484         store.unregister(name, listener);
485      } else {
486         store.write(name, null, contents);
487      }
488      return this;
489   }
490
491   //-----------------------------------------------------------------------------------------------------------------
492   // Lifecycle events
493   //-----------------------------------------------------------------------------------------------------------------
494
495   /**
496    * Persist any changes made to this map and signal all listeners.
497    *
498    * <p>
499    * If the underlying contents of the file have changed, this will reload it and apply the changes
500    * on top of the modified file.
501    *
502    * <p>
503    * Subsequent changes made to the underlying file will also be signaled to all listeners.
504    *
505    * <p>
506    * We try saving the file up to 10 times.
507    * <br>If the file keeps changing on the file system, we throw an exception.
508    *
509    * @return This object.
510    * @throws IOException Thrown by underlying stream.
511    */
512   public ConfigMap commit() throws IOException {
513      try (SimpleLock x = lock.write()) {
514         String newContents = asString();
515         for (int i = 0; i <= 10; i++) {
516            if (i == 10)
517               throw new ConfigException("Unable to store contents of config to store.");
518            String currentContents = store.write(name, contents, newContents);
519            if (currentContents == null)
520               break;
521            onChange(currentContents);
522         }
523         this.changes.clear();
524      }
525      return this;
526   }
527
528
529   //-----------------------------------------------------------------------------------------------------------------
530   // Listeners
531   //-----------------------------------------------------------------------------------------------------------------
532
533   /**
534    * Registers an event listener on this map.
535    *
536    * @param listener The new listener.
537    * @return This object.
538    */
539   public ConfigMap register(ConfigEventListener listener) {
540      listeners.add(listener);
541      imports.forEach(x -> x.register(listener));
542      return this;
543   }
544
545   boolean hasEntry(String section, String key) {
546      ConfigSection cs = entries.get(section);
547      ConfigMapEntry ce = cs == null ? null : cs.entries.get(key);
548      return ce != null;
549   }
550
551   /**
552    * Unregisters an event listener from this map.
553    *
554    * @param listener The listener to remove.
555    * @return This object.
556    */
557   public ConfigMap unregister(ConfigEventListener listener) {
558      listeners.remove(listener);
559      imports.forEach(x -> x.register(listener));
560      return this;
561   }
562
563   /**
564    * Returns the listeners currently associated with this config map.
565    *
566    * @return The listeners currently associated with this config map.
567    */
568   public Set<ConfigEventListener> getListeners() {
569      return unmodifiable(listeners);
570   }
571
572   @Override /* ConfigStoreListener */
573   public void onChange(String newContents) {
574      ConfigEvents changes = null;
575      try (SimpleLock x = lock.write()) {
576         if (ne(contents, newContents)) {
577            changes = findDiffs(newContents);
578            load(newContents);
579
580            // Reapply our changes on top of the modifications.
581            this.changes.forEach(y -> applyChange(false, y));
582         }
583      } catch (IOException e) {
584         throw asRuntimeException(e);
585      }
586      if (changes != null && ! changes.isEmpty())
587         signal(changes);
588   }
589
590   @Override /* Object */
591   public String toString() {
592      try (SimpleLock x = lock.read()) {
593         return asString();
594      }
595   }
596
597   /**
598    * Returns the values in this config map as a map of maps.
599    *
600    * <p>
601    * This is considered a snapshot copy of the config map.
602    *
603    * <p>
604    * The returned map is modifiable, but modifications to the returned map are not reflected in the config map.
605    *
606    * @return A copy of this config as a map of maps.
607    */
608   public JsonMap asMap() {
609      JsonMap m = new JsonMap();
610      try (SimpleLock x = lock.read()) {
611         imports.forEach(y -> m.putAll(y.getConfigMap().asMap()));
612         entries.values().forEach(z -> {
613            Map<String,String> m2 = map();
614            z.entries.values().forEach(y -> m2.put(y.key, y.value));
615            m.put(z.name, m2);
616         });
617      }
618      return m;
619   }
620
621   /**
622    * Serializes this map to the specified writer.
623    *
624    * @param w The writer to serialize to.
625    * @return The same writer passed in.
626    * @throws IOException Thrown by underlying stream.
627    */
628   public Writer writeTo(Writer w) throws IOException {
629      try (SimpleLock x = lock.read()) {
630         for (ConfigSection cs : entries.values())
631            cs.writeTo(w);
632      }
633      return w;
634   }
635
636   /**
637    * Does a rollback of any changes on this map currently in memory.
638    *
639    * @return This object.
640    */
641   public ConfigMap rollback() {
642      if (changes.size() > 0) {
643         try (SimpleLock x = lock.write()) {
644            changes.clear();
645            load(contents);
646         } catch (IOException e) {
647            throw asRuntimeException(e);
648         }
649      }
650      return this;
651   }
652
653
654   //-----------------------------------------------------------------------------------------------------------------
655   // Private methods
656   //-----------------------------------------------------------------------------------------------------------------
657
658   private void checkSectionName(String s) {
659      if (! ("".equals(s) || isValidNewSectionName(s)))
660         throw new IllegalArgumentException("Invalid section name: '"+s+"'");
661   }
662
663   private void checkKeyName(String s) {
664      if (! isValidKeyName(s))
665         throw new IllegalArgumentException("Invalid key name: '"+s+"'");
666   }
667
668   private boolean isValidKeyName(String s) {
669      if (s == null)
670         return false;
671      s = s.trim();
672      if (s.isEmpty())
673         return false;
674      for (int i = 0; i < s.length(); i++) {
675         char c = s.charAt(i);
676         if (c == '/' || c == '\\' || c == '[' || c == ']' || c == '=' || c == '#')
677            return false;
678      }
679      return true;
680   }
681
682   private boolean isValidNewSectionName(String s) {
683      if (s == null)
684         return false;
685      s = s.trim();
686      if (s.isEmpty())
687         return false;
688      for (int i = 0; i < s.length(); i++) {
689         char c = s.charAt(i);
690         if (c == '/' || c == '\\' || c == '[' || c == ']')
691            return false;
692      }
693      return true;
694   }
695
696   private boolean isValidConfigName(String s) {
697      if (s == null)
698         return false;
699      s = s.trim();
700      if (s.isEmpty())
701         return false;
702      for (int i = 0; i < s.length(); i++) {
703         char c = s.charAt(i);
704         if (i == 0) {
705            if (! Character.isJavaIdentifierStart(c))
706               return false;
707         } else {
708            if (! Character.isJavaIdentifierPart(c))
709               return false;
710         }
711      }
712      return true;
713   }
714
715   private void signal(ConfigEvents changes) {
716      if (changes.size() > 0)
717         listeners.forEach(x -> x.onConfigChange(changes));
718   }
719
720   private ConfigEvents findDiffs(String updatedContents) throws IOException {
721      ConfigEvents changes = new ConfigEvents();
722      ConfigMap newMap = new ConfigMap(store, name, updatedContents);
723
724      // Imports added.
725      for (Import i : newMap.imports) {
726         if (! imports.contains(i)) {
727            for (ConfigSection s : i.getConfigMap().entries.values()) {
728               for (ConfigMapEntry e : s.oentries.values()) {
729                  if (! newMap.hasEntry(s.name, e.key)) {
730                     changes.add(ConfigEvent.setEntry(name, s.name, e.key, e.value, e.modifiers, e.comment, e.preLines));
731                  }
732               }
733            }
734         }
735      }
736
737      // Imports removed.
738      for (Import i : imports) {
739         if (! newMap.imports.contains(i)) {
740            for (ConfigSection s : i.getConfigMap().entries.values()) {
741               for (ConfigMapEntry e : s.oentries.values()) {
742                  if (! newMap.hasEntry(s.name, e.key)) {
743                     changes.add(ConfigEvent.removeEntry(name, s.name, e.key));
744                  }
745               }
746            }
747         }
748      }
749
750      for (ConfigSection ns : newMap.oentries.values()) {
751         ConfigSection s = oentries.get(ns.name);
752         if (s == null) {
753            //changes.add(ConfigEvent.setSection(ns.name, ns.preLines));
754            for (ConfigMapEntry ne : ns.entries.values()) {
755               changes.add(ConfigEvent.setEntry(name, ns.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
756            }
757         } else {
758            for (ConfigMapEntry ne : ns.oentries.values()) {
759               ConfigMapEntry e = s.oentries.get(ne.key);
760               if (e == null || ne(e.value, ne.value)) {
761                  changes.add(ConfigEvent.setEntry(name, s.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
762               }
763            }
764            for (ConfigMapEntry e : s.oentries.values()) {
765               ConfigMapEntry ne = ns.oentries.get(e.key);
766               if (ne == null) {
767                  changes.add(ConfigEvent.removeEntry(name, s.name, e.key));
768               }
769            }
770         }
771      }
772
773      for (ConfigSection s : oentries.values()) {
774         ConfigSection ns = newMap.oentries.get(s.name);
775         if (ns == null) {
776            //changes.add(ConfigEvent.removeSection(s.name));
777            for (ConfigMapEntry e : s.oentries.values())
778               changes.add(ConfigEvent.removeEntry(name, s.name, e.key));
779         }
780      }
781
782      return changes;
783   }
784
785   // This method should only be called from behind a lock.
786   private String asString() {
787      try {
788         StringWriter sw = new StringWriter();
789         for (ConfigSection cs : entries.values())
790            cs.writeTo(sw);
791         return sw.toString();
792      } catch (IOException e) {
793         throw asRuntimeException(e);  // Not possible.
794      }
795   }
796
797
798   //---------------------------------------------------------------------------------------------
799   // ConfigSection
800   //---------------------------------------------------------------------------------------------
801
802   class ConfigSection {
803
804      final String name;   // The config section name, or blank if the default section.  Never null.
805
806      final List<String> preLines = synced(list());
807      private final String rawLine;
808
809      final Map<String,ConfigMapEntry> oentries = synced(map());
810      final Map<String,ConfigMapEntry> entries = synced(map());
811
812      /**
813       * Constructor.
814       */
815      ConfigSection(String name) {
816         this.name = name;
817         this.rawLine = "[" + name + "]";
818      }
819
820      /**
821       * Constructor.
822       */
823      ConfigSection(List<String> lines) {
824
825         String name = null, rawLine = null;
826
827         int S1 = 1; // Looking for section.
828         int S2 = 2; // Found section, looking for end.
829         int state = S1;
830         int start = 0;
831
832         for (int i = 0; i < lines.size(); i++) {
833            String l = lines.get(i);
834            char c = StringUtils.firstNonWhitespaceChar(l);
835            if (state == S1) {
836               if (c == '[') {
837                  int i1 = l.indexOf('['), i2 = l.indexOf(']');
838                  name = l.substring(i1+1, i2).trim();
839                  rawLine = l;
840                  state = S2;
841                  start = i+1;
842               } else {
843                  preLines.add(l);
844               }
845            } else {
846               if (c != '#' && l.indexOf('=') != -1) {
847                  ConfigMapEntry e = new ConfigMapEntry(l, lines.subList(start, i));
848                  if (entries.containsKey(e.key))
849                     throw new ConfigException("Duplicate entry found in section [{0}] of configuration:  {1}", name, e.key);
850                  entries.put(e.key, e);
851                  start = i+1;
852               }
853            }
854         }
855
856         this.name = name;
857         this.rawLine = rawLine;
858         this.oentries.putAll(entries);
859      }
860
861      ConfigSection addEntry(String key, String value, String modifiers, String comment, List<String> preLines) {
862         ConfigMapEntry e = new ConfigMapEntry(key, value, modifiers, comment, preLines);
863         this.entries.put(e.key, e);
864         return this;
865      }
866
867      ConfigSection setPreLines(List<String> preLines) {
868         this.preLines.clear();
869         this.preLines.addAll(preLines);
870         return this;
871      }
872
873      Writer writeTo(Writer w) throws IOException {
874         for (String s : preLines)
875            w.append(s).append('\n');
876
877         if (! name.equals(""))
878            w.append(rawLine).append('\n');
879         else {
880            // Need separation between default prelines and first-entry prelines.
881            if (! preLines.isEmpty())
882               w.append('\n');
883         }
884
885         for (ConfigMapEntry e : entries.values())
886            e.writeTo(w);
887
888         return w;
889      }
890   }
891
892
893   //---------------------------------------------------------------------------------------------
894   // Import
895   //---------------------------------------------------------------------------------------------
896
897   class Import {
898
899      private final ConfigMap configMap;
900      private final Map<ConfigEventListener,ConfigEventListener> listenerMap = synced(map());
901
902      Import(ConfigMap configMap) {
903         this.configMap = configMap;
904      }
905
906      synchronized Import register(Collection<ConfigEventListener> listeners) {
907         listeners.forEach(this::register);
908         return this;
909      }
910
911      synchronized Import register(final ConfigEventListener listener) {
912         ConfigEventListener l2 = events -> {
913               ConfigEvents events2 = new ConfigEvents();
914               events.stream().filter(x -> ! hasEntry(x.getSection(), x.getKey())).forEach(x -> events2.add(x));
915               if (events2.size() > 0)
916                  listener.onConfigChange(events2);
917            };
918         listenerMap.put(listener, l2);
919         configMap.register(l2);
920         return this;
921      }
922
923      synchronized Import unregister(final ConfigEventListener listener) {
924         configMap.unregister(listenerMap.remove(listener));
925         return this;
926      }
927
928      synchronized Import unregisterAll() {
929         listenerMap.values().forEach(x -> configMap.unregister(x));
930         listenerMap.clear();
931         return this;
932      }
933
934      String getConfigName() {
935         return configMap.name;
936      }
937
938      ConfigMap getConfigMap() {
939         return configMap;
940      }
941
942      @Override
943      public boolean equals(Object o) {
944         if (o instanceof Import) {
945            Import ir = (Import)o;
946            if (ir.getConfigName().equals(getConfigName()))
947               return true;
948         }
949         return false;
950      }
951
952      @Override
953      public int hashCode() {
954         return getConfigName().hashCode();
955      }
956   }
957}