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