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(x -> x.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 l = new ConfigStoreListener() {
481            @Override
482            public void onChange(String contents) {
483               latch.countDown();
484            }
485         };
486         store.register(name, l);
487         store.write(name, null, contents);
488         latch.await(30, TimeUnit.SECONDS);
489         store.unregister(name, l);
490      } else {
491         store.write(name, null, contents);
492      }
493      return this;
494   }
495
496   //-----------------------------------------------------------------------------------------------------------------
497   // Lifecycle events
498   //-----------------------------------------------------------------------------------------------------------------
499
500   /**
501    * Persist any changes made to this map and signal all listeners.
502    *
503    * <p>
504    * If the underlying contents of the file have changed, this will reload it and apply the changes
505    * on top of the modified file.
506    *
507    * <p>
508    * Subsequent changes made to the underlying file will also be signaled to all listeners.
509    *
510    * <p>
511    * We try saving the file up to 10 times.
512    * <br>If the file keeps changing on the file system, we throw an exception.
513    *
514    * @return This object.
515    * @throws IOException Thrown by underlying stream.
516    */
517   public ConfigMap commit() throws IOException {
518      try (SimpleLock x = lock.write()) {
519         String newContents = asString();
520         for (int i = 0; i <= 10; i++) {
521            if (i == 10)
522               throw new ConfigException("Unable to store contents of config to store.");
523            String currentContents = store.write(name, contents, newContents);
524            if (currentContents == null)
525               break;
526            onChange(currentContents);
527         }
528         this.changes.clear();
529      }
530      return this;
531   }
532
533
534   //-----------------------------------------------------------------------------------------------------------------
535   // Listeners
536   //-----------------------------------------------------------------------------------------------------------------
537
538   /**
539    * Registers an event listener on this map.
540    *
541    * @param listener The new listener.
542    * @return This object.
543    */
544   public ConfigMap register(ConfigEventListener listener) {
545      listeners.add(listener);
546      imports.forEach(x -> x.register(listener));
547      return this;
548   }
549
550   boolean hasEntry(String section, String key) {
551      ConfigSection cs = entries.get(section);
552      ConfigMapEntry ce = cs == null ? null : cs.entries.get(key);
553      return ce != null;
554   }
555
556   /**
557    * Unregisters an event listener from this map.
558    *
559    * @param listener The listener to remove.
560    * @return This object.
561    */
562   public ConfigMap unregister(ConfigEventListener listener) {
563      listeners.remove(listener);
564      imports.forEach(x -> x.register(listener));
565      return this;
566   }
567
568   /**
569    * Returns the listeners currently associated with this config map.
570    *
571    * @return The listeners currently associated with this config map.
572    */
573   public Set<ConfigEventListener> getListeners() {
574      return unmodifiable(listeners);
575   }
576
577   @Override /* ConfigStoreListener */
578   public void onChange(String newContents) {
579      ConfigEvents changes = null;
580      try (SimpleLock x = lock.write()) {
581         if (ne(contents, newContents)) {
582            changes = findDiffs(newContents);
583            load(newContents);
584
585            // Reapply our changes on top of the modifications.
586            this.changes.forEach(y -> applyChange(false, y));
587         }
588      } catch (IOException e) {
589         throw asRuntimeException(e);
590      }
591      if (changes != null && ! changes.isEmpty())
592         signal(changes);
593   }
594
595   @Override /* Object */
596   public String toString() {
597      try (SimpleLock x = lock.read()) {
598         return asString();
599      }
600   }
601
602   /**
603    * Returns the values in this config map as a map of maps.
604    *
605    * <p>
606    * This is considered a snapshot copy of the config map.
607    *
608    * <p>
609    * The returned map is modifiable, but modifications to the returned map are not reflected in the config map.
610    *
611    * @return A copy of this config as a map of maps.
612    */
613   public JsonMap asMap() {
614      JsonMap m = new JsonMap();
615      try (SimpleLock x = lock.read()) {
616         imports.forEach(y -> m.putAll(y.getConfigMap().asMap()));
617         entries.values().forEach(z -> {
618            Map<String,String> m2 = map();
619            z.entries.values().forEach(y -> m2.put(y.key, y.value));
620            m.put(z.name, m2);
621         });
622      }
623      return m;
624   }
625
626   /**
627    * Serializes this map to the specified writer.
628    *
629    * @param w The writer to serialize to.
630    * @return The same writer passed in.
631    * @throws IOException Thrown by underlying stream.
632    */
633   public Writer writeTo(Writer w) throws IOException {
634      try (SimpleLock x = lock.read()) {
635         for (ConfigSection cs : entries.values())
636            cs.writeTo(w);
637      }
638      return w;
639   }
640
641   /**
642    * Does a rollback of any changes on this map currently in memory.
643    *
644    * @return This object.
645    */
646   public ConfigMap rollback() {
647      if (changes.size() > 0) {
648         try (SimpleLock x = lock.write()) {
649            changes.clear();
650            load(contents);
651         } catch (IOException e) {
652            throw asRuntimeException(e);
653         }
654      }
655      return this;
656   }
657
658
659   //-----------------------------------------------------------------------------------------------------------------
660   // Private methods
661   //-----------------------------------------------------------------------------------------------------------------
662
663   private void checkSectionName(String s) {
664      if (! ("".equals(s) || isValidNewSectionName(s)))
665         throw new IllegalArgumentException("Invalid section name: '"+s+"'");
666   }
667
668   private void checkKeyName(String s) {
669      if (! isValidKeyName(s))
670         throw new IllegalArgumentException("Invalid key name: '"+s+"'");
671   }
672
673   private boolean isValidKeyName(String s) {
674      if (s == null)
675         return false;
676      s = s.trim();
677      if (s.isEmpty())
678         return false;
679      for (int i = 0; i < s.length(); i++) {
680         char c = s.charAt(i);
681         if (c == '/' || c == '\\' || c == '[' || c == ']' || c == '=' || c == '#')
682            return false;
683      }
684      return true;
685   }
686
687   private boolean isValidNewSectionName(String s) {
688      if (s == null)
689         return false;
690      s = s.trim();
691      if (s.isEmpty())
692         return false;
693      for (int i = 0; i < s.length(); i++) {
694         char c = s.charAt(i);
695         if (c == '/' || c == '\\' || c == '[' || c == ']')
696            return false;
697      }
698      return true;
699   }
700
701   private boolean isValidConfigName(String s) {
702      if (s == null)
703         return false;
704      s = s.trim();
705      if (s.isEmpty())
706         return false;
707      for (int i = 0; i < s.length(); i++) {
708         char c = s.charAt(i);
709         if (i == 0) {
710            if (! Character.isJavaIdentifierStart(c))
711               return false;
712         } else {
713            if (! Character.isJavaIdentifierPart(c))
714               return false;
715         }
716      }
717      return true;
718   }
719
720   private void signal(ConfigEvents changes) {
721      if (changes.size() > 0)
722         listeners.forEach(x -> x.onConfigChange(changes));
723   }
724
725   private ConfigEvents findDiffs(String updatedContents) throws IOException {
726      ConfigEvents changes = new ConfigEvents();
727      ConfigMap newMap = new ConfigMap(store, name, updatedContents);
728
729      // Imports added.
730      for (Import i : newMap.imports) {
731         if (! imports.contains(i)) {
732            for (ConfigSection s : i.getConfigMap().entries.values()) {
733               for (ConfigMapEntry e : s.oentries.values()) {
734                  if (! newMap.hasEntry(s.name, e.key)) {
735                     changes.add(ConfigEvent.setEntry(name, s.name, e.key, e.value, e.modifiers, e.comment, e.preLines));
736                  }
737               }
738            }
739         }
740      }
741
742      // Imports removed.
743      for (Import i : imports) {
744         if (! newMap.imports.contains(i)) {
745            for (ConfigSection s : i.getConfigMap().entries.values()) {
746               for (ConfigMapEntry e : s.oentries.values()) {
747                  if (! newMap.hasEntry(s.name, e.key)) {
748                     changes.add(ConfigEvent.removeEntry(name, s.name, e.key));
749                  }
750               }
751            }
752         }
753      }
754
755      for (ConfigSection ns : newMap.oentries.values()) {
756         ConfigSection s = oentries.get(ns.name);
757         if (s == null) {
758            //changes.add(ConfigEvent.setSection(ns.name, ns.preLines));
759            for (ConfigMapEntry ne : ns.entries.values()) {
760               changes.add(ConfigEvent.setEntry(name, ns.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
761            }
762         } else {
763            for (ConfigMapEntry ne : ns.oentries.values()) {
764               ConfigMapEntry e = s.oentries.get(ne.key);
765               if (e == null || ne(e.value, ne.value)) {
766                  changes.add(ConfigEvent.setEntry(name, s.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
767               }
768            }
769            for (ConfigMapEntry e : s.oentries.values()) {
770               ConfigMapEntry ne = ns.oentries.get(e.key);
771               if (ne == null) {
772                  changes.add(ConfigEvent.removeEntry(name, s.name, e.key));
773               }
774            }
775         }
776      }
777
778      for (ConfigSection s : oentries.values()) {
779         ConfigSection ns = newMap.oentries.get(s.name);
780         if (ns == null) {
781            //changes.add(ConfigEvent.removeSection(s.name));
782            for (ConfigMapEntry e : s.oentries.values())
783               changes.add(ConfigEvent.removeEntry(name, s.name, e.key));
784         }
785      }
786
787      return changes;
788   }
789
790   // This method should only be called from behind a lock.
791   private String asString() {
792      try {
793         StringWriter sw = new StringWriter();
794         for (ConfigSection cs : entries.values())
795            cs.writeTo(sw);
796         return sw.toString();
797      } catch (IOException e) {
798         throw asRuntimeException(e);  // Not possible.
799      }
800   }
801
802
803   //---------------------------------------------------------------------------------------------
804   // ConfigSection
805   //---------------------------------------------------------------------------------------------
806
807   class ConfigSection {
808
809      final String name;   // The config section name, or blank if the default section.  Never null.
810
811      final List<String> preLines = synced(list());
812      private final String rawLine;
813
814      final Map<String,ConfigMapEntry> oentries = synced(map());
815      final Map<String,ConfigMapEntry> entries = synced(map());
816
817      /**
818       * Constructor.
819       */
820      ConfigSection(String name) {
821         this.name = name;
822         this.rawLine = "[" + name + "]";
823      }
824
825      /**
826       * Constructor.
827       */
828      ConfigSection(List<String> lines) {
829
830         String name = null, rawLine = null;
831
832         int S1 = 1; // Looking for section.
833         int S2 = 2; // Found section, looking for end.
834         int state = S1;
835         int start = 0;
836
837         for (int i = 0; i < lines.size(); i++) {
838            String l = lines.get(i);
839            char c = StringUtils.firstNonWhitespaceChar(l);
840            if (state == S1) {
841               if (c == '[') {
842                  int i1 = l.indexOf('['), i2 = l.indexOf(']');
843                  name = l.substring(i1+1, i2).trim();
844                  rawLine = l;
845                  state = S2;
846                  start = i+1;
847               } else {
848                  preLines.add(l);
849               }
850            } else {
851               if (c != '#' && l.indexOf('=') != -1) {
852                  ConfigMapEntry e = new ConfigMapEntry(l, lines.subList(start, i));
853                  if (entries.containsKey(e.key))
854                     throw new ConfigException("Duplicate entry found in section [{0}] of configuration:  {1}", name, e.key);
855                  entries.put(e.key, e);
856                  start = i+1;
857               }
858            }
859         }
860
861         this.name = name;
862         this.rawLine = rawLine;
863         this.oentries.putAll(entries);
864      }
865
866      ConfigSection addEntry(String key, String value, String modifiers, String comment, List<String> preLines) {
867         ConfigMapEntry e = new ConfigMapEntry(key, value, modifiers, comment, preLines);
868         this.entries.put(e.key, e);
869         return this;
870      }
871
872      ConfigSection setPreLines(List<String> preLines) {
873         this.preLines.clear();
874         this.preLines.addAll(preLines);
875         return this;
876      }
877
878      Writer writeTo(Writer w) throws IOException {
879         for (String s : preLines)
880            w.append(s).append('\n');
881
882         if (! name.equals(""))
883            w.append(rawLine).append('\n');
884         else {
885            // Need separation between default prelines and first-entry prelines.
886            if (! preLines.isEmpty())
887               w.append('\n');
888         }
889
890         for (ConfigMapEntry e : entries.values())
891            e.writeTo(w);
892
893         return w;
894      }
895   }
896
897
898   //---------------------------------------------------------------------------------------------
899   // Import
900   //---------------------------------------------------------------------------------------------
901
902   class Import {
903
904      private final ConfigMap configMap;
905      private final Map<ConfigEventListener,ConfigEventListener> listenerMap = synced(map());
906
907      Import(ConfigMap configMap) {
908         this.configMap = configMap;
909      }
910
911      synchronized Import register(Collection<ConfigEventListener> listeners) {
912         listeners.forEach(x -> register(x));
913         return this;
914      }
915
916      synchronized Import register(final ConfigEventListener listener) {
917         ConfigEventListener l2 = new ConfigEventListener() {
918            @Override
919            public void onConfigChange(ConfigEvents events) {
920               ConfigEvents events2 = new ConfigEvents();
921               events.stream().filter(x -> ! hasEntry(x.getSection(), x.getKey())).forEach(x -> events2.add(x));
922               if (events2.size() > 0)
923                  listener.onConfigChange(events2);
924            }
925         };
926         listenerMap.put(listener, l2);
927         configMap.register(l2);
928         return this;
929      }
930
931      synchronized Import unregister(final ConfigEventListener listener) {
932         configMap.unregister(listenerMap.remove(listener));
933         return this;
934      }
935
936      synchronized Import unregisterAll() {
937         listenerMap.values().forEach(x -> configMap.unregister(x));
938         listenerMap.clear();
939         return this;
940      }
941
942      String getConfigName() {
943         return configMap.name;
944      }
945
946      ConfigMap getConfigMap() {
947         return configMap;
948      }
949
950      @Override
951      public boolean equals(Object o) {
952         if (o instanceof Import) {
953            Import ir = (Import)o;
954            if (ir.getConfigName().equals(getConfigName()))
955               return true;
956         }
957         return false;
958      }
959
960      @Override
961      public int hashCode() {
962         return getConfigName().hashCode();
963      }
964   }
965}