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