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