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   private 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 ArrayList<ConfigEvent>());
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   private final ReadWriteLock lock = new ReentrantReadWriteLock();
052
053   /**
054    * Constructor.
055    *
056    * @param store The config store.
057    * @param name The config name.
058    * @throws IOException
059    */
060   public ConfigMap(ConfigStore store, String name) throws IOException {
061      this.store = store;
062      this.name = name;
063      load(store.read(name));
064   }
065
066   ConfigMap(String contents) {
067      this.store = null;
068      this.name = null;
069      load(contents);
070   }
071
072   private ConfigMap load(String contents) {
073      if (contents == null)
074         contents = "";
075      this.contents = contents;
076
077      entries.clear();
078      oentries.clear();
079
080      List<String> lines = new LinkedList<>();
081      try (Scanner scanner = new Scanner(contents)) {
082         while (scanner.hasNextLine()) {
083            String line = scanner.nextLine();
084            char c = firstChar(line);
085            if (c == '[') {
086               int c2 = StringUtils.lastNonWhitespaceChar(line);
087               String l = line.trim();
088               if (c2 != ']' || ! isValidNewSectionName(l.substring(1, l.length()-1)))
089                  throw new ConfigException("Invalid section name found in configuration:  {0}", line) ;
090            }
091            lines.add(line);
092         }
093      }
094
095      // Add [blank] section.
096      boolean inserted = false;
097      boolean foundComment = false;
098      for (ListIterator<String> li = lines.listIterator(); li.hasNext();) {
099         String l = li.next();
100         char c = firstNonWhitespaceChar(l);
101         if (c != '#') {
102            if (c == 0 && foundComment) {
103               li.set("[]");
104               inserted = true;
105            }
106            break;
107         }
108         foundComment = true;
109      }
110      if (! inserted)
111         lines.add(0, "[]");
112
113      // Collapse any multi-lines.
114      ListIterator<String> li = lines.listIterator(lines.size());
115      String accumulator = null;
116      while (li.hasPrevious()) {
117         String l = li.previous();
118         char c = firstChar(l);
119         if (c == '\t') {
120            c = firstNonWhitespaceChar(l);
121            if (c != '#') {
122               if (accumulator == null)
123                  accumulator = l.substring(1);
124               else
125                  accumulator = l.substring(1) + "\n" + accumulator;
126               li.remove();
127            }
128         } else if (accumulator != null) {
129            li.set(l + "\n" + accumulator);
130            accumulator = null;
131         }
132      }
133
134      lines = new ArrayList<>(lines);
135      int last = lines.size()-1;
136      int S1 = 1; // Looking for section.
137      int S2 = 2; // Found section, looking for start.
138      int state = S1;
139
140      List<ConfigSection> sections = new ArrayList<>();
141
142      for (int i = last; i >= 0; i--) {
143         String l = lines.get(i);
144         char c = firstChar(l);
145
146         if (state == S1) {
147            if (c == '[') {
148               state = S2;
149            }
150         } else {
151            if (c != '#' && (c == '[' || l.indexOf('=') != -1)) {
152               sections.add(new ConfigSection(lines.subList(i+1, last+1)));
153               last = i + 1;// (c == '[' ? i+1 : i);
154               state = (c == '[' ? S2 : S1);
155            }
156         }
157      }
158
159      sections.add(new ConfigSection(lines.subList(0, last+1)));
160
161      for (int i = sections.size() - 1; i >= 0; i--) {
162         ConfigSection cs = sections.get(i);
163         if (entries.containsKey(cs.name))
164            throw new ConfigException("Duplicate section found in configuration:  [{0}]", cs.name);
165         entries.put(cs.name, cs);
166       }
167
168      oentries.putAll(entries);
169      return this;
170   }
171
172
173   //-----------------------------------------------------------------------------------------------------------------
174   // Getters
175   //-----------------------------------------------------------------------------------------------------------------
176
177   /**
178    * Reads an entry from this map.
179    *
180    * @param section
181    *    The section name.
182    *    <br>Must not be <jk>null</jk>.
183    *    <br>Use blank to refer to the default section.
184    * @param key
185    *    The entry key.
186    *    <br>Must not be <jk>null</jk>.
187    * @return The entry, or <jk>null</jk> if the entry doesn't exist.
188    */
189   public ConfigEntry getEntry(String section, String key) {
190      checkSectionName(section);
191      checkKeyName(key);
192      readLock();
193      try {
194         ConfigSection cs = entries.get(section);
195         return cs == null ? null : cs.entries.get(key);
196      } finally {
197         readUnlock();
198      }
199   }
200
201   /**
202    * Returns the pre-lines on the specified section.
203    *
204    * <p>
205    * The pre-lines are all lines such as blank lines and comments that preceed a section.
206    *
207    * @param section
208    *    The section name.
209    *    <br>Must not be <jk>null</jk>.
210    *    <br>Use blank to refer to the default section.
211    * @return
212    *    An unmodifiable list of lines, or <jk>null</jk> if the section doesn't exist.
213    */
214   public List<String> getPreLines(String section) {
215      checkSectionName(section);
216      readLock();
217      try {
218         ConfigSection cs = entries.get(section);
219         return cs == null ? null : cs.preLines;
220      } finally {
221         readUnlock();
222      }
223   }
224
225   /**
226    * Returns the keys of the entries in the specified section.
227    *
228    * @return
229    *    An unmodifiable set of keys.
230    */
231   public Set<String> getSections() {
232      return Collections.unmodifiableSet(entries.keySet());
233   }
234
235   /**
236    * Returns the keys of the entries in the specified section.
237    *
238    * @param section
239    *    The section name.
240    *    <br>Must not be <jk>null</jk>.
241    *    <br>Use blank to refer to the default section.
242    * @return
243    *    An unmodifiable set of keys, or an empty set if the section doesn't exist.
244    */
245   public Set<String> getKeys(String section) {
246      checkSectionName(section);
247      ConfigSection cs = entries.get(section);
248      return cs == null ? Collections.<String>emptySet() : Collections.unmodifiableSet(cs.entries.keySet());
249   }
250
251   /**
252    * Returns <jk>true</jk> if this config has the specified section.
253    *
254    * @param section
255    *    The section name.
256    *    <br>Must not be <jk>null</jk>.
257    *    <br>Use blank to refer to the default section.
258    * @return <jk>true</jk> if this config has the specified section.
259    */
260   public boolean hasSection(String section) {
261      checkSectionName(section);
262      return entries.get(section) != null;
263   }
264
265   //-----------------------------------------------------------------------------------------------------------------
266   // Setters
267   //-----------------------------------------------------------------------------------------------------------------
268
269   /**
270    * Adds a new section or replaces the pre-lines on an existing section.
271    *
272    * @param section
273    *    The section name.
274    *    <br>Must not be <jk>null</jk>.
275    *    <br>Use blank to refer to the default section.
276    * @param preLines
277    *    The pre-lines on the section.
278    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
279    * @return This object (for method chaining).
280    */
281   public ConfigMap setSection(String section, List<String> preLines) {
282      checkSectionName(section);
283      return applyChange(true, ConfigEvent.setSection(section, preLines));
284   }
285
286   /**
287    * Adds or overwrites an existing entry.
288    *
289    * @param section
290    *    The section name.
291    *    <br>Must not be <jk>null</jk>.
292    *    <br>Use blank to refer to the default section.
293    * @param key
294    *    The entry key.
295    *    <br>Must not be <jk>null</jk>.
296    * @param value
297    *    The entry value.
298    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
299    * @param modifiers
300    *    Optional modifiers.
301    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
302    * @param comment
303    *    Optional comment.
304    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
305    * @param preLines
306    *    Optional pre-lines.
307    *    <br>If <jk>null</jk>, the previous value will not be overwritten.
308    * @return This object (for method chaining).
309    */
310   public ConfigMap setEntry(String section, String key, String value, String modifiers, String comment, List<String> preLines) {
311      checkSectionName(section);
312      checkKeyName(key);
313      if (modifiers != null && ! MOD_CHARS.containsOnly(modifiers))
314         throw new ConfigException("Invalid modifiers: {0}", modifiers);
315      return applyChange(true, ConfigEvent.setEntry(section, key, value, modifiers, comment, preLines));
316   }
317
318   /**
319    * Removes a section.
320    *
321    * <p>
322    * This eliminates all entries in the section as well.
323    *
324    * @param section
325    *    The section name.
326    *    <br>Must not be <jk>null</jk>.
327    *    <br>Use blank to refer to the default section.
328    * @return This object (for method chaining).
329    */
330   public ConfigMap removeSection(String section) {
331      checkSectionName(section);
332      return applyChange(true, ConfigEvent.removeSection(section));
333   }
334
335   /**
336    * Removes an entry.
337    *
338    * @param section
339    *    The section name.
340    *    <br>Must not be <jk>null</jk>.
341    *    <br>Use blank to refer to the default section.
342    * @param key
343    *    The entry key.
344    *    <br>Must not be <jk>null</jk>.
345    * @return This object (for method chaining).
346    */
347   public ConfigMap removeEntry(String section, String key) {
348      checkSectionName(section);
349      checkKeyName(key);
350      return applyChange(true, ConfigEvent.removeEntry(section, key));
351   }
352
353   private ConfigMap applyChange(boolean addToChangeList, ConfigEvent ce) {
354      if (ce == null)
355         return this;
356      writeLock();
357      try {
358         String section = ce.getSection();
359         ConfigSection cs = entries.get(section);
360         if (ce.getType() == SET_ENTRY) {
361            if (cs == null) {
362               cs = new ConfigSection(section);
363               entries.put(section, cs);
364            }
365            ConfigEntry oe = cs.entries.get(ce.getKey());
366            if (oe == null)
367               oe = ConfigEntry.NULL;
368            cs.addEntry(
369               ce.getKey(),
370               ce.getValue() == null ? oe.value : ce.getValue(),
371               ce.getModifiers() == null ? oe.modifiers : ce.getModifiers(),
372               ce.getComment() == null ? oe.comment : ce.getComment(),
373               ce.getPreLines() == null ? oe.preLines : ce.getPreLines()
374            );
375         } else if (ce.getType() == SET_SECTION) {
376            if (cs == null) {
377               cs = new ConfigSection(section);
378               entries.put(section, cs);
379            }
380            if (ce.getPreLines() != null)
381               cs.setPreLines(ce.getPreLines());
382         } else if (ce.getType() == REMOVE_ENTRY) {
383            if (cs != null)
384               cs.entries.remove(ce.getKey());
385         } else if (ce.getType() == REMOVE_SECTION) {
386            if (cs != null)
387               entries.remove(section);
388         }
389         if (addToChangeList)
390            changes.add(ce);
391      } finally {
392         writeUnlock();
393      }
394      return this;
395   }
396
397   /**
398    * Overwrites the contents of the config file.
399    *
400    * @param contents The new contents of the config file.
401    * @param synchronous Wait until the change has been persisted before returning this map.
402    * @return This object (for method chaining).
403    * @throws IOException
404    * @throws InterruptedException
405    */
406   public ConfigMap load(String contents, boolean synchronous) throws IOException, InterruptedException {
407
408      if (synchronous) {
409         final CountDownLatch latch = new CountDownLatch(1);
410         ConfigStoreListener l = new ConfigStoreListener() {
411            @Override
412            public void onChange(String contents) {
413               latch.countDown();
414            }
415         };
416         store.register(name, l);
417         store.write(name, null, contents);
418         latch.await(30, TimeUnit.SECONDS);
419         store.unregister(name, l);
420      } else {
421         store.write(name, null, contents);
422      }
423      return this;
424   }
425
426   //-----------------------------------------------------------------------------------------------------------------
427   // Lifecycle events
428   //-----------------------------------------------------------------------------------------------------------------
429
430   /**
431    * Persist any changes made to this map and signal all listeners.
432    *
433    * <p>
434    * If the underlying contents of the file have changed, this will reload it and apply the changes
435    * on top of the modified file.
436    *
437    * <p>
438    * Subsequent changes made to the underlying file will also be signaled to all listeners.
439    *
440    * <p>
441    * We try saving the file up to 10 times.
442    * <br>If the file keeps changing on the file system, we throw an exception.
443    *
444    * @return This object (for method chaining).
445    * @throws IOException
446    */
447   public ConfigMap commit() throws IOException {
448      writeLock();
449      try {
450         String newContents = asString();
451         for (int i = 0; i <= 10; i++) {
452            if (i == 10)
453               throw new ConfigException("Unable to store contents of config to store.");
454            String currentContents = store.write(name, contents, newContents);
455            if (currentContents == null)
456               break;
457            onChange(currentContents);
458         }
459         this.changes.clear();
460      } finally {
461         writeUnlock();
462      }
463      return this;
464   }
465
466
467   //-----------------------------------------------------------------------------------------------------------------
468   // Listeners
469   //-----------------------------------------------------------------------------------------------------------------
470
471   /**
472    * Registers an event listener on this map.
473    *
474    * @param listener The new listener.
475    * @return This object (for method chaining).
476    */
477   public ConfigMap register(ConfigEventListener listener) {
478      listeners.add(listener);
479      return this;
480   }
481
482   /**
483    * Unregisters an event listener from this map.
484    *
485    * @param listener The listener to remove.
486    * @return This object (for method chaining).
487    */
488   public ConfigMap unregister(ConfigEventListener listener) {
489      listeners.remove(listener);
490      return this;
491   }
492
493   @Override /* ConfigStoreListener */
494   public void onChange(String newContents) {
495      List<ConfigEvent> changes = null;
496      writeLock();
497      try {
498         if (! StringUtils.isEquals(contents, newContents)) {
499            changes = findDiffs(newContents);
500            load(newContents);
501
502            // Reapply our changes on top of the modifications.
503            for (ConfigEvent ce : this.changes)
504               applyChange(false, ce);
505         }
506      } finally {
507         writeUnlock();
508      }
509      if (changes != null && ! changes.isEmpty())
510         signal(changes);
511   }
512
513   @Override /* Object */
514   public String toString() {
515      readLock();
516      try {
517         return asString();
518      } finally {
519         readUnlock();
520      }
521   }
522
523   /**
524    * Returns the values in this config map as a map of maps.
525    *
526    * <p>
527    * This is considered a snapshot copy of the config map.
528    *
529    * <p>
530    * The returned map is modifiable, but modifications to the returned map are not reflected in the config map.
531    *
532    * @return A copy of this config as a map of maps.
533    */
534   public ObjectMap asMap() {
535      ObjectMap m = new ObjectMap();
536      readLock();
537      try {
538         for (ConfigSection cs : entries.values()) {
539            Map<String,String> m2 = new LinkedHashMap<>();
540            for (ConfigEntry ce : cs.entries.values())
541               m2.put(ce.key, ce.value);
542            m.put(cs.name, m2);
543         }
544      } finally {
545         readUnlock();
546      }
547      return m;
548   }
549
550   /**
551    * Serializes this map to the specified writer.
552    *
553    * @param w The writer to serialize to.
554    * @return The same writer passed in.
555    * @throws IOException
556    */
557   public Writer writeTo(Writer w) throws IOException {
558      readLock();
559      try {
560         for (ConfigSection cs : entries.values())
561            cs.writeTo(w);
562      } finally {
563         readUnlock();
564      }
565      return w;
566   }
567
568   /**
569    * Does a rollback of any changes on this map currently in memory.
570    *
571    * @return This object (for method chaining).
572    */
573   public ConfigMap rollback() {
574      if (changes.size() > 0) {
575         writeLock();
576         try {
577            changes.clear();
578            load(contents);
579         } finally {
580            writeUnlock();
581         }
582      }
583      return this;
584   }
585
586
587   //-----------------------------------------------------------------------------------------------------------------
588   // Private methods
589   //-----------------------------------------------------------------------------------------------------------------
590
591   private void readLock() {
592      lock.readLock().lock();
593   }
594
595   private void readUnlock() {
596      lock.readLock().unlock();
597   }
598
599   private void writeLock() {
600      lock.writeLock().lock();
601   }
602
603   private void writeUnlock() {
604      lock.writeLock().unlock();
605   }
606
607   private void checkSectionName(String s) {
608      if (! ("".equals(s) || isValidNewSectionName(s)))
609         throw new IllegalArgumentException("Invalid section name: '" + s + "'");
610   }
611
612   private void checkKeyName(String s) {
613      if (! isValidKeyName(s))
614         throw new IllegalArgumentException("Invalid key name: '" + s + "'");
615   }
616
617   private boolean isValidKeyName(String s) {
618      if (s == null)
619         return false;
620      s = s.trim();
621      if (s.isEmpty())
622         return false;
623      for (int i = 0; i < s.length(); i++) {
624         char c = s.charAt(i);
625         if (c == '/' || c == '\\' || c == '[' || c == ']' || c == '=' || c == '#')
626            return false;
627      }
628      return true;
629   }
630
631   private boolean isValidNewSectionName(String s) {
632      if (s == null)
633         return false;
634      s = s.trim();
635      if (s.isEmpty())
636         return false;
637      for (int i = 0; i < s.length(); i++) {
638         char c = s.charAt(i);
639         if (c == '/' || c == '\\' || c == '[' || c == ']')
640            return false;
641      }
642      return true;
643   }
644
645   private void signal(List<ConfigEvent> changes) {
646      for (ConfigEventListener l : listeners)
647         l.onConfigChange(changes);
648   }
649
650   private List<ConfigEvent> findDiffs(String updatedContents) {
651      List<ConfigEvent> changes = new ArrayList<>();
652      ConfigMap newMap = new ConfigMap(updatedContents);
653      for (ConfigSection ns : newMap.oentries.values()) {
654         ConfigSection s = oentries.get(ns.name);
655         if (s == null) {
656            //changes.add(ConfigEvent.setSection(ns.name, ns.preLines));
657            for (ConfigEntry ne : ns.entries.values()) {
658               changes.add(ConfigEvent.setEntry(ns.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
659            }
660         } else {
661            for (ConfigEntry ne : ns.oentries.values()) {
662               ConfigEntry e = s.oentries.get(ne.key);
663               if (e == null || ! isEquals(e.value, ne.value)) {
664                  changes.add(ConfigEvent.setEntry(s.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
665               }
666            }
667            for (ConfigEntry e : s.oentries.values()) {
668               ConfigEntry ne = ns.oentries.get(e.key);
669               if (ne == null) {
670                  changes.add(ConfigEvent.removeEntry(s.name, e.key));
671               }
672            }
673         }
674      }
675      for (ConfigSection s : oentries.values()) {
676         ConfigSection ns = newMap.oentries.get(s.name);
677         if (ns == null) {
678            //changes.add(ConfigEvent.removeSection(s.name));
679            for (ConfigEntry e : s.oentries.values())
680               changes.add(ConfigEvent.removeEntry(s.name, e.key));
681         }
682      }
683      return changes;
684   }
685
686   // This method should only be called from behind a lock.
687   private String asString() {
688      try {
689         StringWriter sw = new StringWriter();
690         for (ConfigSection cs : entries.values())
691            cs.writeTo(sw);
692         return sw.toString();
693      } catch (IOException e) {
694         throw new RuntimeException(e);  // Not possible.
695      }
696   }
697
698
699   //---------------------------------------------------------------------------------------------
700   // ConfigSection
701   //---------------------------------------------------------------------------------------------
702
703   class ConfigSection {
704
705      final String name;   // The config section name, or blank if the default section.  Never null.
706
707      final List<String> preLines = Collections.synchronizedList(new ArrayList<String>());
708      private final String rawLine;
709
710      final Map<String,ConfigEntry> oentries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigEntry>());
711      final Map<String,ConfigEntry> entries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigEntry>());
712
713      /**
714       * Constructor.
715       */
716      ConfigSection(String name) {
717         this.name = name;
718         this.rawLine = "[" + name + "]";
719      }
720
721      /**
722       * Constructor.
723       */
724      ConfigSection(List<String> lines) {
725
726         String name = null, rawLine = null;
727
728         int S1 = 1; // Looking for section.
729         int S2 = 2; // Found section, looking for end.
730         int state = S1;
731         int start = 0;
732
733         for (int i = 0; i < lines.size(); i++) {
734            String l = lines.get(i);
735            char c = StringUtils.firstNonWhitespaceChar(l);
736            if (state == S1) {
737               if (c == '[') {
738                  int i1 = l.indexOf('['), i2 = l.indexOf(']');
739                  name = l.substring(i1+1, i2).trim();
740                  rawLine = l;
741                  state = S2;
742                  start = i+1;
743               } else {
744                  preLines.add(l);
745               }
746            } else {
747               if (c != '#' && l.indexOf('=') != -1) {
748                  ConfigEntry e = new ConfigEntry(l, lines.subList(start, i));
749                  if (entries.containsKey(e.key))
750                     throw new ConfigException("Duplicate entry found in section [{0}] of configuration:  {1}", name, e.key);
751                  entries.put(e.key, e);
752                  start = i+1;
753               }
754            }
755         }
756
757         this.name = name;
758         this.rawLine = rawLine;
759         this.oentries.putAll(entries);
760      }
761
762      ConfigSection addEntry(String key, String value, String modifiers, String comment, List<String> preLines) {
763         ConfigEntry e = new ConfigEntry(key, value, modifiers, comment, preLines);
764         this.entries.put(e.key, e);
765         return this;
766      }
767
768      ConfigSection setPreLines(List<String> preLines) {
769         this.preLines.clear();
770         this.preLines.addAll(preLines);
771         return this;
772      }
773
774      Writer writeTo(Writer w) throws IOException {
775         for (String s : preLines)
776            w.append(s).append('\n');
777
778         if (! name.equals(""))
779            w.append(rawLine).append('\n');
780         else {
781            // Need separation between default prelines and first-entry prelines.
782            if (! preLines.isEmpty())
783               w.append('\n');
784         }
785
786         for (ConfigEntry e : entries.values())
787            e.writeTo(w);
788
789         return w;
790      }
791   }
792}