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