This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 8611b24  Config API refactoring.
8611b24 is described below

commit 8611b246f9a0f5e11f6060197260f0107eef85dd
Author: JamesBognar <[email protected]>
AuthorDate: Tue Feb 20 21:38:07 2018 -0500

    Config API refactoring.
---
 .../apache/juneau/config/ConfigFileWritable.java   |    3 +-
 .../juneau/config/event/ChangeEventListener.java   |    2 +-
 .../org/apache/juneau/config/proto/Config.java     | 1339 ++++++++++++++++++++
 .../apache/juneau/config/proto/ConfigBuilder.java  |  323 +++++
 .../org/apache/juneau/config/proto/ConfigMod.java  |  103 ++
 .../apache/juneau/config/store/ConfigEntry.java    |   31 +-
 .../org/apache/juneau/config/store/ConfigMap.java  |  200 +--
 .../juneau/config/proto/ConfigMapListenerTest.java |   62 +-
 .../src/main/java/org/apache/juneau/Writable.java  |    3 +-
 .../org/apache/juneau/internal/StringUtils.java    |    2 +
 .../org/apache/juneau/utils/StringMessage.java     |    4 +-
 .../java/org/apache/juneau/utils/StringObject.java |    3 +-
 .../org/apache/juneau/rest/ReaderResource.java     |    3 +-
 13 files changed, 1943 insertions(+), 135 deletions(-)

diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java
index f846cb5..f8ee441 100644
--- 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java
@@ -34,10 +34,11 @@ class ConfigFileWritable implements Writable {
        }
 
        @Override /* Writable */
