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