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