-       public void writeTo(Writer out) throws IOException {
+       public Writer writeTo(Writer out) throws IOException {
                cf.readLock();
                try {
                        cf.serializeTo(out);
+                       return out;
                } finally {
                        cf.readUnlock();
                }
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java
index 546f330..335c66c 100644
--- 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java
@@ -24,5 +24,5 @@ public interface ChangeEventListener {
         * 
         * @param events The change events.
         */
-       void onEvents(List<ChangeEvent> events);
+       void onChange(List<ChangeEvent> events);
 }
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/Config.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/Config.java
new file mode 100644
index 0000000..3b1b30f
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/Config.java
@@ -0,0 +1,1339 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              * 
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.config.proto;
+
+import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.internal.ThrowableUtils.*;
+import static org.apache.juneau.config.proto.ConfigMod.*;
+import static java.lang.reflect.Modifier.*;
+
+import java.beans.*;
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.config.encode.*;
+import org.apache.juneau.config.encode.Encoder;
+import org.apache.juneau.config.event.*;
+import org.apache.juneau.config.store.*;
+import org.apache.juneau.config.vars.*;
+import org.apache.juneau.http.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * TODO
+ */
+public final class Config extends Context implements ChangeEventListener, 
Closeable, Writable {
+
+       
//-------------------------------------------------------------------------------------------------------------------
+       // Configurable properties
+       
//-------------------------------------------------------------------------------------------------------------------
+
+       private static final String PREFIX = "Config.";
+
+       /**
+        * Configuration property:  Configuration name.
+        * 
+        * <h5 class='section'>Property:</h5>
+        * <ul>
+        *      <li><b>Name:</b>  <js>"Config.name.s"</js>
+        *      <li><b>Data type:</b>  <code>String</code>
+        *      <li><b>Default:</b>  <js>"Configuration"</js>
+        *      <li><b>Methods:</b> 
+        *              <ul>
+        *                      <li class='jm'>{@link 
ConfigBuilder#name(String)}
+        *              </ul>
+        * </ul>
+        * 
+        * <h5 class='section'>Description:</h5>
+        * <p>
+        * Specifies the configuration name.
+        * <br>This is typically the configuration file name minus the file 
extension, although
+        * the name can be anything identifiable by the {@link Store} used for 
retrieving and storing the configuration.
+        */
+       public static final String CONFIG_name = PREFIX + "name.s";
+
+       /**
+        * Configuration property:  Configuration store.
+        * 
+        * <h5 class='section'>Property:</h5>
+        * <ul>
+        *      <li><b>Name:</b>  <js>"Config.store.o"</js>
+        *      <li><b>Data type:</b>  {@link Store}
+        *      <li><b>Default:</b>  {@link FileStore#DEFAULT}
+        *      <li><b>Methods:</b> 
+        *              <ul>
+        *                      <li class='jm'>{@link 
ConfigBuilder#store(Store)}
+        *              </ul>
+        * </ul>
+        * 
+        * <h5 class='section'>Description:</h5>
+        * <p>
+        * The configuration store used for retrieving and storing 
configurations.
+        */
+       public static final String CONFIG_store = PREFIX + "store.o";
+
+       /**
+        * Configuration property:  POJO serializer.
+        * 
+        * <h5 class='section'>Property:</h5>
+        * <ul>
+        *      <li><b>Name:</b>  <js>"Config.serializer.o"</js>
+        *      <li><b>Data type:</b>  {@link WriterSerializer}
+        *      <li><b>Default:</b>  {@link JsonSerializer#DEFAULT_LAX}
+        *      <li><b>Methods:</b> 
+        *              <ul>
+        *                      <li class='jm'>{@link 
ConfigBuilder#serializer(Class)}
+        *                      <li class='jm'>{@link 
ConfigBuilder#serializer(WriterSerializer)}
+        *              </ul>
+        * </ul>
+        * 
+        * <h5 class='section'>Description:</h5>
+        * <p>
+        * The serializer to use for serializing POJO values.
+        */
+       public static final String CONFIG_serializer = PREFIX + "serializer.o";
+
+       /**
+        * Configuration property:  POJO parser.
+        * 
+        * <h5 class='section'>Property:</h5>
+        * <ul>
+        *      <li><b>Name:</b>  <js>"Config.parser.o"</js>
+        *      <li><b>Data type:</b>  {@link ReaderParser}
+        *      <li><b>Default:</b>  {@link JsonParser#DEFAULT}
+        *      <li><b>Methods:</b> 
+        *              <ul>
+        *                      <li class='jm'>{@link 
ConfigBuilder#parser(Class)}
+        *                      <li class='jm'>{@link 
ConfigBuilder#parser(ReaderParser)}
+        *              </ul>
+        * </ul>
+        * 
+        * <h5 class='section'>Description:</h5>
+        * <p>
+        * The parser to use for parsing values to POJOs.
+        */
+       public static final String CONFIG_parser = PREFIX + "parser.o";
+
+       /**
+        * Configuration property:  Value encoder.
+        * 
+        * <h5 class='section'>Property:</h5>
+        * <ul>
+        *      <li><b>Name:</b>  <js>"Config.encoder.o"</js>
+        *      <li><b>Data type:</b>  {@link Encoder}
+        *      <li><b>Default:</b>  {@link XorEncoder#INSTANCE}
+        *      <li><b>Methods:</b> 
+        *              <ul>
+        *                      <li class='jm'>{@link 
ConfigBuilder#encoder(Class)}
+        *                      <li class='jm'>{@link 
ConfigBuilder#encoder(Encoder)}
+        *              </ul>
+        * </ul>
+        * 
+        * <h5 class='section'>Description:</h5>
+        * <p>
+        * The encoder to use for encoding encoded configuration values.
+        */
+       public static final String CONFIG_encoder = PREFIX + "encoder.o";
+
+       /**
+        * Configuration property:  SVL variable resolver.
+        * 
+        * <h5 class='section'>Property:</h5>
+        * <ul>
+        *      <li><b>Name:</b>  <js>"Config.varResolver.o"</js>
+        *      <li><b>Data type:</b>  {@link VarResolver}
+        *      <li><b>Default:</b>  {@link VarResolver#DEFAULT}
+        *      <li><b>Methods:</b> 
+        *              <ul>
+        *                      <li class='jm'>{@link 
ConfigBuilder#varResolver(Class)}
+        *                      <li class='jm'>{@link 
ConfigBuilder#varResolver(VarResolver)}
+        *              </ul>
+        * </ul>
+        * 
+        * <h5 class='section'>Description:</h5>
+        * <p>
+        * The resolver to use for resolving SVL variables.
+        */
+       public static final String CONFIG_varResolver = PREFIX + 
"varResolver.o";
+
+       /**
+        * Configuration property:  Binary value line length.
+        * 
+        * <h5 class='section'>Property:</h5>
+        * <ul>
+        *      <li><b>Name:</b>  <js>"Config.binaryLineLength.i"</js>
+        *      <li><b>Data type:</b>  <code>Integer</code>
+        *      <li><b>Default:</b>  <code>-1</code>
+        *      <li><b>Methods:</b> 
+        *              <ul>
+        *                      <li class='jm'>{@link 
ConfigBuilder#binaryLineLength(int)}
+        *              </ul>
+        * </ul>
+        * 
+        * <h5 class='section'>Description:</h5>
+        * <p>
+        * When serializing binary values, lines will be split after this many 
characters.
+        * <br>Use <code>-1</code> to represent no line splitting.
+        */
+       public static final String CONFIG_binaryLineLength = PREFIX + 
"binaryLineLength.i";
+
+       /**
+        * Configuration property:  Binary value format.
+        * 
+        * <h5 class='section'>Property:</h5>
+        * <ul>
+        *      <li><b>Name:</b>  <js>"Config.binaryFormat.s"</js>
+        *      <li><b>Data type:</b>  <code>String</code>
+        *      <li><b>Default:</b>  <js>"BASE64"</js>
+        *      <li><b>Methods:</b> 
+        *              <ul>
+        *                      <li class='jm'>{@link 
ConfigBuilder#binaryFormat(String)}
+        *              </ul>
+        * </ul>
+        * 
+        * <h5 class='section'>Description:</h5>
+        * <p>
+        * The format to use when persisting byte arrays.
+        * 
+        * <p>
+        * Possible values:
+        * <ul>
+        *      <li><js>"BASE64"</js> - BASE64-encoded string.
+        *      <li><js>"HEX"</js> - Hexadecimal.
+        *      <li><js>"SPACED_HEX"</js> - Hexadecimal with spaces between 
bytes.
+        * </ul>
+        */
+       public static final String CONFIG_binaryFormat = PREFIX + 
"binaryFormat.s";
+
+       /**
+        * Configuration property:  Beans on separate lines.
+        * 
+        * <h5 class='section'>Property:</h5>
+        * <ul>
+        *      <li><b>Name:</b>  <js>"Config.beanOnSeparateLines.b"</js>
+        *      <li><b>Data type:</b>  <code>Boolean</code>
+        *      <li><b>Default:</b>  <jk>false</jk>
+        *      <li><b>Methods:</b> 
+        *              <ul>
+        *                      <li class='jm'>{@link 
ConfigBuilder#beansOnSeparateLines(boolean)}
+        *              </ul>
+        * </ul>
+        * 
+        * <h5 class='section'>Description:</h5>
+        * <p>
+        * When enabled, serialized POJOs will be placed on a separate line 
from the key.
+        */
+       public static final String CONFIG_beansOnSeparateLines = PREFIX + 
"beansOnSeparateLines.b";
+       
+       
+       
//-------------------------------------------------------------------------------------------------------------------
+       // Instance
+       
//-------------------------------------------------------------------------------------------------------------------
+
+       private final String name;
+       private final Store store;
+       private final WriterSerializer serializer;
+       private final ReaderParser parser;
+       private final Encoder encoder;
+       private final VarResolverSession varSession;
+       private final int binaryLineLength;
+       private final String binaryFormat;
+       private final boolean beansOnSeparateLines;
+       private final ConfigMap configMap;
+       private final BeanSession beanSession;
+       private volatile boolean closed;
+       private final List<ChangeEventListener> listeners = 
Collections.synchronizedList(new LinkedList<ChangeEventListener>());
+
+
+       /**
+        * Instantiates a new clean-slate {@link ConfigBuilder} object.
+        * 
+        * <p>
+        * This is equivalent to simply calling <code><jk>new</jk> 
ConfigBuilder()</code>.
+        * 
+        * @return A new {@link ConfigBuilder} object.
+        */
+       public static ConfigBuilder create() {
+               return new ConfigBuilder();
+       }
+       
+       @Override /* Context */
+       public ConfigBuilder builder() {
+               return new ConfigBuilder(getPropertyStore());
+       }
+       
+       /**
+        * Constructor.
+        * 
+        * @param ps
+        *      The property store containing all the settings for this object.
+        * @throws IOException 
+        */
+       public Config(PropertyStore ps) throws IOException {
+               super(ps);
+               
+               name = getStringProperty(CONFIG_name, "Configuration");
+               store = getInstanceProperty(CONFIG_store, Store.class, 
FileStore.DEFAULT);
+               configMap = store.getMap(name);
+               configMap.register(this);
+               serializer = getInstanceProperty(CONFIG_serializer, 
WriterSerializer.class, JsonSerializer.DEFAULT_LAX);
+               parser = getInstanceProperty(CONFIG_parser, ReaderParser.class, 
JsonParser.DEFAULT);
+               beanSession = parser.createBeanSession();
+               encoder = getInstanceProperty(CONFIG_encoder, Encoder.class, 
XorEncoder.INSTANCE);
+               varSession = getInstanceProperty(CONFIG_varResolver, 
VarResolver.class, VarResolver.DEFAULT)
+                       .builder()
+                       .vars(ConfigFileVar.class)
+                       .contextObject(ConfigFileVar.SESSION_config, this)
+                       .build()
+                       .createSession();
+               binaryLineLength = getIntegerProperty(CONFIG_binaryLineLength, 
-1);
+               binaryFormat = getStringProperty(CONFIG_binaryFormat, 
"BASE64").toUpperCase();
+               beansOnSeparateLines = 
getBooleanProperty(CONFIG_beansOnSeparateLines, false);
+       }
+       
+       Config(Config copyFrom, VarResolverSession varSession) { 
+               super(null);
+               name = copyFrom.name;
+               store = copyFrom.store;
+               configMap = copyFrom.configMap;
+               configMap.register(this);
+               serializer = copyFrom.serializer;
+               parser = copyFrom.parser;
+               encoder = copyFrom.encoder;
+               this.varSession = varSession;
+               binaryLineLength = copyFrom.binaryLineLength;
+               binaryFormat = copyFrom.binaryFormat;
+               beansOnSeparateLines = copyFrom.beansOnSeparateLines;
+               beanSession = copyFrom.beanSession;
+       }
+       
+       /**
+        * Creates a copy of this config using the specified var session for 
resolving variables.
+        * 
+        * <p>
+        * This creates a shallow copy of the config but replacing the variable 
resolver.
+        * 
+        * @param varSession The var session used for resolving string 
variables.
+        * @return A new config object.
+        */
+       public Config resolving(VarResolverSession varSession) {
+               return new Config(this, varSession);
+       }
+       
+
+       
//--------------------------------------------------------------------------------
+       // Workhorse getters
+       
//--------------------------------------------------------------------------------
+
+       /**
+        * Returns the specified value as a string from the config file.
+        * 
+        * <p>
+        * Unlike {@link #getString(String)}, this method doesn't replace SVL 
variables.
+        * 
+        * @param key The key.  
+        * @return The value, or <jk>null</jk> if the section or value doesn't 
exist.
+        */
+       public String get(String key) {
+               
+               String sname = sname(key);
+               String skey = skey(key); 
+               ConfigEntry ce = configMap.getEntry(sname, skey);
+               
+               if (ce == null || ce.getValue() == null)
+                       return null;
+               
+               String val = ce.getValue();
+               for (ConfigMod m : 
ConfigMod.asModifiersReverse(ce.getModifiers())) {
+                       if (m == ENCODED) {
+                               val = encoder.decode(key, val);
+                       }
+               }
+               
+               return val;
+       }
+               
+
+       
//--------------------------------------------------------------------------------
+       // Workhorse setters
+       
//--------------------------------------------------------------------------------
+
+       /**
+        * Sets a value in this config.
+        * 
+        * @param key The key.  
+        * @param value The value.
+        * @return This object (for method chaining).
+        */
+       public Config set(String key, String value) {
+               assertFieldNotNull(key, "key");
+               String sname = sname(key);
+               String skey = skey(key); 
+               ConfigEntry ce = configMap.getEntry(sname, skey);
+               
+               String s = asString(value);
+               for (ConfigMod m : ConfigMod.asModifiers(ce.getModifiers())) {
+                       if (m == ENCODED) {
+                               value = encoder.encode(key, s);
+                       }
+               }
+               
+               configMap.setValue(sname, skey, s);
+               return this;
+       }
+
+       /**
+        * Adds or replaces an entry with the specified key with a POJO 
serialized to a string using the registered
+        * serializer.
+        * 
+        * <p>
+        * Equivalent to calling <code>put(key, value, isEncoded(key))</code>.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param value The new value POJO.
+        * @return The previous value, or <jk>null</jk> if the section or key 
did not previously exist.
+        * @throws SerializeException
+        *      If serializer could not serialize the value or if a serializer 
is not registered with this config file.
+        * @throws UnsupportedOperationException If config file is read only.
+        */
+       public Config set(String key, Object value) throws SerializeException {
+               return set(key, value, null);
+       }
+
+       /**
+        * Same as {@link #set(String, Object)} but allows you to specify the 
serializer to use to serialize the
+        * value.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param value The new value.
+        * @param serializer
+        *      The serializer to use for serializing the object.
+        *      If <jk>null</jk>, then uses the predefined serializer on the 
config file.
+        * @return The previous value, or <jk>null</jk> if the section or key 
did not previously exist.
+        * @throws SerializeException
+        *      If serializer could not serialize the value or if a serializer 
is not registered with this config file.
+        * @throws UnsupportedOperationException If config file is read only.
+        */
+       public Config set(String key, Object value, Serializer serializer) 
throws SerializeException {
+               return set(key, serialize(value, serializer));
+       }
+       
+       /**
+        * Same as {@link #set(String, Object)} but allows you to specify all 
aspects of a value.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param value The new value.
+        * @param serializer
+        *      The serializer to use for serializing the object.
+        *      If <jk>null</jk>, then uses the predefined serializer on the 
config file.
+        * @param modifiers 
+        *      Optional modifiers to apply to the value.
+        *      <br>Can be <jk>null</jk>.
+        * @param comment 
+        *      Optional same-line comment to add to this value.
+        *      <br>Can be <jk>null</jk>.
+        * @param preLines 
+        *      Optional comment or blank lines to add before this entry.
+        *      <br>Can be <jk>null</jk>.
+        * @return The previous value, or <jk>null</jk> if the section or key 
did not previously exist.
+        * @throws SerializeException
+        *      If serializer could not serialize the value or if a serializer 
is not registered with this config file.
+        * @throws UnsupportedOperationException If config file is read only.
+        */
+       public Config set(String key, Object value, Serializer serializer, 
ConfigMod[] modifiers, String comment, List<String> preLines) throws 
SerializeException {
+               assertFieldNotNull(key, "key");
+               String sname = sname(key);
+               String skey = skey(key); 
+               ConfigEntry ce = configMap.getEntry(sname, skey);
+               
+               String s = serialize(value, serializer);
+               for (ConfigMod m : ConfigMod.asModifiers(ce.getModifiers())) {
+                       if (m == ENCODED) {
+                               s = encoder.encode(key, s);
+                       }
+               }
+               
+               configMap.setEntry(sname, skey, s, 
ConfigMod.asString(modifiers), comment, preLines);
+               return this;
+       }
+
+       /**
+        * Removes an entry with the specified key.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @return The previous value, or <jk>null</jk> if the section or key 
did not previously exist.
+        * @throws UnsupportedOperationException If config file is read only.
+        */
+       public Config remove(String key) {
+               return set(key, null);
+       }
+       
+       
+       
//--------------------------------------------------------------------------------
+       // API methods
+       
//--------------------------------------------------------------------------------
+
+       /**
+        * Gets the entry with the specified key.
+        * 
+        * <p>
+        * The key can be in one of the following formats...
+        * <ul class='spaced-list'>
+        *      <li>
+        *              <js>"key"</js> - A value in the default section (i.e. 
defined above any <code>[section]</code> header).
+        *      <li>
+        *              <js>"section/key"</js> - A value from the specified 
section.
+        * </ul>
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @return The value, or <jk>null</jk> if the section or key does not 
exist.
+        */
+       public String getString(String key) {
+               return getString(key, null);
+       }
+
+       /**
+        * Gets the entry with the specified key.
+        * 
+        * <p>
+        * The key can be in one of the following formats...
+        * <ul class='spaced-list'>
+        *      <li>
+        *              <js>"key"</js> - A value in the default section (i.e. 
defined above any <code>[section]</code> header).
+        *      <li>
+        *              <js>"section/key"</js> - A value from the specified 
section.
+        * </ul>
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param def The default value.
+        * @return The value, or the default value if the section or key does 
not exist.
+        */
+       public String getString(String key, String def) {
+               String s = get(key);
+               if (s == null)
+                       return def;
+               if (varSession != null)
+                       s = varSession.resolve(s);
+               return s;
+       }
+
+       /**
+        * Gets the entry with the specified key, splits the value on commas, 
and returns the values as trimmed strings.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @return The value, or an empty list if the section or key does not 
exist.
+        */
+       public String[] getStringArray(String key) {
+               return getStringArray(key, new String[0]);
+       }
+
+       /**
+        * Same as {@link #getStringArray(String)} but returns a default value 
if the value cannot be found.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param def The default value if section or key does not exist.
+        * @return The value, or an empty list if the section or key does not 
exist.
+        */
+       public String[] getStringArray(String key, String[] def) {
+               String s = getString(key);
+               if (s == null)
+                       return def;
+               String[] r = isEmpty(s) ? new String[0] : split(s);
+               return r.length == 0 ? def : r;
+       }
+
+       /**
+        * Convenience method for getting int config values.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @return The value, or <code>0</code> if the section or key does not 
exist or cannot be parsed as an integer.
+        */
+       public int getInt(String key) {
+               return getInt(key, 0);
+       }
+
+       /**
+        * Convenience method for getting int config values.
+        * 
+        * <p>
+        * <js>"K"</js>, <js>"M"</js>, and <js>"G"</js> can be used to identify 
kilo, mega, and giga.
+        * 
+        * <h5 class='section'>Example:</h5>
+        * <ul class='spaced-list'>
+        *      <li>
+        *              <code><js>"100K"</js> => 1024000</code>
+        *      <li>
+        *              <code><js>"100M"</js> => 104857600</code>
+        * </ul>
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param def The default value if config file or value does not exist.
+        * @return The value, or the default value if the section or key does 
not exist or cannot be parsed as an integer.
+        */
+       public int getInt(String key, int def) {
+               String s = getString(key);
+               if (isEmpty(s))
+                       return def;
+               return parseIntWithSuffix(s);
+       }
+
+       /**
+        * Convenience method for getting boolean config values.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @return The value, or <jk>false</jk> if the section or key does not 
exist or cannot be parsed as a boolean.
+        */
+       public boolean getBoolean(String key) {
+               return getBoolean(key, false);
+       }
+
+       /**
+        * Convenience method for getting boolean config values.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param def The default value if config file or value does not exist.
+        * @return The value, or the default value if the section or key does 
not exist or cannot be parsed as a boolean.
+        */
+       public boolean getBoolean(String key, boolean def) {
+               String s = getString(key);
+               return isEmpty(s) ? def : Boolean.parseBoolean(s);
+       }
+
+       /**
+        * Convenience method for getting long config values.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @return The value, or <code>0</code> if the section or key does not 
exist or cannot be parsed as a long.
+        */
+       public long getLong(String key) {
+               return getLong(key, 0);
+       }
+
+       /**
+        * Convenience method for getting long config values.
+        * 
+        * <p>
+        * <js>"K"</js>, <js>"M"</js>, and <js>"G"</js> can be used to identify 
kilo, mega, and giga.
+        * 
+        * <h5 class='section'>Example:</h5>
+        * <ul class='spaced-list'>
+        *      <li>
+        *              <code><js>"100K"</js> => 1024000</code>
+        *      <li>
+        *              <code><js>"100M"</js> => 104857600</code>
+        * </ul>
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param def The default value if config file or value does not exist.
+        * @return The value, or the default value if the section or key does 
not exist or cannot be parsed as an integer.
+        */
+       public long getLong(String key, long def) {
+               String s = getString(key);
+               if (isEmpty(s))
+                       return def;
+               return parseLongWithSuffix(s);
+       }
+       
+       /**
+        * Gets the entry with the specified key and converts it to the 
specified value.
+        * 
+        * <p>
+        * The key can be in one of the following formats...
+        * <ul class='spaced-list'>
+        *      <li>
+        *              <js>"key"</js> - A value in the default section (i.e. 
defined above any <code>[section]</code> header).
+        *      <li>
+        *              <js>"section/key"</js> - A value from the specified 
section.
+        * </ul>
+        * 
+        * <p>
+        * The type can be a simple type (e.g. beans, strings, numbers) or 
parameterized type (collections/maps).
+        * 
+        * <h5 class='section'>Examples:</h5>
+        * <p class='bcode'>
+        *      ConfigFile cf = 
ConfigFile.<jsm>create</jsm>().build(<js>"MyConfig.cfg"</js>);
+        * 
+        *      <jc>// Parse into a linked-list of strings.</jc>
+        *      List l = cf.getObject(<js>"MySection/myListOfStrings"</js>, 
LinkedList.<jk>class</jk>, String.<jk>class</jk>);
+        * 
+        *      <jc>// Parse into a linked-list of beans.</jc>
+        *      List l = cf.getObject(<js>"MySection/myListOfBeans"</js>, 
LinkedList.<jk>class</jk>, MyBean.<jk>class</jk>);
+        * 
+        *      <jc>// Parse into a linked-list of linked-lists of strings.</jc>
+        *      List l = cf.getObject(<js>"MySection/my2dListOfStrings"</js>, 
LinkedList.<jk>class</jk>,
+        *              LinkedList.<jk>class</jk>, String.<jk>class</jk>);
+        * 
+        *      <jc>// Parse into a map of string keys/values.</jc>
+        *      Map m = cf.getObject(<js>"MySection/myMap"</js>, 
TreeMap.<jk>class</jk>, String.<jk>class</jk>,
+        *              String.<jk>class</jk>);
+        * 
+        *      <jc>// Parse into a map containing string keys and values of 
lists containing beans.</jc>
+        *      Map m = cf.getObject(<js>"MySection/myMapOfListsOfBeans"</js>, 
TreeMap.<jk>class</jk>, String.<jk>class</jk>,
+        *              List.<jk>class</jk>, MyBean.<jk>class</jk>);
+        * </p>
+        * 
+        * <p>
+        * <code>Collection</code> classes are assumed to be followed by zero 
or one objects indicating the element type.
+        * 
+        * <p>
+        * <code>Map</code> classes are assumed to be followed by zero or two 
meta objects indicating the key and value
+        * types.
+        * 
+        * <p>
+        * The array can be arbitrarily long to indicate arbitrarily complex 
data structures.
+        * 
+        * <h5 class='section'>Notes:</h5>
+        * <ul class='spaced-list'>
+        *      <li>
+        *              Use the {@link #getObject(String, Class)} method 
instead if you don't need a parameterized map/collection.
+        * </ul>
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param type
+        *      The object type to create.
+        *      <br>Can be any of the following: {@link ClassMeta}, {@link 
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+        * @param args
+        *      The type arguments of the class if it's a collection or map.
+        *      <br>Can be any of the following: {@link ClassMeta}, {@link 
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+        *      <br>Ignored if the main type is not a map or collection.
+        * @throws ParseException If parser could not parse the value or if a 
parser is not registered with this config file.
+        * @return The value, or <jk>null</jk> if the section or key does not 
exist.
+        */
+       public <T> T getObject(String key, Type type, Type...args) throws 
ParseException {
+               return getObject(key, (Parser)null, type, args);
+       }
+
+       /**
+        * Same as {@link #getObject(String, Type, Type...)} but allows you to 
specify the parser to use to parse the value.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param parser
+        *      The parser to use for parsing the object.
+        *      If <jk>null</jk>, then uses the predefined parser on the config 
file.
+        * @param type
+        *      The object type to create.
+        *      <br>Can be any of the following: {@link ClassMeta}, {@link 
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+        * @param args
+        *      The type arguments of the class if it's a collection or map.
+        *      <br>Can be any of the following: {@link ClassMeta}, {@link 
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+        *      <br>Ignored if the main type is not a map or collection.
+        * @throws ParseException If parser could not parse the value or if a 
parser is not registered with this config file.
+        * @return The value, or <jk>null</jk> if the section or key does not 
exist.
+        */
+       public <T> T getObject(String key, Parser parser, Type type, 
Type...args) throws ParseException {
+               assertFieldNotNull(type, "type");
+               return parse(getString(key), parser, type, args);
+       }
+
+       /**
+        * Same as {@link #getObject(String, Type, Type...)} except optimized 
for a non-parameterized class.
+        * 
+        * <p>
+        * This is the preferred parse method for simple types since you don't 
need to cast the results.
+        * 
+        * <h5 class='section'>Examples:</h5>
+        * <p class='bcode'>
+        *      ConfigFile cf = 
ConfigFile.<jsm>create</jsm>().build(<js>"MyConfig.cfg"</js>);
+        * 
+        *      <jc>// Parse into a string.</jc>
+        *      String s = cf.getObject(<js>"MySection/mySimpleString"</js>, 
String.<jk>class</jk>);
+        * 
+        *      <jc>// Parse into a bean.</jc>
+        *      MyBean b = cf.getObject(<js>"MySection/myBean"</js>, 
MyBean.<jk>class</jk>);
+        * 
+        *      <jc>// Parse into a bean array.</jc>
+        *      MyBean[] b = cf.getObject(<js>"MySection/myBeanArray"</js>, 
MyBean[].<jk>class</jk>);
+        * 
+        *      <jc>// Parse into a linked-list of objects.</jc>
+        *      List l = cf.getObject(<js>"MySection/myList"</js>, 
LinkedList.<jk>class</jk>);
+        * 
+        *      <jc>// Parse into a map of object keys/values.</jc>
+        *      Map m = cf.getObject(<js>"MySection/myMap"</js>, 
TreeMap.<jk>class</jk>);
+        * </p>
+        * 
+        * @param <T> The class type of the object being created.
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param type The object type to create.
+        * @return The parsed object.
+        * @throws ParseException
+        *      If the input contains a syntax error or is malformed, or is not 
valid for the specified type.
+        * @see BeanSession#getClassMeta(Type,Type...) for argument syntax for 
maps and collections.
+        */
+       public <T> T getObject(String key, Class<T> type) throws ParseException 
{
+               return getObject(key, (Parser)null, type);
+       }
+
+       /**
+        * Same as {@link #getObject(String, Class)} but allows you to specify 
the parser to use to parse the value.
+        * 
+        * @param <T> The class type of the object being created.
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param parser
+        *      The parser to use for parsing the object.
+        *      If <jk>null</jk>, then uses the predefined parser on the config 
file.
+        * @param type The object type to create.
+        * @return The parsed object.
+        * @throws ParseException
+        *      If the input contains a syntax error or is malformed, or is not 
valid for the specified type.
+        * @see BeanSession#getClassMeta(Type,Type...) for argument syntax for 
maps and collections.
+        */
+       public <T> T getObject(String key, Parser parser, Class<T> type) throws 
ParseException {
+               assertFieldNotNull(type, "c");
+               return parse(getString(key), parser, type);
+       }
+
+       /**
+        * Gets the entry with the specified key and converts it to the 
specified value.
+        * 
+        * <p>
+        * Same as {@link #getObject(String, Class)}, but with a default value.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param def The default value if section or key does not exist.
+        * @param type The class to convert the value to.
+        * @throws ParseException If parser could not parse the value or if a 
parser is not registered with this config file.
+        * @return The value, or <jk>null</jk> if the section or key does not 
exist.
+        */
+       public <T> T getObjectWithDefault(String key, T def, Class<T> type) 
throws ParseException {
+               return getObjectWithDefault(key, null, def, type);
+       }
+
+       /**
+        * Same as {@link #getObjectWithDefault(String, Object, Class)} but 
allows you to specify the parser to use to parse
+        * the value.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param parser
+        *      The parser to use for parsing the object.
+        *      If <jk>null</jk>, then uses the predefined parser on the config 
file.
+        * @param def The default value if section or key does not exist.
+        * @param type The class to convert the value to.
+        * @throws ParseException If parser could not parse the value or if a 
parser is not registered with this config file.
+        * @return The value, or <jk>null</jk> if the section or key does not 
exist.
+        */
+       public <T> T getObjectWithDefault(String key, Parser parser, T def, 
Class<T> type) throws ParseException {
+               assertFieldNotNull(type, "c");
+               T t = parse(getString(key), parser, type);
+               return (t == null ? def : t);
+       }
+
+       /**
+        * Gets the entry with the specified key and converts it to the 
specified value.
+        * 
+        * <p>
+        * Same as {@link #getObject(String, Type, Type...)}, but with a 
default value.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param def The default value if section or key does not exist.
+        * @param type
+        *      The object type to create.
+        *      <br>Can be any of the following: {@link ClassMeta}, {@link 
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+        * @param args
+        *      The type arguments of the class if it's a collection or map.
+        *      <br>Can be any of the following: {@link ClassMeta}, {@link 
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+        *      <br>Ignored if the main type is not a map or collection.
+        * @throws ParseException If parser could not parse the value or if a 
parser is not registered with this config file.
+        * @return The value, or <jk>null</jk> if the section or key does not 
exist.
+        */
+       public <T> T getObjectWithDefault(String key, T def, Type type, 
Type...args) throws ParseException {
+               return getObjectWithDefault(key, null, def, type, args);
+       }
+
+       /**
+        * Same as {@link #getObjectWithDefault(String, Object, Type, Type...)} 
but allows you to specify the parser to use
+        * to parse the value.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @param parser
+        *      The parser to use for parsing the object.
+        *      If <jk>null</jk>, then uses the predefined parser on the config 
file.
+        * @param def The default value if section or key does not exist.
+        * @param type
+        *      The object type to create.
+        *      <br>Can be any of the following: {@link ClassMeta}, {@link 
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+        * @param args
+        *      The type arguments of the class if it's a collection or map.
+        *      <br>Can be any of the following: {@link ClassMeta}, {@link 
Class}, {@link ParameterizedType}, {@link GenericArrayType}
+        *      <br>Ignored if the main type is not a map or collection.
+        * @throws ParseException If parser could not parse the value or if a 
parser is not registered with this config file.
+        * @return The value, or <jk>null</jk> if the section or key does not 
exist.
+        */
+       public <T> T getObjectWithDefault(String key, Parser parser, T def, 
Type type, Type...args) throws ParseException {
+               assertFieldNotNull(type, "type");
+               T t = parse(getString(key), parser, type, args);
+               return (t == null ? def : t);
+       }
+
+       /**
+        * Copies the entries in a section to the specified bean by calling the 
public setters on that bean.
+        * 
+        * @param section The section name to write from.
+        * @param bean The bean to set the properties on.
+        * @param ignoreUnknownProperties
+        *      If <jk>true</jk>, don't throw an {@link 
IllegalArgumentException} if this section contains a key that doesn't
+        *      correspond to a setter method.
+        * @return An object map of the changes made to the bean.
+        * @throws ParseException If parser was not set on this config file or 
invalid properties were found in the section.
+        * @throws IllegalArgumentException
+        * @throws IllegalAccessException
+        * @throws InvocationTargetException
+        */
+       public Config writeProperties(String section, Object bean, boolean 
ignoreUnknownProperties) throws ParseException, IllegalArgumentException, 
IllegalAccessException, InvocationTargetException {
+               assertFieldNotNull(bean, "bean");
+
+               Set<String> keys = configMap.getKeys(section);
+               if (keys == null)
+                       throw new IllegalArgumentException("Section not found");
+               keys = new LinkedHashSet<>(keys);
+               
+               for (Method m : bean.getClass().getMethods()) {
+                       int mod = m.getModifiers();
+                       if (isPublic(mod) && (!isStatic(mod)) && 
m.getName().startsWith("set") && m.getParameterTypes().length == 1) {
+                               Class<?> pt = m.getParameterTypes()[0];
+                               String propName = 
Introspector.decapitalize(m.getName().substring(3));
+                               Object value = getObject(section + '/' + 
propName, pt);
+                               if (value != null) {
+                                       m.invoke(bean, value);
+                                       keys.remove(propName);
+                               }
+                       }
+               }
+               
+               if (! (ignoreUnknownProperties || keys.isEmpty()))
+                       throw new ParseException("Invalid properties found in 
config file section ''{0}'': {1}", section, keys);
+               
+               return this;
+       }
+
+       /**
+        * Shortcut for calling <code>getSectionAsBean(sectionName, c, 
<jk>false</jk>)</code>.
+        * 
+        * @param sectionName The section name to write from.
+        * @param c The bean class to create.
+        * @return A new bean instance.
+        * @throws ParseException
+        */
+       public <T> T getSectionAsBean(String sectionName, Class<T>c) throws 
ParseException {
+               return getSectionAsBean(sectionName, c, false);
+       }
+
+       /**
+        * Converts this config file section to the specified bean instance.
+        * 
+        * <p>
+        * Key/value pairs in the config file section get copied as bean 
property values to the specified bean class.
+        * 
+        * <h5 class='figure'>Example config file</h5>
+        * <p class='bcode'>
+        *      <cs>[MyAddress]</cs>
+        *      <ck>name</ck> = <cv>John Smith</cv>
+        *      <ck>street</ck> = <cv>123 Main Street</cv>
+        *      <ck>city</ck> = <cv>Anywhere</cv>
+        *      <ck>state</ck> = <cv>NY</cv>
+        *      <ck>zip</ck> = <cv>12345</cv>
+        * </p>
+        * 
+        * <h5 class='figure'>Example bean</h5>
+        * <p class='bcode'>
+        *      <jk>public class</jk> Address {
+        *              public String name, street, city;
+        *              public StateEnum state;
+        *              public int zip;
+        *      }
+        * </p>
+        * 
+        * <h5 class='figure'>Example usage</h5>
+        * <p class='bcode'>
+        *      ConfigFile cf = 
ConfigFile.<jsm>create</jsm>().build(<js>"MyConfig.cfg"</js>);
+        *      Address myAddress = cf.getSectionAsBean(<js>"MySection"</js>, 
Address.<jk>class</jk>);
+        * </p>
+        * 
+        * @param section The section name to write from.
+        * @param c The bean class to create.
+        * @param ignoreUnknownProperties
+        *      If <jk>false</jk>, throws a {@link ParseException} if the 
section contains an entry that isn't a bean property
+        *      name.
+        * @return A new bean instance.
+        * @throws ParseException
+        */
+       public <T> T getSectionAsBean(String section, Class<T> c, boolean 
ignoreUnknownProperties) throws ParseException {
+               assertFieldNotNull(c, "c");
+
+               BeanMap<T> bm = beanSession.newBeanMap(c);
+               for (String k : configMap.getKeys(section)) {
+                       BeanPropertyMeta bpm = bm.getPropertyMeta(k);
+                       if (bpm == null) {
+                               if (! ignoreUnknownProperties)
+                                       throw new ParseException("Unknown 
property {0} encountered", k);
+                       } else {
+                               bm.put(k, getObject(section + '/' + k, 
bpm.getClassMeta().getInnerClass()));
+                       }
+               }
+               return bm.getBean();
+       }
+
+       /**
+        * Wraps a config file section inside a Java interface so that values 
in the section can be read and
+        * write using getters and setters.
+        * 
+        * <h5 class='figure'>Example config file</h5>
+        * <p class='bcode'>
+        *      <cs>[MySection]</cs>
+        *      <ck>string</ck> = <cv>foo</cv>
+        *      <ck>int</ck> = <cv>123</cv>
+        *      <ck>enum</ck> = <cv>ONE</cv>
+        *      <ck>bean</ck> = <cv>{foo:'bar',baz:123}</cv>
+        *      <ck>int3dArray</ck> = <cv>[[[123,null],null],null]</cv>
+        *      <ck>bean1d3dListMap</ck> = 
<cv>{key:[[[[{foo:'bar',baz:123}]]]]}</cv>
+        * </p>
+        * 
+        * <h5 class='figure'>Example interface</h5>
+        * <p class='bcode'>
+        *      <jk>public interface</jk> MyConfigInterface {
+        * 
+        *              String getString();
+        *              <jk>void</jk> setString(String x);
+        * 
+        *              <jk>int</jk> getInt();
+        *              <jk>void</jk> setInt(<jk>int</jk> x);
+        * 
+        *              MyEnum getEnum();
+        *              <jk>void</jk> setEnum(MyEnum x);
+        * 
+        *              MyBean getBean();
+        *              <jk>void</jk> setBean(MyBean x);
+        * 
+        *              <jk>int</jk>[][][] getInt3dArray();
+        *              <jk>void</jk> setInt3dArray(<jk>int</jk>[][][] x);
+        * 
+        *              Map&lt;String,List&lt;MyBean[][][]&gt;&gt; 
getBean1d3dListMap();
+        *              <jk>void</jk> 
setBean1d3dListMap(Map&lt;String,List&lt;MyBean[][][]&gt;&gt; x);
+        *      }
+        * </p>
+        * 
+        * <h5 class='figure'>Example usage</h5>
+        * <p class='bcode'>
+        *      ConfigFile cf = 
ConfigFile.<jsm>create</jsm>().build(<js>"MyConfig.cfg"</js>);
+        * 
+        *      MyConfigInterface ci = 
cf.getSectionAsInterface(<js>"MySection"</js>, 
MyConfigInterface.<jk>class</jk>);
+        * 
+        *      <jk>int</jk> myInt = ci.getInt();
+        * 
+        *      ci.setBean(<jk>new</jk> MyBean());
+        * 
+        *      cf.save();
+        * </p>
+        * 
+        * @param sectionName The section name to retrieve as an interface 
proxy.
+        * @param c The proxy interface class.
+        * @return The proxy interface.
+        */
+       @SuppressWarnings("unchecked")
+       public <T> T getSectionAsInterface(final String sectionName, final 
Class<T> c) {
+               assertFieldNotNull(c, "c");
+
+               if (! c.isInterface())
+                       throw new UnsupportedOperationException("Class passed 
to getSectionAsInterface is not an interface.");
+
+               InvocationHandler h = new InvocationHandler() {
+
+                       @Override
+                       public Object invoke(Object proxy, Method method, 
Object[] args) throws Throwable {
+                               BeanInfo bi = Introspector.getBeanInfo(c, null);
+                               for (PropertyDescriptor pd : 
bi.getPropertyDescriptors()) {
+                                       Method rm = pd.getReadMethod(), wm = 
pd.getWriteMethod();
+                                       if (method.equals(rm))
+                                               return 
Config.this.getObject(sectionName + '/' + pd.getName(), 
rm.getGenericReturnType());
+                                       if (method.equals(wm))
+                                               return 
Config.this.set(sectionName + '/' + pd.getName(), args[0]);
+                               }
+                               throw new 
UnsupportedOperationException("Unsupported interface method.  method=[ " + 
method + " ]");
+                       }
+               };
+
+               return (T)Proxy.newProxyInstance(c.getClassLoader(), new 
Class[] { c }, h);
+       }
+
+       /**
+        * Returns <jk>true</jk> if this section contains the specified key and 
the key has a non-blank value.
+        * 
+        * @param key The key.  See {@link #getString(String)} for a 
description of the key.
+        * @return <jk>true</jk> if this section contains the specified key and 
the key has a non-blank value.
+        */
+       public boolean exists(String key) {
+               return ! isEmpty(getString(key, null));
+       }
+
+       /**
+        * Creates the specified section if it doesn't exist.
+        * 
+        * <p>
+        * Returns the existing section if it already exists.
+        * 
+        * @param name 
+        *      The section name.
+        *      <br>Must not be <jk>null</jk>.
+        *      <br>Use <js>"default"</js> for the default section.
+        * @param preLines 
+        *      Optional comment and blank lines to add immediately before the 
section.
+        *      <br>Can be <jk>null</jk>.
+        * @return The appended or existing section.
+        * @throws UnsupportedOperationException If config file is read only.
+        */
+       public Config setSection(String name, List<String> preLines) {
+               try {
+                       return setSection(name, preLines, null);
+               } catch (SerializeException e) {
+                       throw new RuntimeException(e);  // Impossible.
+               }
+       }
+
+       /**
+        * Creates the specified section if it doesn't exist.
+        * 
+        * @param name 
+        *      The section name.
+        *      <br>Must not be <jk>null</jk>.
+        *      <br>Use <js>"default"</js> for the default section.
+        * @param preLines 
+        *      Optional comment and blank lines to add immediately before the 
section.
+        *      <br>Can be <jk>null</jk>.
+        * @param contents 
+        *      Values to set in the new section.
+        *      <br>Can be <jk>null</jk>.
+        * @return The appended or existing section.
+        * @throws SerializeException 
+        * @throws UnsupportedOperationException If config file is read only.
+        */
+       public Config setSection(String name, List<String> preLines, 
Map<String,Object> contents) throws SerializeException {
+               configMap.setSection(name, preLines);
+               
+               if (contents != null)
+                       for (Map.Entry<String,Object> e : contents.entrySet())
+                               set(e.getKey(), e.getValue());
+               
+               return this;
+       }
+
+       /**
+        * Removes the section with the specified name.
+        * 
+        * @param name The name of the section to remove
+        * @return This object (for method chaining).
+        */
+       public Config removeSection(String name) {
+               configMap.removeSection(name);
+               return this;
+       }
+
+       /**
+        * Saves this config to the store.
+        * 
+        * @return This object (for method chaining).
+        * @throws IOException 
+        */
+       public Config save() throws IOException {
+               configMap.save();
+               return this;
+       }
+
+       /**
+        * Saves this config file to the specified writer as an INI file.
+        * 
+        * <p>
+        * The writer will automatically be closed.
+        * 
+        * @param w The writer to send the output to.
+        * @return This object (for method chaining).
+        * @throws IOException If a problem occurred trying to send contents to 
the writer.
+        */
+       @Override /* Writable */
+       public Writer writeTo(Writer w) throws IOException {
+               return configMap.writeTo(w);
+       }
+
+       /**
+        * Add a listener to this config to react to modification events.
+        * 
+        * <p>
+        * Listeners should be removed using {@link 
#removeListener(ChangeEventListener)}.
+        * 
+        * @param listener The new listener to add.
+        * @return This object (for method chaining).
+        * @throws UnsupportedOperationException If config file is read only.
+        */
+       public Config addListener(ChangeEventListener listener) {
+               listeners.add(listener);
+               return this;
+       }
+       
+       /**
+        * Removes a listener from this config.
+        * 
+        * @param listener The listener to remove.
+        * @return This object (for method chaining).
+        * @throws UnsupportedOperationException If config file is read only.
+        */
+       public Config removeListener(ChangeEventListener listener) {
+               listeners.remove(listener);
+               return this;
+       }
+
+       /**
+        * Unused.
+        */
+       @Override /* Context */
+       public Session createSession(SessionArgs args) {
+               throw new UnsupportedOperationException();
+       }
+
+       /**
+        * Unused.
+        */
+       @Override /* Context */
+       public SessionArgs createDefaultSessionArgs() {
+               throw new UnsupportedOperationException();
+       }
+
+       @Override /* Closeable */
+       public void close() throws IOException {
+               configMap.unregister(this);
+               closed = true;
+       }
+       
+       @Override /* Object */
+       protected void finalize() throws Throwable {
+               if (! closed) {
+                       System.err.println("Config object not closed.");
+               }
+       }
+       
+       @Override /* ChangeEventListener */
+       public void onChange(List<ChangeEvent> events) {
+               for (ChangeEventListener l : listeners)
+                       l.onChange(events);
+       }
+       
+       @Override /* Writable */
+       public MediaType getMediaType() {
+               return MediaType.PLAIN;
+       }
+       
+       
+       
//-----------------------------------------------------------------------------------------------------------------
+       // Private methods
+       
//-----------------------------------------------------------------------------------------------------------------
+       
+       private String serialize(Object value, Serializer serializer) throws 
SerializeException {
+               if (value == null)
+                       return "";
+               if (serializer == null)
+                       serializer = this.serializer;
+               Class<?> c = value.getClass();
+               if (isSimpleType(c))
+                       return value.toString();
+
+               if (value instanceof byte[]) {
+                       String s = null;
+                       byte[] b = (byte[])value;
+                       if ("HEX".equals(binaryFormat))
+                               s = toHex(b);
+                       else if ("SPACED_HEX".equals(binaryFormat))
+                               s = toSpacedHex(b);
+                       else
+                               s = base64Encode(b);
+                       int l = binaryLineLength;
+                       if (l <= 0 || s.length() <= l)
+                               return s;
+                       StringBuilder sb = new StringBuilder();
+                       for (int i = 0; i < s.length(); i += l) 
+                               sb.append('\n').append(s.substring(i, 
Math.min(s.length(), i + l)));
+                       return sb.toString();
+               }
+               
+               String r = null;
+               if (beansOnSeparateLines)
+                       r = "\n" + (String)serializer.serialize(value);
+               else
+                       r = (String)serializer.serialize(value);
+
+               if (r.startsWith("'"))
+                       return r.substring(1, r.length()-1);
+               return r;
+       }
+       
+       @SuppressWarnings({ "unchecked" })
+       private <T> T parse(String s, Parser parser, Type type, Type...args) 
throws ParseException {
+
+               if (isEmpty(s))
+                       return null;
+
+               if (isSimpleType(type))
+                       return (T)beanSession.convertToType(s, (Class<?>)type);
+               
+               if (type == byte[].class) {
+                       if (s.indexOf('\n') != -1)
+                               s = s.replaceAll("\n", "");
+                       switch (binaryFormat) {
+                               case "HEX": return (T)fromHex(s);
+                               case "SPACED_HEX": return (T)fromSpacedHex(s);
+                               default: return (T)base64Decode(s);
+                       }
+               }
+               
+               char s1 = firstNonWhitespaceChar(s);
+               if (isArray(type) && s1 != '[')
+                       s = '[' + s + ']';
+               else if (s1 != '[' && s1 != '{' && ! "null".equals(s))
+                       s = '\'' + s + '\'';
+
+               if (parser == null)
+                       parser = this.parser;
+
+               return parser.parse(s, type, args);
+       }
+
+       private boolean isSimpleType(Type t) {
+               if (! (t instanceof Class))
+                       return false;
+               Class<?> c = (Class<?>)t;
+               return (c == String.class || c.isPrimitive() || 
c.isAssignableFrom(Number.class) || c == Boolean.class || c.isEnum());
+       }
+
+       private boolean isArray(Type t) {
+               if (! (t instanceof Class))
+                       return false;
+               Class<?> c = (Class<?>)t;
+               return (c.isArray());
+       }
+
+       private String sname(String key) {
+               assertFieldNotNull(key, "key");
+               int i = key.indexOf('/');
+               if (i == -1)
+                       return "default";
+               return key.substring(0, i);
+       }
+
+       private String skey(String key) {
+               int i = key.indexOf('/');
+               if (i == -1)
+                       return key;
+               return key.substring(i+1);
+       }
+}
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigBuilder.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigBuilder.java
new file mode 100644
index 0000000..0e01141
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigBuilder.java
@@ -0,0 +1,323 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.config.proto;
+
+import static org.apache.juneau.config.proto.Config.*;
+
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.config.encode.*;
+import org.apache.juneau.config.store.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Builder for creating instances of {@link Config Configs}.
+ * 
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ *     Config cf = Config.<jsm>create</jsm>().build(<js>"MyConfig.cfg"</js>);
+ *     String setting = cf.get(<js>"MySection/mysetting"</js>);
+ * </p>
+ * 
+ * <h5 class='section'>See Also:</h5>
+ * <ul class='doctree'>
+ *     <li class='link'><a class='doclink' 
href='../../../../overview-summary.html#juneau-config'>Overview &gt; 
juneau-config</a>
+ * </ul>
+ */
+public class ConfigBuilder extends ContextBuilder {
+
+       /**
+        * Constructor, default settings.
+        */
+       public ConfigBuilder() {
+               super();
+       }
+
+       /**
+        * Constructor.
+        * 
+        * @param ps The initial configuration settings for this builder.
+        */
+       public ConfigBuilder(PropertyStore ps) {
+               super(ps);
+       }
+
+       @Override /* ContextBuilder */
+       public Config build() {
+               return build(Config.class);
+       }
+
+
+       
//--------------------------------------------------------------------------------
+       // Properties
+       
//--------------------------------------------------------------------------------
+
+       /**
+        * Configuration property:  Configuration name.
+        * 
+        * <p>
+        * Specifies the configuration name.
+        * <br>This is typically the configuration file name minus the file 
extension, although
+        * the name can be anything identifiable by the {@link Store} used for 
retrieving and storing the configuration.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is <js>"Configuration"</js>.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder name(String value) {
+               return set(CONFIG_name, value);
+       }
+
+       /**
+        * Configuration property:  Configuration store.
+        * 
+        * <p>
+        * The configuration store used for retrieving and storing 
configurations.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is {@link FileStore#DEFAULT}.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder store(Store value) {
+               return set(CONFIG_store, value);
+       }
+
+       /**
+        * Configuration property:  POJO serializer.
+        * 
+        * <p>
+        * The serializer to use for serializing POJO values.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is {@link JsonSerializer#DEFAULT_LAX}.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder serializer(WriterSerializer value) {
+               return set(CONFIG_serializer, value);
+       }
+
+       /**
+        * Configuration property:  POJO serializer.
+        * 
+        * <p>
+        * The serializer to use for serializing POJO values.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is {@link JsonSerializer#DEFAULT_LAX}.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder serializer(Class<? extends WriterSerializer> 
value) {
+               return set(CONFIG_serializer, value);
+       }
+
+       /**
+        * Configuration property:  POJO parser.
+        * 
+        * <p>
+        * The parser to use for parsing values to POJOs.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is {@link JsonParser#DEFAULT}.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder parser(ReaderParser value) {
+               return set(CONFIG_parser, value);
+       }
+
+       /**
+        * Configuration property:  POJO parser.
+        * 
+        * <p>
+        * The parser to use for parsing values to POJOs.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is {@link JsonParser#DEFAULT}.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder parser(Class<? extends ReaderParser> value) {
+               return set(CONFIG_parser, value);
+       }
+
+       /**
+        * Configuration property:  Value encoder.
+        * 
+        * <p>
+        * The encoder to use for encoding encoded configuration values.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is {@link XorEncoder#INSTANCE}.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder encoder(Encoder value) {
+               return set(CONFIG_encoder, value);
+       }
+
+       /**
+        * Configuration property:  Value encoder.
+        * 
+        * <p>
+        * The encoder to use for encoding encoded configuration values.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is {@link XorEncoder#INSTANCE}.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder encoder(Class<? extends Encoder> value) {
+               return set(CONFIG_encoder, value);
+       }
+       
+       /**
+        * Configuration property:  SVL variable resolver.
+        * 
+        * <p>
+        * The resolver to use for resolving SVL variables.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is {@link VarResolver#DEFAULT}.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder varResolver(VarResolver value) {
+               return set(CONFIG_varResolver, value);
+       }
+
+       /**
+        * Configuration property:  SVL variable resolver.
+        * 
+        * <p>
+        * The resolver to use for resolving SVL variables.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is {@link VarResolver#DEFAULT}.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder varResolver(Class<? extends VarResolver> value) {
+               return set(CONFIG_varResolver, value);
+       }
+       
+       /**
+        * Configuration property:  Binary value line length.
+        * 
+        * <p>
+        * When serializing binary values, lines will be split after this many 
characters.
+        * <br>Use <code>-1</code> to represent no line splitting.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is <code>-1</code>.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder binaryLineLength(int value) {
+               return set(CONFIG_binaryLineLength, value);
+       }
+       
+       /**
+        * Configuration property:  Binary value format.
+        * 
+        * <p>
+        * The format to use when persisting byte arrays.
+        * 
+        * <p>
+        * Possible values:
+        * <ul>
+        *      <li><js>"BASE64"</js> - BASE64-encoded string.
+        *      <li><js>"HEX"</js> - Hexadecimal.
+        *      <li><js>"SPACED_HEX"</js> - Hexadecimal with spaces between 
bytes.
+        * </ul>
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is <js>"BASE64"</js>.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder binaryFormat(String value) {
+               return set(CONFIG_binaryFormat, value);
+       }
+
+       /**
+        * Configuration property:  Beans on separate lines.
+        * 
+        * <p>
+        * When enabled, serialized POJOs will be placed on a separate line 
from the key.
+        * 
+        * @param value 
+        *      The new value for this property.
+        *      <br>The default is <jk>false</jk>.
+        * @return This object (for method chaining).
+        */
+       public ConfigBuilder beansOnSeparateLines(boolean value) {
+               return set(CONFIG_beansOnSeparateLines, value);
+       }
+       
+       
+       @Override /* ContextBuilder */
+       public ConfigBuilder set(String name, Object value) {
+               super.set(name, value);
+               return this;
+       }
+
+       @Override /* ContextBuilder */
+       public ConfigBuilder set(boolean append, String name, Object value) {
+               super.set(append, name, value);
+               return this;
+       }
+
+       @Override /* ContextBuilder */
+       public ConfigBuilder set(Map<String,Object> properties) {
+               super.set(properties);
+               return this;
+       }
+
+       @Override /* ContextBuilder */
+       public ConfigBuilder add(Map<String,Object> properties) {
+               super.add(properties);
+               return this;
+       }
+
+       @Override /* ContextBuilder */
+       public ConfigBuilder addTo(String name, Object value) {
+               super.addTo(name, value);
+               return this;
+       }
+
+       @Override /* ContextBuilder */
+       public ConfigBuilder addTo(String name, String key, Object value) {
+               super.addTo(name, key, value);
+               return this;
+       }
+
+       @Override /* ContextBuilder */
+       public ConfigBuilder removeFrom(String name, Object value) {
+               super.removeFrom(name, value);
+               return this;
+       }
+
+       @Override /* ContextBuilder */
+       public ConfigBuilder apply(PropertyStore copyFrom) {
+               super.apply(copyFrom);
+               return this;
+       }
+}
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigMod.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigMod.java
new file mode 100644
index 0000000..ff1a46e
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigMod.java
@@ -0,0 +1,103 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              * 
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.config.proto;
+
+import java.util.*;
+
+import org.apache.juneau.internal.*;
+import org.apache.juneau.config.encode.*;
+
+/**
+ * Identifies the supported modification types for config entries.
+ */
+public enum ConfigMod {
+       
+       /**
+        * Encoded using the registered {@link Encoder}.
+        */
+       ENCODED("*");
+       
+       private final String c;
+       
+       private ConfigMod(String c) {
+               this.c = c;
+       }
+       
+       /**
+        * Converts an array of modifiers to a modifier string.
+        * 
+        * @param mods The modifiers.
+        * @return A modifier string, or an empty string if there are no 
modifiers.
+        */
+       public static String asString(ConfigMod...mods) {
+               if (mods.length == 0)
+                       return "";
+               if (mods.length == 1)
+                       return mods[0].c;
+               StringBuilder sb = new StringBuilder(mods.length);
+               for (ConfigMod m : mods)
+                       sb.append(m.c);
+               return sb.toString();
+       }
+       
+       private static ConfigMod fromChar(char c) {
+               if (c == '*')
+                       return ENCODED;
+               return null;
+       }
+       
+       /**
+        * Converts a modifier string (e.g. <js>"^*"</js>) into a list of 
{@link ConfigMod Modifiers} 
+        * in reverse order of how they appear in the string.
+        * 
+        * @param s The modifier string.
+        * @return The list of modifiers, or an empty list if the string is 
empty or <jk>null</jk>.
+        */
+       public static List<ConfigMod> asModifiersReverse(String s) {
+               if (StringUtils.isEmpty(s))
+                       return Collections.emptyList();
+               if (s.length() == 1) {
+                       ConfigMod m = fromChar(s.charAt(0));
+                       return m == null ? Collections.<ConfigMod>emptyList() : 
Collections.singletonList(m);
+               }
+               List<ConfigMod> l = new ArrayList<>(s.length());
+               for (int i = s.length()-1; i >= 0; i--) {
+                       ConfigMod m = fromChar(s.charAt(i));
+                       if (m != null)
+                               l.add(m);
+               }
+               return l;
+       }
+       
+       /**
+        * Converts a modifier string (e.g. <js>"^*"</js>) into a list of 
{@link ConfigMod Modifiers}.
+        * 
+        * @param s The modifier string.
+        * @return The list of modifiers, or an empty list if the string is 
empty or <jk>null</jk>.
+        */
+       public static List<ConfigMod> asModifiers(String s) {
+               if (StringUtils.isEmpty(s))
+                       return Collections.emptyList();
+               if (s.length() == 1) {
+                       ConfigMod m = fromChar(s.charAt(0));
+                       return m == null ? Collections.<ConfigMod>emptyList() : 
Collections.singletonList(m);
+               }
+               List<ConfigMod> l = new ArrayList<>(s.length());
+               for (int i = 0; i < s.length(); i++) {
+                       ConfigMod m = fromChar(s.charAt(i));
+                       if (m != null)
+                               l.add(m);
+               }
+               return l;
+       }
+}
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigEntry.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigEntry.java
index 2c4dbe8..7264d64 100644
--- 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigEntry.java
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigEntry.java
@@ -109,34 +109,43 @@ public class ConfigEntry {
        public boolean hasModifier(char m) {
                return modifiers.indexOf(m) != -1;
        }
+
+       /**
+        * Returns the modifiers for this entry.
+        * 
+        * @return The modifiers for this entry, or an empty string if it has 
no modifiers.
+        */
+       public String getModifiers() {
+               return modifiers;
+       }
        
-       Writer writeTo(Writer out) throws IOException {
+       Writer writeTo(Writer w) throws IOException {
                if (value == null)
-                       return out;
+                       return w;
                for (String pl : preLines)
-                       out.append(pl).append('\n');
+                       w.append(pl).append('\n');
                if (rawLine != null) {
                        String l = rawLine;
                        if (l.indexOf('\n') != -1)
                                l = l.replaceAll("(\\r?\\n)", "$1\t");
-                       out.append(l).append('\n');
+                       w.append(l).append('\n');
                } else {
-                       out.append(key);
-                       out.append(modifiers);
-                       out.append(" = ");
+                       w.append(key);
+                       w.append(modifiers);
+                       w.append(" = ");
                        
                        String val = value;
                        if (val.indexOf('\n') != -1)
                                val = val.replaceAll("(\\r?\\n)", "$1\t");
                        if (val.indexOf('#') != -1)
                                val = val.replaceAll("#", "\\\\#");
-                       out.append(val);
+                       w.append(val);
                                
                        if (comment != null) 
-                               out.append(" # ").append(comment);
+                               w.append(" # ").append(comment);
 
-                       out.append('\n');
+                       w.append('\n');
                }
-               return out;
+               return w;
        }
 }
\ No newline at end of file
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMap.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMap.java
index f83388e..4756127 100644
--- 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMap.java
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMap.java
@@ -21,12 +21,13 @@ import java.util.concurrent.locks.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.config.event.*;
+import org.apache.juneau.http.*;
 import org.apache.juneau.internal.*;
 
 /**
  * Represents the parsed contents of a configuration.
  */
-public class ConfigMap implements StoreListener {
+public class ConfigMap implements StoreListener, Writable {
 
        private final Store store;               // The store that created this 
object.
        private volatile String contents;        // The original contents of 
this object.
@@ -210,6 +211,19 @@ public class ConfigMap implements StoreListener {
                }
        }
 
+       /**
+        * Returns the keys of the entries in the specified section.
+        * 
+        * @param section 
+        *      The section name.
+        *      <br>Must not be <jk>null</jk>.
+        * @return
+        *      An unmodifiable set of keys, or <jk>null</jk> if the section 
doesn't exist.
+        */
+       public Set<String> getKeys(String section) {
+               ConfigSection cs = entries.get(section);
+               return cs == null ? null : 
Collections.unmodifiableSet(cs.entries.keySet());
+       }
        
        
//-----------------------------------------------------------------------------------------------------------------
        // Setters
@@ -242,7 +256,7 @@ public class ConfigMap implements StoreListener {
        public ConfigMap removeSection(String section) {
                return applyChange(true, ChangeEvent.removeSection(section));
        }
-       
+               
        /**
         * Sets the pre-lines on an entry without modifying any other 
attributes.
         * 
@@ -433,7 +447,7 @@ public class ConfigMap implements StoreListener {
         * @param listener The new listener.
         * @return This object (for method chaining).
         */
-       public ConfigMap registerListener(ChangeEventListener listener) {
+       public ConfigMap register(ChangeEventListener listener) {
                listeners.add(listener);
                return this;
        }
@@ -444,7 +458,7 @@ public class ConfigMap implements StoreListener {
         * @param listener The listener to remove.
         * @return This object (for method chaining).
         */
-       public ConfigMap unregisterListener(ChangeEventListener listener) {
+       public ConfigMap unregister(ChangeEventListener listener) {
                listeners.remove(listener);
                return this;
        }
@@ -469,9 +483,86 @@ public class ConfigMap implements StoreListener {
                        signal(changes);
        }
        
+       @Override /* Object */
+       public String toString() {
+               readLock();
+               try {
+                       return asString();
+               } finally {
+                       readUnlock();
+               }
+       }
+
+       @Override /* Writable */
+       public Writer writeTo(Writer w) throws IOException {
+               for (ConfigSection cs : entries.values())
+                       cs.writeTo(w);
+               return w;
+       }
+
+       @Override /* Writable */
+       public MediaType getMediaType() {
+               return MediaType.PLAIN;
+       }
+
+       
+       
//--------------------------------------------------------------------------------
+       // Private methods
+       
//--------------------------------------------------------------------------------
+
+       private void readLock() {
+               lock.readLock().lock();
+       }
+
+       private void readUnlock() {
+               lock.readLock().unlock();
+       }
+
+       private void writeLock() {
+               lock.writeLock().lock();
+       }
+
+       private void writeUnlock() {
+               lock.writeLock().unlock();
+       }
+
+       private boolean isValidSectionName(String s) {
+               return "default".equals(s) || isValidNewSectionName(s);
+       }
+       
+       private boolean isValidKeyName(String s) {
+               if (s == null)
+                       return false;
+               s = s.trim();
+               if (s.isEmpty())
+                       return false;
+               for (int i = 0; i < s.length(); i++) {
+                       char c = s.charAt(i);
+                       if (c == '/' || c == '\\' || c == '[' || c == ']' || c 
== '=' || c == '#')
+                               return false;
+               }
+               return true;
+       }
+
+       private boolean isValidNewSectionName(String s) {
+               if (s == null)
+                       return false;
+               s = s.trim();
+               if (s.isEmpty())
+                       return false;
+               if ("default".equals(s))
+                       return false;
+               for (int i = 0; i < s.length(); i++) {
+                       char c = s.charAt(i);
+                       if (c == '/' || c == '\\' || c == '[' || c == ']')
+                               return false;
+               }
+               return true;
+       }
+
        private void signal(List<ChangeEvent> changes) {
                for (ChangeEventListener l : listeners)
-                       l.onEvents(changes);
+                       l.onChange(changes);
        }
 
        private List<ChangeEvent> findDiffs(String updatedContents) {
@@ -510,6 +601,18 @@ public class ConfigMap implements StoreListener {
                return changes;
        }
 
+       // This method should only be called from behind a lock.
+       private String asString() {
+               try {
+                       StringWriter sw = new StringWriter();
+                       for (ConfigSection cs : entries.values())
+                               cs.writeTo(sw);
+                       return sw.toString();
+               } catch (IOException e) {
+                       throw new RuntimeException(e);  // Not possible.
+               }
+       }
+       
        
        
//---------------------------------------------------------------------------------------------
        // ConfigSection
@@ -586,97 +689,22 @@ public class ConfigMap implements StoreListener {
                        return this;
                }
                
-               Writer writeTo(Writer out) throws IOException {
+               Writer writeTo(Writer w) throws IOException {
                        for (String s : preLines)
-                               out.append(s).append('\n');
+                               w.append(s).append('\n');
                        
                        if (! name.equals("default"))
-                               out.append(rawLine).append('\n');
+                               w.append(rawLine).append('\n');
                        else {
                                // Need separation between default prelines and 
first-entry prelines.
                                if (! preLines.isEmpty())
-                                       out.append('\n');
+                                       w.append('\n');
                        }
 
                        for (ConfigEntry e : entries.values()) 
-                               e.writeTo(out);
+                               e.writeTo(w);
                        
-                       return out;
-               }
-       }
-
-       @Override /* Object */
-       public String toString() {
-               readLock();
-               try {
-                       return asString();
-               } finally {
-                       readUnlock();
+                       return w;
                }
        }
-
-       String asString() {
-               try {
-                       StringWriter sw = new StringWriter();
-                       for (ConfigSection cs : entries.values())
-                               cs.writeTo(sw);
-                       return sw.toString();
-               } catch (IOException e) {
-                       throw new RuntimeException(e);  // Not possible.
-               }
-       }
-       
-       private boolean isValidNewSectionName(String s) {
-               if (s == null)
-                       return false;
-               s = s.trim();
-               if (s.isEmpty())
-                       return false;
-               if ("default".equals(s))
-                       return false;
-               for (int i = 0; i < s.length(); i++) {
-                       char c = s.charAt(i);
-                       if (c == '/' || c == '\\' || c == '[' || c == ']')
-                               return false;
-               }
-               return true;
-       }
-
-       private boolean isValidSectionName(String s) {
-               return "default".equals(s) || isValidNewSectionName(s);
-       }
-       
-       private boolean isValidKeyName(String s) {
-               if (s == null)
-                       return false;
-               s = s.trim();
-               if (s.isEmpty())
-                       return false;
-               for (int i = 0; i < s.length(); i++) {
-                       char c = s.charAt(i);
-                       if (c == '/' || c == '\\' || c == '[' || c == ']' || c 
== '=' || c == '#')
-                               return false;
-               }
-               return true;
-       }
-
-       
//--------------------------------------------------------------------------------
-       // Private methods
-       
//--------------------------------------------------------------------------------
-
-       void readLock() {
-               lock.readLock().lock();
-       }
-
-       void readUnlock() {
-               lock.readLock().unlock();
-       }
-
-       void writeLock() {
-               lock.writeLock().lock();
-       }
-
-       void writeUnlock() {
-               lock.writeLock().unlock();
-       }
 }
diff --git 
a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapListenerTest.java
 
b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapListenerTest.java
index bec142e..5e575d3 100644
--- 
a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapListenerTest.java
+++ 
b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapListenerTest.java
@@ -45,12 +45,12 @@ public class ConfigMapListenerTest {
                };
                
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setValue("default", "foo", "baz");
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("foo = baz|", cm.toString());
        }
@@ -72,12 +72,12 @@ public class ConfigMapListenerTest {
                };
                
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setValue("S1", "foo", "baz");
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("[S1]|foo = baz|", cm.toString());
        }
@@ -101,13 +101,13 @@ public class ConfigMapListenerTest {
                };
 
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setValue("default", "k", "vb");
                cm.setValue("S1", "k1", "v1b");
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("k = vb|[S1]|k1 = v1b|", cm.toString());
        }
@@ -127,13 +127,13 @@ public class ConfigMapListenerTest {
                };
 
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setEntry("default", "k", "kb", "^*", "C", 
Arrays.asList("#k"));
                cm.setEntry("S1", "k1", "k1b", "^*", "C1", 
Arrays.asList("#k1"));
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("#k|k^* = kb # C|[S1]|#k1|k1^* = k1b # C1|", 
cm.toString());
        }
@@ -159,13 +159,13 @@ public class ConfigMapListenerTest {
                };
 
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setEntry("default", "k", "kb", "^*", "Cb", 
Arrays.asList("#kb"));
                cm.setEntry("S1", "k1", "k1b", "^*", "Cb1", 
Arrays.asList("#k1b"));
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("#kb|k^* = kb # Cb|#S1|[S1]|#k1b|k1^* = k1b # 
Cb1|", cm.toString());
        }
@@ -192,13 +192,13 @@ public class ConfigMapListenerTest {
                };
 
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setValue("default", "k", null);
                cm.setValue("S1", "k1", null);
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("[S1]|", cm.toString());
        }
@@ -224,13 +224,13 @@ public class ConfigMapListenerTest {
                };
 
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setValue("default", "k", null);
                cm.setValue("S1", "k1", null);
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("#S1|[S1]|", cm.toString());
        }
@@ -254,7 +254,7 @@ public class ConfigMapListenerTest {
                };
 
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setSection("default", Arrays.asList("#D1"));
                cm.setSection("S1", Arrays.asList("#S1"));
                cm.setSection("S2", null);
@@ -263,7 +263,7 @@ public class ConfigMapListenerTest {
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("#D1||#S1|[S1]|[S2]|[S3]|k3 = v3|", 
cm.toString());
        }
@@ -289,7 +289,7 @@ public class ConfigMapListenerTest {
                };
 
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setSection("default", Arrays.asList("#Db"));
                cm.setSection("S1", Arrays.asList("#S1b"));
                cm.setSection("S2", null);
@@ -298,7 +298,7 @@ public class ConfigMapListenerTest {
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("#Db||#S1b|[S1]|[S2]|[S3]|k3 = v3|", 
cm.toString());
        }
@@ -334,7 +334,7 @@ public class ConfigMapListenerTest {
                };
 
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.removeSection("default");
                cm.removeSection("S1");
                cm.removeSection("S2");
@@ -342,7 +342,7 @@ public class ConfigMapListenerTest {
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("", cm.toString());
        }
@@ -365,7 +365,7 @@ public class ConfigMapListenerTest {
                };
                
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                s.update("Foo",
                        "#Da",
                        "",
@@ -382,7 +382,7 @@ public class ConfigMapListenerTest {
                );
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("#Da||k = v # cv||#S1|[S1]|#k1|k1 = v1 # 
cv1|[S2]|#k2|k2 = v2 # cv2|[S3]|", cm.toString());
        }
@@ -411,7 +411,7 @@ public class ConfigMapListenerTest {
                };
                
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setValue("S2", "k2", "v2b");
                s.update("Foo",
                        "[S1]",
@@ -420,7 +420,7 @@ public class ConfigMapListenerTest {
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("[S1]|k1 = v1b|[S2]|k2 = v2b|", cm.toString());
        }
@@ -449,7 +449,7 @@ public class ConfigMapListenerTest {
                };
                
                ConfigMap cm = s.getMap("Foo");
-               cm.registerListener(l);
+               cm.register(l);
                cm.setValue("S1", "k1", "v1c");
                s.update("Foo",
                        "[S1]",
@@ -458,7 +458,7 @@ public class ConfigMapListenerTest {
                cm.save();
                wait(latch);
                assertNull(l.error);
-               cm.unregisterListener(l);
+               cm.unregister(l);
                
                assertTextEquals("[S1]|k1 = v1c|", cm.toString());
        }
@@ -495,12 +495,12 @@ public class ConfigMapListenerTest {
                        };
                        
                        ConfigMap cm = s.getMap("Foo");
-                       cm.registerListener(l);
+                       cm.register(l);
                        cm.setValue("S1", "k1", "v1c");
                        cm.save();
                        wait(latch);
                        assertNull(l.error);
-                       cm.unregisterListener(l);
+                       cm.unregister(l);
                        
                        assertTextEquals("[S1]|k1 = v1c|", cm.toString());
                        
@@ -540,7 +540,7 @@ public class ConfigMapListenerTest {
                        };
                        
                        ConfigMap cm = s.getMap("Foo");
-                       cm.registerListener(l);
+                       cm.register(l);
                        cm.setValue("S1", "k1", "v1c");
                        try {
                                cm.save();
@@ -550,7 +550,7 @@ public class ConfigMapListenerTest {
                        }
                        wait(latch);
                        assertNull(l.error);
-                       cm.unregisterListener(l);
+                       cm.unregister(l);
                        
                        assertTextEquals("[S1]|k1 = v1c|", cm.toString());
                        
@@ -575,7 +575,7 @@ public class ConfigMapListenerTest {
                }
                
                @Override
-               public void onEvents(List<ChangeEvent> events) {
+               public void onChange(List<ChangeEvent> events) {
                        try {
                                check(events);
                        } catch (Exception e) {
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Writable.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Writable.java
index bf507c8..fe926c0 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Writable.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/Writable.java
@@ -28,9 +28,10 @@ public interface Writable {
         * Serialize this object to the specified writer.
         * 
         * @param w The writer to write to.
+        * @return The same writer passed in.
         * @throws IOException
         */
-       void writeTo(Writer w) throws IOException;
+       Writer writeTo(Writer w) throws IOException;
 
        /**
         * Returns the serialized media type for this resource (e.g. 
<js>"text/html"</js>)
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
index b71a6a7..624d9b3 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/StringUtils.java
@@ -1579,6 +1579,8 @@ public final class StringUtils {
        public static List<String> splitEqually(String s, int size) {
                if (s == null)
                        return null;
+               if (size <= 0) 
+                       return Collections.singletonList(s);
                
                List<String> l = new ArrayList<>((s.length() + size - 1) / 
size);
 
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringMessage.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringMessage.java
index 17ed8fa..58ea67d 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringMessage.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringMessage.java
@@ -45,9 +45,9 @@ public class StringMessage implements CharSequence, Writable {
        }
 
        @Override /* Writable */
-       public void writeTo(Writer w) throws IOException {
+       public Writer writeTo(Writer w) throws IOException {
                w.write(toString());
-
+               return w;
        }
 
        @Override /* Writable */
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringObject.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringObject.java
index 2982db6..18546ab 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringObject.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/utils/StringObject.java
@@ -83,9 +83,10 @@ public class StringObject implements CharSequence, Writable {
        }
 
        @Override /* Writable */
-       public void writeTo(Writer w) throws IOException {
+       public Writer writeTo(Writer w) throws IOException {
                try {
                        s.serialize(o, w);
+                       return w;
                } catch (SerializeException e) {
                        throw new IOException(e);
                }
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java
index 22275ad..c235cbd 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/ReaderResource.java
@@ -114,13 +114,14 @@ public class ReaderResource implements Writable {
        }
 
        @Override /* Writeable */
-       public void writeTo(Writer w) throws IOException {
+       public Writer writeTo(Writer w) throws IOException {
                for (String s : contents) {
                        if (varSession != null)
                                varSession.resolveTo(s, w);
                        else
                                w.write(s);
                }
+               return w;
        }
 
        @Override /* Writeable */

-- 
To stop receiving notification emails like this one, please contact
[email protected].

Reply via email to