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