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