001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.config;
018
019import static org.apache.juneau.commons.utils.AssertionUtils.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.IoUtils.*;
022import static org.apache.juneau.commons.utils.StringUtils.*;
023import static org.apache.juneau.commons.utils.ThrowableUtils.*;
024import static org.apache.juneau.commons.utils.Utils.*;
025
026import java.io.*;
027import java.lang.annotation.*;
028import java.lang.reflect.*;
029import java.util.*;
030
031import org.apache.juneau.*;
032import org.apache.juneau.collections.*;
033import org.apache.juneau.commons.collections.*;
034import org.apache.juneau.commons.collections.FluentMap;
035import org.apache.juneau.commons.function.*;
036import org.apache.juneau.config.event.*;
037import org.apache.juneau.config.internal.*;
038import org.apache.juneau.config.mod.*;
039import org.apache.juneau.config.store.*;
040import org.apache.juneau.config.vars.*;
041import org.apache.juneau.json.*;
042import org.apache.juneau.parser.*;
043import org.apache.juneau.serializer.*;
044import org.apache.juneau.svl.*;
045
046/**
047 * Main configuration API class.
048 *
049 * <h5 class='section'>Notes:</h5><ul>
050 *    <li class='note'>This class is thread safe and reusable.
051 * </ul>
052 *
053 * <h5 class='section'>See Also:</h5><ul>
054 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauConfigBasics">juneau-config Basics</a>
055 * </ul>
056 */
057public class Config extends Context implements ConfigEventListener {
058   /**
059    * Builder class.
060    */
061   public static class Builder extends Context.Builder {
062
063      private BinaryFormat binaryFormat;
064      private boolean multiLineValuesOnSeparateLines;
065      private boolean readOnly;
066      private ConfigStore store;
067      private int binaryLineLength;
068      private Map<Character,Mod> mods;
069      private ReaderParser parser;
070      private String name;
071      private VarResolver varResolver;
072      private WriterSerializer serializer;
073
074      /**
075       * Constructor, default settings.
076       */
077      protected Builder() {
078         binaryFormat = env("Config.binaryFormat", BinaryFormat.BASE64);
079         binaryLineLength = env("Config.binaryLineLength", -1);
080         mods = map();
081         mods(XorEncodeMod.INSTANCE);
082         multiLineValuesOnSeparateLines = env("Config.multiLineValuesOnSeparateLines", false);
083         name = env("Config.name", "Configuration.cfg");
084         parser = JsonParser.DEFAULT;
085         readOnly = env("Config.readOnly", false);
086         serializer = Json5Serializer.DEFAULT;
087         store = FileStore.DEFAULT;
088         varResolver = VarResolver.DEFAULT;
089      }
090
091      /**
092       * Copy constructor.
093       *
094       * @param copyFrom The builder to copy from.
095       */
096      protected Builder(Builder copyFrom) {
097         super(copyFrom);
098         binaryFormat = copyFrom.binaryFormat;
099         binaryLineLength = copyFrom.binaryLineLength;
100         mods = copyOf(copyFrom.mods);
101         multiLineValuesOnSeparateLines = copyFrom.multiLineValuesOnSeparateLines;
102         name = copyFrom.name;
103         parser = copyFrom.parser;
104         readOnly = copyFrom.readOnly;
105         serializer = copyFrom.serializer;
106         store = copyFrom.store;
107         varResolver = copyFrom.varResolver;
108      }
109
110      /**
111       * Copy constructor.
112       *
113       * @param copyFrom The bean to copy from.
114       */
115      protected Builder(Config copyFrom) {
116         super(copyFrom);
117         binaryFormat = copyFrom.binaryFormat;
118         binaryLineLength = copyFrom.binaryLineLength;
119         mods = copyOf(copyFrom.mods);
120         multiLineValuesOnSeparateLines = copyFrom.multiLineValuesOnSeparateLines;
121         name = copyFrom.name;
122         parser = copyFrom.parser;
123         readOnly = copyFrom.readOnly;
124         serializer = copyFrom.serializer;
125         store = copyFrom.store;
126         varResolver = copyFrom.varResolver;
127      }
128
129      @Override /* Overridden from Builder */
130      public Builder annotations(Annotation...values) {
131         super.annotations(values);
132         return this;
133      }
134
135      @Override /* Overridden from Builder */
136      public Builder annotations(List<Annotation> values) {
137         super.annotations(values);
138         return this;
139      }
140
141      @Override /* Overridden from Builder */
142      public Builder apply(AnnotationWorkList work) {
143         super.apply(work);
144         return this;
145      }
146
147      @Override /* Overridden from Builder */
148      public Builder applyAnnotations(Class<?>...from) {
149         super.applyAnnotations(from);
150         return this;
151      }
152
153      @Override /* Overridden from Builder */
154      public Builder applyAnnotations(Object...from) {
155         super.applyAnnotations(from);
156         return this;
157      }
158
159      /**
160       * Binary value format.
161       *
162       * <p>
163       * The format to use when persisting byte arrays.
164       *
165       * <ul class='values'>
166       *    <li>{@link BinaryFormat#BASE64} - BASE64-encoded string.
167       *    <li>{@link BinaryFormat#HEX} - Hexadecimal.
168       *    <li>{@link BinaryFormat#SPACED_HEX} - Hexadecimal with spaces between bytes.
169       * </ul>
170       *
171       * @param value
172       *    The new value for this property.
173       *    <br>The default is the first value found:
174       *    <ul>
175       *       <li>System property <js>"Config.binaryFormat"
176       *       <li>Environment variable <js>"CONFIG_BINARYFORMAT"
177       *       <li>{@link BinaryFormat#BASE64}
178       *    </ul>
179       *    <br>Cannot be <jk>null</jk>.
180       * @return This object.
181       */
182      public Builder binaryFormat(BinaryFormat value) {
183         binaryFormat = assertArgNotNull("value", value);
184         return this;
185      }
186
187      /**
188       * Binary value line length.
189       *
190       * <p>
191       * When serializing binary values, lines will be split after this many characters.
192       * <br>Use <c>-1</c> to represent no line splitting.
193       *
194       * @param value
195       *    The new value for this property.
196       *    <br>The default is the first value found:
197       *    <ul>
198       *       <li>System property <js>"Config.binaryLineLength"
199       *       <li>Environment variable <js>"CONFIG_BINARYLINELENGTH"
200       *       <li><c>-1</c>
201       *    </ul>
202       * @return This object.
203       */
204      public Builder binaryLineLength(int value) {
205         binaryLineLength = value;
206         return this;
207      }
208
209      @Override /* Overridden from Context.Builder */
210      public Config build() {
211         return build(Config.class);
212      }
213
214      @Override /* Overridden from Builder */
215      public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) {
216         super.cache(value);
217         return this;
218      }
219
220      @Override /* Overridden from Context.Builder */
221      public Builder copy() {
222         return new Builder(this);
223      }
224
225      @Override /* Overridden from Builder */
226      public Builder debug() {
227         super.debug();
228         return this;
229      }
230
231      @Override /* Overridden from Builder */
232      public Builder debug(boolean value) {
233         super.debug(value);
234         return this;
235      }
236
237      @Override /* Overridden from Builder */
238      public Builder impl(Context value) {
239         super.impl(value);
240         return this;
241      }
242
243      /**
244       * Configuration store.
245       *
246       * <p>
247       * Convenience method for calling <code>store(ConfigMemoryStore.<jsf>DEFAULT</jsf>)</code>.
248       *
249       * @return This object.
250       */
251      public Builder memStore() {
252         store = MemoryStore.DEFAULT;
253         return this;
254      }
255
256      /**
257       * Adds a value modifier.
258       *
259       * <p>
260       * Modifiers are used to modify entry value before being persisted.
261       *
262       * @param values
263       *    The mods to apply to this config.
264       *    <br>Cannot contain <jk>null</jk> values.
265       * @return This object.
266       */
267      public Builder mods(Mod...values) {
268         assertArgNoNulls("values", values);
269         for (var value : values)
270            mods.put(value.getId(), value);
271         return this;
272      }
273
274      /**
275       * Multi-line values on separate lines.
276       *
277       * <p>
278       * When enabled, multi-line values will always be placed on a separate line from the key.
279       *
280       * <p>
281       * The default is the first value found:
282       *    <ul>
283       *       <li>System property <js>"Config.multiLineValuesOnSeparateLine"
284       *       <li>Environment variable <js>"CONFIG_MULTILINEVALUESONSEPARATELINE"
285       *       <li><jk>false</jk>
286       *    </ul>
287       *
288       * @return This object.
289       */
290      public Builder multiLineValuesOnSeparateLines() {
291         multiLineValuesOnSeparateLines = true;
292         return this;
293      }
294
295      /**
296       * Configuration name.
297       *
298       * <p>
299       * Specifies the configuration name.
300       * <br>This is typically the configuration file name, although
301       * the name can be anything identifiable by the {@link ConfigStore} used for retrieving and storing the configuration.
302       *
303       * @param value
304       *    The new value for this property.
305       *    <br>The default is the first value found:
306       *    <ul>
307       *       <li>System property <js>"Config.name"
308       *       <li>Environment variable <js>"CONFIG_NAME"
309       *       <li><js>"Configuration.cfg"</js>
310       *    </ul>
311       *    <br>Cannot be <jk>null</jk>.
312       * @return This object.
313       */
314      public Builder name(String value) {
315         name = assertArgNotNull("value", value);
316         return this;
317      }
318
319      /**
320       * POJO parser.
321       *
322       * <p>
323       * The parser to use for parsing values to POJOs.
324       *
325       * @param value
326       *    The new value for this property.
327       *    <br>The default is {@link JsonParser#DEFAULT}.
328       *    <br>Cannot be <jk>null</jk>.
329       * @return This object.
330       */
331      public Builder parser(ReaderParser value) {
332         parser = assertArgNotNull("value", value);
333         return this;
334      }
335
336      /**
337       * Read-only mode.
338       *
339       * <p>
340       * When enabled, attempts to call any setters on this object will throw an {@link UnsupportedOperationException}.
341       *
342       * <p>
343       *    The default is the first value found:
344       *    <ul>
345       *       <li>System property <js>"Config.readOnly"
346       *       <li>Environment variable <js>"CONFIG_READONLY"
347       *       <li><jk>false</jk>
348       *    </ul>
349       *
350       * @return This object.
351       */
352      public Builder readOnly() {
353         readOnly = true;
354         return this;
355      }
356
357      /**
358       * POJO serializer.
359       *
360       * <p>
361       * The serializer to use for serializing POJO values.
362       *
363       * @param value
364       *    The new value for this property.
365       *    <br>The default is {@link Json5Serializer#DEFAULT}
366       *    <br>Cannot be <jk>null</jk>.
367       * @return This object.
368       */
369      public Builder serializer(WriterSerializer value) {
370         serializer = assertArgNotNull("value", value);
371         return this;
372      }
373
374      /**
375       * Configuration store.
376       *
377       * <p>
378       * The configuration store used for retrieving and storing configurations.
379       *
380       * @param value
381       *    The new value for this property.
382       *    <br>The default is {@link FileStore#DEFAULT}.
383       *    <br>Cannot be <jk>null</jk>.
384       * @return This object.
385       */
386      public Builder store(ConfigStore value) {
387         store = assertArgNotNull("value", value);
388         return this;
389      }
390
391      @Override /* Overridden from Builder */
392      public Builder type(Class<? extends org.apache.juneau.Context> value) {
393         super.type(value);
394         return this;
395      }
396
397      /**
398       * SVL variable resolver.
399       *
400       * <p>
401       * The resolver to use for resolving SVL variables.
402       *
403       * @param value
404       *    The new value for this property.
405       *    <br>The default is {@link VarResolver#DEFAULT}.
406       *    <br>Cannot be <jk>null</jk>.
407       * @return This object.
408       */
409      public Builder varResolver(VarResolver value) {
410         varResolver = assertArgNotNull("value", value);
411         return this;
412      }
413   }
414
415   // Use set(T)/reset() for testing.
416   static final ResettableSupplier<Boolean> DISABLE_AUTO_SYSTEM_PROPS = memr(() -> Boolean.getBoolean("juneau.disableAutoSystemProps"));
417
418   // Use set(T)/reset() for testing.
419   static final ResettableSupplier<Config> SYSTEM_DEFAULT = memr(() -> findSystemDefault());
420
421   /**
422    * Creates a new builder for this object.
423    *
424    * @return A new builder.
425    */
426   public static Builder create() {
427      return new Builder();
428   }
429
430   /**
431    * Same as {@link #create()} but initializes the builder with the specified config name.
432    *
433    * @param name The configuration name.
434    * @return A new builder.
435    */
436   public static Builder create(String name) {
437      return new Builder().name(name);
438   }
439
440   /**
441    * Returns the list of candidate system default configuration file names.
442    *
443    * <p>
444    * If the <js>"juneau.configFile"</js> system property is set, returns a singleton of that value.
445    * <br>Otherwise, returns a list consisting of the following values:
446    * <ol>
447    *    <li>File with same name as jar file but with <js>".cfg"</js> extension.  (e.g. <js>"myjar.cfg"</js>)
448    *    <li>Any file ending in <js>".cfg"</js> in the home directory (names ordered alphabetically).
449    *    <li><js>"juneau.cfg"</js>
450    *    <li><js>"default.cfg"</js>
451    *    <li><js>"application.cfg"</js>
452    *    <li><js>"app.cfg"</js>
453    *    <li><js>"settings.cfg"</js>
454    *    <li><js>"application.properties"</js>
455    * </ol>
456    * <p>
457    *
458    * @return
459    *    A list of candidate file names.
460    *    <br>The returned list is modifiable.
461    *    <br>Each call constructs a new list.
462    */
463   public static synchronized List<String> getCandidateSystemDefaultConfigNames() {
464      var l = listOf(String.class);
465
466      var s = System.getProperty("juneau.configFile");
467      if (nn(s)) {
468         l.add(s);
469         return l;
470      }
471
472      var cmd = System.getProperty("sun.java.command", "not_found").split("\\s+")[0];
473      if (cmd.endsWith(".jar") && ! co(cmd, "surefirebooter")) {
474         cmd = cmd.replaceAll(".*?([^\\\\\\/]+)\\.jar$", "$1");
475         l.add(cmd + ".cfg");
476         cmd = cmd.replaceAll("[\\.\\_].*$", "");  // Try also without version in jar name.
477         l.add(cmd + ".cfg");
478      }
479
480      var fileArray = new File(".").listFiles();
481      if (fileArray != null) {
482         for (var f : fileArray)
483            if (f.getName().endsWith(".cfg"))
484               l.add(f.getName());
485      }
486
487      l.add("juneau.cfg");
488      l.add("default.cfg");
489      l.add("application.cfg");
490      l.add("app.cfg");
491      l.add("settings.cfg");
492      l.add("application.properties");
493
494      return l;
495   }
496
497   /**
498    * Returns the system default configuration.
499    *
500    * @return The system default configuration, or <jk>null</jk> if it doesn't exist.
501    */
502   public static synchronized Config getSystemDefault() { return SYSTEM_DEFAULT.get(); }
503
504   /**
505    * Sets a system default configuration.
506    *
507    * @param systemDefault The new system default configuration.
508    */
509   public static synchronized void setSystemDefault(Config systemDefault) {
510      SYSTEM_DEFAULT.set(systemDefault);
511   }
512
513   private static synchronized Config find(String name) {
514      if (name == null)
515         return null;
516      if (FileStore.DEFAULT.exists(name))
517         return Config.create(name).store(FileStore.DEFAULT).build();
518      if (ClasspathStore.DEFAULT.exists(name))
519         return Config.create(name).store(ClasspathStore.DEFAULT).build();
520      return null;
521   }
522
523   private static synchronized Config findSystemDefault() {
524
525      for (var n : getCandidateSystemDefaultConfigNames()) {
526         var config = find(n);
527         if (nn(config)) {
528            if (! DISABLE_AUTO_SYSTEM_PROPS.get())
529               config.setSystemProperties();
530            return config;
531         }
532      }
533
534      return null;
535   }
536
537   private static boolean isSimpleType(Type t) {
538      if (! (t instanceof Class))
539         return false;
540      var c = (Class<?>)t;
541      return (c == String.class || c.isPrimitive() || c.isAssignableFrom(Number.class) || c == Boolean.class || c.isEnum());
542   }
543   private static String section(String section) {
544      assertArgNotNull("section", section);
545      if (isEmpty(section))
546         return "";
547      return section;
548   }
549   private static String skey(String key) {
550      var i = key.indexOf('/');
551      if (i == -1)
552         return key;
553      return key.substring(i + 1);
554   }
555   private static String sname(String key) {
556      assertArgNotNull("key", key);
557      var i = key.indexOf('/');
558      if (i == -1)
559         return "";
560      return key.substring(0, i);
561   }
562
563   protected final boolean multiLineValuesOnSeparateLines;
564   protected final boolean readOnly;
565   protected final int binaryLineLength;
566   protected final BinaryFormat binaryFormat;
567   protected final BeanSession beanSession;
568   protected final ConfigStore store;
569   protected final Map<Character,Mod> mods;
570   protected final ReaderParser parser;
571   protected final String name;
572   protected final VarResolver varResolver;
573   protected final VarResolverSession varSession;
574   protected final WriterSerializer serializer;
575
576   private final ConfigMap configMap;
577   private final List<ConfigEventListener> listeners = synced(new LinkedList<>());
578
579   /**
580    * Constructor.
581    *
582    * @param builder The builder for this object.
583    * @throws IOException Thrown by underlying stream.
584    */
585   public Config(Builder builder) throws IOException {
586      super(builder);
587
588      binaryFormat = builder.binaryFormat;
589      binaryLineLength = builder.binaryLineLength;
590      mods = u(copyOf(builder.mods));
591      multiLineValuesOnSeparateLines = builder.multiLineValuesOnSeparateLines;
592      name = builder.name;
593      parser = builder.parser;
594      readOnly = builder.readOnly;
595      serializer = builder.serializer;
596      store = builder.store;
597      varResolver = builder.varResolver;
598      configMap = store.getMap(name);
599      configMap.register(this);
600      beanSession = parser.getBeanContext().getSession();
601      varSession = varResolver.copy().vars(ConfigVar.class).bean(Config.class, this).build().createSession();
602   }
603
604   Config(Config copyFrom, VarResolverSession varSession) {
605      super(copyFrom);
606      beanSession = copyFrom.beanSession;
607      binaryFormat = copyFrom.binaryFormat;
608      binaryLineLength = copyFrom.binaryLineLength;
609      configMap = copyFrom.configMap;
610      configMap.register(this);
611      mods = copyFrom.mods;
612      multiLineValuesOnSeparateLines = copyFrom.multiLineValuesOnSeparateLines;
613      name = copyFrom.name;
614      parser = copyFrom.parser;
615      readOnly = copyFrom.readOnly;
616      serializer = copyFrom.serializer;
617      store = copyFrom.store;
618      this.varSession = varSession;
619      varResolver = copyFrom.varResolver;
620   }
621
622   /**
623    * Add a listener to this config to react to modification events.
624    *
625    * <p>
626    * Listeners should be removed using {@link #removeListener(ConfigEventListener)}.
627    *
628    * @param listener The new listener to add.
629    * @return This object.
630    */
631   public synchronized Config addListener(ConfigEventListener listener) {
632      listeners.add(listener);
633      return this;
634   }
635
636   /**
637    * Encodes and unencoded entries in this config.
638    *
639    * <p>
640    * If any entries in the config are marked as encoded but not actually encoded,
641    * this will encode them.
642    *
643    * @return This object.
644    * @throws UnsupportedOperationException If configuration is read only.
645    */
646   public Config applyMods() {
647      checkWrite();
648      for (var section : configMap.getSections()) {
649         for (var key : configMap.getKeys(section)) {
650            var ce = configMap.getEntry(section, key);
651            if (nn(ce.getModifiers())) {
652               var mods2 = ce.getModifiers();
653               var value = ce.getValue();
654               for (var i = 0; i < mods2.length(); i++) {
655                  var mod = getMod(mods2.charAt(i));
656                  if (! mod.isApplied(value)) {
657                     configMap.setEntry(section, key, mod.apply(value), null, null, null);
658                  }
659               }
660            }
661         }
662      }
663
664      return this;
665   }
666
667   /**
668    * Closes this configuration object by unregistering it from the underlying config map.
669    */
670   public void close() {
671      configMap.unregister(this);
672   }
673
674   /**
675    * Commit the changes in this config to the store.
676    *
677    * @return This object.
678    * @throws IOException Thrown by underlying stream.
679    * @throws UnsupportedOperationException If configuration is read only.
680    */
681   public Config commit() throws IOException {
682      checkWrite();
683      configMap.commit();
684      return this;
685   }
686
687   @Override /* Overridden from Context */
688   public Builder copy() {
689      return new Builder(this);
690   }
691
692   /**
693    * Returns <jk>true</jk> if this section contains the specified key and the key has a non-blank value.
694    *
695    * @param key The key.
696    * @return <jk>true</jk> if this section contains the specified key and the key has a non-blank value.
697    */
698   public boolean exists(String key) {
699      return ne(get(key).as(String.class).orElse(null));
700   }
701
702   /**
703    * Gets the entry with the specified key.
704    *
705    * <p>
706    * The key can be in one of the following formats...
707    * <ul class='spaced-list'>
708    *    <li>
709    *       <js>"key"</js> - A value in the default section (i.e. defined above any <c>[section]</c> header).
710    *    <li>
711    *       <js>"section/key"</js> - A value from the specified section.
712    * </ul>
713    *
714    * <p>
715    * If entry does not exist, returns an empty {@link Entry} object.
716    *
717    * @param key The key.
718    * @return The entry bean, never <jk>null</jk>.
719    */
720   public Entry get(String key) {
721      return new Entry(this, configMap, sname(key), skey(key));
722   }
723
724   /**
725    * Returns the keys of the entries in the specified section.
726    *
727    * @param section
728    *    The section name to write from.
729    *    <br>If empty, refers to the default section.
730    *    <br>Must not be <jk>null</jk>.
731    * @return
732    *    An unmodifiable set of keys, or an empty set if the section doesn't exist.
733    */
734   public Set<String> getKeys(String section) {
735      return configMap.getKeys(section(section));
736   }
737
738   /**
739    * Returns the name associated with this config (usually a file name).
740    *
741    * @return The name associated with this config, or <jk>null</jk> if it has no name.
742    */
743   public String getName() { return name; }
744
745   /**
746    * Gets the section with the specified name.
747    *
748    * <p>
749    * If section does not exist, returns an empty {@link Section} object.
750    *
751    * @param name The section name.  <jk>null</jk> and blank refer to the default section.
752    * @return The section bean, never <jk>null</jk>.
753    */
754   public Section getSection(String name) {
755      return new Section(this, configMap, emptyIfNull(name));
756   }
757
758   /**
759    * Returns the section names defined in this config.
760    *
761    * @return The section names defined in this config.
762    */
763   public Set<String> getSectionNames() { return u(configMap.getSections()); }
764
765   /**
766    * Gets the entry with the specified key.
767    *
768    * <p>
769    * The key can be in one of the following formats...
770    * <ul class='spaced-list'>
771    *    <li>
772    *       <js>"key"</js> - A value in the default section (i.e. defined above any <c>[section]</c> header).
773    *    <li>
774    *       <js>"section/key"</js> - A value from the specified section.
775    * </ul>
776    *
777    * <p>
778    * If entry does not exist, returns <jk>null</jk>.
779    *
780    * <h5 class='section'>Notes:</h5><ul>
781    *    <li class='note'>This method is equivalent to calling <c>get(<jv>key</jv>).orElse(<jk>null</jk>);</c>.
782    * </ul>
783    *
784    * @param key The key.
785    * @return The entry value, or <jk>null</jk> if it doesn't exist.
786    */
787   public String getString(String key) {
788      return new Entry(this, configMap, sname(key), skey(key)).orElse(null);
789   }
790
791   /**
792    * Loads the contents of the specified map of maps into this config.
793    *
794    * @param m The maps to load.
795    * @return This object.
796    * @throws SerializeException Value could not be serialized.
797    */
798   public Config load(Map<String,Map<String,Object>> m) throws SerializeException {
799      if (nn(m))
800         for (var e : m.entrySet()) {
801            setSection(e.getKey(), null, e.getValue());
802         }
803      return this;
804   }
805
806   /**
807    * Overwrites the contents of the config file.
808    *
809    * @param contents The new contents of the config file.
810    * @param synchronous Wait until the change has been persisted before returning this map.
811    * @return This object.
812    * @throws IOException Thrown by underlying stream.
813    * @throws InterruptedException Thread was interrupted.
814    * @throws UnsupportedOperationException If configuration is read only.
815    */
816   public Config load(Reader contents, boolean synchronous) throws IOException, InterruptedException {
817      checkWrite();
818      configMap.load(read(contents), synchronous);
819      return this;
820   }
821
822   /**
823    * Overwrites the contents of the config file.
824    *
825    * @param contents The new contents of the config file.
826    * @param synchronous Wait until the change has been persisted before returning this map.
827    * @return This object.
828    * @throws IOException Thrown by underlying stream.
829    * @throws InterruptedException Thread was interrupted.
830    * @throws UnsupportedOperationException If configuration is read only.
831    */
832   public Config load(String contents, boolean synchronous) throws IOException, InterruptedException {
833      checkWrite();
834      configMap.load(contents, synchronous);
835      return this;
836   }
837
838   @Override /* Overridden from ConfigEventListener */
839   public synchronized void onConfigChange(ConfigEvents events) {
840      listeners.forEach(x -> x.onConfigChange(events));
841   }
842
843   /**
844    * Removes an entry with the specified key.
845    *
846    * @param key The key.
847    * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
848    * @throws UnsupportedOperationException If configuration is read only.
849    */
850   public Config remove(String key) {
851      checkWrite();
852      var sname = sname(key);
853      var skey = skey(key);
854      configMap.removeEntry(sname, skey);
855      return this;
856   }
857
858   /**
859    * Removes the import statement with the specified name from the specified section.
860    *
861    * @param sectionName
862    *    The section name where to place the import statement.
863    *    <br>Must not be <jk>null</jk>.
864    *    <br>Use blank for the default section.
865    * @param importName
866    *    The import name.
867    *    <br>Must not be <jk>null</jk>.
868    * @return This object.
869    * @throws UnsupportedOperationException If configuration is read only.
870    */
871   public Config removeImport(String sectionName, String importName) {
872      checkWrite();
873      configMap.removeImport(sectionName, importName);
874      return this;
875   }
876
877   /**
878    * Removes a listener from this config.
879    *
880    * @param listener The listener to remove.
881    * @return This object.
882    */
883   public synchronized Config removeListener(ConfigEventListener listener) {
884      listeners.remove(listener);
885      return this;
886   }
887
888   /**
889    * Removes the section with the specified name.
890    *
891    * @param name The name of the section to remove
892    * @return This object.
893    * @throws UnsupportedOperationException If configuration is read only.
894    */
895   public Config removeSection(String name) {
896      checkWrite();
897      configMap.removeSection(name);
898      return this;
899   }
900
901   /**
902    * Creates a copy of this config using the specified var session for resolving variables.
903    *
904    * <p>
905    * This creates a shallow copy of the config but replacing the variable resolver.
906    *
907    * @param varSession The var session used for resolving string variables.
908    * @return A new config object.
909    */
910   public Config resolving(VarResolverSession varSession) {
911      return new Config(this, varSession);
912   }
913
914   /**
915    * Does a rollback of any changes on this config currently in memory.
916    *
917    * @return This object.
918    * @throws UnsupportedOperationException If configuration is read only.
919    */
920   public Config rollback() {
921      checkWrite();
922      configMap.rollback();
923      return this;
924   }
925
926   /**
927    * Adds or replaces an entry with the specified key with a POJO serialized to a string using the registered
928    * serializer.
929    *
930    * <p>
931    * Equivalent to calling <c>put(key, value, isEncoded(key))</c>.
932    *
933    * @param key The key.
934    * @param value The new value POJO.
935    * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
936    * @throws SerializeException
937    *    If serializer could not serialize the value or if a serializer is not registered with this config file.
938    * @throws UnsupportedOperationException If configuration is read only.
939    */
940   public Config set(String key, Object value) throws SerializeException {
941      return set(key, value, null);
942   }
943
944   /**
945    * Same as {@link #set(String, Object)} but allows you to specify the serializer to use to serialize the
946    * value.
947    *
948    * @param key The key.
949    * @param value The new value.
950    * @param serializer
951    *    The serializer to use for serializing the object.
952    *    If <jk>null</jk>, then uses the predefined serializer on the config file.
953    * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
954    * @throws SerializeException
955    *    If serializer could not serialize the value or if a serializer is not registered with this config file.
956    * @throws UnsupportedOperationException If configuration is read only.
957    */
958   public Config set(String key, Object value, Serializer serializer) throws SerializeException {
959      return set(key, serialize(value, serializer));
960   }
961
962   /**
963    * Same as {@link #set(String, Object)} but allows you to specify all aspects of a value.
964    *
965    * @param key The key.
966    * @param value The new value.
967    * @param serializer
968    *    The serializer to use for serializing the object.
969    *    If <jk>null</jk>, then uses the predefined serializer on the config file.
970    * @param modifiers
971    *    Optional modifiers to apply to the value.
972    *    <br>If <jk>null</jk>, then previous value will not be replaced.
973    * @param comment
974    *    Optional same-line comment to add to this value.
975    *    <br>If <jk>null</jk>, then previous value will not be replaced.
976    * @param preLines
977    *    Optional comment or blank lines to add before this entry.
978    *    <br>If <jk>null</jk>, then previous value will not be replaced.
979    * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
980    * @throws SerializeException
981    *    If serializer could not serialize the value or if a serializer is not registered with this config file.
982    * @throws UnsupportedOperationException If configuration is read only.
983    */
984   public Config set(String key, Object value, Serializer serializer, String modifiers, String comment, List<String> preLines) throws SerializeException {
985      checkWrite();
986      assertArgNotNull("key", key);
987      var sname = sname(key);
988      var skey = skey(key);
989      modifiers = nullIfEmpty(modifiers);
990
991      var s = applyMods(modifiers, serialize(value, serializer));
992
993      configMap.setEntry(sname, skey, s, modifiers, comment, preLines);
994      return this;
995   }
996
997   /**
998    * Sets a value in this config.
999    *
1000    * @param key The key.
1001    * @param value The value.
1002    * @return This object.
1003    * @throws UnsupportedOperationException If configuration is read only.
1004    */
1005   public Config set(String key, String value) {
1006      checkWrite();
1007      assertArgNotNull("key", key);
1008      var sname = sname(key);
1009      var skey = skey(key);
1010
1011      var ce = configMap.getEntry(sname, skey);
1012      if (ce == null && value == null)
1013         return this;
1014
1015      var s = applyMods(ce == null ? null : ce.getModifiers(), s(value));
1016
1017      configMap.setEntry(sname, skey, s, null, null, null);
1018      return this;
1019   }
1020
1021   /**
1022    * Creates the specified import statement if it doesn't exist.
1023    *
1024    * @param sectionName
1025    *    The section name where to place the import statement.
1026    *    <br>Must not be <jk>null</jk>.
1027    *    <br>Use blank for the default section.
1028    * @param importName
1029    *    The import name.
1030    *    <br>Must not be <jk>null</jk>.
1031    * @param preLines
1032    *    Optional comment and blank lines to add immediately before the import statement.
1033    *    <br>If <jk>null</jk>, previous pre-lines will not be replaced.
1034    * @return The appended or existing import statement.
1035    * @throws UnsupportedOperationException If configuration is read only.
1036    */
1037   public Config setImport(String sectionName, String importName, List<String> preLines) {
1038      checkWrite();
1039      configMap.setImport(section(name), importName, preLines);
1040      return this;
1041   }
1042
1043   /**
1044    * Creates the specified section if it doesn't exist.
1045    *
1046    * <p>
1047    * Returns the existing section if it already exists.
1048    *
1049    * @param name
1050    *    The section name.
1051    *    <br>Must not be <jk>null</jk>.
1052    *    <br>Use blank for the default section.
1053    * @param preLines
1054    *    Optional comment and blank lines to add immediately before the section.
1055    *    <br>If <jk>null</jk>, previous pre-lines will not be replaced.
1056    * @return The appended or existing section.
1057    * @throws UnsupportedOperationException If configuration is read only.
1058    */
1059   public Config setSection(String name, List<String> preLines) {
1060      try {
1061         return setSection(section(name), preLines, null);
1062      } catch (SerializeException e) {
1063         throw toRex(e);  // Impossible.
1064      }
1065   }
1066
1067   /**
1068    * Creates the specified section if it doesn't exist.
1069    *
1070    * @param name
1071    *    The section name.
1072    *    <br>Must not be <jk>null</jk>.
1073    *    <br>Use blank for the default section.
1074    * @param preLines
1075    *    Optional comment and blank lines to add immediately before the section.
1076    *    <br>If <jk>null</jk>, previous pre-lines will not be replaced.
1077    * @param contents
1078    *    Values to set in the new section.
1079    *    <br>Can be <jk>null</jk>.
1080    * @return The appended or existing section.
1081    * @throws SerializeException Contents could not be serialized.
1082    * @throws UnsupportedOperationException If configuration is read only.
1083    */
1084   public Config setSection(String name, List<String> preLines, Map<String,Object> contents) throws SerializeException {
1085      checkWrite();
1086      configMap.setSection(section(name), preLines);
1087
1088      if (nn(contents))
1089         for (var e : contents.entrySet())
1090            set(section(name) + '/' + e.getKey(), e.getValue());
1091
1092      return this;
1093   }
1094
1095   /**
1096    * Takes the settings defined in this configuration and sets them as system properties.
1097    *
1098    * @return This object.
1099    */
1100   public Config setSystemProperties() {
1101      for (var section : getSectionNames()) {
1102         for (var key : getKeys(section)) {
1103            var k = (section.isEmpty() ? key : section + '/' + key);
1104            System.setProperty(k, getRaw(k));
1105         }
1106      }
1107      return this;
1108   }
1109
1110   /**
1111    * Returns the contents of this config as a simple map.
1112    *
1113    * @return The contents of this config as a simple map.
1114    */
1115   public JsonMap toMap() {
1116      return configMap.asMap();
1117   }
1118
1119   @Override /* Overridden from Context */
1120   protected FluentMap<String,Object> properties() {
1121      return super.properties()
1122         .a("binaryFormat", binaryFormat)
1123         .a("binaryLineLength", binaryLineLength)
1124         .a("mods", mods)
1125         .a("multiLineValuesOnSeparateLines", multiLineValuesOnSeparateLines)
1126         .a("name", name)
1127         .a("parser", parser)
1128         .a("readOnly", readOnly)
1129         .a("serializer", serializer)
1130         .a("store", store)
1131         .a("varResolver", varResolver);
1132   }
1133
1134   @Override /* Overridden from Object */
1135   public String toString() {
1136      return configMap.toString();
1137   }
1138
1139   /**
1140    * Saves this config file to the specified writer as an INI file.
1141    *
1142    * <p>
1143    * The writer will automatically be closed.
1144    *
1145    * @param w The writer to send the output to.
1146    * @return This object.
1147    * @throws IOException If a problem occurred trying to send contents to the writer.
1148    */
1149   public Writer writeTo(Writer w) throws IOException {
1150      return configMap.writeTo(w);
1151   }
1152
1153   /**
1154    * Returns the specified value as a string from the config file.
1155    *
1156    * <p>
1157    * Unlike {@link #getString(String)}, this method doesn't replace SVL variables.
1158    *
1159    * @param key The key.
1160    * @return The value, or <jk>null</jk> if the section or value doesn't exist.
1161    */
1162   private String getRaw(String key) {
1163
1164      var sname = sname(key);
1165      var skey = skey(key);
1166
1167      var ce = configMap.getEntry(sname, skey);
1168
1169      if (ce == null)
1170         return null;
1171
1172      return removeMods(ce.getModifiers(), ce.getValue());
1173   }
1174
1175   private String nlIfMl(CharSequence cs) {
1176      var s = cs.toString();
1177      if (s.indexOf('\n') != -1 && multiLineValuesOnSeparateLines)
1178         return "\n" + s;
1179      return s;
1180   }
1181
1182   private String serialize(Object value, Serializer serializer) throws SerializeException {
1183      if (value == null)
1184         return "";
1185      if (serializer == null)
1186         serializer = this.serializer;
1187      var c = value.getClass();
1188      if (value instanceof CharSequence cs)
1189         return nlIfMl(cs);
1190      if (isSimpleType(c))
1191         return value.toString();
1192
1193      if (value instanceof byte[] b) {
1194         var s = (String)null;
1195         if (binaryFormat == BinaryFormat.HEX)
1196            s = toHex(b);
1197         else if (binaryFormat == BinaryFormat.SPACED_HEX)
1198            s = toSpacedHex(b);
1199         else
1200            s = base64Encode(b);
1201         var l = binaryLineLength;
1202         if (l <= 0 || s.length() <= l)
1203            return s;
1204         var sb = new StringBuilder();
1205         for (var i = 0; i < s.length(); i += l)
1206            sb.append(binaryLineLength > 0 ? "\n" : "").append(s.substring(i, Math.min(s.length(), i + l)));
1207         return sb.toString();
1208      }
1209
1210      var r = (String)null;
1211      if (multiLineValuesOnSeparateLines)
1212         r = "\n" + (String)serializer.serialize(value);
1213      else
1214         r = (String)serializer.serialize(value);
1215
1216      if (r.startsWith("'"))
1217         return r.substring(1, r.length() - 1);
1218      return r;
1219   }
1220
1221   String applyMods(String mods, String x) {
1222      if (nn(mods) && nn(x))
1223         for (var i = 0; i < mods.length(); i++)
1224            x = getMod(mods.charAt(i)).doApply(x);
1225      return x;
1226   }
1227
1228   void checkWrite() {
1229      if (readOnly)
1230         throw unsupportedOp("Cannot call this method on a read-only configuration.");
1231   }
1232
1233   ConfigMap getConfigMap() { return configMap; }
1234
1235   List<ConfigEventListener> getListeners() { return u(listeners); }
1236
1237   Mod getMod(char id) {
1238      var x = mods.get(id);
1239      return def(x, Mod.NO_OP);
1240   }
1241
1242   String removeMods(String mods, String x) {
1243      if (nn(mods) && nn(x))
1244         for (var i = mods.length() - 1; i > -1; i--)
1245            x = getMod(mods.charAt(i)).doRemove(x);
1246      return x;
1247   }
1248}