juneau-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jamesbog...@apache.org
Subject [juneau] branch master updated: Config API changes.
Date Sun, 25 Feb 2018 20:21:00 GMT
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 b58ff54  Config API changes.
b58ff54 is described below

commit b58ff54748d07c333d115c1417925d90e2e358bb
Author: JamesBognar <jamesbognar@apache.org>
AuthorDate: Sun Feb 25 15:20:51 2018 -0500

    Config API changes.
---
 .../apache/juneau/config/{proto => }/Config.java   |  554 +++--
 .../juneau/config/{proto => }/ConfigBuilder.java   |   54 +-
 .../java/org/apache/juneau/config/ConfigFile.java  | 1242 -----------
 .../apache/juneau/config/ConfigFileBuilder.java    |  333 ---
 .../org/apache/juneau/config/ConfigFileFormat.java |   30 -
 .../org/apache/juneau/config/ConfigFileImpl.java   |  824 --------
 .../apache/juneau/config/ConfigFileWrapped.java    |  294 ---
 .../juneau/config/{proto => }/ConfigMod.java       |    5 +-
 .../java/org/apache/juneau/config/ConfigUtils.java |  104 -
 .../java/org/apache/juneau/config/Section.java     |  577 ------
 .../encode/{Encoder.java => ConfigEncoder.java}    |   14 +-
 .../{XorEncoder.java => ConfigXorEncoder.java}     |   20 +-
 .../event/{ChangeEvent.java => ConfigEvent.java}   |   56 +-
 ...EventListener.java => ConfigEventListener.java} |    4 +-
 .../{ChangeEventType.java => ConfigEventType.java} |    9 +-
 .../config/{listener => event}/package-info.java   |    4 +-
 .../juneau/config/listener/ConfigListener.java     |   50 -
 .../juneau/config/listener/EntryListener.java      |   52 -
 .../juneau/config/listener/SectionListener.java    |   68 -
 .../apache/juneau/config/store/ConfigEntry.java    |    8 +-
 .../store/{FileStore.java => ConfigFileStore.java} |  122 +-
 ...oreBuilder.java => ConfigFileStoreBuilder.java} |   87 +-
 .../org/apache/juneau/config/store/ConfigMap.java  |  393 ++--
 .../{MemoryStore.java => ConfigMemoryStore.java}   |   40 +-
 ...eBuilder.java => ConfigMemoryStoreBuilder.java} |   12 +-
 .../config/store/{Store.java => ConfigStore.java}  |   47 +-
 .../{StoreBuilder.java => ConfigStoreBuilder.java} |   10 +-
 ...StoreListener.java => ConfigStoreListener.java} |    5 +-
 .../juneau/config/store/WatcherSensitivity.java    |    2 +-
 .../vars/{ConfigFileVar.java => ConfigVar.java}    |   16 +-
 .../java/org/apache/juneau/JacocoDummyTest.java    |    4 +-
 .../apache/juneau/config/ConfigBuilderTest.java}   |   63 +-
 .../juneau/config/ConfigFileBuilderTest.java       |  198 --
 .../org/apache/juneau/config/ConfigFileTest.java   | 2157 --------------------
 ...InterfaceTest.java => ConfigInterfaceTest.java} |  102 +-
 .../config/{proto => }/ConfigMapListenerTest.java  |  142 +-
 .../juneau/config/{proto => }/ConfigMapTest.java   |  467 ++---
 .../java/org/apache/juneau/config/ConfigTest.java  | 1740 ++++++++++++++++
 .../apache/juneau/config/store/FileStoreTest.java  |   84 +-
 .../juneau/config/store/MemoryStoreTest.java       |   51 +-
 .../src/main/java/org/apache/juneau/Context.java   |    3 +-
 .../org/apache/juneau/internal/ThrowableUtils.java |    4 +-
 .../org/apache/juneau/svl/VarResolverSession.java  |    1 +
 juneau-doc/src/main/javadoc/overview.html          |  243 +--
 .../juneau-examples-rest/build-overlay/pom.xml     |    2 +-
 juneau-examples/juneau-examples-rest/examples.cfg  |    2 +-
 juneau-examples/juneau-examples-rest/pom.xml       |    2 +-
 .../examples/rest/DockerRegistryResource.java      |    2 +-
 .../juneau/examples/rest/SqlQueryResource.java     |    2 +-
 .../apache/juneau/microservice/Microservice.java   |   84 +-
 .../juneau/microservice/RestMicroservice.java      |    9 +-
 .../microservice/resources/ConfigResource.java     |   22 +-
 .../microservice/resources/DebugResource.java      |    2 +-
 .../microservice/resources/LogsResource.java       |   10 +-
 .../build-overlay/pom.xml                          |    2 +-
 .../my-microservice.cfg                            |    2 +-
 .../juneau-microservice-test.cfg                   |    1 -
 .../apache/juneau/rest/test/ConfigResource.java    |    7 +-
 .../org/apache/juneau/rest/test/GzipResource.java  |    4 +-
 .../apache/juneau/rest/test/HeadersResource.java   |    2 +-
 .../apache/juneau/rest/test/ParamsResource.java    |    4 +-
 .../apache/juneau/rest/test/AcceptCharsetTest.java |    2 -
 .../org/apache/juneau/rest/test/ConfigTest.java    |   11 +-
 .../org/apache/juneau/rest/test/ParamsTest.java    |    4 +-
 .../apache/juneau/rest/RestCallHandlerDefault.java |    3 +-
 .../java/org/apache/juneau/rest/RestContext.java   |   10 +-
 .../org/apache/juneau/rest/RestContextBuilder.java |   46 +-
 .../org/apache/juneau/rest/RestJavaMethod.java     |    2 +-
 .../java/org/apache/juneau/rest/RestParam.java     |    2 +-
 .../org/apache/juneau/rest/RestParamDefaults.java  |   10 +-
 .../java/org/apache/juneau/rest/RestRequest.java   |   22 +-
 .../apache/juneau/rest/annotation/HookEvent.java   |    2 +-
 .../juneau/rest/annotation/ResourceSwagger.java    |   12 +-
 .../juneau/rest/annotation/RestResource.java       |    2 +-
 74 files changed, 3388 insertions(+), 7194 deletions(-)

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/Config.java
similarity index 72%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/Config.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Config.java
index 3b1b30f..9d99aa1 100644
--- 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/Config.java
@@ -10,12 +10,11 @@
 // * "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;
+package org.apache.juneau.config;
 
+import static org.apache.juneau.config.ConfigMod.*;
 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.*;
@@ -24,11 +23,12 @@ 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.encode.ConfigEncoder;
 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.internal.*;
 import org.apache.juneau.json.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.serializer.*;
@@ -37,7 +37,7 @@ import org.apache.juneau.svl.*;
 /**
  * TODO
  */
-public final class Config extends Context implements ChangeEventListener, Closeable, Writable {
+public final class Config extends Context implements ConfigEventListener, Writable {
 
 	//-------------------------------------------------------------------------------------------------------------------
 	// Configurable properties
@@ -52,7 +52,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * <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>Default:</b>  <js>"Configuration.cfg"</js>
 	 * 	<li><b>Methods:</b> 
 	 * 		<ul>
 	 * 			<li class='jm'>{@link ConfigBuilder#name(String)}
@@ -62,8 +62,8 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * <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.
+	 * <br>This is typically the configuration file name, although
+	 * the name can be anything identifiable by the {@link ConfigStore} used for retrieving and storing the configuration.
 	 */
 	public static final String CONFIG_name = PREFIX + "name.s";
 
@@ -73,11 +73,11 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * <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>Data type:</b>  {@link ConfigStore}
+	 * 	<li><b>Default:</b>  {@link ConfigFileStore#DEFAULT}
 	 * 	<li><b>Methods:</b> 
 	 * 		<ul>
-	 * 			<li class='jm'>{@link ConfigBuilder#store(Store)}
+	 * 			<li class='jm'>{@link ConfigBuilder#store(ConfigStore)}
 	 * 		</ul>
 	 * </ul>
 	 * 
@@ -135,12 +135,12 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * <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>Data type:</b>  {@link ConfigEncoder}
+	 * 	<li><b>Default:</b>  {@link ConfigXorEncoder#INSTANCE}
 	 * 	<li><b>Methods:</b> 
 	 * 		<ul>
 	 * 			<li class='jm'>{@link ConfigBuilder#encoder(Class)}
-	 * 			<li class='jm'>{@link ConfigBuilder#encoder(Encoder)}
+	 * 			<li class='jm'>{@link ConfigBuilder#encoder(ConfigEncoder)}
 	 * 		</ul>
 	 * </ul>
 	 * 
@@ -230,7 +230,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 	<li><b>Default:</b>  <jk>false</jk>
 	 * 	<li><b>Methods:</b> 
 	 * 		<ul>
-	 * 			<li class='jm'>{@link ConfigBuilder#beansOnSeparateLines(boolean)}
+	 * 			<li class='jm'>{@link ConfigBuilder#beansOnSeparateLines()}
 	 * 		</ul>
 	 * </ul>
 	 * 
@@ -240,24 +240,43 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 */
 	public static final String CONFIG_beansOnSeparateLines = PREFIX + "beansOnSeparateLines.b";
 	
-	
+	/**
+	 * Configuration property:  Read-only.
+	 * 
+	 * <h5 class='section'>Property:</h5>
+	 * <ul>
+	 * 	<li><b>Name:</b>  <js>"Config.readOnly.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#readOnly()}
+	 * 		</ul>
+	 * </ul>
+	 * 
+	 * <h5 class='section'>Description:</h5>
+	 * <p>
+	 * When enabled, attempts to call any setters on this object will throw an {@link UnsupportedOperationException}.
+	 */
+	public static final String CONFIG_readOnly = PREFIX + "readOnly.b";
+
 	//-------------------------------------------------------------------------------------------------------------------
 	// Instance
 	//-------------------------------------------------------------------------------------------------------------------
 
 	private final String name;
-	private final Store store;
+	private final ConfigStore store;
 	private final WriterSerializer serializer;
 	private final ReaderParser parser;
-	private final Encoder encoder;
+	private final ConfigEncoder encoder;
 	private final VarResolverSession varSession;
 	private final int binaryLineLength;
 	private final String binaryFormat;
-	private final boolean beansOnSeparateLines;
+	private final boolean beansOnSeparateLines, readOnly;
 	private final ConfigMap configMap;
 	private final BeanSession beanSession;
 	private volatile boolean closed;
-	private final List<ChangeEventListener> listeners = Collections.synchronizedList(new LinkedList<ChangeEventListener>());
+	private final List<ConfigEventListener> listeners = Collections.synchronizedList(new LinkedList<ConfigEventListener>());
 
 
 	/**
@@ -287,23 +306,24 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	public Config(PropertyStore ps) throws IOException {
 		super(ps);
 		
-		name = getStringProperty(CONFIG_name, "Configuration");
-		store = getInstanceProperty(CONFIG_store, Store.class, FileStore.DEFAULT);
+		name = getStringProperty(CONFIG_name, "Configuration.cfg");
+		store = getInstanceProperty(CONFIG_store, ConfigStore.class, ConfigFileStore.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);
+		encoder = getInstanceProperty(CONFIG_encoder, ConfigEncoder.class, ConfigXorEncoder.INSTANCE);
 		varSession = getInstanceProperty(CONFIG_varResolver, VarResolver.class, VarResolver.DEFAULT)
 			.builder()
-			.vars(ConfigFileVar.class)
-			.contextObject(ConfigFileVar.SESSION_config, this)
+			.vars(ConfigVar.class)
+			.contextObject(ConfigVar.SESSION_config, this)
 			.build()
 			.createSession();
 		binaryLineLength = getIntegerProperty(CONFIG_binaryLineLength, -1);
 		binaryFormat = getStringProperty(CONFIG_binaryFormat, "BASE64").toUpperCase();
 		beansOnSeparateLines = getBooleanProperty(CONFIG_beansOnSeparateLines, false);
+		readOnly = getBooleanProperty(CONFIG_readOnly, false);
 	}
 	
 	Config(Config copyFrom, VarResolverSession varSession) { 
@@ -319,6 +339,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 		binaryLineLength = copyFrom.binaryLineLength;
 		binaryFormat = copyFrom.binaryFormat;
 		beansOnSeparateLines = copyFrom.beansOnSeparateLines;
+		readOnly = copyFrom.readOnly;
 		beanSession = copyFrom.beanSession;
 	}
 	
@@ -353,6 +374,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 		
 		String sname = sname(key);
 		String skey = skey(key); 
+		
 		ConfigEntry ce = configMap.getEntry(sname, skey);
 		
 		if (ce == null || ce.getValue() == null)
@@ -361,7 +383,8 @@ public final class Config extends Context implements ChangeEventListener, Closea
 		String val = ce.getValue();
 		for (ConfigMod m : ConfigMod.asModifiersReverse(ce.getModifiers())) {
 			if (m == ENCODED) {
-				val = encoder.decode(key, val);
+				if (encoder.isEncoded(val))
+					val = encoder.decode(key, val);
 			}
 		}
 		
@@ -379,21 +402,28 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * @param key The key.  
 	 * @param value The value.
 	 * @return This object (for method chaining).
+	 * @throws UnsupportedOperationException If configuration is read only.
 	 */
 	public Config set(String key, String value) {
+		checkWrite();
 		assertFieldNotNull(key, "key");
 		String sname = sname(key);
 		String skey = skey(key); 
+		
 		ConfigEntry ce = configMap.getEntry(sname, skey);
+		if (ce == null && value == null)
+			return this;
+		
+		String mod = ce == null ? "" : ce.getModifiers();
 		
 		String s = asString(value);
-		for (ConfigMod m : ConfigMod.asModifiers(ce.getModifiers())) {
+		for (ConfigMod m : ConfigMod.asModifiers(mod)) {
 			if (m == ENCODED) {
-				value = encoder.encode(key, s);
+				s = encoder.encode(key, s);
 			}
 		}
-		
-		configMap.setValue(sname, skey, s);
+
+		configMap.setEntry(sname, skey, s, null, null, null);
 		return this;
 	}
 
@@ -409,7 +439,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * @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.
+	 * @throws UnsupportedOperationException If configuration is read only.
 	 */
 	public Config set(String key, Object value) throws SerializeException {
 		return set(key, value, null);
@@ -427,7 +457,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * @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.
+	 * @throws UnsupportedOperationException If configuration is read only.
 	 */
 	public Config set(String key, Object value, Serializer serializer) throws SerializeException {
 		return set(key, serialize(value, serializer));
@@ -441,34 +471,62 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * @param serializer
 	 * 	The serializer to use for serializing the object.
 	 * 	If <jk>null</jk>, then uses the predefined serializer on the config file.
+	 * @param modifier 
+	 * 	Optional modifier to apply to the value.
+	 * 	<br>If <jk>null</jk>, then previous value will not be replaced.
+	 * @param comment 
+	 * 	Optional same-line comment to add to this value.
+	 * 	<br>If <jk>null</jk>, then previous value will not be replaced.
+	 * @param preLines 
+	 * 	Optional comment or blank lines to add before this entry.
+	 * 	<br>If <jk>null</jk>, then previous value will not be replaced.
+	 * @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 configuration is read only.
+	 */
+	public Config set(String key, Object value, Serializer serializer, ConfigMod modifier, String comment, List<String> preLines) throws SerializeException {
+		return set(key, value, serializer, modifier == null ? null : new ConfigMod[]{modifier}, comment, preLines);
+	}
+
+	/**
+	 * 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>.
+	 * 	<br>If <jk>null</jk>, then previous value will not be replaced.
 	 * @param comment 
 	 * 	Optional same-line comment to add to this value.
-	 * 	<br>Can be <jk>null</jk>.
+	 * 	<br>If <jk>null</jk>, then previous value will not be replaced.
 	 * @param preLines 
 	 * 	Optional comment or blank lines to add before this entry.
-	 * 	<br>Can be <jk>null</jk>.
+	 * 	<br>If <jk>null</jk>, then previous value will not be replaced.
 	 * @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.
+	 * @throws UnsupportedOperationException If configuration is read only.
 	 */
 	public Config set(String key, Object value, Serializer serializer, ConfigMod[] modifiers, String comment, List<String> preLines) throws SerializeException {
+		checkWrite();
 		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);
+		if (modifiers != null) {
+			for (ConfigMod m : modifiers) {
+				if (m == ENCODED) {
+					s = encoder.encode(key, s);
+				}
 			}
 		}
 		
-		configMap.setEntry(sname, skey, s, ConfigMod.asString(modifiers), comment, preLines);
+		configMap.setEntry(sname, skey, s, modifiers == null ? null : ConfigMod.asString(modifiers), comment, preLines);
 		return this;
 	}
 
@@ -477,12 +535,39 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 
 	 * @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.
+	 * @throws UnsupportedOperationException If configuration is read only.
 	 */
 	public Config remove(String key) {
-		return set(key, null);
+		checkWrite();
+		String sname = sname(key);
+		String skey = skey(key); 
+		configMap.removeEntry(sname, skey);
+		return this;
 	}
 	
+	/**
+	 * Encodes and unencoded entries in this config.
+	 * 
+	 * <p>
+	 * If any entries in the config are marked as encoded but not actually encoded,
+	 * this will encode them.
+	 * 
+	 * @return This object (for method chaining).
+	 * @throws UnsupportedOperationException If configuration is read only.
+	 */
+	public Config encodeEntries() {
+		checkWrite();
+		for (String section : configMap.getSections()) {
+			for (String key : configMap.getKeys(section)) {
+				ConfigEntry ce = configMap.getEntry(section, key);
+				if (ce != null && ce.hasModifier('*') && ! encoder.isEncoded(ce.getValue())) {
+					configMap.setEntry(section, key, encoder.encode(section + '/' + key, ce.getValue()), null, null, null);
+				}
+			}
+		}
+		
+		return this;
+	}
 	
 	//--------------------------------------------------------------------------------
 	// API methods
@@ -504,7 +589,12 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * @return The value, or <jk>null</jk> if the section or key does not exist.
 	 */
 	public String getString(String key) {
-		return getString(key, null);
+		String s = get(key);
+		if (s == null)
+			return null;
+		if (varSession != null)
+			s = varSession.resolve(s);
+		return s;
 	}
 
 	/**
@@ -525,7 +615,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 */
 	public String getString(String key, String def) {
 		String s = get(key);
-		if (s == null)
+		if (isEmpty(s))
 			return def;
 		if (varSession != null)
 			s = varSession.resolve(s);
@@ -536,7 +626,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 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.
+	 * @return The value, or an empty array if the section or key does not exist.
 	 */
 	public String[] getStringArray(String key) {
 		return getStringArray(key, new String[0]);
@@ -547,13 +637,13 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 
 	 * @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.
+	 * @return The value, or the default value if the section or key does not exist or is blank.
 	 */
 	public String[] getStringArray(String key, String[] def) {
 		String s = getString(key);
-		if (s == null)
+		if (isEmpty(s))
 			return def;
-		String[] r = isEmpty(s) ? new String[0] : split(s);
+		String[] r = split(s);
 		return r.length == 0 ? def : r;
 	}
 
@@ -650,6 +740,47 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	}
 	
 	/**
+	 * Convenience method for getting byte array config values.
+	 * 
+	 * <p>
+	 * This is equivalent to calling the following:
+	 * <p class='bcode'>
+	 * 	<jk>byte</jk>[] b = config.getObject(key, <jk>byte</jk>[].<jk>class</jk>);
+	 * </p>
+	 * 
+	 * Byte arrays are stored as encoded strings, typically BASE64, but dependent on the {@link #CONFIG_binaryFormat} setting.
+	 * 
+	 * @param key The key.  
+	 * @return The value, or <jk>null</jk> if the section or key does not exist.
+	 * @throws ParseException If value could not be converted to a byte array.
+	 */
+	public byte[] getBytes(String key) throws ParseException {
+		String s = get(key);
+		if (s == null)
+			return null;
+		if (s.isEmpty())
+			return new byte[0];
+		return getObject(key, byte[].class);
+	}
+	
+	/**
+	 * Same as {@link #getBytes(String)} but with a default value if the entry doesn't exist.
+	 * 
+	 * @param key The key.  
+	 * @param def The default value.
+	 * @return The value, or the default value if the section or key does not exist.
+	 * @throws ParseException If value could not be converted to a byte array.
+	 */
+	public byte[] getBytes(String key, byte[] def) throws ParseException {
+		String s = get(key);
+		if (s == null)
+			return def;
+		if (s.isEmpty())
+			return def;
+		return getObjectWithDefault(key, def, byte[].class);
+	}
+
+	/**
 	 * Gets the entry with the specified key and converts it to the specified value.
 	 * 
 	 * <p>
@@ -666,7 +797,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 
 	 * <h5 class='section'>Examples:</h5>
 	 * <p class='bcode'>
-	 * 	ConfigFile cf = ConfigFile.<jsm>create</jsm>().build(<js>"MyConfig.cfg"</js>);
+	 * 	Config cf = Config.<jsm>create</jsm>().name(<js>"MyConfig.cfg"</js>).build();
 	 * 
 	 * 	<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>);
@@ -748,7 +879,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 
 	 * <h5 class='section'>Examples:</h5>
 	 * <p class='bcode'>
-	 * 	ConfigFile cf = ConfigFile.<jsm>create</jsm>().build(<js>"MyConfig.cfg"</js>);
+	 * 	Config cf = Config.<jsm>create</jsm>().name(<js>"MyConfig.cfg"</js>).build();
 	 * 
 	 * 	<jc>// Parse into a string.</jc>
 	 * 	String s = cf.getObject(<js>"MySection/mySimpleString"</js>, String.<jk>class</jk>);
@@ -880,9 +1011,26 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	}
 
 	/**
+	 * Returns the keys of the entries in the specified section.
+	 * 
+	 * @param section 
+	 * 	The section name to write from.
+	 * 	<br>If empty or <js>"default"</js>, refers to the default section.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * @return
+	 * 	An unmodifiable set of keys, or an empty set if the section doesn't exist.
+	 */
+	public Set<String> getKeys(String section) {
+		return configMap.getKeys(section(section));
+	}
+
+	/**
 	 * 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 section 
+	 * 	The section name to write from.
+	 * 	<br>If empty or <js>"default"</js>, refers to the default section.
+	 * 	<br>Must not be <jk>null</jk>.
 	 * @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
@@ -892,44 +1040,44 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * @throws IllegalArgumentException
 	 * @throws IllegalAccessException
 	 * @throws InvocationTargetException
+	 * @throws UnsupportedOperationException If configuration is read only.
 	 */
 	public Config writeProperties(String section, Object bean, boolean ignoreUnknownProperties) throws ParseException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
+		checkWrite();
 		assertFieldNotNull(bean, "bean");
+		section = section(section);
 
 		Set<String> keys = configMap.getKeys(section);
 		if (keys == null)
-			throw new IllegalArgumentException("Section not found");
-		keys = new LinkedHashSet<>(keys);
+			throw new IllegalArgumentException("Section '"+section+"' not found in configuration.");
 		
-		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);
-				}
+		BeanMap<?> bm = beanSession.toBeanMap(bean);
+		for (String k : keys) {
+			BeanPropertyMeta bpm = bm.getPropertyMeta(k);
+			if (bpm == null) {
+				if (! ignoreUnknownProperties)
+					throw new ParseException("Unknown property ''{0}'' encountered in configuration section ''{1}''.", k, section);
+			} else {
+				bm.put(k, getObject(section + '/' + k, bpm.getClassMeta().getInnerClass()));
 			}
 		}
 		
-		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 section 
+	 * 	The section name to write from.
+	 * 	<br>If empty or <js>"default"</js>, refers to the default section.
+	 * 	<br>Must not be <jk>null</jk>.
 	 * @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);
+	public <T> T getSectionAsBean(String section, Class<T>c) throws ParseException {
+		return getSectionAsBean(section, c, false);
 	}
 
 	/**
@@ -959,35 +1107,70 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 
 	 * <h5 class='figure'>Example usage</h5>
 	 * <p class='bcode'>
-	 * 	ConfigFile cf = ConfigFile.<jsm>create</jsm>().build(<js>"MyConfig.cfg"</js>);
+	 * 	Config cf = Config.<jsm>create</jsm>().name(<js>"MyConfig.cfg"</js>).build();
 	 * 	Address myAddress = cf.getSectionAsBean(<js>"MySection"</js>, Address.<jk>class</jk>);
 	 * </p>
 	 * 
-	 * @param section The section name to write from.
+	 * @param section 
+	 * 	The section name to write from.
+	 * 	<br>If empty or <js>"default"</js>, refers to the default section.
+	 * 	<br>Must not be <jk>null</jk>.
 	 * @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.
+	 * @return A new bean instance, or <jk>null</jk> if the section doesn't exist.
 	 * @throws ParseException
 	 */
 	public <T> T getSectionAsBean(String section, Class<T> c, boolean ignoreUnknownProperties) throws ParseException {
 		assertFieldNotNull(c, "c");
+		section = section(section);
+		
+		if (! configMap.hasSection(section))
+			return null;
 
+		Set<String> keys = configMap.getKeys(section);
+		
 		BeanMap<T> bm = beanSession.newBeanMap(c);
-		for (String k : configMap.getKeys(section)) {
+		for (String k : keys) {
 			BeanPropertyMeta bpm = bm.getPropertyMeta(k);
 			if (bpm == null) {
 				if (! ignoreUnknownProperties)
-					throw new ParseException("Unknown property {0} encountered", k);
+					throw new ParseException("Unknown property ''{0}'' encountered in configuration section ''{1}''.", k, section);
 			} else {
 				bm.put(k, getObject(section + '/' + k, bpm.getClassMeta().getInnerClass()));
 			}
 		}
+		
 		return bm.getBean();
 	}
 
 	/**
+	 * Returns a section of this config copied into an {@link ObjectMap}.
+	 * 
+	 * @param section 
+	 * 	The section name to write from.
+	 * 	<br>If empty or <js>"default"</js>, refers to the default section.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * @return A new {@link ObjectMap}, or <jk>null</jk> if the section doesn't exist.
+	 * @throws ParseException 
+	 */
+	public ObjectMap getSectionAsMap(String section) throws ParseException {
+		section = section(section);
+		
+		if (! configMap.hasSection(section))
+			return null;
+		
+		Set<String> keys = configMap.getKeys(section);
+		
+		ObjectMap om = new ObjectMap();
+		for (String k : keys) 
+			om.put(k, getObject(section + '/' + k, Object.class));
+		return om;
+	}
+	
+	
+	/**
 	 * Wraps a config file section inside a Java interface so that values in the section can be read and
 	 * write using getters and setters.
 	 * 
@@ -1028,7 +1211,7 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 
 	 * <h5 class='figure'>Example usage</h5>
 	 * <p class='bcode'>
-	 * 	ConfigFile cf = ConfigFile.<jsm>create</jsm>().build(<js>"MyConfig.cfg"</js>);
+	 * 	Config cf = Config.<jsm>create</jsm>().name(<js>"MyConfig.cfg"</js>).build();
 	 * 
 	 * 	MyConfigInterface ci = cf.getSectionAsInterface(<js>"MySection"</js>, MyConfigInterface.<jk>class</jk>);
 	 * 
@@ -1039,16 +1222,25 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 	cf.save();
 	 * </p>
 	 * 
-	 * @param sectionName The section name to retrieve as an interface proxy.
+	 * <h5 class='notes'>
+	 * <ul class='spaced-list'>
+	 * 	<li>Calls to setters when the configuration is read-only will cause {@link UnsupportedOperationException} to be thrown.
+	 * </ul>
+	 * 
+	 * @param section 
+	 * 	The section name to write from.
+	 * 	<br>If empty or <js>"default"</js>, refers to the default section.
+	 * 	<br>Must not be <jk>null</jk>.
 	 * @param c The proxy interface class.
 	 * @return The proxy interface.
 	 */
 	@SuppressWarnings("unchecked")
-	public <T> T getSectionAsInterface(final String sectionName, final Class<T> c) {
+	public <T> T getSectionAsInterface(String section, final Class<T> c) {
 		assertFieldNotNull(c, "c");
+		final String section2 = section(section);
 
 		if (! c.isInterface())
-			throw new UnsupportedOperationException("Class passed to getSectionAsInterface is not an interface.");
+			throw new IllegalArgumentException("Class '"+c.getName()+"' passed to getSectionAsInterface() is not an interface.");
 
 		InvocationHandler h = new InvocationHandler() {
 
@@ -1058,11 +1250,11 @@ public final class Config extends Context implements ChangeEventListener, Closea
 				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());
+						return Config.this.getObject(section2 + '/' + pd.getName(), rm.getGenericReturnType());
 					if (method.equals(wm))
-						return Config.this.set(sectionName + '/' + pd.getName(), args[0]);
+						return Config.this.set(section2 + '/' + pd.getName(), args[0]);
 				}
-				throw new UnsupportedOperationException("Unsupported interface method.  method=[ " + method + " ]");
+				throw new UnsupportedOperationException("Unsupported interface method.  method='" + method + "'");
 			}
 		};
 
@@ -1088,16 +1280,16 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * @param name 
 	 * 	The section name.
 	 * 	<br>Must not be <jk>null</jk>.
-	 * 	<br>Use <js>"default"</js> for the default section.
+	 * 	<br>Use blank or <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>.
+	 * 	<br>If <jk>null</jk>, previous pre-lines will not be replaced.
 	 * @return The appended or existing section.
-	 * @throws UnsupportedOperationException If config file is read only.
+	 * @throws UnsupportedOperationException If configuration is read only.
 	 */
 	public Config setSection(String name, List<String> preLines) {
 		try {
-			return setSection(name, preLines, null);
+			return setSection(section(name), preLines, null);
 		} catch (SerializeException e) {
 			throw new RuntimeException(e);  // Impossible.
 		}
@@ -1112,20 +1304,21 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 	<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>.
+	 * 	<br>If <jk>null</jk>, previous pre-lines will not be replaced.
 	 * @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.
+	 * @throws UnsupportedOperationException If configuration is read only.
 	 */
 	public Config setSection(String name, List<String> preLines, Map<String,Object> contents) throws SerializeException {
-		configMap.setSection(name, preLines);
+		checkWrite();
+		configMap.setSection(section(name), preLines);
 		
 		if (contents != null)
 			for (Map.Entry<String,Object> e : contents.entrySet())
-				set(e.getKey(), e.getValue());
+				set(section(name) + '/' + e.getKey(), e.getValue());
 		
 		return this;
 	}
@@ -1135,19 +1328,38 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 
 	 * @param name The name of the section to remove
 	 * @return This object (for method chaining).
+	 * @throws UnsupportedOperationException If configuration is read only.
 	 */
 	public Config removeSection(String name) {
+		checkWrite();
 		configMap.removeSection(name);
 		return this;
 	}
+	
+	/**
+	 * Loads the contents of the specified map of maps into this config.
+	 * 
+	 * @param m The maps to load.
+	 * @return This object (for method chaining).
+	 * @throws SerializeException
+	 */
+	public Config load(Map<String,Map<String,Object>> m) throws SerializeException {
+		if (m != null)
+			for (Map.Entry<String,Map<String,Object>> e : m.entrySet()) {
+				setSection(e.getKey(), null, e.getValue());
+			}
+		return this;
+	}
 
 	/**
 	 * Saves this config to the store.
 	 * 
 	 * @return This object (for method chaining).
 	 * @throws IOException 
+	 * @throws UnsupportedOperationException If configuration is read only.
 	 */
 	public Config save() throws IOException {
+		checkWrite();
 		configMap.save();
 		return this;
 	}
@@ -1171,13 +1383,12 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * Add a listener to this config to react to modification events.
 	 * 
 	 * <p>
-	 * Listeners should be removed using {@link #removeListener(ChangeEventListener)}.
+	 * Listeners should be removed using {@link #removeListener(ConfigEventListener)}.
 	 * 
 	 * @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) {
+	public Config addListener(ConfigEventListener listener) {
 		listeners.add(listener);
 		return this;
 	}
@@ -1187,14 +1398,88 @@ public final class Config extends Context implements ChangeEventListener, Closea
 	 * 
 	 * @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) {
+	public Config removeListener(ConfigEventListener listener) {
 		listeners.remove(listener);
 		return this;
 	}
 
 	/**
+	 * Closes this configuration object by unregistering it from the underlying config map.
+	 * 
+	 * @throws IOException
+	 */
+	public void close() throws IOException {
+		configMap.unregister(this);
+		closed = true;
+	}
+	
+	/**
+	 * Overwrites the contents of the config file.
+	 * 
+	 * @param contents The new contents of the config file.
+	 * @param synchronous Wait until the change has been persisted before returning this map.
+	 * @return This object (for method chaining).
+	 * @throws IOException
+	 * @throws InterruptedException
+	 * @throws UnsupportedOperationException If configuration is read only.
+	 */
+	public Config write(Reader contents, boolean synchronous) throws IOException, InterruptedException {
+		checkWrite();
+		configMap.write(IOUtils.read(contents), synchronous);
+		return this;
+	}
+
+	/**
+	 * Overwrites the contents of the config file.
+	 * 
+	 * @param contents The new contents of the config file.
+	 * @param synchronous Wait until the change has been persisted before returning this map.
+	 * @return This object (for method chaining).
+	 * @throws IOException
+	 * @throws InterruptedException
+	 * @throws UnsupportedOperationException If configuration is read only.
+	 */
+	public Config write(String contents, boolean synchronous) throws IOException, InterruptedException {
+		checkWrite();
+		configMap.write(contents, synchronous);
+		return this;
+	}
+
+	/**
+	 * Does a rollback of any changes on this config currently in memory.
+	 * 
+	 * @return This object (for method chaining).
+	 * @throws UnsupportedOperationException If configuration is read only.
+	 */
+	public Config rollback() {
+		checkWrite();
+		configMap.rollback();
+		return this;
+	}
+
+	/**
+	 * Returns the values in this config map as a map of maps.
+	 * 
+	 * <p>
+	 * This is considered a snapshot copy of the config map.
+	 * 
+	 * <p>
+	 * The returned map is modifiable, but modifications to the returned map are not reflected in the config map.
+	 * 
+	 * @return A copy of this config as a map of maps.
+	 */
+	@Override /* Context */
+	public ObjectMap asMap() {
+		return configMap.asMap();
+	}
+	
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Interface methods
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	/**
 	 * Unused.
 	 */
 	@Override /* Context */
@@ -1210,30 +1495,17 @@ public final class Config extends Context implements ChangeEventListener, Closea
 		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 /* ConfigEventListener */
+	public void onConfigChange(List<ConfigEvent> events) {
+		for (ConfigEventListener l : listeners)
+			l.onConfigChange(events);
 	}
 	
 	@Override /* Writable */
 	public MediaType getMediaType() {
 		return MediaType.PLAIN;
 	}
-	
+
 	
 	//-----------------------------------------------------------------------------------------------------------------
 	// Private methods
@@ -1289,21 +1561,27 @@ public final class Config extends Context implements ChangeEventListener, Closea
 		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);
+			try {
+				switch (binaryFormat) {
+					case "HEX": return (T)fromHex(s);
+					case "SPACED_HEX": return (T)fromSpacedHex(s);
+					default: return (T)base64Decode(s);
+				}
+			} catch (Exception e) {
+				throw new ParseException("Value could not be converted to a byte array.").initCause(e);
 			}
 		}
 		
-		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;
+		
+		if (parser instanceof JsonParser) {
+			char s1 = firstNonWhitespaceChar(s);
+			if (isArray(type) && s1 != '[')
+				s = '[' + s + ']';
+			else if (s1 != '[' && s1 != '{' && ! "null".equals(s))
+				s = '\'' + s + '\'';
+		}
 
 		return parser.parse(s, type, args);
 	}
@@ -1336,4 +1614,34 @@ public final class Config extends Context implements ChangeEventListener, Closea
 			return key;
 		return key.substring(i+1);
 	}
+
+	private String section(String section) {
+		assertFieldNotNull(section, "section");
+		if (isEmpty(section))
+			return "default";
+		return section;
+	}
+	
+	private void checkWrite() {
+		if (readOnly)
+			throw new UnsupportedOperationException("Cannot call this method on a read-only configuration.");
+	}
+
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Other methods
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Override /* Object */
+	public String toString() {
+		return configMap.toString();
+	}
+	
+	@Override /* Object */
+	protected void finalize() throws Throwable {
+		// If Config objects are not closed, listeners on the underlying ConfigMap don't get cleaned up.
+		if (! closed) {
+			System.err.println("Config object not closed.");
+		}
+	}
 }
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/ConfigBuilder.java
similarity index 82%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigBuilder.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigBuilder.java
index 0e01141..beb84ac 100644
--- 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/ConfigBuilder.java
@@ -10,9 +10,9 @@
 // * "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;
+package org.apache.juneau.config;
 
-import static org.apache.juneau.config.proto.Config.*;
+import static org.apache.juneau.config.Config.*;
 
 import java.util.*;
 
@@ -29,8 +29,8 @@ import org.apache.juneau.svl.*;
  * 
  * <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>);
+ * 	Config cf = Config.<jsm>create</jsm>().name(<js>"MyConfig.cfg"</js>).build();
+ * 	String setting = cf.getString(<js>"MySection/mysetting"</js>);
  * </p>
  * 
  * <h5 class='section'>See Also:</h5>
@@ -71,8 +71,8 @@ public class ConfigBuilder extends ContextBuilder {
 	 * 
 	 * <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.
+	 * <br>This is typically the configuration file name, although
+	 * the name can be anything identifiable by the {@link ConfigStore} used for retrieving and storing the configuration.
 	 * 
 	 * @param value 
 	 * 	The new value for this property.
@@ -91,14 +91,26 @@ public class ConfigBuilder extends ContextBuilder {
 	 * 
 	 * @param value 
 	 * 	The new value for this property.
-	 * 	<br>The default is {@link FileStore#DEFAULT}.
+	 * 	<br>The default is {@link ConfigFileStore#DEFAULT}.
 	 * @return This object (for method chaining).
 	 */
-	public ConfigBuilder store(Store value) {
+	public ConfigBuilder store(ConfigStore value) {
 		return set(CONFIG_store, value);
 	}
 
 	/**
+	 * Configuration property:  Configuration store.
+	 * 
+	 * <p>
+	 * Convenience method for calling <code>store(ConfigMemoryStore.<jsf>DEFAULT</jsf>)</code>.
+	 * 
+	 * @return This object (for method chaining).
+	 */
+	public ConfigBuilder memStore() {
+		return set(CONFIG_store, ConfigMemoryStore.DEFAULT);
+	}
+
+	/**
 	 * Configuration property:  POJO serializer.
 	 * 
 	 * <p>
@@ -166,10 +178,10 @@ public class ConfigBuilder extends ContextBuilder {
 	 * 
 	 * @param value 
 	 * 	The new value for this property.
-	 * 	<br>The default is {@link XorEncoder#INSTANCE}.
+	 * 	<br>The default is {@link ConfigXorEncoder#INSTANCE}.
 	 * @return This object (for method chaining).
 	 */
-	public ConfigBuilder encoder(Encoder value) {
+	public ConfigBuilder encoder(ConfigEncoder value) {
 		return set(CONFIG_encoder, value);
 	}
 
@@ -181,10 +193,10 @@ public class ConfigBuilder extends ContextBuilder {
 	 * 
 	 * @param value 
 	 * 	The new value for this property.
-	 * 	<br>The default is {@link XorEncoder#INSTANCE}.
+	 * 	<br>The default is {@link ConfigXorEncoder#INSTANCE}.
 	 * @return This object (for method chaining).
 	 */
-	public ConfigBuilder encoder(Class<? extends Encoder> value) {
+	public ConfigBuilder encoder(Class<? extends ConfigEncoder> value) {
 		return set(CONFIG_encoder, value);
 	}
 	
@@ -263,15 +275,23 @@ public class ConfigBuilder extends ContextBuilder {
 	 * <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);
+	public ConfigBuilder beansOnSeparateLines() {
+		return set(CONFIG_beansOnSeparateLines, true);
 	}
 	
+	/**
+	 * Configuration property:  Beans on separate lines.
+	 * 
+	 * <p>
+	 * When enabled, attempts to call any setters on this object will throw an {@link UnsupportedOperationException}.
+	 * 
+	 * @return This object (for method chaining).
+	 */
+	public ConfigBuilder readOnly() {
+		return set(CONFIG_readOnly, true);
+	}
 	
 	@Override /* ContextBuilder */
 	public ConfigBuilder set(String name, Object value) {
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFile.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFile.java
deleted file mode 100644
index bdc4ac9..0000000
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFile.java
+++ /dev/null
@@ -1,1242 +0,0 @@
-// ***************************************************************************************************************************
-// * 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;
-
-import static java.lang.reflect.Modifier.*;
-import static org.apache.juneau.internal.ThrowableUtils.*;
-import static org.apache.juneau.internal.StringUtils.*;
-import static org.apache.juneau.config.ConfigFileFormat.*;
-import static org.apache.juneau.config.ConfigUtils.*;
-import static org.apache.juneau.internal.ArrayUtils.*;
-
-import java.beans.*;
-import java.io.*;
-import java.lang.reflect.*;
-import java.util.*;
-
-import org.apache.juneau.*;
-import org.apache.juneau.config.listener.*;
-import org.apache.juneau.internal.*;
-import org.apache.juneau.parser.*;
-import org.apache.juneau.serializer.*;
-import org.apache.juneau.svl.*;
-
-/**
- * Implements the API for accessing the contents of a config file.
- * 
- * <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 abstract class ConfigFile implements Map<String,Section> {
-	
-	/**
-	 * Instantiates a new clean-slate {@link ConfigFileBuilder} object.
-	 * 
-	 * <p>
-	 * This is equivalent to simply calling <code><jk>new</jk> ConfigFileBuilder()</code>.
-	 * 
-	 * @return A new {@link ConfigFileBuilder} object.
-	 */
-	public static ConfigFileBuilder create() {
-		return new ConfigFileBuilder();
-	}
-	
-
-	//--------------------------------------------------------------------------------
-	// Abstract methods
-	//--------------------------------------------------------------------------------
-
-	/**
-	 * Retrieves an entry value from this config file.
-	 * 
-	 * @param sectionName The section name.  Must not be <jk>null</jk>.
-	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
-	 * @return The value, or the default value if the section or value doesn't exist.
-	 */
-	public abstract String get(String sectionName, String sectionKey);
-
-	/**
-	 * Sets an entry value in this config file.
-	 * 
-	 * @param sectionName The section name.  Must not be <jk>null</jk>.
-	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
-	 * @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 encoded If <jk>true</jk>, then encode the value using the encoder associated with this config file.
-	 * @param newline If <jk>true</jk>, then put serialized output on a separate line from the key.
-	 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
-	 * @throws SerializeException If the object value could not be converted to a JSON string for some reason.
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract String put(String sectionName, String sectionKey, Object value, Serializer serializer,
-			boolean encoded, boolean newline) throws SerializeException;
-
-	/**
-	 * Identical to {@link #put(String, String, Object, Serializer, boolean, boolean)} except used when the value is a
-	 * simple string to avoid having to catch a {@link SerializeException}.
-	 * 
-	 * @param sectionName The section name.  Must not be <jk>null</jk>.
-	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
-	 * @param value The new value.
-	 * @param encoded
-	 * @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 abstract String put(String sectionName, String sectionKey, String value, boolean encoded);
-
-
-	/**
-	 * Removes an entry from this config file.
-	 * 
-	 * @param sectionName The section name.  Must not be <jk>null</jk>.
-	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
-	 * @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 abstract String remove(String sectionName, String sectionKey);
-
-	/**
-	 * Returns the current set of keys in the specified section.
-	 * 
-	 * @param sectionName The section name.  Must not be <jk>null</jk>.
-	 * @return The list of keys in the specified section, or <jk>null</jk> if section does not exist.
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract Set<String> getSectionKeys(String sectionName);
-
-	/**
-	 * Reloads this config file object from the persisted file contents if the modified timestamp on the file has changed.
-	 * 
-	 * @return This object (for method chaining).
-	 * @throws IOException If file could not be read, or file is not associated with this object.
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile loadIfModified() throws IOException;
-
-	/**
-	 * Loads this config file object from the persisted file contents.
-	 * 
-	 * @return This object (for method chaining).
-	 * @throws IOException If file could not be read, or file is not associated with this object.
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile load() throws IOException;
-
-	/**
-	 * Loads this config file object from the specified reader.
-	 * 
-	 * @param r The reader to read from.
-	 * @return This object (for method chaining).
-	 * @throws IOException If file could not be read, or file is not associated with this object.
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile load(Reader r) throws IOException;
-
-	/**
-	 * Adds arbitrary lines to the specified config file section.
-	 * 
-	 * <p>
-	 * The lines can be any of the following....
-	 * <ul class='spaced-list'>
-	 * 	<li>
-	 * 		<js>"# comment"</js> - A comment line.
-	 * 	<li>
-	 * 		<js>"key=val"</js> - A key/value pair (equivalent to calling {@link #put(String,Object)}.
-	 * 	<li>
-	 * 		<js>" foobar "</js> - Anything else (interpreted as a comment).
-	 * </ul>
-	 * 
-	 * <p>
-	 * If the section does not exist, it will automatically be created.
-	 * 
-	 * @param section The name of the section to add lines to, or <jk>null</jk> to add to the beginning unnamed section.
-	 * @param lines The lines to add to the section.
-	 * @return This object (for method chaining).
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile addLines(String section, String...lines);
-
-	/**
-	 * Adds header comments to the specified section.
-	 * 
-	 * <p>
-	 * Header comments are defined as lines that start with <jk>"#"</jk> immediately preceding a section header
-	 * <jk>"[section]"</jk>.
-	 * These are handled as part of the section itself instead of being interpreted as comments in the previous section.
-	 * 
-	 * <p>
-	 * Header comments can be of the following formats...
-	 * <ul class='spaced-list'>
-	 * 	<li>
-	 * 		<js>"# comment"</js> - A comment line.
-	 * 	<li>
-	 * 		<js>"comment"</js> - Anything else (will automatically be prefixed with <js>"# "</js>).
-	 * </ul>
-	 * 
-	 * <p>
-	 * If the section does not exist, it will automatically be created.
-	 * 
-	 * @param section The name of the section to add lines to, or <jk>null</jk> to add to the default section.
-	 * @param headerComments The comment lines to add to the section.
-	 * @return This object (for method chaining).
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile addHeaderComments(String section, String...headerComments);
-
-	/**
-	 * Removes any header comments from the specified section.
-	 * 
-	 * @param section The name of the section to remove header comments from.
-	 * @return This object (for method chaining).
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile clearHeaderComments(String section);
-
-	/**
-	 * Returns the reusable bean session associated with this config file.
-	 * 
-	 * <p>
-	 * Used for performing simple datatype conversions.
-	 * 
-	 * @return The reusable bean session associated with this config file.
-	 */
-	protected abstract BeanSession getBeanSession();
-
-	/**
-	 * Converts the specified object to a string.
-	 * 
-	 * <p>
-	 * The serialized output is identical to LAX JSON (JSON with unquoted attributes) except for the following
-	 * exceptions:
-	 * <ul>
-	 * 	<li>Top level strings are not quoted.
-	 * </ul>
-	 * 
-	 * @param o The object to serialize.
-	 * @param serializer
-	 * 	The serializer to use for serializing the object.
-	 * 	If <jk>null</jk>, then uses the predefined serializer on the config file.
-	 * @param newline If <jk>true</jk>, add a newline at the beginning of the value.
-	 * @return The serialized object.
-	 * @throws SerializeException
-	 */
-	protected abstract String serialize(Object o, Serializer serializer, boolean newline) throws SerializeException;
-
-	/**
-	 * Converts the specified string to an object of the specified type.
-	 * 
-	 * @param s The string to parse.
-	 * @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 data type to create.
-	 * @param args The generic type arguments if the type is a {@link Collection} or {@link Map}
-	 * @return The parsed object.
-	 * @throws ParseException
-	 */
-	protected abstract <T> T parse(String s, Parser parser, Type type, Type...args) throws ParseException;
-
-	/**
-	 * Places a read lock on this config file.
-	 */
-	protected abstract void readLock();
-
-	/**
-	 * Removes the read lock on this config file.
-	 */
-	protected abstract void readUnlock();
-
-
-	//--------------------------------------------------------------------------------
-	// API methods
-	//--------------------------------------------------------------------------------
-
-	/**
-	 * Returns the specified value as a string from the config file.
-	 * 
-	 * @param key The key.  See {@link #getString(String)} for a description of the key.
-	 * @param def The default value if the section or value does not exist.
-	 * @return The value, or the default value if the section or value doesn't exist.
-	 */
-	public final String getString(String key, String def) {
-		assertFieldNotNull(key, "key");
-		String s = get(getSectionName(key), getSectionKey(key));
-		return (StringUtils.isEmpty(s) && def != null ? def : s);
-	}
-
-	/**
-	 * 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 final String removeString(String key) {
-		assertFieldNotNull(key, "key");
-		return remove(getSectionName(key), getSectionKey(key));
-	}
-
-	/**
-	 * 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 final <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 final <T> T getObject(String key, Parser parser, Type type, Type...args) throws ParseException {
-		assertFieldNotNull(key, "key");
-		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 final <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 final <T> T getObject(String key, Parser parser, Class<T> type) throws ParseException {
-		assertFieldNotNull(key, "key");
-		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 final <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 final <T> T getObjectWithDefault(String key, Parser parser, T def, Class<T> type) throws ParseException {
-		assertFieldNotNull(key, "key");
-		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 final <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 final <T> T getObjectWithDefault(String key, Parser parser, T def, Type type, Type...args) throws ParseException {
-		assertFieldNotNull(key, "key");
-		assertFieldNotNull(type, "type");
-		T t = parse(getString(key), parser, type, args);
-		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, Class)}, but used when key is already broken into section/key.
-	 * 
-	 * @param sectionName The section name.  Must not be <jk>null</jk>.
-	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
-	 * @param c 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 the default value if the section or value doesn't exist.
-	 */
-	public final <T> T getObject(String sectionName, String sectionKey, Class<T> c) throws ParseException {
-		return getObject(sectionName, sectionKey, null, c);
-	}
-
-	/**
-	 * Same as {@link #getObject(String, String, Class)} but allows you to specify the parser to use to parse the value.
-	 * 
-	 * @param sectionName The section name.  Must not be <jk>null</jk>.
-	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
-	 * @param parser
-	 * 	The parser to use for parsing the object.
-	 * 	If <jk>null</jk>, then uses the predefined parser on the config file.
-	 * @param c 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 the default value if the section or value doesn't exist.
-	 */
-	public final <T> T getObject(String sectionName, String sectionKey, Parser parser, Class<T> c) throws ParseException {
-		assertFieldNotNull(sectionName, "sectionName");
-		assertFieldNotNull(sectionKey, "sectionKey");
-		return parse(get(sectionName, sectionKey), parser, c);
-	}
-
-	/**
-	 * Gets the entry with the specified key and converts it to the specified value.
-	 * 
-	 * <p>
-	 * Same as {@link #getObject(String, Type, Type...)}, but used when key is already broken into section/key.
-	 * 
-	 * @param sectionName The section name.  Must not be <jk>null</jk>.
-	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
-	 * @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 final <T> T getObject(String sectionName, String sectionKey, Type type, Type...args) throws ParseException {
-		return getObject(sectionName, sectionKey, null, type, args);
-	}
-
-	/**
-	 * Same as {@link #getObject(String, String, Type, Type...)} but allows you to specify the parser to use to parse
-	 * the value.
-	 * 
-	 * @param sectionName The section name.  Must not be <jk>null</jk>.
-	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
-	 * @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 final <T> T getObject(String sectionName, String sectionKey, Parser parser, Type type, Type...args)
-			throws ParseException {
-		assertFieldNotNull(sectionName, "sectionName");
-		assertFieldNotNull(sectionKey, "sectionKey");
-		return parse(get(sectionName, sectionKey), parser, type, args);
-	}
-
-	/**
-	 * 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 final String getString(String key) {
-		return getString(key, null);
-	}
-
-	/**
-	 * 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 final 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 final String[] getStringArray(String key, String[] def) {
-		String s = getString(key);
-		if (s == null)
-			return def;
-		String[] r = StringUtils.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 final 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 final int getInt(String key, int def) {
-		String s = getString(key);
-		if (StringUtils.isEmpty(s))
-			return def;
-		return parseIntWithSuffix(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 final 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 final long getLong(String key, long def) {
-		String s = getString(key);
-		if (StringUtils.isEmpty(s))
-			return def;
-		return parseLongWithSuffix(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 final 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 final boolean getBoolean(String key, boolean def) {
-		String s = getString(key);
-		return StringUtils.isEmpty(s) ? def : Boolean.parseBoolean(s);
-	}
-
-	/**
-	 * 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 final String put(String key, Object value) throws SerializeException {
-		return put(key, value, null, isEncoded(key), false);
-	}
-
-	/**
-	 * Same as {@link #put(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 POJO.
-	 * @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 final String put(String key, Object value, Serializer serializer) throws SerializeException {
-		return put(key, value, serializer, isEncoded(key), false);
-	}
-
-	/**
-	 * Adds or replaces an entry with the specified key with the specified value.
-	 * 
-	 * <p>
-	 * The format of the entry depends on the data type of the value.
-	 * <ul class='spaced-list'>
-	 * 	<li>
-	 * 		Simple types (<code>String</code>, <code>Number</code>, <code>Boolean</code>, primitives)
-	 * 		are serialized as plain strings.
-	 * 	<li>
-	 * 		Arrays and collections of simple types are serialized as comma-delimited lists of plain strings.
-	 * 	<li>
-	 * 		Other types (e.g. beans) are serialized using the serializer registered with this config file.
-	 * 	<li
-	 * 		>Arrays and collections of other types are serialized as comma-delimited lists of serialized strings of
-	 * 		each entry.
-	 * </ul>
-	 * 
-	 * @param key The key.  See {@link #getString(String)} for a description of the key.
-	 * @param value The new value.
-	 * @param encoded
-	 * 	If <jk>true</jk>, value is encoded by the registered encoder when the config file is persisted to disk.
-	 * @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 final String put(String key, Object value, boolean encoded) throws SerializeException {
-		return put(key, value, null, encoded, false);
-	}
-
-	/**
-	 * Same as {@link #put(String, Object, boolean)} 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.
-	 * @param encoded
-	 * 	If <jk>true</jk>, value is encoded by the registered encoder when the config file is persisted to disk.
-	 * @param newline If <jk>true</jk>, a newline is added to the beginning of the input.
-	 * @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 final String put(String key, Object value, Serializer serializer, boolean encoded, boolean newline)
-			throws SerializeException {
-		assertFieldNotNull(key, "key");
-		return put(getSectionName(key), getSectionKey(key), serialize(value, serializer, newline), encoded);
-	}
-
-	/**
-	 * Returns the specified section as a map of key/value pairs.
-	 * 
-	 * @param sectionName The section name to retrieve.
-	 * @return A map of the section, or <jk>null</jk> if the section was not found.
-	 */
-	public final ObjectMap getSectionMap(String sectionName) {
-		readLock();
-		try {
-			Set<String> keys = getSectionKeys(sectionName);
-			if (keys == null)
-				return null;
-			ObjectMap m = new ObjectMap();
-			for (String key : keys)
-				m.put(key, get(sectionName, key));
-			return m;
-		} finally {
-			readUnlock();
-		}
-	}
-
-	/**
-	 * Copies the entries in a section to the specified bean by calling the public setters on that bean.
-	 * 
-	 * @param sectionName 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.
-	 * @param permittedPropertyTypes
-	 * 	If specified, only look for setters whose property types are those listed.
-	 * 	If not specified, use all setters.
-	 * @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 final ObjectMap writeProperties(String sectionName, Object bean, boolean ignoreUnknownProperties,
-			Class<?>...permittedPropertyTypes) throws ParseException, IllegalArgumentException, IllegalAccessException,
-			InvocationTargetException {
-		assertFieldNotNull(bean, "bean");
-		ObjectMap om = new ObjectMap();
-		readLock();
-		try {
-			Set<String> keys = getSectionKeys(sectionName);
-			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];
-					if (permittedPropertyTypes == null || permittedPropertyTypes.length == 0 || contains(pt, permittedPropertyTypes)) {
-						String propName = Introspector.decapitalize(m.getName().substring(3));
-						Object value = getObject(sectionName, propName, pt);
-						if (value != null) {
-							m.invoke(bean, value);
-							om.put(propName, value);
-							keys.remove(propName);
-						}
-					}
-				}
-			}
-			if (! (ignoreUnknownProperties || keys.isEmpty()))
-				throw new ParseException("Invalid properties found in config file section ''{0}'': {1}", sectionName, keys);
-			return om;
-		} finally {
-			readUnlock();
-		}
-	}
-
-	/**
-	 * 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 final <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 sectionName 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 final <T> T getSectionAsBean(String sectionName, Class<T> c, boolean ignoreUnknownProperties)
-			throws ParseException {
-		assertFieldNotNull(c, "c");
-		readLock();
-		try {
-			BeanMap<T> bm = getBeanSession().newBeanMap(c);
-			for (String k : getSectionKeys(sectionName)) {
-				BeanPropertyMeta bpm = bm.getPropertyMeta(k);
-				if (bpm == null) {
-					if (! ignoreUnknownProperties)
-						throw new ParseException("Unknown property {0} encountered", k);
-				} else {
-					bm.put(k, getObject(sectionName + '/' + k, bpm.getClassMeta().getInnerClass()));
-				}
-			}
-			return bm.getBean();
-		} finally {
-			readUnlock();
-		}
-	}
-
-	/**
-	 * 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 final <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 ConfigFile.this.getObject(sectionName, pd.getName(), rm.getGenericReturnType());
-					if (method.equals(wm))
-						return ConfigFile.this.put(sectionName, pd.getName(), args[0], null, false, false);
-				}
-				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 final boolean containsNonEmptyValue(String key) {
-		return ! StringUtils.isEmpty(getString(key, null));
-	}
-
-	/**
-	 * Gets the section with the specified name.
-	 * 
-	 * @param name The section name.
-	 * @return The section, or <jk>null</jk> if section does not exist.
-	 */
-	protected abstract Section getSection(String name);
-
-	/**
-	 * Gets the section with the specified name and optionally creates it if it's not there.
-	 * 
-	 * @param name The section name.
-	 * @param create Create the section if it's not there.
-	 * @return The section, or <jk>null</jk> if section does not exist.
-	 * @throws UnsupportedOperationException
-	 * 	If config file is read only and section doesn't exist and <code>create</code> is <jk>true</jk>.
-	 */
-	protected abstract Section getSection(String name, boolean create);
-
-	/**
-	 * Appends a section to this config file if it does not already exist.
-	 * 
-	 * <p>
-	 * Returns the existing section if it already exists.
-	 * 
-	 * @param name The section name, or <jk>null</jk> for the default section.
-	 * @return The appended or existing section.
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile addSection(String name);
-
-	/**
-	 * Creates or overwrites the specified section.
-	 * 
-	 * @param name The section name, or <jk>null</jk> for the default section.
-	 * @param contents The contents of the new section.
-	 * @return The appended or existing section.
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile setSection(String name, Map<String,String> contents);
-
-	/**
-	 * Removes the section with the specified name.
-	 * 
-	 * @param name The name of the section to remove, or <jk>null</jk> for the default section.
-	 * @return The removed section, or <jk>null</jk> if named section does not exist.
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile removeSection(String name);
-
-	/**
-	 * Returns <jk>true</jk> if the encoding flag is set on the specified entry.
-	 * 
-	 * @param key The key.  See {@link #getString(String)} for a description of the key.
-	 * @return <jk>true</jk> if the encoding flag is set on the specified entry.
-	 */
-	public abstract boolean isEncoded(String key);
-
-	/**
-	 * Saves this config file to disk.
-	 * 
-	 * @return This object (for method chaining).
-	 * @throws IOException If a problem occurred trying to save file to disk, or file is not associated with this object.
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile save() throws IOException;
-
-	/**
-	 * Saves this config file to the specified writer as an INI file.
-	 * 
-	 * <p>
-	 * The writer will automatically be closed.
-	 * 
-	 * @param out 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.
-	 */
-	public final ConfigFile serializeTo(Writer out) throws IOException {
-		return serializeTo(out, INI);
-	}
-
-	/**
-	 * Same as {@link #serializeTo(Writer)}, except allows you to explicitly specify a format.
-	 * 
-	 * @param out The writer to send the output to.
-	 * @param format The {@link ConfigFileFormat} of the output.
-	 * @return This object (for method chaining).
-	 * @throws IOException If a problem occurred trying to send contents to the writer.
-	 */
-	public abstract ConfigFile serializeTo(Writer out, ConfigFileFormat format) throws IOException;
-
-	/**
-	 * Add a listener to this config file to react to modification events.
-	 * 
-	 * @param listener The new listener to add.
-	 * @return This object (for method chaining).
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile addListener(ConfigListener listener);
-
-	/**
-	 * Merges the contents of the specified config file into this config file.
-	 * 
-	 * <p>
-	 * Pretty much identical to just replacing this config file, but causes the
-	 * {@link ConfigListener#onChange(ConfigFile, Set)} method to be invoked on differences between the file.
-	 * 
-	 * @param cf The config file whose values should be copied into this config file.
-	 * @return This object (for method chaining).
-	 * @throws UnsupportedOperationException If config file is read only.
-	 */
-	public abstract ConfigFile merge(ConfigFile cf);
-
-	/**
-	 * Returns the config file contents as a string.
-	 * 
-	 * <p>
-	 * The contents of the string are the same as the contents that would be serialized to disk.
-	 */
-	@Override /* Object */
-	public abstract String toString();
-
-	/**
-	 * Returns a wrapped instance of this config file where calls to getters have their values first resolved by the
-	 * specified {@link VarResolver}.
-	 * 
-	 * @param vr The {@link VarResolver} for resolving variables in values.
-	 * @return This config file wrapped in an instance of {@link ConfigFileWrapped}.
-	 */
-	public abstract ConfigFile getResolving(VarResolver vr);
-
-	/**
-	 * Returns a wrapped instance of this config file where calls to getters have their values first resolved by the
-	 * specified {@link VarResolverSession}.
-	 * 
-	 * @param vs The {@link VarResolverSession} for resolving variables in values.
-	 * @return This config file wrapped in an instance of {@link ConfigFileWrapped}.
-	 */
-	public abstract ConfigFile getResolving(VarResolverSession vs);
-
-	/**
-	 * Returns a wrapped instance of this config file where calls to getters have their values first resolved by a
-	 * default {@link VarResolver}.
-	 * 
-	 * The default {@link VarResolver} is registered with the following {@link Var StringVars}:
-	 * <ul class='spaced-list'>
-	 * 	<li>
-	 * 		<code>$S{key}</code>,<code>$S{key,default}</code> - System properties.
-	 * 	<li>
-	 * 		<code>$E{key}</code>,<code>$E{key,default}</code> - Environment variables.
-	 * 	<li>
-	 * 		<code>$C{key}</code>,<code>$C{key,default}</code> - Values in this configuration file.
-	 * </ul>
-	 * 
-	 * @return A new config file that resolves string variables.
-	 */
-	public abstract ConfigFile getResolving();
-
-	/**
-	 * Wraps this config file in a {@link Writable} interface that renders it as plain text.
-	 * 
-	 * @return This config file wrapped in a {@link Writable}.
-	 */
-	public abstract Writable toWritable();
-
-	/**
-	 * @return The string var resolver associated with this config file.
-	 */
-	protected VarResolver getVarResolver() {
-		// Only ConfigFileWrapped returns a value.
-		return null;
-	}
-}
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileBuilder.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileBuilder.java
deleted file mode 100644
index 3d854db..0000000
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileBuilder.java
+++ /dev/null
@@ -1,333 +0,0 @@
-// ***************************************************************************************************************************
-// * 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;
-
-import static org.apache.juneau.config.ConfigFileFormat.*;
-import static org.apache.juneau.internal.FileUtils.*;
-
-import java.io.*;
-import java.nio.charset.*;
-import java.util.*;
-
-import org.apache.juneau.*;
-import org.apache.juneau.config.encode.*;
-import org.apache.juneau.internal.FileWriterBuilder;
-import org.apache.juneau.json.*;
-import org.apache.juneau.parser.*;
-import org.apache.juneau.serializer.*;
-import org.apache.juneau.utils.*;
-
-/**
- * Builder for creating instances of {@link ConfigFile ConfigFiles}.
- * 
- * <h5 class='section'>Example:</h5>
- * <p class='bcode'>
- * 	ConfigFile cf = ConfigFile.<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 ConfigFileBuilder {
-
-	private WriterSerializer serializer = JsonSerializer.DEFAULT_LAX;
-	private ReaderParser parser = JsonParser.DEFAULT;
-	private Encoder encoder = new XorEncoder();
-	private boolean readOnly = false, createIfNotExists = false;
-	private Charset charset = Charset.defaultCharset();
-	private List<File> searchPaths = new AList<File>().append(new File("."));
-
-	/**
-	 * Specify the encoder to use for encoded config file entries (e.g. <js>"mySecret*={...}"</js>).
-	 * 
-	 * <p>
-	 * The default value for this setting is an instance of {@link XorEncoder}.
-	 * 
-	 * @param encoder The new value for this setting.
-	 * @return This object (for method chaining).
-	 */
-	public ConfigFileBuilder encoder(Encoder encoder) {
-		this.encoder = encoder;
-		return this;
-	}
-
-	/**
-	 * Specify the serializer to use for serializing POJOs when using {@link ConfigFile#put(String, Object)}.
-	 * 
-	 * <p>
-	 * The default value for this setting is {@link JsonSerializer#DEFAULT_LAX}.
-	 * 
-	 * @param serializer The new value for this setting.
-	 * @return This object (for method chaining).
-	 */
-	public ConfigFileBuilder serializer(WriterSerializer serializer) {
-		this.serializer = serializer;
-		return this;
-	}
-
-	/**
-	 * Specify the parser to use for parsing POJOs when using {@link ConfigFile#getObject(String,Class)}.
-	 * 
-	 * <p>
-	 * The default value for this setting is {@link JsonParser#DEFAULT}
-	 * 
-	 * @param parser The new value for this setting.
-	 * @return This object (for method chaining).
-	 */
-	public ConfigFileBuilder parser(ReaderParser parser) {
-		this.parser = parser;
-		return this;
-	}
-
-	/**
-	 * Specify the config file character encoding.
-	 * 
-	 * <p>
-	 * The default value for this setting is {@link Charset#defaultCharset()}.
-	 * 
-	 * @param charset The new value for this setting.
-	 * @return This object (for method chaining).
-	 */
-	public ConfigFileBuilder charset(Charset charset) {
-		this.charset = charset;
-		return this;
-	}
-
-	/**
-	 * Specify the search paths for config files.
-	 * 
-	 * <p>
-	 * Can contain relative or absolute paths.
-	 * 
-	 * <p>
-	 * The default value for this setting is <code>[<js>"."</js>]</code>.
-	 * 
-	 * @param searchPaths The new value for this setting.
-	 * @return This object (for method chaining).
-	 */
-	public ConfigFileBuilder paths(String...searchPaths) {
-		this.searchPaths = new LinkedList<>();
-		for (String p : searchPaths)
-			this.searchPaths.add(new File(p));
-		return this;
-	}
-
-	/**
-	 * Make {@link ConfigFile ConfigFiles} read-only.
-	 * 
-	 * <p>
-	 * The default value of this setting is <jk>false</jk>.
-	 * 
-	 * @return This object (for method chaining).
-	 */
-	public ConfigFileBuilder readOnly() {
-		this.readOnly = true;
-		return this;
-	}
-
-	/**
-	 * Create config files if they cannot be found on the file system.
-	 * 
-	 * <p>
-	 * The default value for this setting is <jk>false</jk>.
-	 * 
-	 * @return This object (for method chaining).
-	 */
-	public ConfigFileBuilder createIfNotExists() {
-		this.createIfNotExists = true;
-		return this;
-	}
-
-	/**
-	 * Returns the config file with the specified absolute or relative path.
-	 * 
-	 * @param path The absolute or relative path of the config file.
-	 * @return The config file.
-	 * @throws IOException If config file could not be parsed.
-	 * @throws FileNotFoundException If config file could not be found.
-	 */
-	public ConfigFile build(String path) throws IOException {
-		return new ConfigFileImpl(resolve(path), readOnly, encoder, serializer, parser, charset);
-	}
-
-	/**
-	 * Create a new empty config file not backed by any file.
-	 * 
-	 * @return A new config file.
-	 * @throws IOException
-	 */
-	public ConfigFile build() throws IOException {
-		return new ConfigFileImpl(null, false, encoder, serializer, parser, charset);
-	}
-
-	/**
-	 * Create a new config file backed by the specified file.
-	 * 
-	 * <p>
-	 * This method is provided primarily for testing purposes.
-	 * 
-	 * @param f The file to create a config file from.
-	 * @return A new config file.
-	 * @throws IOException
-	 */
-	public ConfigFile build(File f) throws IOException {
-		return new ConfigFileImpl(f, false, encoder, serializer, parser, charset);
-	}
-
-	/**
-	 * Create a new config file not backed by a file.
-	 * 
-	 * @param r The reader containing an INI-formatted file to initialize the config file from.
-	 * @return A new config file.
-	 * @throws IOException
-	 */
-	public ConfigFile build(Reader r) throws IOException {
-		return new ConfigFileImpl(null, false, encoder, serializer, parser, charset).load(r);
-	}
-
-	private File resolve(String path) throws IOException {
-
-		// Handle absolute file.
-		File f = new File(path);
-		if (f.isAbsolute()) {
-			if (createIfNotExists)
-				create(f);
-			if (f.exists())
-				return f;
-			throw new FileNotFoundException("Could not find config file '"+path+"'");
-		}
-
-		if (searchPaths.isEmpty())
-			throw new FileNotFoundException("No search paths specified in ConfigFileBuilder.");
-
-		// Handle paths relative to search paths.
-		for (File sf : searchPaths) {
-			f = new File(sf.getAbsolutePath() + "/" + path);
-			if (f.exists())
-				return f;
-		}
-
-		if (createIfNotExists) {
-			f = new File(searchPaths.get(0).getAbsolutePath() + "/" + path);
-			create(f);
-			return f;
-		}
-
-		throw new FileNotFoundException("Could not find config file '"+path+"'");
-	}
-
-	/**
-	 * Implements command-line features for working with INI configuration files.
-	 * 
-	 * <p>
-	 * Invoke as a normal Java program...
-	 * <p class='bcode'>
-	 * 	java org.apache.juneau.config.ConfigFileBuilder [args]
-	 * </p>
-	 * 
-	 * <p>
-	 * Arguments can be any of the following...
-	 * <ul class='spaced-list'>
-	 * 	<li>
-	 * 		No arguments
-	 * 		<br>Prints usage message.
-	 * 	<li>
-	 * 		<code>createBatchEnvFile -configfile &lt;configFile&gt; -envfile &lt;batchFile&gt; [-verbose]</code>
-	 * 		<br>Creates a batch file that will set each config file entry as an environment variable.
-	 * 		<br>Characters in the keys that are not valid as environment variable names (e.g. <js>'/'</js> and <js>'.'</js>)
-	 * 		will be converted to underscores.
-	 * 	<li>
-	 * 		<code>createShellEnvFile -configFile &lt;configFile&gt; -envFile &lt;configFile&gt; [-verbose]</code>
-	 * 		Creates a shell script that will set each config file entry as an environment variable.
-	 * 		<br>Characters in the keys that are not valid as environment variable names (e.g. <js>'/'</js> and <js>'.'</js>)
-	 * 		will be converted to underscores.
-	 * 	<li>
-	 * 		<code>setVals -configFile &lt;configFile&gt; -vals [var1=val1 [var2=val2...]] [-verbose]</code>
-	 * 		Sets values in config files.
-	 * </ul>
-	 * 
-	 * <p>
-	 * For example, the following command will create the file <code>'MyConfig.bat'</code> from the contents of the
-	 * file <code>'MyConfig.cfg'</code>.
-	 * <p class='bcode'>
-	 * 	java org.apache.juneau.config.ConfigFileBuilder createBatchEnvFile -configfile C:\foo\MyConfig.cfg
-	 * 		-batchfile C:\foo\MyConfig.bat
-	 * </p>
-	 * 
-	 * @param args Command-line arguments
-	 */
-	public static void main(String[] args) {
-
-		Args a = new Args(args);
-		String command = a.getArg(0);
-		String configFile = a.getArg("configFile");
-		String envFile = a.getArg("envFile");
-		List<String> vals = a.getArgs("vals");
-
-		if (command == null || ! (command.equals("createBatchEnvFile") || command.equals("createShellEnvFile") || command.equals("setVals")))
-			printUsageAndExit();
-		else if (configFile.isEmpty())
-			printUsageAndExit();
-		else if (command.equals("setVals") && vals.isEmpty())
-			printUsageAndExit();
-		else if ((command.equals("createBatchEnvFile") || command.equals("createShellEnvFile")) && envFile.isEmpty())
-			printUsageAndExit();
-		else {
-			try {
-				ConfigFile cf = new ConfigFileBuilder().build(configFile);
-
-				if (command.equalsIgnoreCase("setVals")) {
-					for (String val : vals) {
-						String[] x = val.split("\\=");
-						if (x.length != 2)
-							throw new FormattedRuntimeException(
-								"Invalid format for value: ''{0}''.  Must be in the format 'key=value'",
-								val
-							);
-						cf.put(x[0], x[1]);
-					}
-					cf.save();
-					return;
-
-				} else if (command.equalsIgnoreCase("createBatchEnvFile")) {
-					try (Writer fw = FileWriterBuilder.create(envFile).build()) {
-						cf.serializeTo(fw, BATCH);
-					}
-					return;
-
-				} else if (command.equalsIgnoreCase("createShellEnvFile")) {
-					try (Writer fw = FileWriterBuilder.create(envFile).build()) {
-						cf.serializeTo(fw, SHELL);
-					}
-					return;
-				}
-
-			} catch (Exception e) {
-				e.printStackTrace();
-			}
-		}
-	}
-
-	private static void printUsageAndExit() {
-		System.err.println("---Usage---"); // NOT DEBUG
-		System.err.println("java -cp juneau.jar org.apache.juneau.config.ConfigFile createBatchEnvFile -configFile <configFile> -envFile <envFile> [-verbose]"); // NOT DEBUG
-		System.err.println("java -cp juneau.jar org.apache.juneau.config.ConfigFile createShellEnvFile -configFile <configFile> -envFile <envFile> [-verbose]"); // NOT DEBUG
-		System.err.println("java -cp juneau.jar org.apache.juneau.config.ConfigFile setVals -configFile <configFile> -vals [var1 val1 [var2 val2...]] [-verbose]"); // NOT DEBUG
-		int rc = Integer.getInteger("exit.2", 2);
-		if (rc != 0)
-			System.exit(rc);
-	}
-}
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileFormat.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileFormat.java
deleted file mode 100644
index 8577d5a..0000000
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileFormat.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// ***************************************************************************************************************************
-// * 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;
-
-import java.io.*;
-
-/**
- * Valid formats that can be passed to the {@link ConfigFile#serializeTo(Writer, ConfigFileFormat)} method.
- */
-public enum ConfigFileFormat {
-
-	/** Normal INI file format*/
-	INI,
-
-	/** Batch file with "set X" commands */
-	BATCH,
-
-	/** Shell script file with "export X" commands */
-	SHELL;
-}
\ No newline at end of file
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileImpl.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileImpl.java
deleted file mode 100644
index e54283d..0000000
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileImpl.java
+++ /dev/null
@@ -1,824 +0,0 @@
-// ***************************************************************************************************************************
-// * 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;
-
-import static org.apache.juneau.internal.ThrowableUtils.*;
-import static org.apache.juneau.config.ConfigUtils.*;
-import static org.apache.juneau.internal.StringUtils.*;
-import static org.apache.juneau.internal.CollectionUtils.*;
-
-import java.io.*;
-import java.lang.reflect.*;
-import java.nio.charset.*;
-import java.util.*;
-import java.util.concurrent.locks.*;
-
-import org.apache.juneau.*;
-import org.apache.juneau.config.encode.*;
-import org.apache.juneau.config.listener.*;
-import org.apache.juneau.config.vars.*;
-import org.apache.juneau.internal.*;
-import org.apache.juneau.json.*;
-import org.apache.juneau.parser.*;
-import org.apache.juneau.serializer.*;
-import org.apache.juneau.svl.*;
-import org.apache.juneau.svl.vars.*;
-
-/**
- * Implementation class for {@link ConfigFile}.
- * 
- * <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 final class ConfigFileImpl extends ConfigFile {
-
-	private final File file;
-	private final Encoder encoder;
-	private final WriterSerializer serializer;
-	private final ReaderParser parser;
-	private final BeanSession pBeanSession;
-	private final Charset charset;
-	final List<ConfigListener> listeners = Collections.synchronizedList(new ArrayList<ConfigListener>());
-
-	Map<String,Section> sections;  // The actual data.
-
-	private static final String DEFAULT = "default";
-
-	private final boolean readOnly;
-
-	volatile boolean hasBeenModified = false;
-	private ReadWriteLock lock = new ReentrantReadWriteLock();
-
-	long modifiedTimestamp;
-
-	/**
-	 * Constructor.
-	 * 
-	 * <p>
-	 * Loads the contents of the specified file into this config file.
-	 * 
-	 * <p>
-	 * If file does not initially exist, this object will start off empty.
-	 * 
-	 * @param file
-	 * 	The INI file on disk.
-	 * 	If <jk>null</jk>, create an in-memory config file.
-	 * @param readOnly
-	 * 	Make this configuration file read-only.
-	 * 	Attempting to set any values on this config file will cause {@link UnsupportedOperationException} to be thrown.
-	 * @param encoder
-	 * 	The encoder to use for encoding sensitive values in this configuration file.
-	 * 	If <jk>null</jk>, defaults to {@link XorEncoder#INSTANCE}.
-	 * @param serializer
-	 * 	The serializer to use for serializing POJOs in the {@link #put(String, Object)} method.
-	 * 	If <jk>null</jk>, defaults to {@link JsonSerializer#DEFAULT}.
-	 * @param parser
-	 * 	The parser to use for parsing POJOs in the {@link #getObject(String,Class)} method.
-	 * 	If <jk>null</jk>, defaults to {@link JsonParser#DEFAULT}.
-	 * @param charset
-	 * 	The charset on the files.
-	 * 	If <jk>null</jk>, defaults to {@link Charset#defaultCharset()}.
-	 * @throws IOException
-	 */
-	public ConfigFileImpl(File file, boolean readOnly, Encoder encoder, WriterSerializer serializer, ReaderParser parser,
-			Charset charset) throws IOException {
-		this.file = file;
-		this.encoder = encoder == null ? XorEncoder.INSTANCE : encoder;
-		this.serializer = serializer == null ? JsonSerializer.DEFAULT : serializer;
-		this.parser = parser == null ? JsonParser.DEFAULT : parser;
-		this.charset = charset == null ? Charset.defaultCharset() : charset;
-		load();
-		this.readOnly = readOnly;
-		if (readOnly) {
-			this.sections = immutableMap(this.sections);
-			for (Section s : sections.values())
-				s.setReadOnly();
-		}
-		this.pBeanSession = this.parser.createSession();
-	}
-
-	/**
-	 * Constructor.
-	 * 
-	 * <p>
-	 * Shortcut for calling <code><jk>new</jk> ConfigFileImpl(file, <jk>false</jk>, <jk>null</jk>, <jk>null</jk>,
-	 * <jk>null</jk>, <jk>null</jk>);</code>
-	 * 
-	 * @param file The config file.  Does not need to exist.
-	 * @throws IOException
-	 */
-	public ConfigFileImpl(File file) throws IOException {
-		this(file, false, null, null, null, null);
-	}
-
-	/**
-	 * Constructor.
-	 * 
-	 * <p>
-	 * Shortcut for calling <code><jk>new</jk> ConfigFileImpl(<jk>null</jk>, <jk>false</jk>, <jk>null</jk>,
-	 * <jk>null</jk>, <jk>null</jk>, <jk>null</jk>);</code>
-	 * 
-	 * @throws IOException
-	 */
-	public ConfigFileImpl() throws IOException {
-		this(null);
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFileImpl loadIfModified() throws IOException {
-		if (file == null)
-			return this;
-		writeLock();
-		try {
-			if (file.lastModified() > modifiedTimestamp)
-				load();
-		} finally {
-			writeUnlock();
-		}
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFileImpl load() throws IOException {
-		try (Reader r = FileReaderBuilder.create(file).charset(charset).allowNoFile().build()) {
-			load(r);
-		}
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFileImpl load(Reader r) throws IOException {
-		assertFieldNotNull(r, "r");
-		writeLock();
-		try {
-			this.sections = Collections.synchronizedMap(new LinkedHashMap<String,Section>());
-			try (BufferedReader in = new BufferedReader(r)) {
-				writeLock();
-				hasBeenModified = false;
-				try {
-					sections.clear();
-					String line = null;
-					Section section = getSection(null, true);
-					ArrayList<String> lines = new ArrayList<>();
-					boolean canAppend = false;
-					while ((line = in.readLine()) != null) {
-						if (isSection(line)) {
-							section.addLines(null, lines.toArray(new String[lines.size()]));
-							lines.clear();
-							canAppend = false;
-							String sn = replaceUnicodeSequences(line.substring(line.indexOf('[')+1, line.indexOf(']')).trim());
-							section = getSection(sn, true).addHeaderComments(section.removeTrailingComments());
-						} else {
-							char c = line.isEmpty() ? 0 : line.charAt(0);
-							if ((c == ' ' || c == '\t') && canAppend && ! (isComment(line) || isAssignment(line)))
-								lines.add(lines.remove(lines.size()-1) + '\n' + line.substring(1));
-							else {
-								lines.add(line);
-								if (isAssignment(line))
-									canAppend = true;
-								else
-									canAppend = canAppend && ! (StringUtils.isEmpty(line) || isComment(line));
-							}
-						}
-					}
-					section.addLines(null, lines.toArray(new String[lines.size()]));
-					in.close();
-					if (hasBeenModified)  // Set when values need to be encoded.
-						save();
-					if (file != null)
-						modifiedTimestamp = file.lastModified();
-				} finally {
-					writeUnlock();
-				}
-			}
-		} finally {
-			writeUnlock();
-		}
-		for (ConfigListener l : listeners)
-			l.onLoad(this);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	protected String serialize(Object value, Serializer serializer, boolean newline) throws SerializeException {
-		if (value == null)
-			return "";
-		if (serializer == null)
-			serializer = this.serializer;
-		Class<?> c = value.getClass();
-		if (isSimpleType(c))
-			return value.toString();
-
-		String r = null;
-		if (newline)
-			r = "\n" + (String)serializer.serialize(value);
-		else
-			r = (String)serializer.serialize(value);
-
-		if (r.startsWith("'"))
-			return r.substring(1, r.length()-1);
-		return r;
-	}
-
-	@Override /* ConfigFile */
-	@SuppressWarnings({ "unchecked" })
-	protected <T> T parse(String s, Parser parser, Type type, Type...args) throws ParseException {
-
-		if (StringUtils.isEmpty(s))
-			return null;
-
-		if (isSimpleType(type))
-			return (T)pBeanSession.convertToType(s, (Class<?>)type);
-
-		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 static 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 static boolean isArray(Type t) {
-		if (! (t instanceof Class))
-			return false;
-		Class<?> c = (Class<?>)t;
-		return (c.isArray());
-	}
-
-
-	//--------------------------------------------------------------------------------
-	// Map methods
-	//--------------------------------------------------------------------------------
-
-	@Override /* Map */
-	public Section get(Object key) {
-		if (StringUtils.isEmpty(key))
-			key = DEFAULT;
-		readLock();
-		try {
-			return sections.get(key);
-		} finally {
-			readUnlock();
-		}
-	}
-
-	@Override /* Map */
-	public Section put(String key, Section section) {
-		Set<String> changes = createChanges();
-		Section old = put(key, section, changes);
-		signalChanges(changes);
-		return old;
-	}
-
-	private Section put(String key, Section section, Set<String> changes) {
-		if (StringUtils.isEmpty(key))
-			key = DEFAULT;
-		writeLock();
-		try {
-			Section prev = sections.put(key, section);
-			findChanges(changes, prev, section);
-			return prev;
-		} finally {
-			writeUnlock();
-		}
-	}
-
-	@Override /* Map */
-	public void putAll(Map<? extends String,? extends Section> map) {
-		Set<String> changes = createChanges();
-		writeLock();
-		try {
-			for (Map.Entry<? extends String,? extends Section> e : map.entrySet())
-				put(e.getKey(), e.getValue(), changes);
-		} finally {
-			writeUnlock();
-		}
-		signalChanges(changes);
-	}
-
-	@Override /* Map */
-	public void clear() {
-		Set<String> changes = createChanges();
-		writeLock();
-		try {
-			for (Section s : values())
-				findChanges(changes, s, null);
-			sections.clear();
-		} finally {
-			writeUnlock();
-		}
-		signalChanges(changes);
-	}
-
-	@Override /* Map */
-	public boolean containsKey(Object key) {
-		if (StringUtils.isEmpty(key))
-			key = DEFAULT;
-		return sections.containsKey(key);
-	}
-
-	@Override /* Map */
-	public boolean containsValue(Object value) {
-		return sections.containsValue(value);
-	}
-
-	@Override /* Map */
-	public Set<Map.Entry<String,Section>> entrySet() {
-
-		// We need to create our own set so that entries are removed correctly.
-		return new AbstractSet<Map.Entry<String,Section>>() {
-			@Override /* Map */
-			public Iterator<Map.Entry<String,Section>> iterator() {
-				return new Iterator<Map.Entry<String,Section>>() {
-					Iterator<Map.Entry<String,Section>> i = sections.entrySet().iterator();
-					Map.Entry<String,Section> i2;
-
-					@Override /* Iterator */
-					public boolean hasNext() {
-						return i.hasNext();
-					}
-
-					@Override /* Iterator */
-					public Map.Entry<String,Section> next() {
-						i2 = i.next();
-						return i2;
-					}
-
-					@Override /* Iterator */
-					public void remove() {
-						Set<String> changes = createChanges();
-						findChanges(changes, i2.getValue(), null);
-						i.remove();
-						signalChanges(changes);
-					}
-				};
-			}
-
-			@Override /* Map */
-			public int size() {
-				return sections.size();
-			}
-		};
-	}
-
-	@Override /* Map */
-	public boolean isEmpty() {
-		return sections.isEmpty();
-	}
-
-	@Override /* Map */
-	public Set<String> keySet() {
-
-		// We need to create our own set so that sections are removed correctly.
-		return new AbstractSet<String>() {
-			@Override /* Set */
-			public Iterator<String> iterator() {
-				return new Iterator<String>() {
-					Iterator<String> i = sections.keySet().iterator();
-					String i2;
-
-					@Override /* Iterator */
-					public boolean hasNext() {
-						return i.hasNext();
-					}
-
-					@Override /* Iterator */
-					public String next() {
-						i2 = i.next();
-						return i2;
-					}
-
-					@Override /* Iterator */
-					public void remove() {
-						Set<String> changes = createChanges();
-						findChanges(changes, sections.get(i2), null);
-						i.remove();
-						signalChanges(changes);
-					}
-				};
-			}
-
-			@Override /* Set */
-			public int size() {
-				return sections.size();
-			}
-		};
-	}
-
-	@Override /* Map */
-	public int size() {
-		return sections.size();
-	}
-
-	@Override /* Map */
-	public Collection<Section> values() {
-		return new AbstractCollection<Section>() {
-			@Override /* Collection */
-			public Iterator<Section> iterator() {
-				return new Iterator<Section>() {
-					Iterator<Section> i = sections.values().iterator();
-					Section i2;
-
-					@Override /* Iterator */
-					public boolean hasNext() {
-						return i.hasNext();
-					}
-
-					@Override /* Iterator */
-					public Section next() {
-						i2 = i.next();
-						return i2;
-					}
-
-					@Override /* Iterator */
-					public void remove() {
-						Set<String> changes = createChanges();
-						findChanges(changes, i2, null);
-						i.remove();
-						signalChanges(changes);
-					}
-				};
-			}
-			@Override /* Collection */
-			public int size() {
-				return sections.size();
-			}
-		};
-	}
-
-	@Override /* Map */
-	public Section remove(Object key) {
-		Set<String> changes = createChanges();
-		Section prev = remove(key, changes);
-		signalChanges(changes);
-		return prev;
-	}
-
-	private Section remove(Object key, Set<String> changes) {
-		writeLock();
-		try {
-			Section prev = sections.remove(key);
-			findChanges(changes, prev, null);
-			return prev;
-		} finally {
-			writeUnlock();
-		}
-	}
-
-
-	//--------------------------------------------------------------------------------
-	// API methods
-	//--------------------------------------------------------------------------------
-
-	@Override /* ConfigFile */
-	public String get(String sectionName, String sectionKey) {
-		assertFieldNotNull(sectionKey, "sectionKey");
-		Section s = get(sectionName);
-		if (s == null)
-			return null;
-		Object s2 = s.get(sectionKey);
-		return (s2 == null ? null : s2.toString());
-	}
-
-	@Override /* ConfigFile */
-	public String put(String sectionName, String sectionKey, Object value, Serializer serializer, boolean encoded,
-			boolean newline) throws SerializeException {
-		assertFieldNotNull(sectionKey, "sectionKey");
-		Section s = getSection(sectionName, true);
-		return s.put(sectionKey, serialize(value, serializer, newline), encoded);
-	}
-
-	@Override /* ConfigFile */
-	public String put(String sectionName, String sectionKey, String value, boolean encoded) {
-		assertFieldNotNull(sectionKey, "sectionKey");
-		Section s = getSection(sectionName, true);
-		return s.put(sectionKey, value, encoded);
-	}
-
-	@Override /* ConfigFile */
-	public String remove(String sectionName, String sectionKey) {
-		assertFieldNotNull(sectionKey, "sectionKey");
-		Section s = getSection(sectionName, false);
-		if (s == null)
-			return null;
-		return s.remove(sectionKey);
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFileImpl addLines(String section, String...lines) {
-		Set<String> changes = createChanges();
-		writeLock();
-		try {
-			getSection(section, true).addLines(changes, lines);
-		} finally {
-			writeUnlock();
-		}
-		signalChanges(changes);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFileImpl addHeaderComments(String section, String...headerComments) {
-		writeLock();
-		try {
-			if (headerComments == null)
-				headerComments = new String[0];
-			getSection(section, true).addHeaderComments(Arrays.asList(headerComments));
-		} finally {
-			writeUnlock();
-		}
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFileImpl clearHeaderComments(String section) {
-		writeLock();
-		try {
-			Section s = getSection(section, false);
-			if (s != null)
-				s.clearHeaderComments();
-		} finally {
-			writeUnlock();
-		}
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public Section getSection(String name) {
-		return getSection(name, false);
-	}
-
-	@Override /* ConfigFile */
-	public Section getSection(String name, boolean create) {
-		if (StringUtils.isEmpty(name))
-			name = DEFAULT;
-		Section s = sections.get(name);
-		if (s != null)
-			return s;
-		if (create) {
-			s = new Section().setParent(this).setName(name);
-			sections.put(name, s);
-			return s;
-		}
-		return null;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFileImpl addSection(String name) {
-		writeLock();
-		try {
-			getSection(name, true);
-		} finally {
-			writeUnlock();
-		}
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile setSection(String name, Map<String,String> contents) {
-		writeLock();
-		try {
-			put(name, new Section(contents).setParent(this).setName(name));
-		} finally {
-			writeUnlock();
-		}
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFileImpl removeSection(String name) {
-		Set<String> changes = createChanges();
-		writeLock();
-		try {
-			Section prev = sections.remove(name);
-			if (changes != null && prev != null)
-				findChanges(changes, prev, null);
-		} finally {
-			writeUnlock();
-		}
-		signalChanges(changes);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public Set<String> getSectionKeys(String sectionName) {
-		Section s = get(sectionName);
-		if (s == null)
-			return null;
-		return s.keySet();
-	}
-
-	@Override /* ConfigFile */
-	public boolean isEncoded(String key) {
-		assertFieldNotNull(key, "key");
-		String section = getSectionName(key);
-		Section s = getSection(section, false);
-		if (s == null)
-			return false;
-		return s.isEncoded(getSectionKey(key));
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFileImpl save() throws IOException {
-		writeLock();
-		try {
-			if (file == null)
-				throw new UnsupportedOperationException("No backing file specified for config file.");
-			try (Writer out = FileWriterBuilder.create(file).charset(charset).build()) {
-				serializeTo(out);
-				hasBeenModified = false;
-				modifiedTimestamp = file.lastModified();
-			}
-			for (ConfigListener l : listeners)
-				l.onSave(this);
-			return this;
-		} finally {
-			writeUnlock();
-		}
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFileImpl serializeTo(Writer out, ConfigFileFormat format) throws IOException {
-		readLock();
-		try {
-			PrintWriter pw = (out instanceof PrintWriter ? (PrintWriter)out : new PrintWriter(out));
-			for (Section s : sections.values())
-				s.writeTo(pw, format);
-			pw.flush();
-			pw.close();
-			out.close();
-		} finally {
-			readUnlock();
-		}
-		return this;
-	}
-
-	void setHasBeenModified() {
-		hasBeenModified = true;
-	}
-
-	@Override /* ConfigFile */
-	public String toString() {
-		try {
-			StringWriter sw = new StringWriter();
-			toWritable().writeTo(sw);
-			return sw.toString();
-		} catch (IOException e) {
-			return e.getLocalizedMessage();
-		}
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile addListener(ConfigListener listener) {
-		assertFieldNotNull(listener, "listener");
-		writeLock();
-		try {
-			this.listeners.add(listener);
-			return this;
-		} finally {
-			writeUnlock();
-		}
-	}
-
-	List<ConfigListener> getListeners() {
-		return listeners;
-	}
-
-	@Override /* ConfigFile */
-	public Writable toWritable() {
-		return new ConfigFileWritable(this);
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile merge(ConfigFile cf) {
-		assertFieldNotNull(cf, "cf");
-		Set<String> changes = createChanges();
-		writeLock();
-		try {
-			for (String sectionName : this.keySet())
-				if (! cf.containsKey(sectionName))
-					remove(sectionName, changes);
-
-			for (Map.Entry<String,Section> e : cf.entrySet())
-				put(e.getKey(), e.getValue(), changes);
-
-		} finally {
-			writeUnlock();
-		}
-		signalChanges(changes);
-		return this;
-	}
-
-	Encoder getEncoder() {
-		return encoder;
-	}
-
-	@Override /* ConfigFile */
-	protected BeanSession getBeanSession() {
-		return pBeanSession;
-	}
-
-	@Override /* ConfigFile */
-	protected void readLock() {
-		lock.readLock().lock();
-	}
-
-	@Override /* ConfigFile */
-	protected void readUnlock() {
-		lock.readLock().unlock();
-	}
-
-	private void writeLock() {
-		if (readOnly)
-			throw new UnsupportedOperationException("Cannot modify read-only ConfigFile.");
-		lock.writeLock().lock();
-		hasBeenModified = true;
-	}
-
-	private void writeUnlock() {
-		lock.writeLock().unlock();
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile getResolving(VarResolver vr) {
-		assertFieldNotNull(vr, "vr");
-		return new ConfigFileWrapped(this, vr);
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile getResolving(VarResolverSession vs) {
-		assertFieldNotNull(vs, "vs");
-		return new ConfigFileWrapped(this, vs);
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile getResolving() {
-		return getResolving(
-			new VarResolverBuilder()
-				.vars(SystemPropertiesVar.class, EnvVariablesVar.class, SwitchVar.class, IfVar.class, ConfigFileVar.class,
-					IfVar.class, SwitchVar.class)
-				.contextObject(ConfigFileVar.SESSION_config, this)
-				.build()
-		);
-	}
-
-	/*
-	 * Finds the keys that are different between the two sections and adds it to
-	 * the specified set.
-	 */
-	static final void findChanges(Set<String> s, Section a, Section b) {
-		if (s == null)
-			return;
-		String sname = (a == null ? b.name : a.name);
-		if (a == null) {
-			for (String k : b.keySet())
-				s.add(getFullKey(sname, k));
-		} else if (b == null) {
-			for (String k : a.keySet())
-				s.add(getFullKey(sname, k));
-		} else {
-			for (String k : a.keySet())
-				addChange(s, sname, k, a.get(k), b.get(k));
-			for (String k : b.keySet())
-				addChange(s, sname, k, a.get(k), b.get(k));
-		}
-	}
-
-	static final void addChange(Set<String> changes, String section, String key, String oldVal, String newVal) {
-		if (! isEquals(oldVal, newVal))
-			changes.add(getFullKey(section, key));
-	}
-
-	final Set<String> createChanges() {
-		return (listeners.size() > 0 ? new LinkedHashSet<String>() : null);
-	}
-
-	final void signalChanges(Set<String> changes) {
-		if (changes != null && ! changes.isEmpty())
-			for (ConfigListener l : listeners)
-				l.onChange(this, changes);
-	}
-}
\ No newline at end of file
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWrapped.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWrapped.java
deleted file mode 100644
index 1ffba8b..0000000
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWrapped.java
+++ /dev/null
@@ -1,294 +0,0 @@
-// ***************************************************************************************************************************
-// * 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;
-
-import static org.apache.juneau.internal.ThrowableUtils.*;
-
-import java.io.*;
-import java.lang.reflect.*;
-import java.util.*;
-
-import org.apache.juneau.*;
-import org.apache.juneau.config.listener.*;
-import org.apache.juneau.config.vars.*;
-import org.apache.juneau.parser.*;
-import org.apache.juneau.serializer.*;
-import org.apache.juneau.svl.*;
-
-/**
- * Wraps an instance of {@link ConfigFileImpl} in an interface that will automatically replace {@link VarResolver}
- * variables.
- * 
- * <p>
- * The {@link ConfigFile#getResolving(VarResolver)} returns an instance of this class.
- * 
- * <p>
- * This class overrides the {@link #getString(String, String)} to resolve string variables.
- * <br>All other method calls are passed through to the inner config file.
- * 
- * <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 final class ConfigFileWrapped extends ConfigFile {
-
-	private final ConfigFileImpl cf;
-	private final VarResolverSession vs;
-
-	ConfigFileWrapped(ConfigFileImpl cf, VarResolver vr) {
-		this.cf = cf;
-		this.vs = vr.builder()
-			.vars(ConfigFileVar.class)
-			.contextObject(ConfigFileVar.SESSION_config, cf)
-			.build()
-			.createSession();
-	}
-
-	ConfigFileWrapped(ConfigFileImpl cf, VarResolverSession vs) {
-		this.cf = cf;
-		this.vs = vs;
-	}
-
-	@Override /* ConfigFile */
-	public void clear() {
-		cf.clear();
-	}
-
-	@Override /* ConfigFile */
-	public boolean containsKey(Object key) {
-		return cf.containsKey(key);
-	}
-
-	@Override /* ConfigFile */
-	public boolean containsValue(Object value) {
-		return cf.containsValue(value);
-	}
-
-	@Override /* ConfigFile */
-	public Set<java.util.Map.Entry<String,Section>> entrySet() {
-		return cf.entrySet();
-	}
-
-	@Override /* ConfigFile */
-	public Section get(Object key) {
-		return cf.get(key);
-	}
-
-	@Override /* ConfigFile */
-	public boolean isEmpty() {
-		return cf.isEmpty();
-	}
-
-	@Override /* ConfigFile */
-	public Set<String> keySet() {
-		return cf.keySet();
-	}
-
-	@Override /* ConfigFile */
-	public Section put(String key, Section value) {
-		return cf.put(key, value);
-	}
-
-	@Override /* ConfigFile */
-	public void putAll(Map<? extends String,? extends Section> map) {
-		cf.putAll(map);
-	}
-
-	@Override /* ConfigFile */
-	public Section remove(Object key) {
-		return cf.remove(key);
-	}
-
-	@Override /* ConfigFile */
-	public int size() {
-		return cf.size();
-	}
-
-	@Override /* ConfigFile */
-	public Collection<Section> values() {
-		return cf.values();
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile loadIfModified() throws IOException {
-		cf.loadIfModified();
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile load() throws IOException {
-		cf.load();
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile load(Reader r) throws IOException {
-		cf.load(r);
-		return this;
-	}
-
-
-	@Override /* ConfigFile */
-	public boolean isEncoded(String key) {
-		return cf.isEncoded(key);
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile addLines(String section, String... lines) {
-		cf.addLines(section, lines);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile addHeaderComments(String section, String... headerComments) {
-		cf.addHeaderComments(section, headerComments);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile clearHeaderComments(String section) {
-		cf.clearHeaderComments(section);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public Section getSection(String name) {
-		return cf.getSection(name);
-	}
-
-	@Override /* ConfigFile */
-	public Section getSection(String name, boolean create) {
-		return cf.getSection(name, create);
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile addSection(String name) {
-		cf.addSection(name);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile setSection(String name, Map<String,String> contents) {
-		cf.setSection(name, contents);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile removeSection(String name) {
-		cf.removeSection(name);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile save() throws IOException {
-		cf.save();
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile serializeTo(Writer out, ConfigFileFormat format) throws IOException {
-		cf.serializeTo(out, format);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public String toString() {
-		return cf.toString();
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile getResolving(VarResolver varResolver) {
-		assertFieldNotNull(varResolver, "vr");
-		return new ConfigFileWrapped(cf, varResolver);
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile getResolving(VarResolverSession varSession) {
-		assertFieldNotNull(varSession, "vs");
-		return new ConfigFileWrapped(cf, varSession);
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile getResolving() {
-		return new ConfigFileWrapped(cf, VarResolver.DEFAULT);
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile addListener(ConfigListener listener) {
-		cf.addListener(listener);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	public Writable toWritable() {
-		return cf.toWritable();
-	}
-
-	@Override /* ConfigFile */
-	public ConfigFile merge(ConfigFile newCf) {
-		cf.merge(newCf);
-		return this;
-	}
-
-	@Override /* ConfigFile */
-	protected BeanSession getBeanSession() {
-		return cf.getBeanSession();
-	}
-
-	@Override /* ConfigFile */
-	public String get(String sectionName, String sectionKey) {
-		return vs.resolve(cf.get(sectionName, sectionKey));
-	}
-
-	@Override /* ConfigFile */
-	public String put(String sectionName, String sectionKey, String value, boolean encoded) {
-		return cf.put(sectionName, sectionKey, value, encoded);
-	}
-
-	@Override /* ConfigFile */
-	public String put(String sectionName, String sectionKey, Object value, Serializer serializer, boolean encoded,
-			boolean newline) throws SerializeException {
-		return cf.put(sectionName, sectionKey, value, serializer, encoded, newline);
-	}
-
-	@Override /* ConfigFile */
-	public String remove(String sectionName, String sectionKey) {
-		return cf.remove(sectionName, sectionKey);
-	}
-
-	@Override /* ConfigFile */
-	public Set<String> getSectionKeys(String sectionName) {
-		return cf.getSectionKeys(sectionName);
-	}
-
-	@Override /* ConfigFile */
-	protected void readLock() {
-		cf.readLock();
-	}
-
-	@Override /* ConfigFile */
-	protected void readUnlock() {
-		cf.readUnlock();
-	}
-
-	@Override /* ConfigFile */
-	protected String serialize(Object o, Serializer s, boolean newline) throws SerializeException {
-		return cf.serialize(o, s, newline);
-	}
-
-	@Override /* ConfigFile */
-	protected <T> T parse(String s, Parser parser, Type type, Type... args) throws ParseException {
-		return cf.parse(s, parser, type, args);
-	}
-}
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/ConfigMod.java
similarity index 97%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigMod.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigMod.java
index ff1a46e..c592c9d 100644
--- 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/ConfigMod.java
@@ -10,7 +10,7 @@
 // * "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;
+package org.apache.juneau.config;
 
 import java.util.*;
 
@@ -23,10 +23,11 @@ import org.apache.juneau.config.encode.*;
 public enum ConfigMod {
 	
 	/**
-	 * Encoded using the registered {@link Encoder}.
+	 * Encoded using the registered {@link ConfigEncoder}.
 	 */
 	ENCODED("*");
 	
+	
 	private final String c;
 	
 	private ConfigMod(String c) {
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigUtils.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigUtils.java
deleted file mode 100644
index 0844371..0000000
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigUtils.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// ***************************************************************************************************************************
-// * 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;
-
-/**
- * Internal utility methods.
- */
-public class ConfigUtils {
-
-	/**
-	 * Parses a config key and returns just the section name.
-	 * 
-	 * @param key The config key.
-	 * @return The section name.
-	 */
-	public static final String getSectionName(String key) {
-		int i = key.indexOf('/');
-		if (i == -1)
-			return "default";
-		return key.substring(0, i);
-	}
-
-	/**
-	 * Parses a config key and returns just the section key.
-	 * 
-	 * @param key The config key.
-	 * @return The section key.
-	 */
-	public static final String getSectionKey(String key) {
-		int i = key.indexOf('/');
-		if (i == -1)
-			return key;
-		return key.substring(i+1);
-	}
-
-	static final String getFullKey(String section, String key) {
-		if (section.equals("default"))
-			return key;
-		return section + '/' + key;
-	}
-
-	static final boolean isComment(String line) {
-		for (int i = 0; i < line.length(); i++) {
-			char c = line.charAt(i);
-			if (! Character.isWhitespace(c))
-				return c == '#';
-		}
-		return false;
-	}
-
-	static final boolean isAssignment(String line) {
-		int S1 = 1; // Looking for char;
-		int S2 = 2; // Found char, looking for whitespace or =
-		int S3 = 3; // Found whitespace, looking for =
-		int state = S1;
-		for (int i = 0; i < line.length(); i++) {
-			char c = line.charAt(i);
-			if (state == S1) {
-				if (! Character.isWhitespace(c))
-					state = S2;
-			} else if (state == S2) {
-				if (c == '=')
-					return true;
-				if (Character.isWhitespace(c))
-					state = S3;
-			} else if (state == S3) {
-				if (c == '=')
-					return true;
-			}
-		}
-		return false;
-	}
-
-	static final boolean isSection(String line) {
-		int S1 = 1; // Looking for [;
-		int S2 = 2; // Found [, looking for ]
-		int state = S1;
-		for (int i = 0; i < line.length(); i++) {
-			char c = line.charAt(i);
-			if (state == S1) {
-				if (! Character.isWhitespace(c)) {
-					if (c == '[')
-						state = S2;
-					else
-						return false;
-				}
-			} else if (state == S2) {
-				if (c == ']')
-					return true;
-			}
-		}
-		return false;
-	}
-}
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Section.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Section.java
deleted file mode 100644
index b01396f..0000000
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/Section.java
+++ /dev/null
@@ -1,577 +0,0 @@
-// ***************************************************************************************************************************
-// * 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;
-
-import static org.apache.juneau.config.ConfigFileFormat.*;
-import static org.apache.juneau.config.ConfigUtils.*;
-import static org.apache.juneau.internal.StringUtils.*;
-import static org.apache.juneau.internal.CollectionUtils.*;
-
-import java.io.*;
-import java.util.*;
-import java.util.concurrent.locks.*;
-
-import org.apache.juneau.annotation.*;
-import org.apache.juneau.config.listener.*;
-
-/**
- * Defines a section in a config file.
- */
-public final class Section implements Map<String,String> {
-
-	private ConfigFileImpl configFile;
-	String name;   // The config section name, or "default" if the default section.  Never null.
-
-	// The data structures that make up this object.
-	// These must be kept synchronized.
-	private LinkedList<String> lines = new LinkedList<>();
-	private List<String> headerComments = new LinkedList<>();
-	Map<String,String> entries;
-
-	private ReadWriteLock lock = new ReentrantReadWriteLock();
-	private boolean readOnly;
-
-	/**
-	 * Constructor.
-	 */
-	public Section() {
-		this.entries = new LinkedHashMap<>();
-	}
-
-	/**
-	 * Constructor with predefined contents.
-	 * 
-	 * @param contents Predefined contents to copy into this section.
-	 */
-	public Section(Map<String,String> contents) {
-		this.entries = new LinkedHashMap<>(contents);
-	}
-
-	Section setReadOnly() {
-		// This method is only called once from ConfigFileImpl constructor.
-		this.readOnly = true;
-		this.entries = immutableMap(entries);
-		return this;
-	}
-
-	/**
-	 * Sets the config file that this section belongs to.
-	 * 
-	 * @param configFile The config file that this section belongs to.
-	 * @return This object (for method chaining).
-	 */
-	@ParentProperty
-	public Section setParent(ConfigFileImpl configFile) {
-		this.configFile = configFile;
-		return this;
-	}
-
-	/**
-	 * Sets the section name
-	 * 
-	 * @param name The section name.
-	 * @return This object (for method chaining).
-	 */
-	@NameProperty
-	public Section setName(String name) {
-		this.name = name;
-		return this;
-	}
-
-
-	//--------------------------------------------------------------------------------
-	// Map methods
-	//--------------------------------------------------------------------------------
-
-	@Override /* Map */
-	public void clear() {
-		Set<String> changes = createChanges();
-		writeLock();
-		try {
-			if (changes != null)
-				for (String k : keySet())
-					changes.add(getFullKey(name, k));
-			entries.clear();
-			lines.clear();
-			headerComments.clear();
-		} finally {
-			writeUnlock();
-		}
-		signalChanges(changes);
-	}
-
-	@Override /* Map */
-	public boolean containsKey(Object key) {
-		return entries.containsKey(key);
-	}
-
-	@Override /* Map */
-	public boolean containsValue(Object value) {
-		return entries.containsValue(value);
-	}
-
-	@Override /* Map */
-	public Set<Map.Entry<String,String>> entrySet() {
-
-		// We need to create our own set so that entries are removed correctly.
-		return new AbstractSet<Map.Entry<String,String>>() {
-			@Override /* Set */
-			public Iterator<Map.Entry<String,String>> iterator() {
-				return new Iterator<Map.Entry<String,String>>() {
-					Iterator<Map.Entry<String,String>> i = entries.entrySet().iterator();
-					Map.Entry<String,String> i2;
-
-					@Override /* Iterator */
-					public boolean hasNext() {
-						return i.hasNext();
-					}
-
-					@Override /* Iterator */
-					public Map.Entry<String,String> next() {
-						i2 = i.next();
-						return i2;
-					}
-
-					@Override /* Iterator */
-					public void remove() {
-						Set<String> changes = createChanges();
-						String key = i2.getKey(), val = i2.getValue();
-						addChange(changes, key, val, null);
-						writeLock();
-						try {
-							i.remove();
-							removeLine(key);
-						} finally {
-							writeUnlock();
-						}
-						signalChanges(changes);
-					}
-				};
-			}
-
-			@Override /* Set */
-			public int size() {
-				return entries.size();
-			}
-		};
-	}
-
-	@Override /* Map */
-	public String get(Object key) {
-		String s = entries.get(key);
-		return s;
-	}
-
-	@Override /* Map */
-	public boolean isEmpty() {
-		return entries.isEmpty();
-	}
-
-	@Override /* Map */
-	public Set<String> keySet() {
-
-		// We need to create our own set so that sections are removed correctly.
-		return new AbstractSet<String>() {
-			@Override /* Set */
-			public Iterator<String> iterator() {
-				return new Iterator<String>() {
-					Iterator<String> i = entries.keySet().iterator();
-					String i2;
-
-					@Override /* Iterator */
-					public boolean hasNext() {
-						return i.hasNext();
-					}
-
-					@Override /* Iterator */
-					public String next() {
-						i2 = i.next();
-						return i2;
-					}
-
-					@Override /* Iterator */
-					public void remove() {
-						Set<String> changes = createChanges();
-						String key = i2;
-						String val = entries.get(key);
-						addChange(changes, key, val, null);
-						writeLock();
-						try {
-							i.remove();
-							removeLine(key);
-						} finally {
-							writeUnlock();
-						}
-						signalChanges(changes);
-					}
-				};
-			}
-
-			@Override /* Set */
-			public int size() {
-				return entries.size();
-			}
-		};
-	}
-
-	@Override /* Map */
-	public String put(String key, String value) {
-		return put(key, value, false);
-	}
-
-	/**
-	 * Sets the specified value in this section.
-	 * 
-	 * @param key The section key.
-	 * @param value The new value.
-	 * @param encoded Whether this value should be encoded during save.
-	 * @return The previous value.
-	 */
-	public String put(String key, String value, boolean encoded) {
-		Set<String> changes = createChanges();
-		String s = put(key, value, encoded, changes);
-		signalChanges(changes);
-		return s;
-	}
-
-	String put(String key, String value, boolean encoded, Set<String> changes) {
-		writeLock();
-		try {
-			addLine(key, encoded);
-			String prev = entries.put(key, value);
-			addChange(changes, key, prev, value);
-			return prev;
-		} finally {
-			writeUnlock();
-		}
-	}
-
-	@Override /* Map */
-	public void putAll(Map<? extends String,? extends String> map) {
-		Set<String> changes = createChanges();
-		for (Map.Entry<? extends String,? extends String> e : map.entrySet())
-			put(e.getKey(), e.getValue(), false, changes);
-		signalChanges(changes);
-	}
-
-	@Override /* Map */
-	public String remove(Object key) {
-		Set<String> changes = createChanges();
-		String old = remove(key, changes);
-		signalChanges(changes);
-		return old;
-	}
-
-	String remove(Object key, Set<String> changes) {
-		writeLock();
-		try {
-			String prev = entries.remove(key);
-			addChange(changes, key.toString(), prev, null);
-			removeLine(key.toString());
-			return prev;
-		} finally {
-			writeUnlock();
-		}
-	}
-
-	void removeLine(String key) {
-		for (Iterator<String> i = lines.iterator(); i.hasNext();) {
-			String k = i.next();
-			if (k.startsWith("*") || k.startsWith(">")) {
-				if (k.substring(1).equals(key)) {
-					i.remove();
-					break;
-				}
-			}
-		}
-	}
-
-	@Override /* Map */
-	public int size() {
-		return entries.size();
-	}
-
-	@Override /* Map */
-	public Collection<String> values() {
-		return Collections.unmodifiableCollection(entries.values());
-	}
-
-
-	//--------------------------------------------------------------------------------
-	// API methods
-	//--------------------------------------------------------------------------------
-
-	/**
-	 * Returns <jk>true</jk> if the specified entry is encoded.
-	 * 
-	 * @param key The key.
-	 * @return <jk>true</jk> if the specified entry is encoded.
-	 */
-	public boolean isEncoded(String key) {
-		readLock();
-		try {
-			for (String s : lines)
-				if (s.length() > 1)
-					if (s.substring(1).equals(key))
-						return s.charAt(0) == '*';
-			return false;
-		} finally {
-			readUnlock();
-		}
-	}
-
-	/**
-	 * Adds header comments to this section.
-	 * 
-	 * @see ConfigFile#addHeaderComments(String, String...) for a description.
-	 * @param comments The comment lines to add to this section.
-	 * @return This object (for method chaining).
-	 */
-	public Section addHeaderComments(List<String> comments) {
-		writeLock();
-		try {
-			for (String c : comments) {
-				if (c == null)
-					c = "";
-				if (! c.startsWith("#"))
-					c = "#" + c;
-				this.headerComments.add(c);
-			}
-			return this;
-		} finally {
-			writeUnlock();
-		}
-	}
-
-	/**
-	 * Removes all header comments from this section.
-	 */
-	public void clearHeaderComments() {
-		writeLock();
-		try {
-			this.headerComments.clear();
-		} finally {
-			writeUnlock();
-		}
-	}
-
-	/**
-	 * Serialize this section.
-	 * 
-	 * @param out What to serialize to.
-	 * @param format The format (e.g. INI, BATCH, SHELL).
-	 */
-	public void writeTo(PrintWriter out, ConfigFileFormat format) {
-		readLock();
-		try {
-			if (format == INI) {
-				for (String s : headerComments)
-					out.append(s).println();
-				if (! name.equals("default"))
-					out.append('[').append(name).append(']').println();
-				for (String l : lines) {
-					char c = (l.length() > 0 ? l.charAt(0) : 0);
-					if (c == '>' || c == '*'){
-						boolean encode = c == '*';
-						String key = l.substring(1);
-						String val = entries.get(key);
-						if (val.indexOf('\n') != -1)
-							val = val.replaceAll("(\\r?\\n)", "$1\t");
-						if (val.indexOf('=') != -1)
-							val = val.replace("=", "\\u003D");
-						if (val.indexOf('#') != -1)
-							val = val.replace("#", "\\u0023");
-						out.append(key);
-						if (encode)
-							out.append('*');
-						out.append(" = ");
-						if (encode)
-							out.append('{').append(configFile.getEncoder().encode(key, val)).append('}');
-						else
-							out.append(val);
-						out.println();
-					} else {
-						out.append(l).println();
-					}
-				}
-
-			} else if (format == BATCH) {
-				String section = name.replaceAll("\\.\\/", "_");
-				for (String l : headerComments) {
-					l = trimComment(l);
-					if (! l.isEmpty())
-						out.append("rem ").append(l);
-					out.println();
-				}
-				for (String l : lines) {
-					char c = (l.length() > 0 ? l.charAt(0) : 0);
-					if (c == '>' || c == '*') {
-						String key = l.substring(1);
-						String val = entries.get(key);
-						out.append("set ");
-						if (! name.equals("default"))
-							out.append(section).append('_');
-						out.append(key.replaceAll("\\.\\/", "_")).append(" = ").append(val).println();
-					} else {
-						l = trimComment(l);
-						if (! l.isEmpty())
-							out.append("rem ").append(l);
-						out.println();
-					}
-				}
-
-			} else if (format == SHELL) {
-				String section = name.replaceAll("\\.\\/", "_");
-				for (String l : headerComments) {
-					l = trimComment(l);
-					if (! l.isEmpty())
-						out.append("# ").append(l);
-					out.println();
-				}
-				for (String l : lines) {
-					char c = (l.length() > 0 ? l.charAt(0) : 0);
-					if (c == '>' || c == '*'){
-						String key = l.substring(1);
-						String val = entries.get(key).replaceAll("\\\\", "\\\\\\\\");
-						out.append("export ");
-						if (! name.equals("default"))
-							out.append(section).append('_');
-						out.append(key.replaceAll("\\.\\/", "_")).append('=').append('"').append(val).append('"').println();
-					} else {
-						l = trimComment(l);
-						if (! l.isEmpty())
-							out.append("# ").append(l);
-						out.println();
-					}
-				}
-			}
-		} finally {
-			readUnlock();
-		}
-	}
-
-
-	//--------------------------------------------------------------------------------
-	// Protected methods used by ConfigFile
-	//--------------------------------------------------------------------------------
-
-	/*
-	 * Add lines to this section.
-	 */
-	Section addLines(Set<String> changes, String...l) {
-		writeLock();
-		try {
-			if (l == null)
-				l = new String[0];
-			for (int i = 0; i < l.length; i++) {
-				String line = l[i];
-				if (line == null)
-					line = "";
-				if (isComment(line))
-					this.lines.add(line);
-				else if (isAssignment(line)) {
-					// Key/value pairs are stored as either ">key" or "*key";
-					String key = replaceUnicodeSequences(line.substring(0, line.indexOf('=')).trim());
-					String val = replaceUnicodeSequences(line.substring(line.indexOf('=')+1).trim());
-					boolean encoded = key.length() > 1 && key.endsWith("*");
-					if (encoded) {
-						key = key.substring(0, key.lastIndexOf('*'));
-						String v = val.toString().trim();
-						if (v.startsWith("{") && v.endsWith("}"))
-							val = configFile.getEncoder().decode(key, v.substring(1, v.length()-1));
-						else
-							configFile.setHasBeenModified();
-					}
-					if (containsKey(key)) {
-						entries.remove(key);
-						lines.remove('*' + key);
-						lines.remove('>' + key);
-					}
-					lines.add((encoded ? '*' : '>') + key);
-					addChange(changes, key, entries.put(key, val), val);
-				} else {
-					this.lines.add(line);
-				}
-			}
-			return this;
-		} finally {
-			writeUnlock();
-		}
-	}
-
-	/*
-	 * Remove all "#*" lines at the end of this section so they can
-	 * be associated with the next section.
-	 */
-	List<String> removeTrailingComments() {
-		LinkedList<String> l = new LinkedList<>();
-		while ((! lines.isEmpty()) && lines.getLast().startsWith("#"))
-			l.addFirst(lines.removeLast());
-		return l;
-	}
-
-
-	//--------------------------------------------------------------------------------
-	// Private methods
-	//--------------------------------------------------------------------------------
-
-	private void addLine(String key, boolean encoded) {
-		for (Iterator<String> i = lines.iterator(); i.hasNext();) {
-			String k = i.next();
-			if ((k.startsWith("*") || k.startsWith(">")) && k.substring(1).equals(key)) {
-				if (k.startsWith("*") && encoded || k.startsWith(">") && ! encoded)
-					return;
-				i.remove();
-			}
-		}
-		lines.add((encoded ? "*" : ">") + key);
-	}
-
-	void readLock() {
-		lock.readLock().lock();
-	}
-
-	void readUnlock() {
-		lock.readLock().unlock();
-	}
-
-	void writeLock() {
-		if (readOnly)
-			throw new UnsupportedOperationException("Cannot modify read-only ConfigFile.");
-		lock.writeLock().lock();
-	}
-
-	void writeUnlock() {
-		lock.writeLock().unlock();
-	}
-
-	private static String trimComment(String s) {
-		return s.replaceAll("^\\s*\\#\\s*", "").trim();
-	}
-
-	Set<String> createChanges() {
-		return (configFile != null && configFile.getListeners().size() > 0 ? new LinkedHashSet<String>() : null);
-	}
-
-	void signalChanges(Set<String> changes) {
-		if (changes != null && ! changes.isEmpty())
-			for (ConfigListener l : configFile.getListeners())
-				l.onChange(configFile, changes);
-	}
-
-	void addChange(Set<String> changes, String key, String oldVal, String newVal) {
-		if (changes != null)
-			if (! isEquals(oldVal, newVal))
-				changes.add(getFullKey(name, key));
-	}
-}
\ No newline at end of file
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/Encoder.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/ConfigEncoder.java
similarity index 85%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/Encoder.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/ConfigEncoder.java
index c297d91..4ddf74d 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/Encoder.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/ConfigEncoder.java
@@ -15,14 +15,14 @@ package org.apache.juneau.config.encode;
 import org.apache.juneau.config.*;
 
 /**
- * API for defining a string encoding/decoding mechanism for entries in {@link ConfigFile}.
+ * API for defining a string encoding/decoding mechanism for entries in {@link Config}.
  * 
  * <h5 class='section'>See Also:</h5>
  * <ul class='doctree'>
  * 	<li class='link'><a class='doclink' href='../../../../overview-summary.html#juneau-config.EncodedEntries'>Overview &gt; juneau-config &gt; Encoded Entries</a>
  * </ul>
  */
-public interface Encoder {
+public interface ConfigEncoder {
 
 	/**
 	 * Encode a string.
@@ -41,4 +41,14 @@ public interface Encoder {
 	 * @return The decoded output string.
 	 */
 	public String decode(String fieldName, String in);
+	
+	/**
+	 * Returns <jk>true</jk> if the specified string is encoded.
+	 * 
+	 * @param in The input string.
+	 * @return 
+	 * 	<jk>true</jk> if the specified string is encoded.
+	 * 	<br>Returns <jk>false</jk> if the string is <jk>null</jk>.
+	 */
+	public boolean isEncoded(String in);
 }
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/XorEncoder.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/ConfigXorEncoder.java
similarity index 81%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/XorEncoder.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/ConfigXorEncoder.java
index 3b7db9c..3a32ea6 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/XorEncoder.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/encode/ConfigXorEncoder.java
@@ -27,26 +27,29 @@ import static org.apache.juneau.internal.IOUtils.*;
  * 	<li class='link'><a class='doclink' href='../../../../overview-summary.html#juneau-config.EncodedEntries'>Overview &gt; juneau-config &gt; Encoded Entries</a>
  * </ul>
  */
-public final class XorEncoder implements Encoder {
+public final class ConfigXorEncoder implements ConfigEncoder {
 
-	/** Reusable XOR-Encoder instance. */
-	public static final XorEncoder INSTANCE = new XorEncoder();
+	/** Reusable XOR-ConfigEncoder instance. */
+	public static final ConfigXorEncoder INSTANCE = new ConfigXorEncoder();
 
 	private static final String key = System.getProperty("org.apache.juneau.config.XorEncoder.key",
 		"nuy7og796Vh6G9O6bG230SHK0cc8QYkH");	// The super-duper-secret key
 
-	@Override /* Encoder */
+	@Override /* ConfigEncoder */
 	public String encode(String fieldName, String in) {
 		byte[] b = in.getBytes(UTF8);
 		for (int i = 0; i < b.length; i++) {
 				int j = i % key.length();
 			b[i] = (byte)(b[i] ^ key.charAt(j));
 		}
-		return base64Encode(b);
+		return '{' + base64Encode(b) + '}';
 	}
 
-	@Override /* Encoder */
+	@Override /* ConfigEncoder */
 	public String decode(String fieldName, String in) {
+		if (! isEncoded(in))
+			return in;
+		in = in.substring(1, in.length()-1);
 		byte[] b = base64Decode(in);
 		for (int i = 0; i < b.length; i++) {
 			int j = i % key.length();
@@ -54,4 +57,9 @@ public final class XorEncoder implements Encoder {
 	}
 		return new String(b, UTF8);
 	}
+
+	@Override /* ConfigEncoder */
+	public boolean isEncoded(String in) {
+		return in != null && in.length() > 1 && in.charAt(0) == '{' && in.charAt(in.length()-1) == '}';
+	}
 }
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEvent.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ConfigEvent.java
similarity index 80%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEvent.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ConfigEvent.java
index 2b87b98..70fd1ce 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEvent.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ConfigEvent.java
@@ -12,7 +12,7 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.config.event;
 
-import static org.apache.juneau.config.event.ChangeEventType.*;
+import static org.apache.juneau.config.event.ConfigEventType.*;
 
 import java.util.*;
 
@@ -23,23 +23,21 @@ import org.apache.juneau.internal.*;
  * Represents a change to a config.
  */
 @BeanIgnore
-public class ChangeEvent {
+public class ConfigEvent {
 	
-	private final ChangeEventType type;
+	private final ConfigEventType type;
 	private final String section, key, value, comment;
 	private final List<String> preLines;
 	private final String modifiers;
 	
-	private ChangeEvent(ChangeEventType type, String section, String key, String value, String modifiers, String comment, List<String> preLines) {
+	private ConfigEvent(ConfigEventType type, String section, String key, String value, String modifiers, String comment, List<String> preLines) {
 		this.type = type;
 		this.section = section;
 		this.key = key;
 		this.value = value;
 		this.comment = comment;
-		if (preLines == null)
-			preLines = Collections.emptyList();
 		this.preLines = preLines;
-		this.modifiers = StringUtils.emptyIfNull(modifiers);
+		this.modifiers = modifiers;
 	}
 	
 	//---------------------------------------------------------------------------------------------
@@ -66,10 +64,10 @@ public class ChangeEvent {
 	 * 	Comment lines that occur before this entry.
 	 * 	<br>Must not be <jk>null</jk>.
 	 * @return
-	 * 	A new {@link ChangeEvent} object.
+	 * 	A new {@link ConfigEvent} object.
 	 */
-	public static ChangeEvent setEntry(String section, String key, String value, String modifiers, String comment, List<String> prelines) {
-		return new ChangeEvent(SET_ENTRY, section, key, value, modifiers, comment, prelines);
+	public static ConfigEvent setEntry(String section, String key, String value, String modifiers, String comment, List<String> prelines) {
+		return new ConfigEvent(SET_ENTRY, section, key, value, modifiers, comment, prelines);
 	}
 	
 	/**
@@ -82,13 +80,12 @@ public class ChangeEvent {
 	 * 	The entry name.
 	 * 	<br>Must not be <jk>null</jk>.
 	 * @return
-	 * 	A new {@link ChangeEvent} object.
+	 * 	A new {@link ConfigEvent} object.
 	 */
-	public static ChangeEvent removeEntry(String section, String key) {
-		return new ChangeEvent(SET_ENTRY, section, key, null, null, null, null);
+	public static ConfigEvent removeEntry(String section, String key) {
+		return new ConfigEvent(REMOVE_ENTRY, section, key, null, null, null, null);
 	}
 	
-	
 	/**
 	 * Adds a new empty section to the config.
 	 * 
@@ -98,10 +95,10 @@ public class ChangeEvent {
 	 * 	Comment lines that occur before this section.
 	 * 	<br>Must not be <jk>null</jk>.
 	 * @return
-	 * 	A new {@link ChangeEvent} object.
+	 * 	A new {@link ConfigEvent} object.
 	 */
-	public static ChangeEvent setSection(String section, List<String> prelines) {
-		return new ChangeEvent(SET_SECTION, section, null, null, null, null, prelines);
+	public static ConfigEvent setSection(String section, List<String> prelines) {
+		return new ConfigEvent(SET_SECTION, section, null, null, null, null, prelines);
 	}
 	
 	/**
@@ -110,10 +107,10 @@ public class ChangeEvent {
 	 * @param section
 	 * 	The section name.
 	 * @return
-	 * 	A new {@link ChangeEvent} object.
+	 * 	A new {@link ConfigEvent} object.
 	 */
-	public static ChangeEvent removeSection(String section) {
-		return new ChangeEvent(REMOVE_SECTION, section, null, null, null, null, null);
+	public static ConfigEvent removeSection(String section) {
+		return new ConfigEvent(REMOVE_SECTION, section, null, null, null, null, null);
 	}
 	
 	
@@ -126,7 +123,7 @@ public class ChangeEvent {
 	 *
 	 * @return The event type.
 	 */
-	public ChangeEventType getType() {
+	public ConfigEventType getType() {
 		return type;
 	}
 
@@ -176,16 +173,6 @@ public class ChangeEvent {
 	}
 
 	/**
-	 * Returns whether this entry is encoded.
-	 * 
-	 * @param c The modifier character.
-	 * @return Whether this entry is encoded.
-	 */
-	public boolean hasModifier(char c) {
-		return modifiers.indexOf(c) != -1;
-	}
-
-	/**
 	 * Returns the modifiers on this entry.
 	 * 
 	 * @return 
@@ -201,12 +188,15 @@ public class ChangeEvent {
 		switch(type) {
 			case REMOVE_SECTION:
 				return "REMOVE_SECTION("+section+")";
+			case REMOVE_ENTRY:
+				return "REMOVE_ENTRY("+section+"/"+key+")";
 			case SET_SECTION:
 				return "SET_SECTION("+section+", preLines="+StringUtils.join(preLines, '|')+")";
 			case SET_ENTRY:
 				StringBuilder out = new StringBuilder("SET(");
 				out.append(key);
-				out.append(modifiers);
+				if (modifiers != null)
+					out.append(modifiers);
 				out.append(" = ");
 				String val = value == null ? "null" : value;
 				if (val.indexOf('\n') != -1)
@@ -214,7 +204,7 @@ public class ChangeEvent {
 				if (val.indexOf('#') != -1)
 					val = val.replaceAll("#", "\\\\#");
 				out.append(val);
-				if (comment != null) 
+				if (! StringUtils.isEmpty(comment)) 
 					out.append(" # ").append(comment);
 				out.append(')');
 				return out.toString();
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/ConfigEventListener.java
similarity index 93%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ConfigEventListener.java
index 335c66c..b14657a 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/ConfigEventListener.java
@@ -17,12 +17,12 @@ import java.util.*;
 /**
  * Listener that can be used to listen for change events in config maps.
  */
-public interface ChangeEventListener {
+public interface ConfigEventListener {
 
 	/**
 	 * Gets called immediately after a config file has been loaded.
 	 * 
 	 * @param events The change events.
 	 */
-	void onChange(List<ChangeEvent> events);
+	void onConfigChange(List<ConfigEvent> events);
 }
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventType.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ConfigEventType.java
similarity index 91%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventType.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ConfigEventType.java
index 3ecca35..09402bf 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventType.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ConfigEventType.java
@@ -13,9 +13,9 @@
 package org.apache.juneau.config.event;
 
 /**
- * Possible event types for the {@link ChangeEvent} class.
+ * Possible event types for the {@link ConfigEvent} class.
  */
-public enum ChangeEventType {
+public enum ConfigEventType {
 
 	/**
 	 * Set an individual entry value in a config.
@@ -23,6 +23,11 @@ public enum ChangeEventType {
 	SET_ENTRY,
 	
 	/**
+	 * Removes an entry value from a config.
+	 */
+	REMOVE_ENTRY,
+
+	/**
 	 * Adds or replaces a section in a config.
 	 */
 	SET_SECTION,
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/package-info.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/package-info.java
similarity index 93%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/package-info.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/package-info.java
index e79c195..f5c5d9c 100755
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/package-info.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/package-info.java
@@ -12,7 +12,7 @@
 // ***************************************************************************************************************************
 
 /**
- * Config Event and Listener Support
+ * Config Event Support
  */
-package org.apache.juneau.config.listener;
+package org.apache.juneau.config.event;
 
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/ConfigListener.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/ConfigListener.java
deleted file mode 100644
index da22018..0000000
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/ConfigListener.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// ***************************************************************************************************************************
-// * 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.listener;
-
-import java.util.*;
-
-import org.apache.juneau.config.*;
-
-/**
- * Listener that can be used to listen for change events in config files.
- * 
- * <h5 class='section'>See Also:</h5>
- * <ul class='doctree'>
- * 	<li class='link'><a class='doclink' href='../../../../overview-summary.html#juneau-config.Listeners'>Overview &gt; juneau-config &gt; Listeners</a>
- * </ul>
- */
-public class ConfigListener {
-
-	/**
-	 * Gets called immediately after a config file has been loaded.
-	 * 
-	 * @param cf The config file being loaded.
-	 */
-	public void onLoad(ConfigFile cf) {}
-
-	/**
-	 * Gets called immediately after a config file has been saved.
-	 * 
-	 * @param cf The config file being saved.
-	 */
-	public void onSave(ConfigFile cf) {}
-
-	/**
-	 * Signifies that the specified values have changed.
-	 * 
-	 * @param cf The config file being modified.
-	 * @param changes The full keys (e.g. <js>"Section/key"</js>) of entries that have changed in the config file.
-	 */
-	public void onChange(ConfigFile cf, Set<String> changes) {}
-}
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/EntryListener.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/EntryListener.java
deleted file mode 100644
index 61cb785..0000000
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/EntryListener.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// ***************************************************************************************************************************
-// * 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.listener;
-
-import java.util.*;
-
-import org.apache.juneau.config.*;
-
-/**
- * Listener that can be used to listen for change events for a specific entry in a config file.
- * 
- * <h5 class='section'>See Also:</h5>
- * <ul class='doctree'>
- * 	<li class='link'><a class='doclink' href='../../../../overview-summary.html#juneau-config.Listeners'>Overview &gt; juneau-config &gt; Listeners</a>
- * </ul>
- */
-public class EntryListener extends ConfigListener {
-
-	private String fullKey;
-
-	/**
-	 * Constructor.
-	 * 
-	 * @param fullKey The key in the config file to listen for changes on.
-	 */
-	public EntryListener(String fullKey) {
-		this.fullKey = fullKey;
-	}
-
-	@Override /* ConfigListener */
-	public void onChange(ConfigFile cf, Set<String> changes) {
-		if (changes.contains(fullKey))
-			onChange(cf);
-	}
-
-	/**
-	 * Signifies that the config file entry changed.
-	 * 
-	 * @param cf The config file being changed.
-	 */
-	public void onChange(ConfigFile cf) {}
-}
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/SectionListener.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/SectionListener.java
deleted file mode 100644
index b6d170c..0000000
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/SectionListener.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// ***************************************************************************************************************************
-// * 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.listener;
-
-import static org.apache.juneau.internal.StringUtils.*;
-
-import java.util.*;
-
-import org.apache.juneau.config.*;
-
-/**
- * Listener that can be used to listen for change events for a specific section in a config file.
- * 
- * 
- * <h5 class='section'>See Also:</h5>
- * <ul class='doctree'>
- * 	<li class='link'><a class='doclink' href='../../../../overview-summary.html#juneau-config.Listeners'>Overview &gt; juneau-config &gt; Listeners</a>
- * </ul>
- */
-public class SectionListener extends ConfigListener {
-
-	private boolean isDefault;
-	private String prefix;
-
-	/**
-	 * Constructor.
-	 * 
-	 * @param section The name of the section in the config file to listen to.
-	 */
-	public SectionListener(String section) {
-		isDefault = isEmpty(section);
-		prefix = isDefault ? null : (section + '/');
-	}
-
-	@Override /* ConfigListener */
-	public void onChange(ConfigFile cf, Set<String> changes) {
-		for (String c : changes) {
-			if (isDefault) {
-				if (c.indexOf('/') == -1) {
-					onChange(cf);
-					return;
-				}
-			} else {
-				if (c.startsWith(prefix)) {
-					onChange(cf);
-					return;
-				}
-			}
-		}
-	}
-
-	/**
-	 * Signifies that the config file entry changed.
-	 * 
-	 * @param cf The config file being modified.
-	 */
-	public void onChange(ConfigFile cf) {}
-}
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 7264d64..6977292 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
@@ -13,6 +13,7 @@
 package org.apache.juneau.config.store;
 
 import static org.apache.juneau.internal.CollectionUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
 
 import java.io.*;
 import java.util.*;
@@ -30,6 +31,8 @@ public class ConfigEntry {
 	final String modifiers;
 	final List<String> preLines;
 	
+	static final ConfigEntry NULL = new ConfigEntry(null, null, null, null, null);
+	
 	private final static AsciiSet MOD_CHARS = new AsciiSet("#$%&*+^@~");
 
 	ConfigEntry(String line, List<String> preLines) {
@@ -131,7 +134,8 @@ public class ConfigEntry {
 			w.append(l).append('\n');
 		} else {
 			w.append(key);
-			w.append(modifiers);
+			if (modifiers != null)
+				w.append(modifiers);
 			w.append(" = ");
 			
 			String val = value;
@@ -141,7 +145,7 @@ public class ConfigEntry {
 				val = val.replaceAll("#", "\\\\#");
 			w.append(val);
 				
-			if (comment != null) 
+			if (! isEmpty(comment)) 
 				w.append(" # ").append(comment);
 
 			w.append('\n');
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigFileStore.java
similarity index 76%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigFileStore.java
index b3063d5..8d89336 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigFileStore.java
@@ -24,7 +24,6 @@ import java.nio.file.*;
 import java.util.concurrent.*;
 
 import org.apache.juneau.*;
-import org.apache.juneau.internal.*;
 
 /**
  * Filesystem-based storage location for configuration files.
@@ -32,26 +31,26 @@ import org.apache.juneau.internal.*;
  * <p>
  * Points to a file system directory containing configuration files.
  */
-public class FileStore extends Store {
+public class ConfigFileStore extends ConfigStore {
 
 	//-------------------------------------------------------------------------------------------------------------------
 	// Configurable properties
 	//-------------------------------------------------------------------------------------------------------------------
 
-	private static final String PREFIX = "FileStore.";
+	private static final String PREFIX = "ConfigFileStore.";
 
 	/**
 	 * Configuration property:  Local file system directory.
 	 * 
 	 * <h5 class='section'>Property:</h5>
 	 * <ul>
-	 * 	<li><b>Name:</b>  <js>"FileStore.directory.s"</js>
+	 * 	<li><b>Name:</b>  <js>"ConfigFileStore.directory.s"</js>
 	 * 	<li><b>Data type:</b>  <code>String</code>
 	 * 	<li><b>Default:</b>  <js>"."</js>
 	 * 	<li><b>Methods:</b> 
 	 * 		<ul>
-	 * 			<li class='jm'>{@link FileStoreBuilder#directory(String)}
-	 * 			<li class='jm'>{@link FileStoreBuilder#directory(File)}
+	 * 			<li class='jm'>{@link ConfigFileStoreBuilder#directory(String)}
+	 * 			<li class='jm'>{@link ConfigFileStoreBuilder#directory(File)}
 	 * 		</ul>
 	 * </ul>
 	 * 
@@ -66,13 +65,13 @@ public class FileStore extends Store {
 	 * 
 	 * <h5 class='section'>Property:</h5>
 	 * <ul>
-	 * 	<li><b>Name:</b>  <js>"FileStore.charset.s"</js>
+	 * 	<li><b>Name:</b>  <js>"ConfigFileStore.charset.s"</js>
 	 * 	<li><b>Data type:</b>  {@link Charset}
 	 * 	<li><b>Default:</b>  {@link Charset#defaultCharset()}
 	 * 	<li><b>Methods:</b> 
 	 * 		<ul>
-	 * 			<li class='jm'>{@link FileStoreBuilder#charset(String)}
-	 * 			<li class='jm'>{@link FileStoreBuilder#charset(Charset)}
+	 * 			<li class='jm'>{@link ConfigFileStoreBuilder#charset(String)}
+	 * 			<li class='jm'>{@link ConfigFileStoreBuilder#charset(Charset)}
 	 * 		</ul>
 	 * </ul>
 	 * 
@@ -87,13 +86,12 @@ public class FileStore extends Store {
 	 * 
 	 * <h5 class='section'>Property:</h5>
 	 * <ul>
-	 * 	<li><b>Name:</b>  <js>"FileStore.useWatcher.b"</js>
+	 * 	<li><b>Name:</b>  <js>"ConfigFileStore.useWatcher.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 FileStoreBuilder#useWatcher()}
-	 * 			<li class='jm'>{@link FileStoreBuilder#useWatcher(boolean)}
+	 * 			<li class='jm'>{@link ConfigFileStoreBuilder#useWatcher()}
 	 * 		</ul>
 	 * </ul>
 	 * 
@@ -113,13 +111,13 @@ public class FileStore extends Store {
 	 * 
 	 * <h5 class='section'>Property:</h5>
 	 * <ul>
-	 * 	<li><b>Name:</b>  <js>"FileStore.watcherSensitivity.s"</js>
+	 * 	<li><b>Name:</b>  <js>"ConfigFileStore.watcherSensitivity.s"</js>
 	 * 	<li><b>Data type:</b>  {@link WatcherSensitivity}
 	 * 	<li><b>Default:</b>  {@link WatcherSensitivity#MEDIUM}
 	 * 	<li><b>Methods:</b> 
 	 * 		<ul>
-	 * 			<li class='jm'>{@link FileStoreBuilder#watcherSensitivity(WatcherSensitivity)}
-	 * 			<li class='jm'>{@link FileStoreBuilder#watcherSensitivity(String)}
+	 * 			<li class='jm'>{@link ConfigFileStoreBuilder#watcherSensitivity(WatcherSensitivity)}
+	 * 			<li class='jm'>{@link ConfigFileStoreBuilder#watcherSensitivity(String)}
 	 * 		</ul>
 	 * </ul>
 	 * 
@@ -135,24 +133,28 @@ public class FileStore extends Store {
 	public static final String FILESTORE_watcherSensitivity = PREFIX + "watcherSensitivity.s";
 	
 	/**
-	 * Configuration property:  Config file extension.
+	 * Configuration property:  Update-on-write.
 	 * 
 	 * <h5 class='section'>Property:</h5>
 	 * <ul>
-	 * 	<li><b>Name:</b>  <js>"FileStore.ext.s"</js>
-	 * 	<li><b>Data type:</b>  <code>String</code>
-	 * 	<li><b>Default:</b>  <js>"cfg"</js>
+	 * 	<li><b>Name:</b>  <js>"ConfigFileStore.updateOnWrite.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 FileStoreBuilder#ext(String)}
+	 * 			<li class='jm'>{@link ConfigFileStoreBuilder#updateOnWrite()}
 	 * 		</ul>
 	 * </ul>
 	 * 
 	 * <h5 class='section'>Description:</h5>
 	 * <p>
-	 * File extension identifier for config files.
+	 * When enabled, the {@link #update(String, String)} method will be called immediately following
+	 * calls to {@link #write(String, String, String)} when the contents are changing.
+	 * <br>This allows for more immediate responses to configuration changes on file systems that use
+	 * polling watchers.
+	 * <br>This may cause double-triggering of {@link ConfigStoreListener ConfigStoreListeners}.
 	 */
-	public static final String FILESTORE_ext = PREFIX + "ext.s";
+	public static final String FILESTORE_updateOnWrite = PREFIX + "updateOnWrite.b";
 
 	
 	//-------------------------------------------------------------------------------------------------------------------
@@ -160,7 +162,7 @@ public class FileStore extends Store {
 	//-------------------------------------------------------------------------------------------------------------------
 
 	/** Default file store, all default values.*/
-	public static final FileStore DEFAULT = FileStore.create().build();
+	public static final ConfigFileStore DEFAULT = ConfigFileStore.create().build();
 
 
 	//-------------------------------------------------------------------------------------------------------------------
@@ -172,19 +174,19 @@ public class FileStore extends Store {
 	 * 
 	 * @return A new builder for this object.
 	 */
-	public static FileStoreBuilder create() {
-		return new FileStoreBuilder();
+	public static ConfigFileStoreBuilder create() {
+		return new ConfigFileStoreBuilder();
 	}
 	
 	@Override /* Context */
-	public FileStoreBuilder builder() {
-		return new FileStoreBuilder(getPropertyStore());
+	public ConfigFileStoreBuilder builder() {
+		return new ConfigFileStoreBuilder(getPropertyStore());
 	}
 
 	private final File dir;
-	private final String ext;
 	private final Charset charset;
 	private final WatcherThread watcher;
+	private final boolean updateOnWrite;
 	private final ConcurrentHashMap<String,String> cache = new ConcurrentHashMap<>();
 	
 	/**
@@ -192,13 +194,13 @@ public class FileStore extends Store {
 	 * 
 	 * @param ps The settings for this content store.
 	 */
-	protected FileStore(PropertyStore ps) {
+	protected ConfigFileStore(PropertyStore ps) {
 		super(ps);
 		try {
 			dir = new File(getStringProperty(FILESTORE_directory, ".")).getCanonicalFile();
 			dir.mkdirs();
-			ext = getStringProperty(FILESTORE_ext, "cfg");
 			charset = getProperty(FILESTORE_charset, Charset.class, Charset.defaultCharset());
+			updateOnWrite = getBooleanProperty(FILESTORE_updateOnWrite, false);
 			WatcherSensitivity ws = getProperty(FILESTORE_watcherSensitivity, WatcherSensitivity.class, WatcherSensitivity.MEDIUM);
 			watcher = getBooleanProperty(FILESTORE_useWatcher, false) ? new WatcherThread(dir, ws) : null;
 			if (watcher != null)
@@ -208,16 +210,19 @@ public class FileStore extends Store {
 		}
 	}
 	
-	@Override /* Store */
+	@Override /* ConfigStore */
 	public synchronized String read(String name) throws IOException {
 		String s = cache.get(name);
 		if (s != null)
 			return s;
 		
 		dir.mkdirs();
-		Path p = dir.toPath().resolve(name + '.' + ext);
+		
+		// If file doesn't exist, don't trigger creation.
+		Path p = dir.toPath().resolve(name);
 		if (! Files.exists(p)) 
-			return null;
+			return "";
+		
 		try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) {
 			try (FileLock lock = fc.lock()) {
 				ByteBuffer buf = ByteBuffer.allocate(1024);
@@ -234,16 +239,25 @@ public class FileStore extends Store {
 		return cache.get(name);
 	}
 
-	@Override /* Store */
+	@Override /* ConfigStore */
 	public synchronized String write(String name, String expectedContents, String newContents) throws IOException {
+
+		// This is a no-op.
+		if (isEquals(expectedContents, newContents))
+			return null;
+
 		dir.mkdirs();
-		Path p = dir.toPath().resolve(name + '.' + ext);
+		Path p = dir.toPath().resolve(name);
+		
 		boolean exists = Files.exists(p);
-		if (expectedContents != null && ! exists)
+		
+		// Don't create the file if we're not going to match.
+		if ((!exists) && (!isEmpty(expectedContents)))
 			return "";
+		
 		try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) {
 			try (FileLock lock = fc.lock()) {
-				String currentContents = null;
+				String currentContents = "";
 				if (exists) {
 					ByteBuffer buf = ByteBuffer.allocate(1024);
 					StringBuilder sb = new StringBuilder();
@@ -253,7 +267,7 @@ public class FileStore extends Store {
 					}
 					currentContents = sb.toString();
 				}
-				if (! isEquals(expectedContents, currentContents)) {
+				if (expectedContents != null && ! isEquals(currentContents, expectedContents)) {
 					if (currentContents == null)
 						cache.remove(name);
 					else
@@ -262,14 +276,19 @@ public class FileStore extends Store {
 				}
 				fc.position(0);
 				fc.write(charset.encode(newContents));
-				cache.put(name, newContents);
 			}
 		}
+		
+		if (updateOnWrite)
+			update(name, newContents);
+		else 
+			cache.remove(name);  // Invalidate the cache.
+		
 		return null;
 	}
 		
-	@Override /* Store */
-	public synchronized FileStore update(String name, String newContents) {
+	@Override /* ConfigStore */
+	public synchronized ConfigFileStore update(String name, String newContents) {
 		cache.put(name, newContents);
 		super.update(name, newContents);
 		return this;
@@ -320,7 +339,7 @@ public class FileStore extends Store {
 				    for (WatchEvent<?> event : key.pollEvents()) {
 				        WatchEvent.Kind<?> kind = event.kind();
 				        if (kind != OVERFLOW) 
-				        		FileStore.this.onFileEvent(((WatchEvent<Path>)event));
+				        		ConfigFileStore.this.onFileEvent(((WatchEvent<Path>)event));
 				    }
 				    if (! key.reset())
 				    		break;
@@ -347,21 +366,16 @@ public class FileStore extends Store {
 	 * Gets called when the watcher service on this store is triggered with a file system change.
 	 * 
 	 * @param e The file system event.
-	 * @throws Exception
+	 * @throws IOException
 	 */
-	protected synchronized void onFileEvent(WatchEvent<Path> e) throws Exception {
+	protected synchronized void onFileEvent(WatchEvent<Path> e) throws IOException {
 		String fn = e.context().getFileName().toString();
-		String bn = FileUtils.getBaseName(fn);
-		String ext = FileUtils.getExtension(fn);
 		
-		if (isEquals(this.ext, ext)) {
-			String oldContents = cache.get(bn);
-			cache.remove(bn);
-			String newContents = read(bn);
-			if (! isEquals(oldContents, newContents)) {
-				update(bn, newContents);
-			}
+		String oldContents = cache.get(fn);
+		cache.remove(fn);
+		String newContents = read(fn);
+		if (! isEquals(oldContents, newContents)) {
+			update(fn, newContents);
 		}
 	}
-		
 }
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStoreBuilder.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigFileStoreBuilder.java
similarity index 73%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStoreBuilder.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigFileStoreBuilder.java
index 0ae98c0..daf16d9 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStoreBuilder.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigFileStoreBuilder.java
@@ -12,7 +12,7 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.config.store;
 
-import static org.apache.juneau.config.store.FileStore.*;
+import static org.apache.juneau.config.store.ConfigFileStore.*;
 
 import java.io.*;
 import java.nio.charset.*;
@@ -20,14 +20,14 @@ import java.nio.charset.*;
 import org.apache.juneau.*;
 
 /**
- * Builder for {@link FileStore} objects.
+ * Builder for {@link ConfigFileStore} objects.
  */
-public class FileStoreBuilder extends StoreBuilder {
+public class ConfigFileStoreBuilder extends ConfigStoreBuilder {
 
 	/**
 	 * Constructor, default settings.
 	 */
-	public FileStoreBuilder() {
+	public ConfigFileStoreBuilder() {
 		super();
 	}
 
@@ -36,7 +36,7 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 
 	 * @param ps The initial configuration settings for this builder.
 	 */
-	public FileStoreBuilder(PropertyStore ps) {
+	public ConfigFileStoreBuilder(PropertyStore ps) {
 		super(ps);
 	}
 
@@ -53,7 +53,7 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 
 	 * <h5 class='section'>See Also:</h5>
 	 * <ul>
-	 * 	<li class='jf'>{@link FileStore#FILESTORE_directory}
+	 * 	<li class='jf'>{@link ConfigFileStore#FILESTORE_directory}
 	 * </ul>
 	 * 
 	 * @param value 
@@ -61,7 +61,7 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 	<br>The default is <js>"."</js>.
 	 * @return This object (for method chaining).
 	 */
-	public FileStoreBuilder directory(String value) {
+	public ConfigFileStoreBuilder directory(String value) {
 		super.set(FILESTORE_directory, value);
 		return this;
 	}
@@ -74,7 +74,7 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 
 	 * <h5 class='section'>See Also:</h5>
 	 * <ul>
-	 * 	<li class='jf'>{@link FileStore#FILESTORE_directory}
+	 * 	<li class='jf'>{@link ConfigFileStore#FILESTORE_directory}
 	 * </ul>
 	 * 
 	 * @param value 
@@ -82,7 +82,7 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 	<br>The default is <js>"."</js>.
 	 * @return This object (for method chaining).
 	 */
-	public FileStoreBuilder directory(File value) {
+	public ConfigFileStoreBuilder directory(File value) {
 		super.set(FILESTORE_directory, value);
 		return this;
 	}
@@ -95,7 +95,7 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 
 	 * <h5 class='section'>See Also:</h5>
 	 * <ul>
-	 * 	<li class='jf'>{@link FileStore#FILESTORE_charset}
+	 * 	<li class='jf'>{@link ConfigFileStore#FILESTORE_charset}
 	 * </ul>
 	 * 
 	 * @param value 
@@ -103,7 +103,7 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 	<br>The default is <js>"."</js>.
 	 * @return This object (for method chaining).
 	 */
-	public FileStoreBuilder charset(String value) {
+	public ConfigFileStoreBuilder charset(String value) {
 		super.set(FILESTORE_charset, value);
 		return this;
 	}
@@ -116,7 +116,7 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 
 	 * <h5 class='section'>See Also:</h5>
 	 * <ul>
-	 * 	<li class='jf'>{@link FileStore#FILESTORE_charset}
+	 * 	<li class='jf'>{@link ConfigFileStore#FILESTORE_charset}
 	 * </ul>
 	 * 
 	 * @param value 
@@ -124,7 +124,7 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 	<br>The default is <js>"."</js>.
 	 * @return This object (for method chaining).
 	 */
-	public FileStoreBuilder charset(Charset value) {
+	public ConfigFileStoreBuilder charset(Charset value) {
 		super.set(FILESTORE_charset, value);
 		return this;
 	}
@@ -133,37 +133,16 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * Configuration property:  Use watcher.
 	 * 
 	 * <p>
-	 * Use a file system watcher for file system changes.
-	 * 
-	 * <h5 class='section'>See Also:</h5>
-	 * <ul>
-	 * 	<li class='jf'>{@link FileStore#FILESTORE_useWatcher}
-	 * </ul>
-	 * 
-	 * @param value 
-	 * 	The new value for this property.
-	 * 	<br>The default is <jk>false</jk>.
-	 * @return This object (for method chaining).
-	 */
-	public FileStoreBuilder useWatcher(boolean value) {
-		super.set(FILESTORE_useWatcher, value);
-		return this;
-	}
-
-	/**
-	 * Configuration property:  Use watcher.
-	 * 
-	 * <p>
 	 * Shortcut for calling <code>useWatcher(<jk>true</jk>)</code>.
 	 * 
 	 * <h5 class='section'>See Also:</h5>
 	 * <ul>
-	 * 	<li class='jf'>{@link FileStore#FILESTORE_useWatcher}
+	 * 	<li class='jf'>{@link ConfigFileStore#FILESTORE_useWatcher}
 	 * </ul>
 	 * 
 	 * @return This object (for method chaining).
 	 */
-	public FileStoreBuilder useWatcher() {
+	public ConfigFileStoreBuilder useWatcher() {
 		super.set(FILESTORE_useWatcher, true);
 		return this;
 	}
@@ -176,7 +155,7 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 
 	 * <h5 class='section'>See Also:</h5>
 	 * <ul>
-	 * 	<li class='jf'>{@link FileStore#FILESTORE_watcherSensitivity}
+	 * 	<li class='jf'>{@link ConfigFileStore#FILESTORE_watcherSensitivity}
 	 * </ul>
 	 * 
 	 * @param value 
@@ -184,50 +163,52 @@ public class FileStoreBuilder extends StoreBuilder {
 	 * 	<br>The default is {@link WatcherSensitivity#MEDIUM}
 	 * @return This object (for method chaining).
 	 */
-	public FileStoreBuilder watcherSensitivity(WatcherSensitivity value) {
+	public ConfigFileStoreBuilder watcherSensitivity(WatcherSensitivity value) {
 		super.set(FILESTORE_watcherSensitivity, value);
 		return this;
 	}
 
 	/**
-	 * Configuration property:  Watcher sensitivity.
+	 * Configuration property:  Update-on-write.
 	 * 
 	 * <p>
-	 * Determines how frequently the file system is polled for updates.
+	 * Shortcut for calling <code>useWatcher(<jk>true</jk>)</code>.
 	 * 
 	 * <h5 class='section'>See Also:</h5>
 	 * <ul>
-	 * 	<li class='jf'>{@link FileStore#FILESTORE_watcherSensitivity}
+	 * 	<li class='jf'>{@link ConfigFileStore#FILESTORE_updateOnWrite}
 	 * </ul>
 	 * 
-	 * @param value 
-	 * 	The new value for this property.
-	 * 	<br>The default is {@link WatcherSensitivity#MEDIUM}
 	 * @return This object (for method chaining).
 	 */
-	public FileStoreBuilder watcherSensitivity(String value) {
-		super.set(FILESTORE_watcherSensitivity, value);
+	public ConfigFileStoreBuilder updateOnWrite() {
+		super.set(FILESTORE_updateOnWrite, true);
 		return this;
 	}
 
 	/**
-	 * Configuration property:  Config file extension.
+	 * Configuration property:  Watcher sensitivity.
 	 * 
 	 * <p>
-	 * File extension identifier for config files.
+	 * Determines how frequently the file system is polled for updates.
+	 * 
+	 * <h5 class='section'>See Also:</h5>
+	 * <ul>
+	 * 	<li class='jf'>{@link ConfigFileStore#FILESTORE_watcherSensitivity}
+	 * </ul>
 	 * 
 	 * @param value 
 	 * 	The new value for this property.
-	 * 	<br>The default is <js>"cfg"</js>.
+	 * 	<br>The default is {@link WatcherSensitivity#MEDIUM}
 	 * @return This object (for method chaining).
 	 */
-	public FileStoreBuilder ext(String value) {
-		super.set(FILESTORE_ext, value);
+	public ConfigFileStoreBuilder watcherSensitivity(String value) {
+		super.set(FILESTORE_watcherSensitivity, value);
 		return this;
 	}
 
 	@Override /* ContextBuilder */
-	public FileStore build() {
-		return new FileStore(getPropertyStore());
+	public ConfigFileStore build() {
+		return new ConfigFileStore(getPropertyStore());
 	}
 }
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 4756127..efa837d 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
@@ -13,33 +13,33 @@
 package org.apache.juneau.config.store;
 
 import static org.apache.juneau.internal.StringUtils.*;
-import static org.apache.juneau.config.event.ChangeEventType.*;
+import static org.apache.juneau.config.event.ConfigEventType.*;
 
 import java.io.*;
 import java.util.*;
+import java.util.concurrent.*;
 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, Writable {
+public class ConfigMap implements ConfigStoreListener {
 
-	private final Store store;               // The store that created this object.
+	private final ConfigStore store;               // The store that created this object.
 	private volatile String contents;        // The original contents of this object.
 	private final String name;               // The name  of this object.
 
 	private final static AsciiSet MOD_CHARS = new AsciiSet("#$%&*+^@~");
 	
 	// Changes that have been applied since the last load.
-	private final List<ChangeEvent> changes = Collections.synchronizedList(new ArrayList<ChangeEvent>());
+	private final List<ConfigEvent> changes = Collections.synchronizedList(new ArrayList<ConfigEvent>());
 	
 	// Registered listeners listening for changes during saves or reloads.
-	private final Set<ChangeEventListener> listeners = Collections.synchronizedSet(new HashSet<ChangeEventListener>());
+	private final Set<ConfigEventListener> listeners = Collections.synchronizedSet(new HashSet<ConfigEventListener>());
 
 	// The parsed entries of this map with all changes applied.
 	final Map<String,ConfigSection> entries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigSection>());
@@ -56,7 +56,7 @@ public class ConfigMap implements StoreListener, Writable {
 	 * @param name The config name.
 	 * @throws IOException
 	 */
-	public ConfigMap(Store store, String name) throws IOException {
+	public ConfigMap(ConfigStore store, String name) throws IOException {
 		this.store = store;
 		this.name = name;
 		load(store.read(name));
@@ -118,11 +118,14 @@ public class ConfigMap implements StoreListener, Writable {
 			String l = li.previous();
 			char c = l.isEmpty() ? 0 : l.charAt(0);
 			if (c == '\t') {
-				if (accumulator == null)
-					accumulator = l.substring(1);
-				else
-					accumulator = l.substring(1) + "\n" + accumulator;
-				li.remove();
+				c = firstNonWhitespaceChar(l);
+				if (c != '#') {
+					if (accumulator == null)
+						accumulator = l.substring(1);
+					else
+						accumulator = l.substring(1) + "\n" + accumulator;
+					li.remove();
+				}
 			} else if (accumulator != null) {
 				li.set(l + "\n" + accumulator);
 				accumulator = null;
@@ -175,11 +178,18 @@ public class ConfigMap implements StoreListener, Writable {
 	/**
 	 * Reads an entry from this map.
 	 * 
-	 * @param section The section name.
-	 * @param key The entry key.
+	 * @param section 
+	 * 	The section name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * 	<br>Use <js>"default"</js> to refer to the default section.
+	 * @param key 
+	 * 	The entry key.
+	 * 	<br>Must not be <jk>null</jk>.
 	 * @return The entry, or <jk>null</jk> if the entry doesn't exist.
 	 */
 	public ConfigEntry getEntry(String section, String key) {
+		checkSectionName(section);
+		checkKeyName(key);
 		readLock();
 		try {
 			ConfigSection cs = entries.get(section);
@@ -198,10 +208,12 @@ public class ConfigMap implements StoreListener, Writable {
 	 * @param section 
 	 * 	The section name.
 	 * 	<br>Must not be <jk>null</jk>.
+	 * 	<br>Use <js>"default"</js> to refer to the default section.
 	 * @return
 	 * 	An unmodifiable list of lines, or <jk>null</jk> if the section doesn't exist.
 	 */
 	public List<String> getPreLines(String section) {
+		checkSectionName(section);
 		readLock();
 		try {
 			ConfigSection cs = entries.get(section);
@@ -214,15 +226,41 @@ public class ConfigMap implements StoreListener, Writable {
 	/**
 	 * Returns the keys of the entries in the specified section.
 	 * 
+	 * @return
+	 * 	An unmodifiable set of keys.
+	 */
+	public Set<String> getSections() {
+		return Collections.unmodifiableSet(entries.keySet());
+	}
+
+	/**
+	 * Returns the keys of the entries in the specified section.
+	 * 
 	 * @param section 
 	 * 	The section name.
 	 * 	<br>Must not be <jk>null</jk>.
+	 * 	<br>Use <js>"default"</js> to refer to the default section.
 	 * @return
-	 * 	An unmodifiable set of keys, or <jk>null</jk> if the section doesn't exist.
+	 * 	An unmodifiable set of keys, or an empty set if the section doesn't exist.
 	 */
 	public Set<String> getKeys(String section) {
+		checkSectionName(section);
 		ConfigSection cs = entries.get(section);
-		return cs == null ? null : Collections.unmodifiableSet(cs.entries.keySet());
+		return cs == null ? Collections.<String>emptySet() : Collections.unmodifiableSet(cs.entries.keySet());
+	}
+	
+	/**
+	 * Returns <jk>true</jk> if this config has the specified section.
+	 * 
+	 * @param section 
+	 * 	The section name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * 	<br>Use <js>"default"</js> to refer to the default section.
+	 * @return <jk>true</jk> if this config has the specified section.
+	 */
+	public boolean hasSection(String section) {
+		checkSectionName(section);
+		return entries.get(section) != null;
 	}
 	
 	//-----------------------------------------------------------------------------------------------------------------
@@ -232,139 +270,88 @@ public class ConfigMap implements StoreListener, Writable {
 	/**
 	 * Adds a new section or replaces the pre-lines on an existing section.
 	 * 
-	 * @param section The section name.
+	 * @param section 
+	 * 	The section name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * 	<br>Use <js>"default"</js> to refer to the default section.
 	 * @param preLines
 	 * 	The pre-lines on the section.
-	 * 	<br>Can be <jk>null</jk>.
+	 * 	<br>If <jk>null</jk>, the previous value will not be overwritten.
 	 * @return This object (for method chaining).
 	 */
 	public ConfigMap setSection(String section, List<String> preLines) {
-		if (! isValidSectionName(section))
-			throw new ConfigException("Invalid section name: {0}", section);
-		return applyChange(true, ChangeEvent.setSection(section, preLines));
+		checkSectionName(section);
+		return applyChange(true, ConfigEvent.setSection(section, preLines));
 	}
 	
 	/**
-	 * Removes a section.
-	 * 
-	 * <p>
-	 * This eliminates all entries in the section as well.
-	 * 
-	 * @param section The section name.
-	 * @return This object (for method chaining).
-	 */
-	public ConfigMap removeSection(String section) {
-		return applyChange(true, ChangeEvent.removeSection(section));
-	}
-		
-	/**
-	 * Sets the pre-lines on an entry without modifying any other attributes.
+	 * Adds or overwrites an existing entry.
 	 * 
-	 * @param section The section name.
-	 * @param key The entry key.
+	 * @param section 
+	 * 	The section name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * 	<br>Use <js>"default"</js> to refer to the default section.
+	 * @param key 
+	 * 	The entry key.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * @param value 
+	 * 	The entry value.
+	 * 	<br>If <jk>null</jk>, the previous value will not be overwritten.
+	 * @param modifiers 
+	 * 	Optional modifiers.
+	 * 	<br>If <jk>null</jk>, the previous value will not be overwritten.
+	 * @param comment 
+	 * 	Optional comment.  
+	 * 	<br>If <jk>null</jk>, the previous value will not be overwritten.
 	 * @param preLines 
-	 * 	The new pre-lines.
-	 * 	<br>Can be <jk>null</jk>.
+	 * 	Optional pre-lines.
+	 * 	<br>If <jk>null</jk>, the previous value will not be overwritten.
 	 * @return This object (for method chaining).
 	 */
-	public ConfigMap setPreLines(String section, String key, List<String> preLines) {
-		ChangeEvent cv = null;
-		readLock();
-		try {
-			ConfigSection cs = entries.get(section);
-			ConfigEntry ce = (cs  == null ? null : cs.entries.get(key));
-			if (ce != null)
-				cv = ChangeEvent.setEntry(section, key, ce.value, ce.modifiers, ce.comment, preLines);
-		} finally {
-			readUnlock();
-		}
-		return applyChange(true, cv);
+	public ConfigMap setEntry(String section, String key, String value, String modifiers, String comment, List<String> preLines) {
+		checkSectionName(section);
+		checkKeyName(key);
+		if (modifiers != null && ! MOD_CHARS.containsOnly(modifiers))
+			throw new ConfigException("Invalid modifiers: {0}", modifiers);
+		return applyChange(true, ConfigEvent.setEntry(section, key, value, modifiers, comment, preLines));
 	}
 	
 	/**
-	 * Sets the value on an entry without modifying any other attributes.
+	 * Removes a section.
 	 * 
-	 * @param section The section name.
-	 * @param key The entry key.
-	 * @param value 
-	 * 	The new value.
-	 * 	<br>Can be <jk>null</jk> which will delete the entry.
-	 * @return This object (for method chaining).
-	 */
-	public ConfigMap setValue(String section, String key, String value) {
-		
-		if (! isValidSectionName(section))
-			throw new ConfigException("Invalid section name: {0}", section);
-		if (! isValidKeyName(key))
-			throw new ConfigException("Invalid key name: {0}", key);
-
-		ChangeEvent cv = null;
-		readLock();
-		try {
-			ConfigSection cs = entries.get(section);
-			ConfigEntry ce = (cs  == null ? null : cs.entries.get(key));
-			if (ce != null)
-				cv = ChangeEvent.setEntry(section, key, value, ce.modifiers, ce.comment, ce.preLines);
-			else
-				cv = ChangeEvent.setEntry(section, key, value, null, null, null);
-		} finally {
-			readUnlock();
-		}
-		return applyChange(true, cv);
-	}
-	
-	/**
-	 * Sets the comment on an entry without modifying any other attributes.
+	 * <p>
+	 * This eliminates all entries in the section as well.
 	 * 
-	 * @param section The section name.
-	 * @param key The entry key.
-	 * @param comment 
-	 * 	The new comment.
-	 * 	<br>Can be <jk>null</jk>.
+	 * @param section 
+	 * 	The section name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * 	<br>Use <js>"default"</js> to refer to the default section.
 	 * @return This object (for method chaining).
 	 */
-	public ConfigMap setComment(String section, String key, String comment) {
-		ChangeEvent cv = null;
-		readLock();
-		try {
-			ConfigSection cs = entries.get(section);
-			ConfigEntry ce = (cs  == null ? null : cs.entries.get(key));
-			if (ce != null)
-				cv = ChangeEvent.setEntry(section, key, ce.value, ce.modifiers, comment, ce.preLines);
-		} finally {
-			readUnlock();
-		}
-		return applyChange(true, cv);
+	public ConfigMap removeSection(String section) {
+		checkSectionName(section);
+		return applyChange(true, ConfigEvent.removeSection(section));
 	}
-	
+		
 	/**
-	 * Adds or overwrites an existing entry.
+	 * Removes an entry.
 	 * 
-	 * @param section The section name.
-	 * @param key The entry key.
-	 * @param value The entry value.
-	 * @param modifiers 
-	 * 	Optional modifiers.
-	 * 	<br>Can be <jk>null</jk>.
-	 * @param comment 
-	 * 	Optional comment.  
-	 * 	<br>Can be <jk>null</jk>.
-	 * @param preLines 
-	 * 	Optional pre-lines.
-	 * 	<br>Can be <jk>null</jk>.
+	 * @param section 
+	 * 	The section name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * 	<br>Use <js>"default"</js> to refer to the default section.
+	 * @param key 
+	 * 	The entry key.
+	 * 	<br>Must not be <jk>null</jk>.
 	 * @return This object (for method chaining).
 	 */
-	public ConfigMap setEntry(String section, String key, String value, String modifiers, String comment, List<String> preLines) {
-		if (! isValidSectionName(section))
-			throw new ConfigException("Invalid section name: {0}", section);
-		if (! isValidKeyName(key))
-			throw new ConfigException("Invalid key name: {0}", key);
-		if (modifiers != null && ! MOD_CHARS.containsOnly(modifiers))
-			throw new ConfigException("Invalid modifiers: {0}", modifiers);
-		return applyChange(true, ChangeEvent.setEntry(section, key, value, modifiers, comment, preLines));
+	public ConfigMap removeEntry(String section, String key) {
+		checkSectionName(section);
+		checkKeyName(key);
+		return applyChange(true, ConfigEvent.removeEntry(section, key));
 	}
-	
-	private ConfigMap applyChange(boolean addToChangeList, ChangeEvent ce) {
+
+	private ConfigMap applyChange(boolean addToChangeList, ConfigEvent ce) {
 		if (ce == null)
 			return this;
 		writeLock();
@@ -376,14 +363,27 @@ public class ConfigMap implements StoreListener, Writable {
 					cs = new ConfigSection(section);
 					entries.put(section, cs);
 				}
-				cs.addEntry(ce.getKey(), ce.getValue(), ce.getModifiers(), ce.getComment(), ce.getPreLines());
+				ConfigEntry oe = cs.entries.get(ce.getKey());
+				if (oe == null)
+					oe = ConfigEntry.NULL;
+				cs.addEntry(
+					ce.getKey(), 
+					ce.getValue() == null ? oe.value : ce.getValue(), 
+					ce.getModifiers() == null ? oe.modifiers : ce.getModifiers(), 
+					ce.getComment() == null ? oe.comment : ce.getComment(), 
+					ce.getPreLines() == null ? oe.preLines : ce.getPreLines()
+				);
 			} else if (ce.getType() == SET_SECTION) {
 				if (cs == null) {
 					cs = new ConfigSection(section);
 					entries.put(section, cs);
 				}
-				cs.setPreLines(ce.getPreLines());
-			} else {
+				if (ce.getPreLines() != null)
+					cs.setPreLines(ce.getPreLines());
+			} else if (ce.getType() == REMOVE_ENTRY) {
+				if (cs != null)
+					cs.entries.remove(ce.getKey());
+			} else if (ce.getType() == REMOVE_SECTION) {
 				if (cs != null)
 					entries.remove(section);
 			}
@@ -395,6 +395,34 @@ public class ConfigMap implements StoreListener, Writable {
 		return this;	
 	}	
 	
+	/**
+	 * Overwrites the contents of the config file.
+	 * 
+	 * @param contents The new contents of the config file.
+	 * @param synchronous Wait until the change has been persisted before returning this map.
+	 * @return This object (for method chaining).
+	 * @throws IOException
+	 * @throws InterruptedException
+	 */
+	public ConfigMap write(String contents, boolean synchronous) throws IOException, InterruptedException {
+		
+		if (synchronous) {
+			final CountDownLatch latch = new CountDownLatch(1);
+			ConfigStoreListener l = new ConfigStoreListener() {
+				@Override
+				public void onChange(String contents) {
+					latch.countDown();
+				}
+			};
+			store.register(name, l);
+			store.write(name, null, contents);
+			latch.await(30, TimeUnit.SECONDS);
+			store.unregister(name, l);
+		} else {
+			store.write(name, null, contents);
+		}
+		return this;
+	}
 	
 	//-----------------------------------------------------------------------------------------------------------------
 	// Lifecycle events
@@ -427,7 +455,7 @@ public class ConfigMap implements StoreListener, Writable {
 				String currentContents = store.write(name, contents, newContents);
 				if (currentContents == null) 
 					break;
-				onChange(name, currentContents);
+				onChange(currentContents);
 			}
 			this.changes.clear();
 		} finally {
@@ -447,7 +475,7 @@ public class ConfigMap implements StoreListener, Writable {
 	 * @param listener The new listener.
 	 * @return This object (for method chaining).
 	 */
-	public ConfigMap register(ChangeEventListener listener) {
+	public ConfigMap register(ConfigEventListener listener) {
 		listeners.add(listener);
 		return this;
 	}
@@ -458,14 +486,14 @@ public class ConfigMap implements StoreListener, Writable {
 	 * @param listener The listener to remove.
 	 * @return This object (for method chaining).
 	 */
-	public ConfigMap unregister(ChangeEventListener listener) {
+	public ConfigMap unregister(ConfigEventListener listener) {
 		listeners.remove(listener);
 		return this;
 	}
 	
-	@Override /* StoreListener */
-	public void onChange(String name, String newContents) {
-		List<ChangeEvent> changes = null;
+	@Override /* ConfigStoreListener */
+	public void onChange(String newContents) {
+		List<ConfigEvent> changes = null;
 		writeLock();
 		try {
 			if (! StringUtils.isEquals(contents, newContents)) {
@@ -473,7 +501,7 @@ public class ConfigMap implements StoreListener, Writable {
 				load(newContents);
 				
 				// Reapply our changes on top of the modifications.
-				for (ChangeEvent ce : this.changes)
+				for (ConfigEvent ce : this.changes)
 					applyChange(false, ce);
 			}
 		} finally {
@@ -492,17 +520,68 @@ public class ConfigMap implements StoreListener, Writable {
 			readUnlock();
 		}
 	}
+	
+	/**
+	 * Returns the values in this config map as a map of maps.
+	 * 
+	 * <p>
+	 * This is considered a snapshot copy of the config map.
+	 * 
+	 * <p>
+	 * The returned map is modifiable, but modifications to the returned map are not reflected in the config map.
+	 * 
+	 * @return A copy of this config as a map of maps.
+	 */
+	public ObjectMap asMap() {
+		ObjectMap m = new ObjectMap();
+		readLock();
+		try {
+			for (ConfigSection cs : entries.values()) {
+				Map<String,String> m2 = new LinkedHashMap<>();
+				for (ConfigEntry ce : cs.entries.values())
+					m2.put(ce.key, ce.value);
+				m.put(cs.name, m2);
+			}
+		} finally {
+			readUnlock();
+		}
+		return m;
+	}
 
-	@Override /* Writable */
+	/**
+	 * Serializes this map to the specified writer.
+	 * 
+	 * @param w The writer to serialize to.
+	 * @return The same writer passed in.
+	 * @throws IOException
+	 */
 	public Writer writeTo(Writer w) throws IOException {
-		for (ConfigSection cs : entries.values())
-			cs.writeTo(w);
+		readLock();
+		try {
+			for (ConfigSection cs : entries.values())
+				cs.writeTo(w);
+		} finally {
+			readUnlock();
+		}
 		return w;
 	}
 
-	@Override /* Writable */
-	public MediaType getMediaType() {
-		return MediaType.PLAIN;
+	/**
+	 * Does a rollback of any changes on this map currently in memory.
+	 * 
+	 * @return This object (for method chaining).
+	 */
+	public ConfigMap rollback() {
+		if (changes.size() > 0) {
+			writeLock();
+			try {
+				changes.clear();
+				load(contents);
+		 	} finally {
+				writeUnlock();
+			}
+		}
+		return this;
 	}
 
 	
@@ -526,10 +605,16 @@ public class ConfigMap implements StoreListener, Writable {
 		lock.writeLock().unlock();
 	}
 
-	private boolean isValidSectionName(String s) {
-		return "default".equals(s) || isValidNewSectionName(s);
+	private void checkSectionName(String s) {
+		if (! ("default".equals(s) || isValidNewSectionName(s)))
+			throw new IllegalArgumentException("Invalid section name: '" + s + "'");
 	}
-	
+
+	private void checkKeyName(String s) {
+		if (! isValidKeyName(s))
+			throw new IllegalArgumentException("Invalid key name: '" + s + "'");
+	}
+
 	private boolean isValidKeyName(String s) {
 		if (s == null)
 			return false;
@@ -560,32 +645,32 @@ public class ConfigMap implements StoreListener, Writable {
 		return true;
 	}
 
-	private void signal(List<ChangeEvent> changes) {
-		for (ChangeEventListener l : listeners)
-			l.onChange(changes);
+	private void signal(List<ConfigEvent> changes) {
+		for (ConfigEventListener l : listeners)
+			l.onConfigChange(changes);
 	}
 
-	private List<ChangeEvent> findDiffs(String updatedContents) {
-		List<ChangeEvent> changes = new ArrayList<>();
+	private List<ConfigEvent> findDiffs(String updatedContents) {
+		List<ConfigEvent> changes = new ArrayList<>();
 		ConfigMap newMap = new ConfigMap(updatedContents);
 		for (ConfigSection ns : newMap.oentries.values()) {
 			ConfigSection s = oentries.get(ns.name);
 			if (s == null) {
-				//changes.add(ChangeEvent.setSection(ns.name, ns.preLines));
+				//changes.add(ConfigEvent.setSection(ns.name, ns.preLines));
 				for (ConfigEntry ne : ns.entries.values()) {
-					changes.add(ChangeEvent.setEntry(ns.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
+					changes.add(ConfigEvent.setEntry(ns.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
 				}
 			} else {
 				for (ConfigEntry ne : ns.oentries.values()) {
 					ConfigEntry e = s.oentries.get(ne.key);
 					if (e == null || ! isEquals(e.value, ne.value)) {
-						changes.add(ChangeEvent.setEntry(s.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
+						changes.add(ConfigEvent.setEntry(s.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines));
 					}
 				}
 				for (ConfigEntry e : s.oentries.values()) {
 					ConfigEntry ne = ns.oentries.get(e.key);
 					if (ne == null) {
-						changes.add(ChangeEvent.removeEntry(s.name, e.key));
+						changes.add(ConfigEvent.removeEntry(s.name, e.key));
 					}
 				}
 			}
@@ -593,9 +678,9 @@ public class ConfigMap implements StoreListener, Writable {
 		for (ConfigSection s : oentries.values()) {
 			ConfigSection ns = newMap.oentries.get(s.name);
 			if (ns == null) {
-				//changes.add(ChangeEvent.removeSection(s.name));
+				//changes.add(ConfigEvent.removeSection(s.name));
 				for (ConfigEntry e : s.oentries.values())
-					changes.add(ChangeEvent.removeEntry(s.name, e.key));
+					changes.add(ConfigEvent.removeEntry(s.name, e.key));
 			}
 		}
 		return changes;
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/MemoryStore.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMemoryStore.java
similarity index 78%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/MemoryStore.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMemoryStore.java
index 6cbcddc..5bbbd2d 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/MemoryStore.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMemoryStore.java
@@ -18,7 +18,6 @@ import java.io.*;
 import java.util.concurrent.*;
 
 import org.apache.juneau.*;
-import org.apache.juneau.internal.*;
 
 /**
  * Filesystem-based storage location for configuration files.
@@ -26,14 +25,14 @@ import org.apache.juneau.internal.*;
  * <p>
  * Points to a file system directory containing configuration files.
  */
-public class MemoryStore extends Store {
+public class ConfigMemoryStore extends ConfigStore {
 
 	//-------------------------------------------------------------------------------------------------------------------
 	// Predefined instances
 	//-------------------------------------------------------------------------------------------------------------------
 
 	/** Default memory store, all default values.*/
-	public static final MemoryStore DEFAULT = MemoryStore.create().build();
+	public static final ConfigMemoryStore DEFAULT = ConfigMemoryStore.create().build();
 
 
 	//-------------------------------------------------------------------------------------------------------------------
@@ -45,13 +44,13 @@ public class MemoryStore extends Store {
 	 * 
 	 * @return A new builder for this object.
 	 */
-	public static MemoryStoreBuilder create() {
-		return new MemoryStoreBuilder();
+	public static ConfigMemoryStoreBuilder create() {
+		return new ConfigMemoryStoreBuilder();
 	}
 	
 	@Override /* Context */
-	public MemoryStoreBuilder builder() {
-		return new MemoryStoreBuilder(getPropertyStore());
+	public ConfigMemoryStoreBuilder builder() {
+		return new ConfigMemoryStoreBuilder(getPropertyStore());
 	}
 
 	private final ConcurrentHashMap<String,String> cache = new ConcurrentHashMap<>();
@@ -61,33 +60,36 @@ public class MemoryStore extends Store {
 	 * 
 	 * @param ps The settings for this content store.
 	 */
-	protected MemoryStore(PropertyStore ps) {
+	protected ConfigMemoryStore(PropertyStore ps) {
 		super(ps);
 	}
 	
-	@Override /* Store */
+	@Override /* ConfigStore */
 	public synchronized String read(String name) {
-		return cache.get(name);
+		return emptyIfNull(cache.get(name));
 	}
 
-	@Override /* Store */
+	@Override /* ConfigStore */
 	public synchronized String write(String name, String expectedContents, String newContents) {
+
+		// This is a no-op.
+		if (isEquals(expectedContents, newContents))
+			return null;
+		
 		String currentContents = read(name);
 		
-		if (! isEquals(currentContents, expectedContents)) 
-			return StringUtils.emptyIfNull(currentContents);
+		if (expectedContents != null && ! isEquals(currentContents, expectedContents)) 
+			return currentContents;
 		
-		if (! isEquals(currentContents, newContents)) {
-			cache.put(name, newContents);
-			update(name, newContents);
-		}
+		cache.put(name, newContents);
+		update(name, newContents);
 		
 		return null;
 	}
 
 	
-	@Override /* Store */
-	public synchronized MemoryStore update(String name, String newContents) {
+	@Override /* ConfigStore */
+	public synchronized ConfigMemoryStore update(String name, String newContents) {
 		cache.put(name, newContents);
 		super.update(name, newContents);
 		return this;
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/MemoryStoreBuilder.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMemoryStoreBuilder.java
similarity index 86%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/MemoryStoreBuilder.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMemoryStoreBuilder.java
index d475ab8..198085b 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/MemoryStoreBuilder.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigMemoryStoreBuilder.java
@@ -15,14 +15,14 @@ package org.apache.juneau.config.store;
 import org.apache.juneau.*;
 
 /**
- * Builder for {@link MemoryStore} objects.
+ * Builder for {@link ConfigMemoryStore} objects.
  */
-public class MemoryStoreBuilder extends StoreBuilder {
+public class ConfigMemoryStoreBuilder extends ConfigStoreBuilder {
 
 	/**
 	 * Constructor, default settings.
 	 */
-	public MemoryStoreBuilder() {
+	public ConfigMemoryStoreBuilder() {
 		super();
 	}
 
@@ -31,12 +31,12 @@ public class MemoryStoreBuilder extends StoreBuilder {
 	 * 
 	 * @param ps The initial configuration settings for this builder.
 	 */
-	public MemoryStoreBuilder(PropertyStore ps) {
+	public ConfigMemoryStoreBuilder(PropertyStore ps) {
 		super(ps);
 	}
 
 	@Override /* ContextBuilder */
-	public MemoryStore build() {
-		return new MemoryStore(getPropertyStore());
+	public ConfigMemoryStore build() {
+		return new ConfigMemoryStore(getPropertyStore());
 	}
 }
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/Store.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
similarity index 74%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/Store.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
index e8c7004..f805670 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/Store.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStore.java
@@ -25,13 +25,12 @@ import org.apache.juneau.*;
  * Content stores require two methods to be implemented:
  * <ul>
  * 	<li class='jm'>{@link #read(String)} - Retrieve a config file.
- * 	<li class='jm'>{@link #write(String,String,String)} - Store a config file.
+ * 	<li class='jm'>{@link #write(String,String,String)} - ConfigStore a config file.
  * </ul>
  */
-public abstract class Store extends Context implements Closeable {
-	
-	private final List<StoreListener> listeners = new LinkedList<>();
+public abstract class ConfigStore extends Context implements Closeable {
 	
+	private final ConcurrentHashMap<String,Set<ConfigStoreListener>> listeners = new ConcurrentHashMap<>();
 	private final ConcurrentHashMap<String,ConfigMap> configMaps = new ConcurrentHashMap<>();
 	
 	/**
@@ -39,7 +38,7 @@ public abstract class Store extends Context implements Closeable {
 	 * 
 	 * @param ps The settings for this content store.
 	 */
-	protected Store(PropertyStore ps) {
+	protected ConfigStore(PropertyStore ps) {
 		super(ps);
 	}
 
@@ -47,7 +46,10 @@ public abstract class Store extends Context implements Closeable {
 	 * Returns the contents of the configuration file.
 	 * 
 	 * @param name The config file name.
-	 * @return The contents of the configuration file.
+	 * @return 
+	 * 	The contents of the configuration file.
+	 * 	<br>A blank string if the config does not exist.
+	 * 	<br>Never <jk>null</jk>.
 	 * @throws IOException
 	 */
 	public abstract String read(String name) throws IOException;
@@ -68,22 +70,31 @@ public abstract class Store extends Context implements Closeable {
 	/**
 	 * Registers a new listener on this store.
 	 * 
+	 * @param name The configuration name to listen for.
 	 * @param l The new listener.
 	 * @return This object (for method chaining).
 	 */
-	public Store register(StoreListener l) {
-		this.listeners.add(l);
+	public synchronized ConfigStore register(String name, ConfigStoreListener l) {
+		Set<ConfigStoreListener> s = listeners.get(name);
+		if (s == null) {
+			s = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<ConfigStoreListener,Boolean>()));
+			listeners.put(name, s);
+		}
+		s.add(l);
 		return this;
 	}
 	
 	/**
 	 * Unregisters a listener from this store.
 	 * 
+	 * @param name The configuration name to listen for.
 	 * @param l The listener to unregister.
 	 * @return This object (for method chaining).
 	 */
-	public Store unregister(StoreListener l) {
-		this.listeners.remove(l);
+	public synchronized ConfigStore unregister(String name, ConfigStoreListener l) {
+		Set<ConfigStoreListener> s = listeners.get(name);
+		if (s != null) 
+			s.remove(l);
 		return this;
 	}
 
@@ -96,7 +107,7 @@ public abstract class Store extends Context implements Closeable {
 	 * 	<br>Never <jk>null</jk>.
 	 * @throws IOException
 	 */
-	public ConfigMap getMap(String name) throws IOException {
+	public synchronized ConfigMap getMap(String name) throws IOException {
 		ConfigMap cm = configMaps.get(name);
 		if (cm != null)
 			return cm;
@@ -104,7 +115,7 @@ public abstract class Store extends Context implements Closeable {
 		ConfigMap cm2 = configMaps.putIfAbsent(name, cm);
 		if (cm2 != null)
 			return cm2;
-		listeners.add(cm);
+		register(name, cm);
 		return cm;
 	}
 	
@@ -112,15 +123,17 @@ public abstract class Store extends Context implements Closeable {
 	 * Called when the physical contents of a config file have changed.
 	 * 
 	 * <p>
-	 * Triggers calls to {@link StoreListener#onChange(String, String)} on all registered listeners.
+	 * Triggers calls to {@link ConfigStoreListener#onChange(String)} on all registered listeners.
 	 * 
 	 * @param name The config name (e.g. the filename without the extension).
 	 * @param contents The new contents.
 	 * @return This object (for method chaining).
 	 */
-	public Store update(String name, String contents) {
-		for (StoreListener l : listeners)
-			l.onChange(name, contents);
+	public synchronized ConfigStore update(String name, String contents) {
+		Set<ConfigStoreListener> s = listeners.get(name);
+		if (s != null)
+			for (ConfigStoreListener l : listeners.get(name))
+				l.onChange(contents);
 		return this;
 	}
 
@@ -131,7 +144,7 @@ public abstract class Store extends Context implements Closeable {
 	 * @param contentLines The new contents.
 	 * @return This object (for method chaining).
 	 */
-	public Store update(String name, String...contentLines) {
+	public synchronized ConfigStore update(String name, String...contentLines) {
 		StringBuilder sb = new StringBuilder();
 		for (String l : contentLines)
 			sb.append(l).append('\n');
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/StoreBuilder.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStoreBuilder.java
similarity index 88%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/StoreBuilder.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStoreBuilder.java
index 1ec7056..5aea461 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/StoreBuilder.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStoreBuilder.java
@@ -15,14 +15,14 @@ package org.apache.juneau.config.store;
 import org.apache.juneau.*;
 
 /**
- * Base builder class for {@link Store} objects.
+ * Base builder class for {@link ConfigStore} objects.
  */
-public abstract class StoreBuilder extends ContextBuilder {
+public abstract class ConfigStoreBuilder extends ContextBuilder {
 
 	/**
 	 * Constructor, default settings.
 	 */
-	public StoreBuilder() {
+	public ConfigStoreBuilder() {
 		super();
 	}
 
@@ -31,10 +31,10 @@ public abstract class StoreBuilder extends ContextBuilder {
 	 * 
 	 * @param ps The initial configuration settings for this builder.
 	 */
-	public StoreBuilder(PropertyStore ps) {
+	public ConfigStoreBuilder(PropertyStore ps) {
 		super(ps);
 	}
 
 	@Override
-	public abstract Store build();
+	public abstract ConfigStore build();
 }
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/StoreListener.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStoreListener.java
similarity index 91%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/StoreListener.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStoreListener.java
index 8d85ac0..9771afe 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/StoreListener.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/ConfigStoreListener.java
@@ -15,13 +15,12 @@ package org.apache.juneau.config.store;
 /**
  * Listens for changes to stored config files.
  */
-public interface StoreListener {
+public interface ConfigStoreListener {
 	
 	/**
 	 * Called when the physical contents of a config file have changed.
 	 * 
-	 * @param name The config name (e.g. the filename without the extension).
 	 * @param contents The new config contents;
 	 */
-	void onChange(String name, String contents);
+	void onChange(String contents);
 }
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/WatcherSensitivity.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/WatcherSensitivity.java
index ad789a5..6c12dbe 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/WatcherSensitivity.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/WatcherSensitivity.java
@@ -13,7 +13,7 @@
 package org.apache.juneau.config.store;
 
 /**
- * Determines how often the file system is polled by the watcher in {@link FileStore}.
+ * Determines how often the file system is polled by the watcher in {@link ConfigFileStore}.
  * 
  * <h5 class='section'>Notes:</h5>
  * <ul class='spaced-list'>
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/vars/ConfigFileVar.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/vars/ConfigVar.java
similarity index 86%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/vars/ConfigFileVar.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/vars/ConfigVar.java
index 79f44fe..f6eebc8 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/vars/ConfigFileVar.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/vars/ConfigVar.java
@@ -20,16 +20,16 @@ import org.apache.juneau.svl.*;
  * 
  * <p>
  * The format for this var is <js>"$C{key[,defaultValue]}"</js>.
- * See {@link ConfigFile#getString(String)} for the format of the key.
+ * See {@link Config#getString(String)} for the format of the key.
  * 
  * <p>
- * This variable resolver requires that a {@link ConfigFile} object be set as a context object on the resolver or a
+ * This variable resolver requires that a {@link Config} object be set as a context object on the resolver or a
  * session object on the resolver session.
  * 
  * <h5 class='section'>Example:</h5>
  * <p class='bcode'>
- * 	<jc>// Create a config file object.</jc>
- * 	ConfigFile configFile = new ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>);
+ * 	<jc>// Create a config object.</jc>
+ * 	Config config = Config.<jsm>create</jsm>().name(<js>"MyConfig.cfg"</js>).build();
  * 
  * 	<jc>// Create a variable resolver that resolves config file entries (e.g. "$C{MySection/myKey}")</jc>
  * 	VarResolver r = <jk>new</jk> VarResolver().addVars(ConfigVar.<js>class</js>)
@@ -49,10 +49,10 @@ import org.apache.juneau.svl.*;
  * 	<li class='link'><a class='doclink' href='../../../../../overview-summary.html#juneau-config.Variables'>Overview &gt; juneau-config &gt; Variables</a>
  * </ul>
  */
-public class ConfigFileVar extends DefaultingVar {
+public class ConfigVar extends DefaultingVar {
 
 	/**
-	 * The name of the session or context object that identifies the {@link ConfigFile} object.
+	 * The name of the session or context object that identifies the {@link Config} object.
 	 */
 	public static final String SESSION_config = "config";
 
@@ -62,12 +62,12 @@ public class ConfigFileVar extends DefaultingVar {
 	/**
 	 * Constructor.
 	 */
-	public ConfigFileVar() {
+	public ConfigVar() {
 		super(NAME);
 	}
 
 	@Override /* Var */
 	public String resolve(VarResolverSession session, String key) {
-		return session.getSessionObject(ConfigFile.class, SESSION_config).getString(key);
+		return session.getSessionObject(Config.class, SESSION_config).getString(key);
 	}
 }
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/JacocoDummyTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/JacocoDummyTest.java
index fb3d6f8..e5559e4 100755
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/JacocoDummyTest.java
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/JacocoDummyTest.java
@@ -14,7 +14,6 @@ package org.apache.juneau;
 
 import java.lang.reflect.*;
 
-import org.apache.juneau.config.*;
 import org.apache.juneau.internal.*;
 import org.apache.juneau.jena.*;
 import org.apache.juneau.xml.annotation.*;
@@ -29,7 +28,7 @@ public class JacocoDummyTest {
 	public void accessPrivateConstructorsOnStaticUtilityClasses() throws Exception {
 
 		Class<?>[] classes = new Class[] {
-			StringUtils.class, ArrayUtils.class, ClassUtils.class, CollectionUtils.class, ConfigUtils.class
+			StringUtils.class, ArrayUtils.class, ClassUtils.class, CollectionUtils.class
 		};
 
 		for (Class<?> c : classes) {
@@ -38,7 +37,6 @@ public class JacocoDummyTest {
 			c1.newInstance();
 		}
 
-		ConfigFileFormat.valueOf(ConfigFileFormat.INI.toString());
 		RdfCollectionFormat.valueOf(RdfCollectionFormat.DEFAULT.toString());
 		XmlFormat.valueOf(XmlFormat.DEFAULT.toString());
 		Visibility.valueOf(Visibility.DEFAULT.toString());
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigBuilderTest.java
old mode 100644
new mode 100755
similarity index 55%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java
rename to juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigBuilderTest.java
index f8ee441..9cebe35
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileWritable.java
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigBuilderTest.java
@@ -12,40 +12,49 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.config;
 
+import static org.apache.juneau.TestUtils.*;
+import static org.junit.Assert.*;
+import static org.apache.juneau.internal.FileUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
 import java.io.*;
 
-import org.apache.juneau.*;
-import org.apache.juneau.http.*;
+import org.apache.juneau.config.store.*;
+import org.junit.*;
 
-/**
- * Wraps a {@link ConfigFile} in a {@link Writable} to be rendered as plain text.
- * 
- * <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>
- */
-class ConfigFileWritable implements Writable {
+public class ConfigBuilderTest {
 
-	private ConfigFileImpl cf;
+	private static File tempDir;
+	private static String TEMP_DIR;
 
-	protected ConfigFileWritable(ConfigFileImpl cf) {
-		this.cf = cf;
+	@BeforeClass
+	public static void setup() {
+		tempDir = new File(System.getProperty("java.io.tmpdir"), generateUUID(12));
+		TEMP_DIR = tempDir.getAbsolutePath();
 	}
 
-	@Override /* Writable */
-	public Writer writeTo(Writer out) throws IOException {
-		cf.readLock();
-		try {
-			cf.serializeTo(out);
-			return out;
-		} finally {
-			cf.readUnlock();
-		}
+	@AfterClass
+	public static void teardown() {
+		delete(tempDir);
 	}
 
-	@Override /* Writable */
-	public MediaType getMediaType() {
-		return MediaType.PLAIN;
+	@Test
+	public void testGet() throws Exception {
+		File f;
+		ConfigFileStore cfs = ConfigFileStore.create().directory(TEMP_DIR).useWatcher().watcherSensitivity(WatcherSensitivity.HIGH).build();
+		ConfigBuilder cb = Config.create().store(cfs).name("TestGet.cfg");
+		
+		Config cf = cb.build();
+		cf.set("Test/A", "a");
+
+		f = new File(tempDir, "TestGet.cfg");
+		assertFalse(f.exists());
+
+		cf.save();
+		assertObjectEquals("{'default':{},Test:{A:'a'}}", cf.asMap());
+
+		String NL = System.getProperty("line.separator");
+		cf = cf.write("[Test]"+NL+"A = b"+NL, true);
+		assertObjectEquals("{'default':{},Test:{A:'b'}}", cf.asMap());
 	}
-}
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigFileBuilderTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigFileBuilderTest.java
deleted file mode 100755
index 1eb752f..0000000
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigFileBuilderTest.java
+++ /dev/null
@@ -1,198 +0,0 @@
-// ***************************************************************************************************************************
-// * 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;
-
-import static org.apache.juneau.TestUtils.*;
-import static org.junit.Assert.*;
-import static org.apache.juneau.internal.FileUtils.*;
-import static org.apache.juneau.internal.StringUtils.*;
-import static org.apache.juneau.internal.IOUtils.*;
-
-import java.io.*;
-
-import org.apache.juneau.config.listener.*;
-import org.apache.juneau.svl.*;
-import org.junit.*;
-
-public class ConfigFileBuilderTest {
-
-	private static File tempDir;
-	private static String[] TEMP_DIR;
-
-	@BeforeClass
-	public static void setup() {
-		tempDir = new File(System.getProperty("java.io.tmpdir"), generateUUID(12));
-		mkdirs(tempDir, true);
-		TEMP_DIR = new String[]{tempDir.getAbsolutePath()};
-	}
-
-	@AfterClass
-	public static void teardown() {
-		delete(tempDir);
-	}
-
-	/**
-	 * 
-	 * @throws Exception
-	 */
-	@Test
-	public void testGet() throws Exception {
-		File f;
-		ConfigFileBuilder b1 = new ConfigFileBuilder().paths(TEMP_DIR).createIfNotExists();
-
-		ConfigFile cf = b1.build("TestGet.cfg");
-		cf.put("Test/A", "a");
-
-		f = new File(tempDir, "TestGet.cfg");
-		assertTrue(f.exists());
-
-		cf.save();
-		assertTextEquals("[Test]|A = a|", read(f));
-
-		cf = b1.build("TestGet.cfg");
-		assertObjectEquals("{'default':{},Test:{A:'a'}}", cf);
-
-		String NL = System.getProperty("line.separator");
-		cf = b1.build(new StringReader(("[Test]"+NL+"A = a"+NL)));
-		assertObjectEquals("{'default':{},Test:{A:'a'}}", cf);
-
-		b1.charset(UTF8);
-		cf = b1.build("TestGet.cfg");
-		assertObjectEquals("{'default':{},Test:{A:'a'}}", cf);
-	}
-
-	/**
-	 * Retrieving config file should fail if the file doesn't exist and createIfNotExist == false.
-	 */
-	@Test
-	public void testFailOnNonExistentFiles() throws Exception {
-		ConfigFileBuilder b = new ConfigFileBuilder().paths(new String[]{tempDir.getAbsolutePath()});
-		try { b.build("TestGet2.cfg"); fail(); } catch (FileNotFoundException e) {}
-		try { b.build(tempDir.getAbsolutePath() + "TestGet2.cfg"); fail(); } catch (FileNotFoundException e) {}
-
-		b = new ConfigFileBuilder().paths().createIfNotExists();
-		try { b.build("TestGet.cfg"); fail(); } catch (FileNotFoundException e) {}
-	}
-
-
-	//====================================================================================================
-	// loadIfModified()
-	//====================================================================================================
-	@Test
-	public void testLoadIfModified() throws Exception {
-		ConfigFileBuilder b = new ConfigFileBuilder().paths(TEMP_DIR).createIfNotExists();
-		File f;
-		ConfigFile cf = b.build("TestGet.cfg");
-		cf.put("Test/A", "a");
-
-		f = new File(tempDir, "TestGet.cfg");
-		String NL = System.getProperty("line.separator");
-		write(f, new StringReader("[Test]"+NL+"A = b"+NL));
-		modifyTimestamp(f);
-
-		cf.loadIfModified();
-		assertEquals("b", cf.getString("Test/A"));
-		cf.loadIfModified();
-		assertEquals("b", cf.getString("Test/A"));
-
-		// Config file with no backing file.
-		cf = b.build();
-		cf.put("Test/B", "b");
-		cf.loadIfModified();
-		cf.loadIfModified();
-		assertEquals("b", cf.getString("Test/B"));
-	}
-
-	//====================================================================================================
-	// read only
-	//====================================================================================================
-	@Test
-	public void testReadOnly() throws Exception {
-		ConfigFileBuilder cm = new ConfigFileBuilder().paths(TEMP_DIR).createIfNotExists().readOnly();
-		ConfigFile cf = cm.build("TestGet.cfg");
-
-		// All these should fail.
-		try { cf.loadIfModified(); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.load(); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.load(new StringReader("")); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.put("A","b"); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.put("A","b",true); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.put("A","b"); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.put("A","b",true); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.removeString("A"); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.addLines("A","b=c"); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.addHeaderComments("A", "b=c"); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.clearHeaderComments("A"); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.addSection("A"); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.setSection("A",null); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.removeSection("A"); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.save(); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.merge(cf); fail(); } catch (UnsupportedOperationException e) {}
-		try { cf.addListener(new ConfigListener(){}); fail(); } catch (UnsupportedOperationException e) {}
-
-		// All these should succeed.
-		cf.getObject("A", String.class);
-		cf.getObject("A", "a", String.class);
-		cf.getString("A");
-		cf.getString("A","a");
-		cf.getObject("A", String.class);
-		cf.getObject("A", "a", String.class);
-		cf.getObject("A", String[].class);
-		cf.getStringArray("A");
-		cf.getStringArray("A", null);
-		cf.getInt("A");
-		cf.getInt("A", 0);
-		cf.getBoolean("A");
-		cf.getBoolean("A", true);
-		cf.containsNonEmptyValue("A");
-		cf.getSectionMap("A");
-		cf.serializeTo(new StringWriter());
-		cf.serializeTo(new StringWriter(), ConfigFileFormat.INI);
-		cf.getResolving(VarResolver.DEFAULT);
-		cf.toWritable();
-	}
-
-	//====================================================================================================
-	// main(String[] args)
-	//====================================================================================================
-	@Test
-	public void testMain() throws Exception {
-		System.setProperty("exit.2", "0");
-		ConfigFileBuilder cm = new ConfigFileBuilder().paths(TEMP_DIR).createIfNotExists();
-
-		ConfigFile cf = cm.build("Test.cfg")
-			.addLines(null, "# c1", "\t# c2", " c3 ", "  ", "x1=1", "x2=true", "x3=null")
-			.addLines("s1", "#c4", "k1=1", "#c5 foo=bar", "k2 = true", "k3  = \tnull");
-		cf.save();
-
-		File configFile = new File(tempDir, "Test.cfg");
-		File envFile = new File(tempDir, "Test.bat");
-
-		ConfigFileBuilder.main(new String[]{"createBatchEnvFile", "-configFile", configFile.getAbsolutePath(), "-envFile", envFile.getAbsolutePath()});
-		String expected = "rem c1|rem c2|rem c3||set x1 = 1|set x2 = true|set x3 = null|rem c4|set s1_k1 = 1|rem c5 foo=bar|set s1_k2 = true|set s1_k3 = null|";
-		String actual = read(envFile);
-		assertTextEquals(expected, actual);
-
-		ConfigFileBuilder.main(new String[]{"createShellEnvFile", "-configFile", configFile.getAbsolutePath(), "-envFile", envFile.getAbsolutePath()});
-		expected = "# c1|# c2|# c3||export x1=\"1\"|export x2=\"true\"|export x3=\"null\"|# c4|export s1_k1=\"1\"|# c5 foo=bar|export s1_k2=\"true\"|export s1_k3=\"null\"|";
-		actual = read(envFile);
-		assertTextEquals(expected, actual);
-
-		ConfigFileBuilder.main(new String[]{"setVals", "-configFile", configFile.getAbsolutePath(), "-vals", "x1=2", "s1/k1=2", "s2/k1=3"});
-		modifyTimestamp(configFile);
-		cf.loadIfModified();
-		assertObjectEquals("{'default':{x1:'2',x2:'true',x3:'null'},s1:{k1:'2',k2:'true',k3:'null'},s2:{k1:'3'}}", cf);
-
-		ConfigFileBuilder.main(new String[]{});
-	}
-}
\ No newline at end of file
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigFileTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigFileTest.java
deleted file mode 100755
index d617e45..0000000
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigFileTest.java
+++ /dev/null
@@ -1,2157 +0,0 @@
-// ***************************************************************************************************************************
-// * 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;
-
-import static org.apache.juneau.TestUtils.*;
-import static org.junit.Assert.*;
-import static org.apache.juneau.internal.IOUtils.*;
-import static org.apache.juneau.internal.FileUtils.*;
-
-import java.io.*;
-import java.net.*;
-import java.util.*;
-import java.util.concurrent.*;
-
-import org.apache.juneau.*;
-import org.apache.juneau.annotation.*;
-import org.apache.juneau.config.encode.*;
-import org.apache.juneau.config.listener.*;
-import org.apache.juneau.json.*;
-import org.apache.juneau.parser.*;
-import org.apache.juneau.svl.*;
-import org.apache.juneau.utils.*;
-import org.junit.*;
-
-public class ConfigFileTest {
-
-	private ConfigFileBuilder configFileBuilder = new ConfigFileBuilder();
-
-	private File getFreshFile() {
-		String tempDir = System.getProperty("java.io.tmpdir");
-		File f = new File(tempDir, "Test.cfg");
-		if (f.exists())
-			f.delete();
-		f.deleteOnExit();
-		return f;
-	}
-
-	//====================================================================================================
-	// testCommentsOnly
-	//====================================================================================================
-	@Test
-	public void testCommentsOnly() throws Exception {
-
-		File f = getFreshFile();
-
-		ConfigFile cf = configFileBuilder.build(f)
-			.addLines(null, "# c1", "\t# c2", " c3 ");
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		String expected = "# c1|\t# c2| c3 |";
-		assertTextEquals(expected, cf);
-		assertTextEquals(expected, cfw);
-
-		cf.save();
-		cf = configFileBuilder.build(f);
-		expected = "# c1|\t# c2| c3 |";
-		assertTextEquals(expected, cf);
-		assertTextEquals(expected, cfw);
-
-	}
-
-	//====================================================================================================
-	// testBasics
-	//====================================================================================================
-	@Test
-	public void testBasics() throws Exception {
-
-		File f = getFreshFile();
-
-		ConfigFile cf = configFileBuilder.build(f)
-			.addLines(null, "# c1", "\t# c2", " c3 ", "x1=1", "x2=true", "x3=null")
-			.addLines("s1", "#c4", "k1=1", "#c5 foo=bar", "k2 = true", "k3  = \tnull");
-		ConfigFile cfw = cf.getResolving().getResolving();
-
-		String expected = "# c1|\t# c2| c3 |x1 = 1|x2 = true|x3 = null|[s1]|#c4|k1 = 1|#c5 foo=bar|k2 = true|k3 = null|";
-		assertTextEquals(expected, cf);
-		assertTextEquals(expected, cfw);
-
-		cf.save();
-		cf = configFileBuilder.build(f);
-		cfw = cf.getResolving(VarResolver.DEFAULT);
-		assertEquals(1, cf.getInt("x1"));
-		assertEquals(true, cf.getBoolean("x2"));
-		assertEquals("null", cf.getString("x3"));
-		assertEquals(1, cf.getInt("s1/k1"));
-		assertEquals(true, cf.getBoolean("s1/k2"));
-		assertEquals("null", cf.getString("s1/k3"));
-
-		assertEquals(-1, cf.getInt("X1/k1", -1));
-		assertEquals(true, cf.getBoolean("X1/k2", true));
-		assertEquals("null", cf.getString("X1/k3", "null"));
-
-		assertEquals(1, cfw.getInt("x1"));
-		assertEquals(true, cfw.getBoolean("x2"));
-		assertEquals("null", cfw.getString("x3"));
-		assertEquals(1, cfw.getInt("s1/k1"));
-		assertEquals(true, cfw.getBoolean("s1/k2"));
-		assertEquals("null", cfw.getString("s1/k3"));
-
-		assertEquals(-1, cfw.getInt("X1/k1", -1));
-		assertEquals(true, cfw.getBoolean("X1/k2", true));
-		assertEquals("null", cfw.getString("X1/k3", "null"));
-
-		cf.put("x1", 2);
-		cf.put("x2", false);
-		cf.put("x3", "foo");
-		cf.put("s1/k1", 2);
-		cf.put("s1/k2", false);
-		cf.put("s1/k3", "bar");
-
-		expected = "# c1|\t# c2| c3 |x1 = 2|x2 = false|x3 = foo|[s1]|#c4|k1 = 2|#c5 foo=bar|k2 = false|k3 = bar|";
-		assertTextEquals(expected, cf);
-		assertTextEquals(expected, cfw);
-
-		cfw.put("x1", 3);
-		cfw.put("x2", true);
-		cfw.put("x3", "bar");
-		cfw.put("s1/k1", 4);
-		cfw.put("s1/k2", true);
-		cfw.put("s1/k3", "baz");
-
-		expected = "# c1|\t# c2| c3 |x1 = 3|x2 = true|x3 = bar|[s1]|#c4|k1 = 4|#c5 foo=bar|k2 = true|k3 = baz|";
-		assertTextEquals(expected, cf);
-		assertTextEquals(expected, cfw);
-
-		// Null strings.
-		cf.put("x1", (String)null);
-		expected = "# c1|\t# c2| c3 |x1 = |x2 = true|x3 = bar|[s1]|#c4|k1 = 4|#c5 foo=bar|k2 = true|k3 = baz|";
-		assertTextEquals(expected, cf);
-	}
-
-	@Test
-	public void testSerialization() throws Exception {
-
-		ConfigFile cf = configFileBuilder.build();
-		cf.put("x1", 1);
-
-		String expected = "{'default':{x1:'1'}}";
-		assertObjectEquals(expected, cf);
-		cf = cf.getResolving(VarResolver.DEFAULT);
-		assertObjectEquals(expected, cf);
-	}
-
-	//====================================================================================================
-	// testHeaderComments
-	//====================================================================================================
-	@Test
-	public void testHeaderComments() throws Exception {
-		ConfigFile[] cff = {
-			configFileBuilder.build(getFreshFile()).addLines(null, "x").addLines("s1", "#c3", "#c4").addHeaderComments("s1", "#c1", "#c2"),
-			configFileBuilder.build(getFreshFile()).addLines(null, "x").addLines("s1", "#c3", "#c4").addHeaderComments("s1", "#c1", "#c2").getResolving()
-		};
-
-		for (ConfigFile cf : cff) {
-
-			String expected = "x|#c1|#c2|[s1]|#c3|#c4|";
-			assertTextEquals(expected, cf);
-
-			cf.save();
-			cf.load();
-
-			cf.clearHeaderComments("s1");
-			expected = "x|[s1]|#c3|#c4|";
-			assertTextEquals(expected, cf);
-
-			cf.clearHeaderComments("x1");
-
-			cf.addHeaderComments("s1", "#c5", "c6");
-			expected = "x|#c5|#c6|[s1]|#c3|#c4|";
-			assertTextEquals(expected, cf);
-		}
-	}
-
-	//====================================================================================================
-	// testRemoveEntries
-	//====================================================================================================
-	@Test
-	public void testRemoveEntries() throws Exception {
-
-		File f = getFreshFile();
-
-		ConfigFile cf = configFileBuilder.build(f)
-			.addLines(null, "x1=1")
-			.addLines("s1", "x2=2");
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		String expected = "x1 = 1|[s1]|x2 = 2|";
-		assertTextEquals(expected, cf);
-		assertTextEquals(expected, cfw);
-
-		cf.save();
-		cf.load();
-
-		cf.removeString("x1");
-		expected = "[s1]|x2 = 2|";
-		assertTextEquals(expected, cf);
-		assertTextEquals(expected, cfw);
-
-		cfw.save();
-		cfw.load();
-
-		cf.removeString("s1/x2");
-		expected = "[s1]|";
-		assertTextEquals(expected, cf);
-		assertTextEquals(expected, cfw);
-
-		cf.removeSection("s1");
-		assertEquals("", cf.toString());
-		assertEquals("", cfw.toString());
-	}
-
-	//====================================================================================================
-	// testPut
-	//====================================================================================================
-	@Test
-	public void testPut() throws Exception {
-
-		ConfigFile cf = configFileBuilder.build();
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		cf.addSection(null);
-		cf.put("x1", "1");
-		cf.addHeaderComments(null, "#h1");
-		cf.addLines(null, "#c1", "c2");
-		cf.addSection("s1");
-		cf.put("s1/x2", "1");
-		cf.addHeaderComments("s1","#h2");
-		cf.addLines("s1", "#c3", "c4");
-
-		String expected = "#h1|x1 = 1|#c1|c2|#h2|[s1]|x2 = 1|#c3|c4|";
-		assertTextEquals(expected, cf);
-		assertTextEquals(expected, cfw);
-
-		cfw.addSection(null);
-		cfw.put("x2", "2");
-		cfw.addHeaderComments(null, "#h2");
-		cfw.addLines(null, "#c2", "c3");
-		cfw.addSection("s2");
-		cfw.put("s2/x3", "2");
-		cfw.addHeaderComments("s2","#h3");
-		cfw.addLines("s2", "#c4", "c5");
-
-		expected = "#h1|#h2|x1 = 1|#c1|c2|x2 = 2|#c2|c3|#h2|[s1]|x2 = 1|#c3|c4|#h3|[s2]|x3 = 2|#c4|c5|";
-		assertTextEquals(expected, cf);
-		assertTextEquals(expected, cfw);
-	}
-
-	//====================================================================================================
-	// testExampleInConfigFile - Example in ConfigFile
-	//====================================================================================================
-	@Test
-	public void testExampleInConfigFile() throws Exception {
-
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "# Default section", "key1 = 1", "key2 = true", "key3 = [1,2,3]", "key4 = http://foo", "")
-			.addHeaderComments("section1", "# Section 1")
-			.addLines("section1", "key1 = 2", "key2 = false", "key3 = [4,5,6]", "key4 = http://bar");
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		assertEquals(1, cf.getInt("key1"));
-		assertEquals(true, cf.getBoolean("key2"));
-		assertEquals(3, cf.getObject("key3", int[].class)[2]);
-		assertEquals(6, cf.getObjectWithDefault("xkey3", new int[]{4,5,6}, int[].class)[2]);
-		assertEquals(6, cf.getObjectWithDefault("X/key3", new int[]{4,5,6}, int[].class)[2]);
-		assertEquals(new URL("http://foo").toString(), cf.getObject("key4", URL.class).toString());
-
-		assertEquals(1, cfw.getInt("key1"));
-		assertEquals(true, cfw.getBoolean("key2"));
-		assertEquals(3, cfw.getObject("key3", int[].class)[2]);
-		assertEquals(6, cfw.getObjectWithDefault("xkey3", new int[]{4,5,6}, int[].class)[2]);
-		assertEquals(6, cfw.getObjectWithDefault("X/key3", new int[]{4,5,6}, int[].class)[2]);
-		assertEquals(new URL("http://foo").toString(), cfw.getObject("key4", URL.class).toString());
-
-		assertEquals(2, cf.getInt("section1/key1"));
-		assertEquals(false, cf.getBoolean("section1/key2"));
-		assertEquals(6, cf.getObject("section1/key3", int[].class)[2]);
-		assertEquals(new URL("http://bar").toString(), cf.getObject("section1/key4", URL.class).toString());
-
-		assertEquals(2, cfw.getInt("section1/key1"));
-		assertEquals(false, cfw.getBoolean("section1/key2"));
-		assertEquals(6, cfw.getObject("section1/key3", int[].class)[2]);
-		assertEquals(new URL("http://bar").toString(), cfw.getObject("section1/key4", URL.class).toString());
-
-		cf = configFileBuilder.build(getFreshFile())
-			.addLines(null, "# Default section")
-			.addHeaderComments("section1", "# Section 1");
-		cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		cf.put("key1", 1);
-		cf.put("key2", true);
-		cf.put("key3", new int[]{1,2,3});
-		cf.put("key4", new URL("http://foo"));
-		cf.put("section1/key1", 2);
-		cf.put("section1/key2", false);
-		cf.put("section1/key3", new int[]{4,5,6});
-		cf.put("section1/key4", new URL("http://bar"));
-
-		cf.save();
-		cf.load();
-
-		assertEquals(1, cf.getInt("key1"));
-		assertEquals(true, cf.getBoolean("key2"));
-		assertEquals(3, cf.getObject("key3", int[].class)[2]);
-		assertEquals(new URL("http://foo").toString(), cf.getObject("key4", URL.class).toString());
-
-		assertEquals(1, cfw.getInt("key1"));
-		assertEquals(true, cfw.getBoolean("key2"));
-		assertEquals(3, cfw.getObject("key3", int[].class)[2]);
-		assertEquals(new URL("http://foo").toString(), cfw.getObject("key4", URL.class).toString());
-
-		assertEquals(2, cf.getInt("section1/key1"));
-		assertEquals(false, cf.getBoolean("section1/key2"));
-		assertEquals(6, cf.getObject("section1/key3", int[].class)[2]);
-		assertEquals(new URL("http://bar").toString(), cf.getObject("section1/key4", URL.class).toString());
-
-		assertEquals(2, cfw.getInt("section1/key1"));
-		assertEquals(false, cfw.getBoolean("section1/key2"));
-		assertEquals(6, cfw.getObject("section1/key3", int[].class)[2]);
-		assertEquals(new URL("http://bar").toString(), cfw.getObject("section1/key4", URL.class).toString());
-
-		cfw.put("key1", 2);
-		cfw.put("key2", false);
-		cfw.put("key3", new int[]{4,5,6});
-		cfw.put("key4", new URL("http://bar"));
-		cfw.put("section1/key1", 3);
-		cfw.put("section1/key2", true);
-		cfw.put("section1/key3", new int[]{7,8,9});
-		cfw.put("section1/key4", new URL("http://baz"));
-
-		cfw.save();
-		cfw.load();
-
-		assertEquals(2, cf.getInt("key1"));
-		assertEquals(false, cf.getBoolean("key2"));
-		assertEquals(6, cf.getObject("key3", int[].class)[2]);
-		assertEquals(new URL("http://bar").toString(), cf.getObject("key4", URL.class).toString());
-
-		assertEquals(2, cfw.getInt("key1"));
-		assertEquals(false, cfw.getBoolean("key2"));
-		assertEquals(6, cfw.getObject("key3", int[].class)[2]);
-		assertEquals(new URL("http://bar").toString(), cfw.getObject("key4", URL.class).toString());
-
-		assertEquals(3, cf.getInt("section1/key1"));
-		assertEquals(true, cf.getBoolean("section1/key2"));
-		assertEquals(9, cf.getObject("section1/key3", int[].class)[2]);
-		assertEquals(new URL("http://baz").toString(), cf.getObject("section1/key4", URL.class).toString());
-
-		assertEquals(3, cfw.getInt("section1/key1"));
-		assertEquals(true, cfw.getBoolean("section1/key2"));
-		assertEquals(9, cfw.getObject("section1/key3", int[].class)[2]);
-		assertEquals(new URL("http://baz").toString(), cfw.getObject("section1/key4", URL.class).toString());
-	}
-
-	//====================================================================================================
-	// testEnum
-	//====================================================================================================
-	@Test
-	public void testEnum() throws Exception {
-		ConfigFile cf = configFileBuilder.build(getFreshFile())
-			.addLines(null, "key1 = MINUTES");
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		assertEquals(TimeUnit.MINUTES, cf.getObject("key1", TimeUnit.class));
-		assertEquals(TimeUnit.MINUTES, cfw.getObject("key1", TimeUnit.class));
-
-		cf.save();
-		cf.load();
-
-		assertEquals(TimeUnit.MINUTES, cf.getObject("key1", TimeUnit.class));
-		assertEquals(TimeUnit.MINUTES, cfw.getObject("key1", TimeUnit.class));
-	}
-
-	//====================================================================================================
-	// testBatchFileGeneration
-	//====================================================================================================
-	@Test
-	public void testBatchFileGeneration() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "# c1", "\t# c2", " c3 ", "", "  ", "x1=1", "x2=true", "x3=null")
-			.addHeaderComments(null, "header null", "", null)
-			.addLines("s1", "#c4", "k1=1", "#c5 foo=bar", "k2 = true", "k3  = \tnull")
-			.addHeaderComments("s1", "header s1", "", null);
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		String expected = "rem header null|||rem c1|rem c2|rem c3|||set x1 = 1|set x2 = true|set x3 = null|rem header s1|||rem c4|set s1_k1 = 1|rem c5 foo=bar|set s1_k2 = true|set s1_k3 = null|";
-
-		StringWriter sw = new StringWriter();
-		cf.serializeTo(new PrintWriter(sw), ConfigFileFormat.BATCH);
-		assertTextEquals(expected, sw);
-
-		sw = new StringWriter();
-		cfw.serializeTo(new PrintWriter(sw), ConfigFileFormat.BATCH);
-		assertTextEquals(expected, sw);
-	}
-
-	//====================================================================================================
-	// testShellScriptGeneration
-	//====================================================================================================
-	@Test
-	public void testShellScriptGeneration() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "# c1", "\t# c2", " c3 ", "", " ", "x1=1", "x2=true", "x3=null")
-			.addHeaderComments(null, "header null", "", null)
-			.addLines("s1", "#c4", "k1=1", "#c5 foo=bar", "k2 = true", "k3  = \tnull")
-			.addHeaderComments("s1", "header s1", "", null);
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		String expected = "# header null|||# c1|# c2|# c3|||export x1=\"1\"|export x2=\"true\"|export x3=\"null\"|# header s1|||# c4|export s1_k1=\"1\"|# c5 foo=bar|export s1_k2=\"true\"|export s1_k3=\"null\"|";
-
-		StringWriter sw = new StringWriter();
-		cf.serializeTo(new PrintWriter(sw), ConfigFileFormat.SHELL);
-		assertTextEquals(expected, sw);
-
-		sw = new StringWriter();
-		cfw.serializeTo(new PrintWriter(sw), ConfigFileFormat.SHELL);
-		assertTextEquals(expected, sw);
-	}
-
-	//====================================================================================================
-	// testEncodedValues
-	//====================================================================================================
-	@Test
-	public void testEncodedValues() throws Exception {
-		File f = getFreshFile();
-
-		ConfigFile cf = configFileBuilder.build(f)
-			.addLines("s1", "", "foo* = mypassword")
-			.getResolving(VarResolver.DEFAULT);
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		assertEquals("mypassword", cf.getString("s1/foo"));
-		assertEquals("mypassword", cfw.getString("s1/foo"));
-		assertTrue(cf.isEncoded("s1/foo"));
-		assertTrue(cfw.isEncoded("s1/foo"));
-		cf.save();
-		String expected = "[s1]||foo* = {AwwJVhwUQFZEMg==}|";
-		String actual = read(new FileReader(f));
-		assertTextEquals(expected, actual);
-		cf.load();
-		assertEquals("mypassword", cf.getString("s1/foo"));
-		assertEquals("mypassword", cfw.getString("s1/foo"));
-
-		write(f, new StringReader("[s1]\nfoo* = mypassword2\n"));
-		modifyTimestamp(f);
-		cf.loadIfModified();
-		assertTrue(cf.isEncoded("s1/foo"));
-		assertEquals("mypassword2", cf.getString("s1/foo"));
-		cf.put("s1/foo", "mypassword");
-		assertTrue(cf.isEncoded("s1/foo"));
-
-		// INI output should be encoded
-		StringWriter sw = new StringWriter();
-		cf.serializeTo(new PrintWriter(sw), ConfigFileFormat.INI);
-		expected = "[s1]|foo* = {AwwJVhwUQFZEMg==}|";
-		assertTextEquals(expected, sw);
-
-		// BATCH output should not be encoded
-		sw = new StringWriter();
-		cf.serializeTo(new PrintWriter(sw), ConfigFileFormat.BATCH);
-		expected = "set s1_foo = mypassword|";
-		assertTextEquals(expected, sw);
-
-		// SHELL output should not be encoded
-		sw = new StringWriter();
-		cf.serializeTo(new PrintWriter(sw), ConfigFileFormat.SHELL);
-		expected = "export s1_foo=\"mypassword\"|";
-		assertTextEquals(expected, sw);
-	}
-
-	//====================================================================================================
-	// testVariables
-	//====================================================================================================
-	@Test
-	public void testVariables() throws Exception {
-
-		ConfigFile cf = configFileBuilder.build()
-			.addLines("s1",
-				"f1 = $S{foo}",
-				"f2 = $S{foo,bar}",
-				"f3 = $S{$S{baz,bing},bar}"
-		);
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		System.getProperties().remove("foo");
-		System.getProperties().remove("bar");
-		System.getProperties().remove("baz");
-		System.getProperties().remove("bing");
-
-		assertEquals("$S{foo}", cf.getString("s1/f1"));
-		assertEquals("$S{foo,bar}", cf.getString("s1/f2"));
-		assertEquals("$S{$S{baz,bing},bar}", cf.getString("s1/f3"));
-		assertEquals("", cfw.getString("s1/f1"));
-		assertEquals("bar", cfw.getString("s1/f2"));
-		assertEquals("bar", cfw.getString("s1/f3"));
-
-		System.setProperty("foo", "123");
-		assertEquals("$S{foo}", cf.getString("s1/f1"));
-		assertEquals("$S{foo,bar}", cf.getString("s1/f2"));
-		assertEquals("$S{$S{baz,bing},bar}", cf.getString("s1/f3"));
-		assertEquals("123", cfw.getString("s1/f1"));
-		assertEquals("123", cfw.getString("s1/f2"));
-		assertEquals("bar", cfw.getString("s1/f3"));
-
-		System.setProperty("foo", "$S{bar}");
-		System.setProperty("bar", "baz");
-		assertEquals("$S{foo}", cf.getString("s1/f1"));
-		assertEquals("$S{foo,bar}", cf.getString("s1/f2"));
-		assertEquals("$S{$S{baz,bing},bar}", cf.getString("s1/f3"));
-		assertEquals("baz", cfw.getString("s1/f1"));
-		assertEquals("baz", cfw.getString("s1/f2"));
-		assertEquals("bar", cfw.getString("s1/f3"));
-
-		System.setProperty("bing", "$S{foo}");
-		assertEquals("$S{$S{baz,bing},bar}", cf.getString("s1/f3"));
-		assertEquals("baz", cfw.getString("s1/f3"));
-
-		System.setProperty("baz", "foo");
-		System.setProperty("foo", "123");
-		assertEquals("$S{$S{baz,bing},bar}", cf.getString("s1/f3"));
-		assertEquals("123", cfw.getString("s1/f3"));
-	}
-
-	//====================================================================================================
-	// testXorEncoder
-	//====================================================================================================
-	@Test
-	public void testXorEncoder() throws Exception {
-		testXor("foo");
-		testXor("");
-		testXor("123");
-		testXor("€");  // 3-byte UTF-8 character
-		testXor("𤭢"); // 4-byte UTF-8 character
-	}
-
-	private void testXor(String in) {
-		XorEncoder e = new XorEncoder();
-		String s = e.encode("", in);
-		String s2 = e.decode("", s);
-		assertEquals(in, s2);
-	}
-
-	//====================================================================================================
-	// testMultiLines
-	//====================================================================================================
-	@Test
-	public void testMultiLines() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines("s1",
-				"f1 = x \ny \n  z"
-		);
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		assertEquals("x \ny \n  z", cf.getString("s1/f1"));
-		assertEquals("x \ny \n  z", cfw.getString("s1/f1"));
-
-		StringWriter sw = new StringWriter();
-		cf.serializeTo(sw);
-		String expected = "[s1]|f1 = x |\ty |\t  z|";
-		assertTextEquals(expected, sw);
-
-		sw = new StringWriter();
-		cfw.serializeTo(sw);
-		assertTextEquals(expected, sw);
-	}
-
-	//====================================================================================================
-	// testNumberShortcuts
-	//====================================================================================================
-	@Test
-	public void testNumberShortcuts() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines("s1",
-				"f1 = 1M",
-				"f2 = 1K",
-				"f3 = 1 M",
-				"f4 = 1 K"
-		);
-		ConfigFile cfw = cf.getResolving(VarResolver.DEFAULT);
-
-		assertEquals(1048576, cf.getInt("s1/f1"));
-		assertEquals(1024, cf.getInt("s1/f2"));
-		assertEquals(1048576, cf.getInt("s1/f3"));
-		assertEquals(1024, cf.getInt("s1/f4"));
-
-		assertEquals(1048576, cfw.getInt("s1/f1"));
-		assertEquals(1024, cfw.getInt("s1/f2"));
-		assertEquals(1048576, cfw.getInt("s1/f3"));
-		assertEquals(1024, cfw.getInt("s1/f4"));
-	}
-
-	//====================================================================================================
-	// testListeners
-	//====================================================================================================
-	@Test
-	public void testListeners() throws Exception {
-		ConfigFile[] cff = {
-			configFileBuilder.build(createTempFile("ConfigFileTest.cfg")).addLines(null, "a1=1").addLines("B", "b1=1"),
-			configFileBuilder.build(createTempFile("ConfigFileTest.cfg")).addLines(null, "a1=1").addLines("B", "b1=1").getResolving(VarResolver.DEFAULT)
-		};
-
-		for (ConfigFile cf : cff) {
-			final Set<String> changes = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
-			final int[] count = new int[]{0};
-
-			cf.addListener(
-				new ConfigListener() {
-					@Override /* ConfigListener */
-					public void onLoad(ConfigFile f) {
-						super.onLoad(f);
-						changes.add("<load>");
-						count[0]++;
-					}
-					@Override /* ConfigListener */
-					public void onSave(ConfigFile f) {
-						super.onSave(f);
-						changes.add("<save>");
-						count[0]++;
-					}
-					@Override /* ConfigListener */
-					public void onChange(ConfigFile f, Set<String> ss) {
-						super.onChange(f, ss);
-						for (String sss : ss)
-							changes.add(sss + '=' + f.getString(sss));
-						count[0]++;
-					}
-				}
-			);
-
-			// ConfigFile.addLines(section,lines)
-			changes.clear();
-			count[0] = 0;
-			cf.addLines(null, "a1=3", "a3=3").addLines("B", "b1=3","b3=3");
-			assertObjectEquals("['a1=3','a3=3','B/b1=3','B/b3=3']", changes);
-			assertEquals(2, count[0]);
-
-			// ConfigFile.put(key,value,encoded)
-			changes.clear();
-			count[0] = 0;
-			cf.put("a1", "2");
-			cf.put("B/b1", "2");
-			cf.put("a2", "2");
-			cf.put("B/b2", "2");
-			cf.put("C/c1", "2");
-			cf.put("C/c2", "2");
-			assertObjectEquals("['a1=2','a2=2','B/b1=2','B/b2=2','C/c1=2','C/c2=2']", changes);
-			assertEquals(6, count[0]);
-
-			// put(key,value,encoded)
-			changes.clear();
-			count[0] = 0;
-			cf.put("a4", "4", true);
-			cf.put("B/b4", "4", true);
-			assertObjectEquals("['a4=4','B/b4=4']", changes);
-			assertEquals(2, count[0]);
-
-			// put(key,value)
-			changes.clear();
-			count[0] = 0;
-			cf.put("a5", "5");
-			cf.put("B/b5", "5");
-			assertObjectEquals("['a5=5','B/b5=5']", changes);
-			assertEquals(2, count[0]);
-
-			// put(key,value,encoded)
-			changes.clear();
-			count[0] = 0;
-			cf.put("a6", "6", true);
-			cf.put("B/b6", "6", true);
-			assertObjectEquals("['a6=6','B/b6=6']", changes);
-			assertEquals(2, count[0]);
-
-			// removeString(key)
-			changes.clear();
-			count[0] = 0;
-			cf.removeString("a6");
-			cf.removeString("B/b6");
-			cf.removeString("B/bx");
-			cf.removeString("X/bx");
-			assertObjectEquals("['a6=null','B/b6=null']", changes);
-			assertEquals(2, count[0]);
-
-			// addSection(name)
-			changes.clear();
-			count[0] = 0;
-			cf.addSection("D");
-			assertObjectEquals("[]", changes);
-			assertEquals(0, count[0]);
-
-			// setSection(name,contents)
-			changes.clear();
-			count[0] = 0;
-			cf.setSection("E", new AMap<String,String>().append("e1", "1").append("e2", "2"));
-			assertObjectEquals("['E/e1=1','E/e2=2']", changes);
-			assertEquals(1, count[0]);
-			cf.removeSection("E");
-
-			// removeSection(name)
-			changes.clear();
-			count[0] = 0;
-			cf.removeSection("B");
-			assertObjectEquals("['B/b1=null','B/b2=null','B/b3=null','B/b4=null','B/b5=null']", changes);
-			assertEquals(1, count[0]);
-
-			// removeSection(name)
-			changes.clear();
-			count[0] = 0;
-			cf.removeSection("B");
-			assertObjectEquals("[]", changes);
-			assertEquals(0, count[0]);
-
-			// put(key) {
-			changes.clear();
-			count[0] = 0;
-			Section c = cf.get("C");
-			cf.put("B", c);
-			assertObjectEquals("['C/c1=2','C/c2=2']", changes);
-			assertEquals(1, count[0]);
-
-			// remove(key)
-			cf.remove("B");
-			cf.addLines("B", "b1=1","b2=2");
-			changes.clear();
-			count[0] = 0;
-			cf.remove("B");
-			assertObjectEquals("['B/b1=null','B/b2=null']", changes);
-			assertEquals(1, count[0]);
-
-			// putAll(map)
-			ConfigFile cf2 = configFileBuilder.build();
-			cf2.addLines("D", "d1=1","d2=1").addLines("E", "e1=1","e2=2");
-			changes.clear();
-			count[0] = 0;
-			cf.putAll(cf2);
-			assertObjectEquals("['a1=null','a2=null','a3=null','a4=null','a5=null','D/d1=1','D/d2=1','E/e1=1','E/e2=2']", changes);
-			assertEquals(1, count[0]);
-
-			// clear()
-			cf.clear();
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			cf.clear();
-			assertObjectEquals("['a1=null','a2=null','B/b1=null','B/b2=null']", changes);
-			assertEquals(1, count[0]);
-
-			// entrySet()
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			for (Iterator<Map.Entry<String,Section>> i = cf.entrySet().iterator(); i.hasNext();) {
-				i.next();
-				i.remove();
-			}
-			assertObjectEquals("['a1=null','a2=null','B/b1=null','B/b2=null']", changes);
-			assertEquals(2, count[0]);
-
-			// keySet()
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			for (Iterator<String> i = cf.keySet().iterator(); i.hasNext();) {
-				i.next();
-				i.remove();
-			}
-			assertObjectEquals("['a1=null','a2=null','B/b1=null','B/b2=null']", changes);
-			assertEquals(2, count[0]);
-
-			// values()
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			for (Iterator<Section> i = cf.values().iterator(); i.hasNext();) {
-				i.next();
-				i.remove();
-			}
-			assertObjectEquals("['a1=null','a2=null','B/b1=null','B/b2=null']", changes);
-			assertEquals(2, count[0]);
-
-			// ConfigFile.merge()
-			cf.clear();
-			cf.addLines(null, "a1=1", "a2=1", "a3=1").addLines("B", "b1=1", "b2=1", "b3=1").addLines("C", "c1=1", "c2=1", "c3=1");
-			cf2.clear();
-			cf2.addLines(null, "a2=1", "a3=2", "a4=2").addLines("B", "b2=1", "b3=2", "b4=2").addLines("D", "d2=1", "d3=2", "d4=2");
-			changes.clear();
-			count[0] = 0;
-			cf.merge(cf2);
-			assertObjectEquals("['a1=null','a3=2','a4=2','B/b1=null','B/b3=2','B/b4=2','C/c1=null','C/c2=null','C/c3=null','D/d2=1','D/d3=2','D/d4=2']", changes);
-			assertEquals(1, count[0]);
-
-			// ConfigFile.save()
-			// ConfigFile.load()
-			changes.clear();
-			count[0] = 0;
-			cf.save();
-			cf.load();
-			assertObjectEquals("['<load>','<save>']", changes);
-			assertEquals(2, count[0]);
-
-			// Section.clear()
-			cf.clear();
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			cf.get("default").clear();
-			cf.get("B").clear();
-			assertObjectEquals("['a1=null','a2=null','B/b1=null','B/b2=null']", changes);
-			assertEquals(2, count[0]);
-
-			// Section.put(key,value)
-			cf.clear();
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			cf.get("default").put("a1", "2");
-			cf.get("default").put("a3", "2");
-			cf.get("B").put("b1", "2");
-			cf.get("B").put("b3", "2");
-			assertObjectEquals("['a1=2','a3=2','B/b1=2','B/b3=2']", changes);
-			assertEquals(4, count[0]);
-
-			// Section put(key,value,encoded)
-			cf.clear();
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			cf.get("default").put("a1", "2", true);
-			cf.get("default").put("a3", "2", true);
-			cf.get("B").put("b1", "2", true);
-			cf.get("B").put("b3", "2", true);
-			assertObjectEquals("['a1=2','a3=2','B/b1=2','B/b3=2']", changes);
-			assertEquals(4, count[0]);
-
-			// Section.putAll(map)
-			cf.clear();
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			cf.get("default").putAll(new AMap<String,String>().append("a1","1").append("a2","2").append("a3","2"));
-			cf.get("B").putAll(new AMap<String,String>().append("b1","1").append("b2","2").append("b3","2"));
-			assertObjectEquals("['a2=2','a3=2','B/b2=2','B/b3=2']", changes);
-			assertEquals(2, count[0]);
-
-			// Section.remove(key)
-			cf.clear();
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			cf.get("default").remove("a1");
-			cf.get("default").remove("ax");
-			cf.get("B").remove("b1");
-			cf.get("B").remove("bx");
-			assertObjectEquals("['a1=null','B/b1=null']", changes);
-			assertEquals(2, count[0]);
-
-			// Section.entrySet()
-			cf.clear();
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			Section n = cf.get("default");
-			for (Iterator<Map.Entry<String,String>> i = n.entrySet().iterator(); i.hasNext();) {
-				i.next();
-				i.remove();
-			}
-			n = cf.get("B");
-			for (Iterator<Map.Entry<String,String>> i = n.entrySet().iterator(); i.hasNext();) {
-				i.next();
-				i.remove();
-			}
-			assertObjectEquals("['a1=null','a2=null','B/b1=null','B/b2=null']", changes);
-			assertEquals(4, count[0]);
-
-			// Section.keySet()
-			cf.clear();
-			cf.addLines("default", "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			changes.clear();
-			count[0] = 0;
-			for (Iterator<String> i = cf.get("default").keySet().iterator(); i.hasNext();) {
-				i.next();
-				i.remove();
-			}
-			for (Iterator<String> i = cf.get("B").keySet().iterator(); i.hasNext();) {
-				i.next();
-				i.remove();
-			}
-			assertObjectEquals("['a1=null','a2=null','B/b1=null','B/b2=null']", changes);
-			assertEquals(4, count[0]);
-
-			// Section.values()
-			cf.clear();
-			cf.addLines(null, "a1=1", "a2=1").addLines("B", "b1=1", "b2=1");
-			try {
-				Iterator<String> i = cf.get("default").values().iterator();
-				i.next();
-				i.remove();
-				fail("Exception expected");
-			} catch (UnsupportedOperationException e) {}
-			try {
-				Iterator<String> i = cf.get("B").values().iterator();
-				i.next();
-				i.remove();
-				fail("Exception expected");
-			} catch (UnsupportedOperationException e) {}
-		}
-	}
-
-	//====================================================================================================
-	// testEntryListener
-	//====================================================================================================
-	@Test
-	public void testEntryListener() throws Exception {
-		ConfigFile[] cff = {
-			configFileBuilder.build().addLines(null, "a1=1").addLines("B", "b1=1"),
-			configFileBuilder.build().addLines(null, "a1=1").addLines("B", "b1=1").getResolving(VarResolver.DEFAULT)
-		};
-
-		for (ConfigFile cf : cff) {
-			final Set<String> changes = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
-			final int[] count = new int[]{0};
-
-			cf.addListener(
-				new EntryListener("a1") {
-					@Override /* EntryListener */
-					public void onChange(ConfigFile f) {
-						super.onChange(f);
-						changes.add("a1=" + f.getString("a1"));
-						count[0]++;
-					}
-				}
-			);
-
-			cf.addListener(
-				new EntryListener("B/b1") {
-					@Override /* EntryListener */
-					public void onChange(ConfigFile f) {
-						changes.add("B/b1=" + f.getString("B/b1"));
-						count[0]++;
-					}
-				}
-			);
-
-			cf.put("a1", "2");
-			cf.put("a2", "2");
-			cf.put("B/b1", "2");
-			cf.put("B/b2", "2");
-			assertObjectEquals("['a1=2','B/b1=2']", changes);
-			assertEquals(2, count[0]);
-		}
-	}
-
-	//====================================================================================================
-	// testSectionListener
-	//====================================================================================================
-	@Test
-	public void testSectionListener() throws Exception {
-		ConfigFile[] cff = {
-			configFileBuilder.build().addLines(null, "a1=1").addLines("B", "b1=1"),
-			configFileBuilder.build().addLines(null, "a1=1").addLines("B", "b1=1").getResolving(VarResolver.DEFAULT)
-		};
-
-		for (ConfigFile cf : cff) {
-
-			final Set<String> changes = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
-			final int[] count = new int[]{0};
-
-			cf.addListener(
-				new SectionListener(null) {
-					@Override /* SectionListener */
-					public void onChange(ConfigFile f) {
-						super.onChange(f);
-						changes.add("x=" + f.getString("a1"));
-						count[0]++;
-					}
-				}
-			);
-
-			cf.addListener(
-				new SectionListener("") {
-					@Override /* SectionListener */
-					public void onChange(ConfigFile f) {
-						changes.add("y=" + f.getString("a1"));
-						count[0]++;
-					}
-				}
-			);
-
-			cf.addListener(
-				new SectionListener("B") {
-					@Override /* SectionListener */
-					public void onChange(ConfigFile f) {
-						changes.add("z=" + f.getString("B/b1"));
-						count[0]++;
-					}
-				}
-			);
-
-			cf.put("a1", "2");
-			cf.put("a2", "2");
-			cf.put("B/b1", "2");
-			cf.put("B/b2", "2");
-			assertObjectEquals("['x=2','y=2','z=2']", changes);
-			assertEquals(6, count[0]);
-		}
-	}
-
-	//====================================================================================================
-	// testMerge
-	//====================================================================================================
-	@Test
-	public void testMerge() throws Exception {
-		ConfigFile cf1 = configFileBuilder.build()
-			.addLines(null, "# comment a1", "a1=1")
-			.addLines("B", "# comment b1", "b1=1").addHeaderComments("B", "# comment B1")
-			.addLines("C", "# comment c1", "bc=1").addHeaderComments("C", "# comment C1");
-		ConfigFile cf2 = configFileBuilder.build()
-			.addLines(null, "# comment a2", "a2=2")
-			.addLines("B", "# comment b2", "b2=2").addHeaderComments("B", "# comment B2")
-			.addLines("D", "# comment d2", "d2=2").addHeaderComments("D", "# comment D2");
-		cf1.merge(cf2);
-
-		String expected = "# comment a2|a2 = 2|# comment B2|[B]|# comment b2|b2 = 2|# comment D2|[D]|# comment d2|d2 = 2|";
-		assertTextEquals(expected, cf1);
-
-		cf1 = configFileBuilder.build()
-			.addLines(null, "# comment a1", "a1=1")
-			.addLines("B", "# comment b1", "b1=1").addHeaderComments("B", "# comment B1")
-			.addLines("C", "# comment c1", "bc=1").addHeaderComments("C", "# comment C1").getResolving(VarResolver.DEFAULT);
-		cf2 = configFileBuilder.build()
-			.addLines(null, "# comment a2", "a2=2")
-			.addLines("B", "# comment b2", "b2=2").addHeaderComments("B", "# comment B2")
-			.addLines("D", "# comment d2", "d2=2").addHeaderComments("D", "# comment D2").getResolving(VarResolver.DEFAULT);
-
-		cf1.merge(cf2);
-		assertTextEquals(expected, cf1);
-	}
-
-	//====================================================================================================
-	// testDefaultSection
-	// Default section should be keyed by either null, "", or "default"
-	//====================================================================================================
-	@Test
-	public void testDefaultSection() throws Exception {
-		ConfigFile[] cff = {
-			configFileBuilder.build().addLines(null, "a1=1").addLines("", "a2=2").addLines("default", "a3=3"),
-			configFileBuilder.build().addLines(null, "a1=1").addLines("", "a2=2").addLines("default", "a3=3").getResolving(VarResolver.DEFAULT)
-		};
-
-		for (ConfigFile cf : cff) {
-			assertObjectEquals("{'default':{a1:'1',a2:'2',a3:'3'}}", cf);
-			assertTextEquals("a1 = 1|a2 = 2|a3 = 3|", cf);
-
-			assertObjectEquals("{a1:'1',a2:'2',a3:'3'}", cf.get(null));
-			assertObjectEquals("{a1:'1',a2:'2',a3:'3'}", cf.get(""));
-			assertObjectEquals("{a1:'1',a2:'2',a3:'3'}", cf.get("default"));
-
-			assertEquals("1", cf.getString("a1"));
-			assertEquals("1", cf.getString("default/a1"));
-
-			assertObjectEquals("{a1:'1',a2:'2',a3:'3'}", cf.getSectionMap(null));
-			assertObjectEquals("{a1:'1',a2:'2',a3:'3'}", cf.getSectionMap(""));
-			assertObjectEquals("{a1:'1',a2:'2',a3:'3'}", cf.getSectionMap("default"));
-
-			cf.put(null, cf.get(null));
-			assertObjectEquals("{a1:'1',a2:'2',a3:'3'}", cf.get(null));
-			cf.put("", cf.get(null));
-			assertObjectEquals("{a1:'1',a2:'2',a3:'3'}", cf.get(null));
-			cf.put("default", cf.get(null));
-			assertObjectEquals("{a1:'1',a2:'2',a3:'3'}", cf.get(null));
-		}
-	}
-
-	//====================================================================================================
-	// ConfigFileImpl(File)
-	// ConfigFileImpl()
-	//====================================================================================================
-	@Test
-	public void testAlternateConstructors() throws Exception {
-		ConfigFile cf = new ConfigFileImpl();
-		cf.put("A", "a");
-		try { cf.save(); fail(); } catch (UnsupportedOperationException e) {}
-		cf.loadIfModified();
-		assertEquals("a", cf.getString("A"));
-	}
-
-	//====================================================================================================
-	// containsKey(Object)
-	// containsValue(Object)
-	// size()
-	//====================================================================================================
-	@Test
-	public void testContains() throws Exception {
-		ConfigFile cf = configFileBuilder.build().addLines(null, "a1=1").addLines("", "a2=2").addLines("default", "a3=3").addLines("A", "a4=4");
-		ConfigFile cfw = cf.getResolving();
-
-		assertTrue(cf.containsKey(null));
-		assertTrue(cf.containsKey(""));
-		assertTrue(cf.containsKey("default"));
-		assertTrue(cf.containsKey("A"));
-		assertFalse(cf.containsKey("x"));
-
-		assertTrue(cfw.containsKey(null));
-		assertTrue(cfw.containsKey(""));
-		assertTrue(cfw.containsKey("default"));
-		assertTrue(cfw.containsKey("A"));
-		assertFalse(cfw.containsKey("x"));
-
-		Section s = cf.get(null);
-		assertTrue(cf.containsValue(s));
-		assertTrue(cfw.containsValue(s));
-		s = cf.get("A");
-		assertTrue(cf.containsValue(s));
-		assertTrue(cfw.containsValue(s));
-
-		assertFalse(cf.isEmpty());
-		assertFalse(cfw.isEmpty());
-
-		cf.clear();
-		assertTrue(cf.isEmpty());
-		assertTrue(cfw.isEmpty());
-		assertFalse(cf.containsKey(null));
-		assertFalse(cfw.containsKey(null));
-		assertEquals(0, cf.size());
-		assertEquals(0, cfw.size());
-		assertEquals(0, cf.keySet().size());
-		assertEquals(0, cfw.keySet().size());
-		assertEquals(0, cf.entrySet().size());
-		assertEquals(0, cfw.entrySet().size());
-		assertEquals(0, cf.values().size());
-		assertEquals(0, cfw.values().size());
-	}
-
-	//====================================================================================================
-	// getObjectArray(Class c, String key)
-	// getObjectArray(Class c, String key, T[] def)
-	//====================================================================================================
-	@Test
-	public void testGetObjectArray() throws Exception {
-		ConfigFile cf = configFileBuilder.build().addLines("A", "a1=[1,2,3]");
-		ConfigFile cfw = cf.getResolving();
-		assertObjectEquals("[1,2,3]", cf.getObject("A/a1", Integer[].class));
-		assertObjectEquals("[1,2,3]", cfw.getObject("A/a1", Integer[].class));
-		assertObjectEquals("[4,5,6]", cf.getObjectWithDefault("A/a2", new Integer[]{4,5,6}, Integer[].class));
-		assertObjectEquals("[4,5,6]", cfw.getObjectWithDefault("A/a2", new Integer[]{4,5,6}, Integer[].class));
-		assertObjectEquals("[7,8,9]", cf.getObjectWithDefault("B/a1", new Integer[]{7,8,9}, Integer[].class));
-		assertObjectEquals("[7,8,9]", cfw.getObjectWithDefault("B/a1", new Integer[]{7,8,9}, Integer[].class));
-		assertNull(cf.getObject("B/a1", Integer[].class));
-		assertNull(cfw.getObject("B/a1", Integer[].class));
-
-		cf = configFileBuilder.build().addLines("A", "a1 = [1 ,\n\t2 ,\n\t3] ");
-		assertObjectEquals("[1,2,3]", cf.getObject("A/a1", Integer[].class));
-		assertObjectEquals("[1,2,3]", cfw.getObject("A/a1", Integer[].class));
-
-		// We cannot cast primitive arrays to Object[], so the following throws exceptions.
-		assertObjectEquals("[1,2,3]", cf.getObject("A/a1", int[].class));
-		assertEquals("int", cf.getObject("A/a1", int[].class).getClass().getComponentType().getSimpleName());
-		assertNull(cf.getObject("B/a1", int[].class));
-		assertEquals("int", cf.getObjectWithDefault("B/a1", new int[0], int[].class).getClass().getComponentType().getSimpleName());
-		assertNull(cf.getObject("A/a2", int[].class));
-		assertEquals("int", cf.getObjectWithDefault("A/a2", new int[0], int[].class).getClass().getComponentType().getSimpleName());
-
-		assertObjectEquals("[1,2,3]", cf.getObjectWithDefault("A/a1", new int[]{4}, int[].class));
-		assertEquals("int", cf.getObjectWithDefault("A/a1", new int[]{4}, int[].class).getClass().getComponentType().getSimpleName());
-		assertObjectEquals("[4]", cf.getObjectWithDefault("B/a1", new int[]{4}, int[].class));
-		assertEquals("int", cf.getObjectWithDefault("B/a1", new int[]{4}, int[].class).getClass().getComponentType().getSimpleName());
-		assertObjectEquals("[4]", cf.getObjectWithDefault("A/a2", new int[]{4}, int[].class));
-		assertEquals("int", cf.getObjectWithDefault("A/a2", new int[]{4}, int[].class).getClass().getComponentType().getSimpleName());
-
-		System.setProperty("X", "[4,5,6]");
-		cf = configFileBuilder.build().addLines(null, "x1=$C{A/a1}", "x2=$S{X}", "x3=$S{Y}").addLines("A", "a1=[1,2,3]").getResolving();
-		assertObjectEquals("[1,2,3]", cf.getObjectWithDefault("x1", new int[]{9}, int[].class));
-		assertObjectEquals("[4,5,6]", cf.getObjectWithDefault("x2", new int[]{9}, int[].class));
-		assertObjectEquals("[9]", cf.getObjectWithDefault("x3", new int[]{9}, int[].class));
-		System.clearProperty("X");
-	}
-
-	//====================================================================================================
-	// getStringArray(String key)
-	// getStringArray(String key, String[] def)
-	//====================================================================================================
-	@Test
-	public void testGetStringArray() throws Exception {
-		ConfigFile cf = configFileBuilder.build().addLines("A", "a1=1,2,3");
-		ConfigFile cfw = cf.getResolving();
-		assertObjectEquals("['1','2','3']", cf.getStringArray("A/a1"));
-		assertObjectEquals("['1','2','3']", cfw.getStringArray("A/a1"));
-		assertObjectEquals("['4','5','6']", cf.getStringArray("A/a2", new String[]{"4","5","6"}));
-		assertObjectEquals("['4','5','6']", cfw.getStringArray("A/a2", new String[]{"4","5","6"}));
-		assertObjectEquals("['7','8','9']", cf.getStringArray("B/a1", new String[]{"7","8","9"}));
-		assertObjectEquals("['7','8','9']", cfw.getStringArray("B/a1", new String[]{"7","8","9"}));
-		assertObjectEquals("[]", cf.getStringArray("B/a1"));
-		assertObjectEquals("[]", cfw.getStringArray("B/a1"));
-
-		cf = configFileBuilder.build().addLines("A", "a1 = 1 ,\n\t2 ,\n\t3 ");
-		assertObjectEquals("['1','2','3']", cf.getStringArray("A/a1"));
-		assertObjectEquals("['1','2','3']", cfw.getStringArray("A/a1"));
-
-		System.setProperty("X", "4,5,6");
-		cf = configFileBuilder.build().addLines(null, "x1=$C{A/a1}", "x2=$S{X}", "x3=$S{Y}", "x4=$S{Y,$S{X}}").addLines("A", "a1=1,2,3").getResolving();
-		assertObjectEquals("['1','2','3']", cf.getStringArray("x1", new String[]{"9"}));
-		assertObjectEquals("['4','5','6']", cf.getStringArray("x2", new String[]{"9"}));
-		assertObjectEquals("['9']", cf.getStringArray("x3", new String[]{"9"}));
-
-		// TODO - Doesn't work yet.
-		// assertObjectEquals("['4','5','6']", cf.getStringArray("x4", new String[]{"9"}));
-		System.clearProperty("X");
-	}
-
-	//====================================================================================================
-	// getSectionMap(String name)
-	//====================================================================================================
-	@Test
-	public void testGetSectionMap() throws Exception {
-		ConfigFile cf = configFileBuilder.build().addLines("A", "a1=1", "").addLines("D", "d1=$C{A/a1}","d2=$S{X}");
-
-		assertObjectEquals("{a1:'1'}", cf.getSectionMap("A"));
-		assertNull(cf.getSectionMap("B"));
-		assertObjectEquals("null", cf.getSectionMap("C"));
-
-		ObjectMap m = cf.getSectionMap("A");
-		assertObjectEquals("{a1:'1'}", m);
-
-		m = cf.getSectionMap("D");
-		assertObjectEquals("{d1:'$C{A/a1}',d2:'$S{X}'}", m);
-
-		cf = cf.getResolving();
-
-		System.setProperty("X", "x");
-		m = cf.getSectionMap("D");
-		assertObjectEquals("{d1:'1',d2:'x'}", m);
-		System.clearProperty("X");
-	}
-
-	//====================================================================================================
-	// load(Reader)
-	//====================================================================================================
-	@Test
-	public void testLoadFromReader() throws Exception {
-		ConfigFile[] cff = {
-			configFileBuilder.build().addLines(null, "a1=1"),
-			configFileBuilder.build().addLines(null, "a1=1").getResolving(VarResolver.DEFAULT)
-		};
-
-		for (ConfigFile cf : cff) {
-			cf.load(new StringReader("[B]\nb1=1"));
-			assertObjectEquals("{'default':{},B:{b1:'1'}}", cf);
-		}
-	}
-
-
-	//====================================================================================================
-	// toWritable()
-	//====================================================================================================
-	@Test
-	public void testToWritable() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "a=b");
-		ConfigFile cfw = cf.getResolving();
-
-		StringWriter sw = new StringWriter();
-		cf.toWritable().writeTo(sw);
-		assertTextEquals("a = b|", sw);
-
-		sw = new StringWriter();
-		cfw.toWritable().writeTo(sw);
-		assertTextEquals("a = b|", sw);
-
-		assertEquals("text/plain", cf.toWritable().getMediaType().toString());
-		assertEquals("text/plain", cfw.toWritable().getMediaType().toString());
-	}
-
-	//====================================================================================================
-	// containsNonEmptyKey()
-	//====================================================================================================
-	@Test
-	public void testContainsNonEmptyKey() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "a=b","c=");
-		ConfigFile cfw = cf.getResolving();
-
-		assertTrue(cf.containsNonEmptyValue("a"));
-		assertFalse(cf.containsNonEmptyValue("c"));
-		assertFalse(cf.containsNonEmptyValue("d"));
-
-		assertTrue(cfw.containsNonEmptyValue("a"));
-		assertFalse(cfw.containsNonEmptyValue("c"));
-		assertFalse(cfw.containsNonEmptyValue("d"));
-
-		cf.addLines("A", "a1=$S{X}", "a2=$S{Y,$S{X}}");
-		assertFalse(cfw.containsNonEmptyValue("A/a1"));
-		assertFalse(cfw.containsNonEmptyValue("A/a2"));
-
-		System.setProperty("X", "x");
-		assertTrue(cfw.containsNonEmptyValue("A/a1"));
-		assertTrue(cfw.containsNonEmptyValue("A/a2"));
-		System.clearProperty("X");
-	}
-
-	//====================================================================================================
-	// getSectionKeys(String sectionName)
-	//====================================================================================================
-	@Test
-	public void testGetSectionKeys() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "x1=1", "x2=")
-			.addLines("A", "a1=1", "a2=");
-
-		assertObjectEquals("['x1','x2']", cf.getSectionKeys(null));
-		assertObjectEquals("['x1','x2']", cf.getSectionKeys(""));
-		assertObjectEquals("['x1','x2']", cf.getSectionKeys("default"));
-		assertObjectEquals("['a1','a2']", cf.getSectionKeys("A"));
-		assertNull(cf.getSectionKeys("B"));
-
-		cf = cf.getResolving();
-
-		assertObjectEquals("['x1','x2']", cf.getSectionKeys(null));
-		assertObjectEquals("['x1','x2']", cf.getSectionKeys(""));
-		assertObjectEquals("['x1','x2']", cf.getSectionKeys("default"));
-		assertObjectEquals("['a1','a2']", cf.getSectionKeys("A"));
-		assertNull(cf.getSectionKeys("B"));
-	}
-
-	//====================================================================================================
-	// addLines(String section, String...lines)
-	//====================================================================================================
-	@Test
-	public void testAddLines() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1");
-
-		cf.addLines(null, "# comment1", "x1=2", "x2=1", "foobar");
-		cf.addLines("A", "# comment2", "a1=2", "a2=1", "foobar");
-		cf.addLines("B", "# comment3", "b1=2", "b2=1", "foobar");
-		cf.addLines("C", (String[])null);  // Should be OK.
-		cf.addLines("C", (String)null);  // Should be OK.
-		assertObjectEquals("{'default':{x1:'2',x2:'1'},A:{a1:'2',a2:'1'},B:{b1:'2',b2:'1'},C:{}}", cf);
-		assertTextEquals("# comment1|x1 = 2|x2 = 1|foobar|[A]|# comment2|a1 = 2|a2 = 1|foobar|[B]|# comment3|b1 = 2|b2 = 1|foobar|[C]||", cf);
-
-		cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1")
-			.getResolving();
-
-		cf.addLines(null, "# comment1", "x1=2", "x2=1", "foobar");
-		cf.addLines("A", "# comment2", "a1=2", "a2=1", "foobar");
-		cf.addLines("B", "# comment3", "b1=2", "b2=1", "foobar");
-		cf.addLines("C", (String[])null);  // Should be OK.
-		cf.addLines("C", (String)null);  // Should be OK.
-		assertObjectEquals("{'default':{x1:'2',x2:'1'},A:{a1:'2',a2:'1'},B:{b1:'2',b2:'1'},C:{}}", cf);
-		assertTextEquals("# comment1|x1 = 2|x2 = 1|foobar|[A]|# comment2|a1 = 2|a2 = 1|foobar|[B]|# comment3|b1 = 2|b2 = 1|foobar|[C]||", cf);
-
-		cf = configFileBuilder.build()
-			.addLines(null, "x1=$C{A/a2}")
-			.addLines("A", "a1=1")
-			.getResolving();
-		assertObjectEquals("{'default':{x1:'$C{A/a2}'},A:{a1:'1'}}", cf);
-		assertTextEquals("x1 = $C{A/a2}|[A]|a1 = 1|", cf);
-
-		assertEquals("", cf.getString("x1"));
-
-		cf.addLines("A", "a2=2");
-
-		assertEquals("2", cf.getString("x1"));
-	}
-
-
-	//====================================================================================================
-	// addHeaderComments(String section, String...headerComments)
-	// clearHeaderComments(String section)
-	//====================================================================================================
-	@Test
-	public void testAddHeaderComments() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1");
-
-		cf.addHeaderComments(null, "# h1");
-		cf.addHeaderComments("", "# h2");
-		cf.addHeaderComments("default", "# h3");
-		cf.addHeaderComments("A", "# h4");
-		cf.addHeaderComments("B", "# h5");
-		cf.addHeaderComments("C", (String[])null);
-		cf.addHeaderComments("C", (String)null);
-
-		assertTextEquals("# h1|# h2|# h3|x1 = 1|# h4|[A]|a1 = 1|# h5|[B]|#|[C]|", cf);
-
-		cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1");
-
-		cf.addHeaderComments(null, "h1");
-		cf.addHeaderComments("", "h2");
-		cf.addHeaderComments("default", "h3");
-		cf.addHeaderComments("A", "h4");
-		cf.addHeaderComments("B", "h5");
-		cf.addHeaderComments("C", (String[])null);
-		cf.addHeaderComments("C", (String)null);
-
-		assertTextEquals("#h1|#h2|#h3|x1 = 1|#h4|[A]|a1 = 1|#h5|[B]|#|[C]|", cf);
-
-		cf.clearHeaderComments(null).clearHeaderComments("A").clearHeaderComments("B").clearHeaderComments("C");
-		assertTextEquals("x1 = 1|[A]|a1 = 1|[B]|[C]|", cf);
-
-		cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1")
-			.getResolving();
-
-		cf.addHeaderComments(null, "h1");
-		cf.addHeaderComments("", "h2");
-		cf.addHeaderComments("default", "h3");
-		cf.addHeaderComments("A", "h4");
-		cf.addHeaderComments("B", "h5");
-		cf.addHeaderComments("C", (String[])null);
-		cf.addHeaderComments("C", (String)null);
-
-		assertTextEquals("#h1|#h2|#h3|x1 = 1|#h4|[A]|a1 = 1|#h5|[B]|#|[C]|", cf);
-
-		cf.clearHeaderComments(null).clearHeaderComments("A").clearHeaderComments("B").clearHeaderComments("C");
-		assertTextEquals("x1 = 1|[A]|a1 = 1|[B]|[C]|", cf);
-	}
-
-	//====================================================================================================
-	// getString(String key)
-	// getString(String key, String def)
-	//====================================================================================================
-	@Test
-	public void testGetString() throws Exception {
-		System.setProperty("S1", "1");
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "x1=1", "x2=$C{A/a2}", "x3=$S{S1,2}", "x4=$S{S2,3}")
-			.addLines("A", "a1=1", "a2=$C{A/a1}", "a3=$S{S1,2}", "a4=$S{S2,3}");
-
-		assertEquals("1", cf.getString("x1"));
-		assertEquals("1", cf.getString("x1", "x"));
-		assertEquals("$C{A/a2}", cf.getString("x2"));
-		assertEquals("$C{A/a2}", cf.getString("x2", "x"));
-		assertEquals("$S{S1,2}", cf.getString("x3"));
-		assertEquals("$S{S1,2}", cf.getString("x3", "x"));
-		assertEquals("$S{S2,3}", cf.getString("x4"));
-		assertEquals("$S{S2,3}", cf.getString("x4", "x"));
-		assertNull(cf.getString("x5"));
-		assertEquals("x", cf.getString("x5", "x"));
-
-		assertEquals("1", cf.getString("A/a1"));
-		assertEquals("1", cf.getString("A/a1", "x"));
-		assertEquals("$C{A/a1}", cf.getString("A/a2"));
-		assertEquals("$C{A/a1}", cf.getString("A/a2", "x"));
-		assertEquals("$S{S1,2}", cf.getString("A/a3"));
-		assertEquals("$S{S1,2}", cf.getString("A/a3", "x"));
-		assertEquals("$S{S2,3}", cf.getString("A/a4"));
-		assertEquals("$S{S2,3}", cf.getString("A/a4", "x"));
-		assertNull(cf.getString("A/a5"));
-		assertEquals("x", cf.getString("A/a5", "x"));
-
-		assertNull(cf.getString("B/b1"));
-		assertEquals("x", cf.getString("B/b1", "x"));
-
-		cf = cf.getResolving();
-
-		assertEquals("1", cf.getString("x1"));
-		assertEquals("1", cf.getString("x1", "x"));
-		assertEquals("1", cf.getString("x2"));
-		assertEquals("1", cf.getString("x2", "x"));
-		assertEquals("1", cf.getString("x3"));
-		assertEquals("1", cf.getString("x3", "x"));
-		assertEquals("3", cf.getString("x4"));
-		assertEquals("3", cf.getString("x4", "x"));
-		assertNull(cf.getString("x5"));
-		assertEquals("x", cf.getString("x5", "x"));
-
-		assertEquals("1", cf.getString("A/a1"));
-		assertEquals("1", cf.getString("A/a1", "x"));
-		assertEquals("1", cf.getString("A/a2"));
-		assertEquals("1", cf.getString("A/a2", "x"));
-		assertEquals("1", cf.getString("A/a3"));
-		assertEquals("1", cf.getString("A/a3", "x"));
-		assertEquals("3", cf.getString("A/a4"));
-		assertEquals("3", cf.getString("A/a4", "x"));
-		assertNull(cf.getString("A/a5"));
-		assertEquals("x", cf.getString("A/a5", "x"));
-
-		assertNull(cf.getString("B/b1"));
-		assertEquals("x", cf.getString("B/b1", "x"));
-
-		System.clearProperty("S1");
-	}
-
-	//====================================================================================================
-	// put(String key, Object value)
-	//====================================================================================================
-	@Test
-	public void testPutString() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1");
-
-		cf.put("x1", 2);
-		cf.put("x2", 3);
-		cf.put("A/a1", 2);
-		cf.put("A/a2", 3);
-		cf.put("B/b1", 2);
-
-		assertObjectEquals("{'default':{x1:'2',x2:'3'},A:{a1:'2',a2:'3'},B:{b1:'2'}}", cf);
-		assertTextEquals("x1 = 2|x2 = 3|[A]|a1 = 2|a2 = 3|[B]|b1 = 2|", cf);
-
-		cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1")
-			.getResolving();
-
-		cf.put("x1", 2);
-		cf.put("x2", 3);
-		cf.put("A/a1", 2);
-		cf.put("A/a2", 3);
-		cf.put("B/b1", 2);
-
-		assertObjectEquals("{'default':{x1:'2',x2:'3'},A:{a1:'2',a2:'3'},B:{b1:'2'}}", cf);
-		assertTextEquals("x1 = 2|x2 = 3|[A]|a1 = 2|a2 = 3|[B]|b1 = 2|", cf);
-
-		cf.put("x1", 9);
-		cf.put("x2", "$C{x1}");
-		cf.put("A/a1", "$C{x1}");
-		cf.put("A/a2", "$C{x1}");
-		cf.put("B/b1", "$C{x1}");
-
-		assertObjectEquals("{'default':{x1:'9',x2:'$C{x1}'},A:{a1:'$C{x1}',a2:'$C{x1}'},B:{b1:'$C{x1}'}}", cf);
-		assertTextEquals("x1 = 9|x2 = $C{x1}|[A]|a1 = $C{x1}|a2 = $C{x1}|[B]|b1 = $C{x1}|", cf);
-
-		assertEquals("9", cf.getString("x1"));
-		assertEquals("9", cf.getString("x2"));
-		assertEquals("9", cf.getString("A/a1"));
-		assertEquals("9", cf.getString("A/a2"));
-		assertEquals("9", cf.getString("B/b1"));
-	}
-
-	//====================================================================================================
-	// put(String key, Object value, boolean encoded)
-	//====================================================================================================
-	@Test
-	public void testPutStringEncoded() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1");
-
-		cf.put("x1", 2, true);
-		cf.put("x2", 3, true);
-		cf.put("A/a1", 2, true);
-		cf.put("A/a2", 3, true);
-		cf.put("B/b1", 2, true);
-
-		assertObjectEquals("{'default':{x1:'2',x2:'3'},A:{a1:'2',a2:'3'},B:{b1:'2'}}", cf);
-		assertTextEquals("x1* = {XA==}|x2* = {XQ==}|[A]|a1* = {XA==}|a2* = {XQ==}|[B]|b1* = {XA==}|", cf);
-
-		cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1")
-			.getResolving();
-
-		cf.put("x1", 2, true);
-		cf.put("x2", 3, true);
-		cf.put("A/a1", 2, true);
-		cf.put("A/a2", 3, true);
-		cf.put("B/b1", 2, true);
-
-		assertObjectEquals("{'default':{x1:'2',x2:'3'},A:{a1:'2',a2:'3'},B:{b1:'2'}}", cf);
-		assertTextEquals("x1* = {XA==}|x2* = {XQ==}|[A]|a1* = {XA==}|a2* = {XQ==}|[B]|b1* = {XA==}|", cf);
-
-		cf.put("x1", 9, true);
-		cf.put("x2", "$C{x1}", true);
-		cf.put("A/a1", "$C{x1}", true);
-		cf.put("A/a2", "$C{x1}", true);
-		cf.put("B/b1", "$C{x1}", true);
-
-		assertObjectEquals("{'default':{x1:'9',x2:'$C{x1}'},A:{a1:'$C{x1}',a2:'$C{x1}'},B:{b1:'$C{x1}'}}", cf);
-		assertTextEquals("x1* = {Vw==}|x2* = {SjYCT14a}|[A]|a1* = {SjYCT14a}|a2* = {SjYCT14a}|[B]|b1* = {SjYCT14a}|", cf);
-
-		assertEquals("9", cf.getString("x1"));
-		assertEquals("9", cf.getString("x2"));
-		assertEquals("9", cf.getString("A/a1"));
-		assertEquals("9", cf.getString("A/a2"));
-		assertEquals("9", cf.getString("B/b1"));
-	}
-
-	//====================================================================================================
-	// removeString(String key)
-	//====================================================================================================
-	@Test
-	public void testRemoveString() throws Exception {
-		ConfigFile cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1");
-
-		cf.removeString("x1");
-		cf.removeString("x2");
-		cf.removeString("A/a1");
-		cf.removeString("A/a2");
-
-		assertObjectEquals("{'default':{},A:{}}", cf);
-		assertTextEquals("[A]|", cf);
-
-		cf = configFileBuilder.build()
-			.addLines(null, "x1=1")
-			.addLines("A", "a1=1")
-			.getResolving();
-
-		cf.removeString("x1");
-		cf.removeString("x2");
-		cf.removeString("A/a1");
-		cf.removeString("A/a2");
-
-		assertObjectEquals("{'default':{},A:{}}", cf);
-		assertTextEquals("[A]|", cf);
-	}
-
-	//====================================================================================================
-	// getObject(Class c, String key)
-	// getObject(Class c, String key, T def)
-	//====================================================================================================
-	@Test
-	public void testGetObject() throws Exception {
-		ConfigFile cf = configFileBuilder.build().addLines("A", "a1=[1,2,3]", "a2=1", "a3=true", "a4=1.2", "a5=[1.2,3.4]");
-		ConfigFile cfw = cf.getResolving();
-
-		assertObjectEquals("['1','2','3']", cf.getObject("A/a1", String[].class));
-		assertObjectEquals("'[1,2,3]'", cf.getObject("A/a1", String.class));
-		assertObjectEquals("'foobar'", cf.getObjectWithDefault("X/a1", "foobar", String.class));
-		assertObjectEquals("1", cf.getObject("A/a2", int.class));
-		assertObjectEquals("1", cf.getObject("A/a2", Integer.class));
-		assertObjectEquals("true", cf.getObject("A/a3", boolean.class));
-		assertObjectEquals("true", cf.getObject("A/a3", Boolean.class));
-		assertObjectEquals("1.2", cf.getObject("A/a4", Float.class));
-		assertObjectEquals("[1.2,3.4]", cf.getObject("A/a5", Float[].class));
-		assertObjectEquals("1.2", cf.getObject("A/a4", float.class));
-		assertObjectEquals("[1.2,3.4]", cf.getObject("A/a5", float[].class));
-		assertNull(cf.getObject("B/a4", String.class));
-
-		assertObjectEquals("['1','2','3']", cfw.getObject("A/a1", String[].class));
-		assertObjectEquals("'[1,2,3]'", cfw.getObject("A/a1", String.class));
-		assertObjectEquals("'foobar'", cfw.getObjectWithDefault("X/a1", "foobar", String.class));
-		assertObjectEquals("1", cfw.getObject("A/a2", int.class));
-		assertObjectEquals("1", cfw.getObject("A/a2", Integer.class));
-		assertObjectEquals("true", cfw.getObject("A/a3", boolean.class));
-		assertObjectEquals("true", cfw.getObject("A/a3", Boolean.class));
-		assertObjectEquals("1.2", cfw.getObject("A/a4", Float.class));
-		assertObjectEquals("[1.2,3.4]", cfw.getObject("A/a5", Float[].class));
-		assertObjectEquals("1.2", cfw.getObject("A/a4", float.class));
-		assertObjectEquals("[1.2,3.4]", cfw.getObject("A/a5", float[].class));
-		assertNull(cfw.getObject("B/a4", String.class));
-	}
-
-	//====================================================================================================
-	// getInt(String key)
-	// getInt(String key, int def)
-	//====================================================================================================
-	@Test
-	public void testGetInt() throws Exception {
-		System.setProperty("X", "1");
-		ConfigFile cf = configFileBuilder.build().addLines(null, "x1=$C{A/a1}", "x2=$S{X}", "x3=$S{Y}", "x4=$S{Y,2}").addLines("A", "a1=1");
-
-		try {
-			cf.getInt("x1");
-			fail();
-		} catch (NumberFormatException e) {}
-		try {
-			cf.getInt("x2");
-			fail();
-		} catch (NumberFormatException e) {}
-		try {
-			cf.getInt("x3");
-			fail();
-		} catch (NumberFormatException e) {}
-		try {
-			cf.getInt("x4");
-			fail();
-		} catch (NumberFormatException e) {}
-		assertEquals(1, cf.getInt("A/a1"));
-
-		cf = cf.getResolving();
-
-		assertEquals(1, cf.getInt("x1"));
-		assertEquals(1, cf.getInt("x2"));
-		assertEquals(0, cf.getInt("x3"));
-		assertEquals(9, cf.getInt("x3", 9));
-		assertEquals(2, cf.getInt("x4"));
-		assertEquals(9, cf.getInt("x5", 9));
-		assertEquals(1, cf.getInt("A/a1"));
-		assertEquals(9, cf.getInt("A/a2", 9));
-		assertEquals(9, cf.getInt("B/b1", 9));
-
-		System.clearProperty("X");
-	}
-
-	//====================================================================================================
-	// getBoolean(String key)
-	// getBoolean(String key, boolean def)
-	//====================================================================================================
-	@Test
-	public void testGetBoolean() throws Exception {
-		System.setProperty("X", "true");
-		ConfigFile cf = configFileBuilder.build().addLines(null, "x1=$C{A/a1}", "x2=$S{X}", "x3=$S{Y}", "x4=$S{Y,true}").addLines("A", "a1=true");
-
-		assertFalse(cf.getBoolean("x1"));
-		assertFalse(cf.getBoolean("x2"));
-		assertFalse(cf.getBoolean("x3"));
-		assertFalse(cf.getBoolean("x4"));
-		assertTrue(cf.getBoolean("A/a1"));
-
-		cf = cf.getResolving();
-
-		assertTrue(cf.getBoolean("x1"));
-		assertTrue(cf.getBoolean("x2"));
-		assertFalse(cf.getBoolean("x3"));
-		assertTrue(cf.getBoolean("x3", true));
-		assertTrue(cf.getBoolean("x4"));
-		assertTrue(cf.getBoolean("x5", true));
-		assertTrue(cf.getBoolean("A/a1"));
-		assertTrue(cf.getBoolean("A/a2", true));
-		assertTrue(cf.getBoolean("B/b1", true));
-
-		System.clearProperty("X");
-	}
-
-	//====================================================================================================
-	// getSectionAsBean(String,Class)
-	//====================================================================================================
-	@Test
-	public void testGetSectionAsBean() throws Exception {
-		ConfigFile cf = configFileBuilder.build();
-		cf.put("A/a", "1");
-		cf.put("A/b", "2");
-
-		A a = cf.getSectionAsBean("A", A.class);
-		assertObjectEquals("{a:1,b:2}", a);
-
-		cf.put("A/c", "3");
-		try { cf.getSectionAsBean("A",A.class);} catch (ParseException e) {}
-		a = cf.getSectionAsBean("A", A.class, true);
-		assertObjectEquals("{a:1,b:2}", a);
-
-		System.setProperty("X", "3");
-		cf.put("A/a", "$S{X}");
-		cf.put("A/b", "$S{Y,$S{X}}");
-		cf = cf.getResolving();
-		a = cf.getSectionAsBean("A", A.class, true);
-		assertObjectEquals("{a:3,b:3}", a);
-		System.clearProperty("X");
-	}
-
-	public static class A {
-		public int a;
-		private int b;
-		public int getB() {
-			return b;
-		}
-		public void setB(int b) {
-			this.b = b;
-		}
-	}
-
-	//====================================================================================================
-	// writeProperties(...)
-	//====================================================================================================
-	@Test
-	public void testWriteProperties() throws Exception {
-		ConfigFile cf = configFileBuilder.build();
-		cf.put("B/a", "1");
-
-		B b = new B();
-		cf.writeProperties("B", b, false);
-		assertObjectEquals("{a:1}", b);
-
-		cf.put("B/b", "2");
-		try { cf.writeProperties("B", b, false);} catch (ParseException e) {}
-		cf.writeProperties("B", b, true);
-		assertObjectEquals("{a:1}", b);
-		cf.removeString("B/b");
-
-		cf.put("B/c", "2");
-		try { cf.writeProperties("B", b, false);} catch (ParseException e) {}
-		cf.writeProperties("B", b, true);
-		assertObjectEquals("{a:1}", b);
-		cf.removeString("B/c");
-
-		cf.put("B/c2", "2");
-		try { cf.writeProperties("B", b, false);} catch (ParseException e) {}
-		cf.writeProperties("B", b, true);
-		assertObjectEquals("{a:1}", b);
-		cf.removeString("B/c2");
-
-		cf.put("B/d", "2");
-		try { cf.writeProperties("B", b, false);} catch (ParseException e) {}
-		cf.writeProperties("B", b, true);
-		assertObjectEquals("{a:1}", b);
-		cf.removeString("B/d");
-
-		cf.put("B/e", "2");
-		cf.writeProperties("B", b, false);
-		assertObjectEquals("{a:1,e:2}", b);
-
-		cf.put("B/f", "foobar");
-		try {
-			cf.writeProperties("B", b, false, int.class, Integer.class);
-		} catch (ParseException e) {
-			assertTrue(e.getLocalizedMessage().startsWith("Invalid"));
-		}
-		assertObjectEquals("{a:1,e:2}", b);
-
-		cf.removeString("B/f");
-		System.setProperty("X", "3");
-		cf.put("B/a", "$S{X}");
-		cf.put("B/e", "$S{Y,$S{X}}");
-		cf = cf.getResolving();
-		cf.writeProperties("B", b, true);
-		assertObjectEquals("{a:3,e:3}", b);
-
-		System.clearProperty("X");
-	}
-
-	@Bean(sort=true)
-	public static class B {
-		private int a;
-		private Integer e;
-		public int getA() {
-			return a;
-		}
-		public void setA(int a) {
-			this.a = a;
-		}
-		public static void setB(String b) {
-			throw new RuntimeException("Should not be called.");
-		}
-		protected void setC(String c) {
-			throw new RuntimeException("Should not be called.");
-		}
-		protected static void setC2(String c2) {
-			throw new RuntimeException("Should not be called.");
-		}
-		public void setD(String d, String x) {
-			throw new RuntimeException("Should not be called.");
-		}
-		public Integer getE() {
-			return e;
-		}
-		public void setE(Integer e) {
-			this.e = e;
-		}
-		public String getF() {
-			return null;
-		}
-		public void setF(String f) {
-			throw new RuntimeException("Should not be called.");
-		}
-	}
-
-	//====================================================================================================
-	// Bad input
-	//====================================================================================================
-	@Test
-	public void testBadInput() throws Exception {
-		ConfigFile[] cff = {
-			configFileBuilder.build().addLines("A", "a1=1", ""),
-			configFileBuilder.build().addLines("A", "a1=1", "").getResolving()
-		};
-
-		for (ConfigFile cf : cff) {
-			try {
-				cf.load(null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'r' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getString(null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getString(null, null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.put(null, (Object)null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.put(null, null, true);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.removeString(null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getObject(null, Object.class);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getObject("", null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'c' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getStringArray(null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getStringArray(null, null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getInt(null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getInt(null, -1);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getBoolean(null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getBoolean(null, true);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.put(null, (String)null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.put(null, null, true);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.writeProperties(null, null, true);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'bean' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getSectionAsBean(null, null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'c' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getSectionAsBean(null, null, true);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'c' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.containsNonEmptyValue(null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.isEncoded(null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'key' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.addListener(null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'listener' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.merge(null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'cf' cannot be null.", e.getLocalizedMessage());
-			}
-			try {
-				cf.getResolving((VarResolver)null);
-				fail();
-			} catch (IllegalArgumentException e) {
-				assertEquals("Field 'vr' cannot be null.", e.getLocalizedMessage());
-			}
-		}
-	}
-
-	//====================================================================================================
-	// Config to and from JSON
-	//====================================================================================================
-	@Test
-	public void testSerializedAsJson() throws Exception {
-		ConfigFile cf = configFileBuilder.build();
-		cf.put("a", "1");
-		cf.put("B/a", "2");
-
-		String json = JsonSerializer.DEFAULT_LAX.toString(cf);
-		assertEquals("{'default':{a:'1'},B:{a:'2'}}", json);
-
-		cf = JsonParser.DEFAULT.parse(json, ConfigFileImpl.class);
-		assertObjectEquals("{'default':{a:'1'},B:{a:'2'}}", cf);
-
-	}
-
-	//====================================================================================================
-	// Test resolving with override
-	//====================================================================================================
-	@Test
-	public void testResolvingWithOverride() throws Exception {
-		ConfigFile cf = configFileBuilder.build();
-		cf.put("a", "$A{X}");
-		cf.put("b", "$B{X}");
-		cf.put("c", "$A{$B{X}}");
-		cf.put("d", "$B{$A{X}}");
-		cf.put("e", "$D{X}");
-
-		VarResolver vr = new VarResolverBuilder().vars(ALVar.class, BLVar.class).build();
-
-		cf = cf.getResolving(vr);
-
-		assertEquals("aXa", cf.getString("a"));
-		assertEquals("bXb", cf.getString("b"));
-		assertEquals("abXba", cf.getString("c"));
-		assertEquals("baXab", cf.getString("d"));
-		assertEquals("$D{X}", cf.getString("e"));
-
-		// Create new resolver that addx $C and overrides $A
-		VarResolver vr2 = vr.builder().vars(AUVar.class, DUVar.class).build();
-
-		// true == augment by adding existing as parent to the new resolver
-		cf = cf.getResolving(vr2);
-		assertEquals("AXA", cf.getString("a"));
-		assertEquals("bXb", cf.getString("b"));
-		assertEquals("AbXbA", cf.getString("c"));
-		assertEquals("bAXAb", cf.getString("d"));
-		assertEquals("DXD", cf.getString("e"));
-	}
-
-	public static class ALVar extends SimpleVar {
-		public ALVar() {
-			super("A");
-		}
-		@Override
-		public String resolve(VarResolverSession session, String key) {
-			return 'a' + key + 'a';
-		}
-	}
-
-	public static class AUVar extends SimpleVar {
-		public AUVar() {
-			super("A");
-		}
-		@Override
-		public String resolve(VarResolverSession session, String key) {
-			return 'A' + key + 'A';
-		}
-	}
-
-	public static class BLVar extends SimpleVar {
-		public BLVar() {
-			super("B");
-		}
-		@Override
-		public String resolve(VarResolverSession session, String key) {
-			return 'b' + key + 'b';
-		}
-	}
-
-	public static class DUVar extends SimpleVar {
-		public DUVar() {
-			super("D");
-		}
-		@Override
-		public String resolve(VarResolverSession session, String key) {
-			return 'D' + key + 'D';
-		}
-	}
-
-	//====================================================================================================
-	// Test multiline values.
-	//====================================================================================================
-	@Test
-	public void testMultilineValues() throws Exception {
-		File f = getFreshFile();
-
-		ConfigFile cf = configFileBuilder.build(f);
-		cf.put("a", "a,\nb,\nc");
-		cf.put("A/a", "a,\nb,\nc");
-
-		assertTextEquals("a = a,|\tb,|\tc|[A]|a = a,|\tb,|\tc|", cf);
-		cf.save();
-		assertTextEquals("a = a,|\tb,|\tc|[A]|a = a,|\tb,|\tc|", read(f));
-
-		cf.load();
-		assertEquals("a,\nb,\nc", cf.getString("a"));
-		assertEquals("a,\nb,\nc", cf.getString("A/a"));
-
-		assertObjectEquals("['a','b','c']", cf.getStringArray("a", null));
-		assertObjectEquals("['a','b','c']", cf.getStringArray("A/a", null));
-	}
-
-	//====================================================================================================
-	// Test special character encoding.
-	//====================================================================================================
-	@Test
-	public void testSpecialCharacterEncoding() throws Exception {
-		File f = getFreshFile();
-
-		ConfigFile cf = configFileBuilder.build(f);
-		cf.put("a", "a,#b,=c");
-		cf.put("A/a", "a,#b,=c");
-
-		assertTextEquals("a = a,\\u0023b,\\u003Dc|[A]|a = a,\\u0023b,\\u003Dc|", cf);
-		cf.save();
-		assertTextEquals("a = a,\\u0023b,\\u003Dc|[A]|a = a,\\u0023b,\\u003Dc|", read(f));
-
-		cf.load();
-		assertEquals("a,#b,=c", cf.getString("a"));
-		assertEquals("a,#b,=c", cf.getString("A/a"));
-	}
-}
\ No newline at end of file
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigFileInterfaceTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigInterfaceTest.java
similarity index 87%
rename from juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigFileInterfaceTest.java
rename to juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigInterfaceTest.java
index faf6fc8..e1ff988 100644
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigFileInterfaceTest.java
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigInterfaceTest.java
@@ -21,13 +21,13 @@ import org.apache.juneau.test.pojos.*;
 import org.apache.juneau.utils.*;
 import org.junit.*;
 
-public class ConfigFileInterfaceTest {
+public class ConfigInterfaceTest {
 
-	ConfigFile cf;
+	Config cf;
 	ConfigInterface proxy;
 
-	public ConfigFileInterfaceTest() throws Exception {
-		ConfigFileBuilder configFileBuilder = new ConfigFileBuilder();
+	public ConfigInterfaceTest() throws Exception {
+		ConfigBuilder configFileBuilder = new ConfigBuilder();
 		cf = configFileBuilder.build();
 		proxy = cf.getSectionAsInterface("A", ConfigInterface.class);
 	}
@@ -41,21 +41,21 @@ public class ConfigFileInterfaceTest {
 	public void testString() throws Exception {
 		proxy.setString("foo");
 		assertEquals("foo", proxy.getString());
-		assertEquals("foo", cf.get("A", "string"));
+		assertEquals("foo", cf.get("A/string"));
 	}
 
 	@Test
 	public void testInt() throws Exception {
 		proxy.setInt(1);
 		assertEquals(1, proxy.getInt());
-		assertEquals("1", cf.get("A", "int"));
+		assertEquals("1", cf.get("A/int"));
 	}
 
 	@Test
 	public void testInteger() throws Exception {
 		proxy.setInteger(2);
 		assertEquals(2, proxy.getInteger().intValue());
-		assertEquals("2", cf.get("A", "integer"));
+		assertEquals("2", cf.get("A/integer"));
 		assertType(Integer.class, proxy.getInteger());
 	}
 
@@ -63,14 +63,14 @@ public class ConfigFileInterfaceTest {
 	public void testBoolean() throws Exception {
 		proxy.setBoolean(true);
 		assertEquals(true, proxy.isBoolean());
-		assertEquals("true", cf.get("A", "boolean"));
+		assertEquals("true", cf.get("A/boolean"));
 	}
 
 	@Test
 	public void testBooleanObject() throws Exception {
 		proxy.setBooleanObject(true);
 		assertEquals(true, proxy.getBooleanObject().booleanValue());
-		assertEquals("true", cf.get("A", "booleanObject"));
+		assertEquals("true", cf.get("A/booleanObject"));
 		assertType(Boolean.class, proxy.getBooleanObject());
 	}
 
@@ -78,21 +78,21 @@ public class ConfigFileInterfaceTest {
 	public void testFloat() throws Exception {
 		proxy.setFloat(1f);
 		assertTrue(1f == proxy.getFloat());
-		assertEquals("1.0", cf.get("A", "float"));
+		assertEquals("1.0", cf.get("A/float"));
 	}
 
 	@Test
 	public void testFloatObject() throws Exception {
 		proxy.setFloatObject(1f);
 		assertTrue(1f == proxy.getFloatObject().floatValue());
-		assertEquals("1.0", cf.get("A", "floatObject"));
+		assertEquals("1.0", cf.get("A/floatObject"));
 		assertType(Float.class, proxy.getFloatObject());
 	}
 
 	@Test
 	public void testInt3dArray() throws Exception {
 		proxy.setInt3dArray(new int[][][]{{{1,2},null},null});
-		assertEquals("[[[1,2],null],null]", cf.get("A", "int3dArray"));
+		assertEquals("[[[1,2],null],null]", cf.get("A/int3dArray"));
 		assertObjectEquals("[[[1,2],null],null]", proxy.getInt3dArray());
 		assertType(int[][][].class, proxy.getInt3dArray());
 	}
@@ -101,7 +101,7 @@ public class ConfigFileInterfaceTest {
 	public void testInteger3dArray() throws Exception {
 		proxy.setInteger3dArray(new Integer[][][]{{{1,null},null},null});
 		assertObjectEquals("[[[1,null],null],null]", proxy.getInteger3dArray());
-		assertEquals("[[[1,null],null],null]", cf.get("A", "integer3dArray"));
+		assertEquals("[[[1,null],null],null]", cf.get("A/integer3dArray"));
 		assertType(Integer.class, proxy.getInteger3dArray()[0][0][0]);
 	}
 
@@ -109,14 +109,14 @@ public class ConfigFileInterfaceTest {
 	public void testString3dArray() throws Exception {
 		proxy.setString3dArray(new String[][][]{{{"foo",null},null},null});
 		assertObjectEquals("[[['foo',null],null],null]", proxy.getString3dArray());
-		assertEquals("[[['foo',null],null],null]", cf.get("A", "string3dArray"));
+		assertEquals("[[['foo',null],null],null]", cf.get("A/string3dArray"));
 	}
 
 	@Test
 	public void testIntegerList() throws Exception {
 		proxy.setIntegerList(new AList<Integer>().append(1).append(null));
 		assertObjectEquals("[1,null]", proxy.getIntegerList());
-		assertEquals("[1,null]", cf.get("A", "integerList"));
+		assertEquals("[1,null]", cf.get("A/integerList"));
 		assertType(Integer.class, proxy.getIntegerList().get(0));
 	}
 
@@ -132,7 +132,7 @@ public class ConfigFileInterfaceTest {
 			.append(null)
 		);
 		assertObjectEquals("[[[1,null],null],null]", proxy.getInteger3dList());
-		assertEquals("[[[1,null],null],null]", cf.get("A", "integer3dList"));
+		assertEquals("[[[1,null],null],null]", cf.get("A/integer3dList"));
 		assertType(Integer.class, proxy.getInteger3dList().get(0).get(0).get(0));
 	}
 
@@ -140,7 +140,7 @@ public class ConfigFileInterfaceTest {
 	public void testInteger1d3dList() throws Exception {
 		proxy.setInteger1d3dList(new AList<Integer[][][]>().append(new Integer[][][]{{{1,null},null},null}).append(null));
 		assertObjectEquals("[[[[1,null],null],null],null]", proxy.getInteger1d3dList());
-		assertEquals("[[[[1,null],null],null],null]", cf.get("A", "integer1d3dList"));
+		assertEquals("[[[[1,null],null],null],null]", cf.get("A/integer1d3dList"));
 		assertType(Integer.class, proxy.getInteger1d3dList().get(0)[0][0][0]);
 	}
 
@@ -148,7 +148,7 @@ public class ConfigFileInterfaceTest {
 	public void testInt1d3dList() throws Exception {
 		proxy.setInt1d3dList(new AList<int[][][]>().append(new int[][][]{{{1,2},null},null}).append(null));
 		assertObjectEquals("[[[[1,2],null],null],null]", proxy.getInt1d3dList());
-		assertEquals("[[[[1,2],null],null],null]", cf.get("A", "int1d3dList"));
+		assertEquals("[[[[1,2],null],null],null]", cf.get("A/int1d3dList"));
 		assertType(int[][][].class, proxy.getInt1d3dList().get(0));
 	}
 
@@ -156,7 +156,7 @@ public class ConfigFileInterfaceTest {
 	public void testStringList() throws Exception {
 		proxy.setStringList(Arrays.asList("foo","bar",null));
 		assertObjectEquals("['foo','bar',null]", proxy.getStringList());
-		assertEquals("['foo','bar',null]", cf.get("A", "stringList"));
+		assertEquals("['foo','bar',null]", cf.get("A/stringList"));
 	}
 
 	// Beans
@@ -165,7 +165,7 @@ public class ConfigFileInterfaceTest {
 	public void testBean() throws Exception {
 		proxy.setBean(new ABean().init());
 		assertObjectEquals("{a:1,b:'foo'}", proxy.getBean());
-		assertEquals("{a:1,b:'foo'}", cf.get("A", "bean"));
+		assertEquals("{a:1,b:'foo'}", cf.get("A/bean"));
 		assertType(ABean.class, proxy.getBean());
 	}
 
@@ -173,7 +173,7 @@ public class ConfigFileInterfaceTest {
 	public void testBean3dArray() throws Exception {
 		proxy.setBean3dArray(new ABean[][][]{{{new ABean().init(),null},null},null});
 		assertObjectEquals("[[[{a:1,b:'foo'},null],null],null]", proxy.getBean3dArray());
-		assertEquals("[[[{a:1,b:'foo'},null],null],null]", cf.get("A", "bean3dArray"));
+		assertEquals("[[[{a:1,b:'foo'},null],null],null]", cf.get("A/bean3dArray"));
 		assertType(ABean.class, proxy.getBean3dArray()[0][0][0]);
 	}
 
@@ -181,7 +181,7 @@ public class ConfigFileInterfaceTest {
 	public void testBeanList() throws Exception {
 		proxy.setBeanList(Arrays.asList(new ABean().init()));
 		assertObjectEquals("[{a:1,b:'foo'}]", proxy.getBeanList());
-		assertEquals("[{a:1,b:'foo'}]", cf.get("A", "beanList"));
+		assertEquals("[{a:1,b:'foo'}]", cf.get("A/beanList"));
 		assertType(ABean.class, proxy.getBeanList().get(0));
 	}
 
@@ -189,7 +189,7 @@ public class ConfigFileInterfaceTest {
 	public void testBean1d3dList() throws Exception {
 		proxy.setBean1d3dList(new AList<ABean[][][]>().append(new ABean[][][]{{{new ABean().init(),null},null},null}).append(null));
 		assertObjectEquals("[[[[{a:1,b:'foo'},null],null],null],null]", proxy.getBean1d3dList());
-		assertEquals("[[[[{a:1,b:'foo'},null],null],null],null]", cf.get("A", "bean1d3dList"));
+		assertEquals("[[[[{a:1,b:'foo'},null],null],null],null]", cf.get("A/bean1d3dList"));
 		assertType(ABean.class, proxy.getBean1d3dList().get(0)[0][0][0]);
 	}
 
@@ -197,7 +197,7 @@ public class ConfigFileInterfaceTest {
 	public void testBeanMap() throws Exception {
 		proxy.setBeanMap(new AMap<String,ABean>().append("foo",new ABean().init()));
 		assertObjectEquals("{foo:{a:1,b:'foo'}}", proxy.getBeanMap());
-		assertEquals("{foo:{a:1,b:'foo'}}", cf.get("A", "beanMap"));
+		assertEquals("{foo:{a:1,b:'foo'}}", cf.get("A/beanMap"));
 		assertType(ABean.class, proxy.getBeanMap().get("foo"));
 	}
 
@@ -205,7 +205,7 @@ public class ConfigFileInterfaceTest {
 	public void testBeanListMap() throws Exception {
 		proxy.setBeanListMap(new AMap<String,List<ABean>>().append("foo",Arrays.asList(new ABean().init())));
 		assertObjectEquals("{foo:[{a:1,b:'foo'}]}", proxy.getBeanListMap());
-		assertEquals("{foo:[{a:1,b:'foo'}]}", cf.get("A", "beanListMap"));
+		assertEquals("{foo:[{a:1,b:'foo'}]}", cf.get("A/beanListMap"));
 		assertType(ABean.class, proxy.getBeanListMap().get("foo").get(0));
 	}
 
@@ -213,7 +213,7 @@ public class ConfigFileInterfaceTest {
 	public void testBean1d3dListMap() throws Exception {
 		proxy.setBean1d3dListMap(new AMap<String,List<ABean[][][]>>().append("foo",new AList<ABean[][][]>().append(new ABean[][][]{{{new ABean().init(),null},null},null}).append(null)));
 		assertObjectEquals("{foo:[[[[{a:1,b:'foo'},null],null],null],null]}", proxy.getBean1d3dListMap());
-		assertEquals("{foo:[[[[{a:1,b:'foo'},null],null],null],null]}", cf.get("A", "bean1d3dListMap"));
+		assertEquals("{foo:[[[[{a:1,b:'foo'},null],null],null],null]}", cf.get("A/bean1d3dListMap"));
 		assertType(ABean.class, proxy.getBean1d3dListMap().get("foo").get(0)[0][0][0]);
 	}
 
@@ -221,7 +221,7 @@ public class ConfigFileInterfaceTest {
 	public void testBeanListMapIntegerKeys() throws Exception {
 		proxy.setBeanListMapIntegerKeys(new AMap<Integer,List<ABean>>().append(1,Arrays.asList(new ABean().init())));
 		assertObjectEquals("{'1':[{a:1,b:'foo'}]}", proxy.getBeanListMapIntegerKeys());
-		assertEquals("{'1':[{a:1,b:'foo'}]}", cf.get("A", "beanListMapIntegerKeys"));
+		assertEquals("{'1':[{a:1,b:'foo'}]}", cf.get("A/beanListMapIntegerKeys"));
 		assertType(ABean.class, proxy.getBeanListMapIntegerKeys().get(1).get(0));
 	}
 
@@ -231,7 +231,7 @@ public class ConfigFileInterfaceTest {
 	public void testTypedBean() throws Exception {
 		proxy.setTypedBean(new TypedBeanImpl().init());
 		assertObjectEquals("{_type:'TypedBeanImpl',a:1,b:'foo'}", proxy.getTypedBean());
-		assertEquals("{_type:'TypedBeanImpl',a:1,b:'foo'}", cf.get("A", "typedBean"));
+		assertEquals("{_type:'TypedBeanImpl',a:1,b:'foo'}", cf.get("A/typedBean"));
 		assertType(TypedBeanImpl.class, proxy.getTypedBean());
 	}
 
@@ -239,7 +239,7 @@ public class ConfigFileInterfaceTest {
 	public void testTypedBean3dArray() throws Exception {
 		proxy.setTypedBean3dArray(new TypedBean[][][]{{{new TypedBeanImpl().init(),null},null},null});
 		assertObjectEquals("[[[{_type:'TypedBeanImpl',a:1,b:'foo'},null],null],null]", proxy.getTypedBean3dArray());
-		assertEquals("[[[{_type:'TypedBeanImpl',a:1,b:'foo'},null],null],null]", cf.get("A", "typedBean3dArray"));
+		assertEquals("[[[{_type:'TypedBeanImpl',a:1,b:'foo'},null],null],null]", cf.get("A/typedBean3dArray"));
 		assertType(TypedBeanImpl.class, proxy.getTypedBean3dArray()[0][0][0]);
 	}
 
@@ -247,7 +247,7 @@ public class ConfigFileInterfaceTest {
 	public void testTypedBeanList() throws Exception {
 		proxy.setTypedBeanList(Arrays.asList((TypedBean)new TypedBeanImpl().init()));
 		assertObjectEquals("[{_type:'TypedBeanImpl',a:1,b:'foo'}]", proxy.getTypedBeanList());
-		assertEquals("[{_type:'TypedBeanImpl',a:1,b:'foo'}]", cf.get("A", "typedBeanList"));
+		assertEquals("[{_type:'TypedBeanImpl',a:1,b:'foo'}]", cf.get("A/typedBeanList"));
 		assertType(TypedBeanImpl.class, proxy.getTypedBeanList().get(0));
 	}
 
@@ -255,7 +255,7 @@ public class ConfigFileInterfaceTest {
 	public void testTypedBean1d3dList() throws Exception {
 		proxy.setTypedBean1d3dList(new AList<TypedBean[][][]>().append(new TypedBean[][][]{{{new TypedBeanImpl().init(),null},null},null}).append(null));
 		assertObjectEquals("[[[[{_type:'TypedBeanImpl',a:1,b:'foo'},null],null],null],null]", proxy.getTypedBean1d3dList());
-		assertEquals("[[[[{_type:'TypedBeanImpl',a:1,b:'foo'},null],null],null],null]", cf.get("A", "typedBean1d3dList"));
+		assertEquals("[[[[{_type:'TypedBeanImpl',a:1,b:'foo'},null],null],null],null]", cf.get("A/typedBean1d3dList"));
 		assertType(TypedBeanImpl.class, proxy.getTypedBean1d3dList().get(0)[0][0][0]);
 	}
 
@@ -263,7 +263,7 @@ public class ConfigFileInterfaceTest {
 	public void testTypedBeanMap() throws Exception {
 		proxy.setTypedBeanMap(new AMap<String,TypedBean>().append("foo",new TypedBeanImpl().init()));
 		assertObjectEquals("{foo:{_type:'TypedBeanImpl',a:1,b:'foo'}}", proxy.getTypedBeanMap());
-		assertEquals("{foo:{_type:'TypedBeanImpl',a:1,b:'foo'}}", cf.get("A", "typedBeanMap"));
+		assertEquals("{foo:{_type:'TypedBeanImpl',a:1,b:'foo'}}", cf.get("A/typedBeanMap"));
 		assertType(TypedBeanImpl.class, proxy.getTypedBeanMap().get("foo"));
 	}
 
@@ -271,7 +271,7 @@ public class ConfigFileInterfaceTest {
 	public void testTypedBeanListMap() throws Exception {
 		proxy.setTypedBeanListMap(new AMap<String,List<TypedBean>>().append("foo",Arrays.asList((TypedBean)new TypedBeanImpl().init())));
 		assertObjectEquals("{foo:[{_type:'TypedBeanImpl',a:1,b:'foo'}]}", proxy.getTypedBeanListMap());
-		assertEquals("{foo:[{_type:'TypedBeanImpl',a:1,b:'foo'}]}", cf.get("A", "typedBeanListMap"));
+		assertEquals("{foo:[{_type:'TypedBeanImpl',a:1,b:'foo'}]}", cf.get("A/typedBeanListMap"));
 		assertType(TypedBeanImpl.class, proxy.getTypedBeanListMap().get("foo").get(0));
 	}
 
@@ -279,7 +279,7 @@ public class ConfigFileInterfaceTest {
 	public void testTypedBean1d3dListMap() throws Exception {
 		proxy.setTypedBean1d3dListMap(new AMap<String,List<TypedBean[][][]>>().append("foo",new AList<TypedBean[][][]>().append(new TypedBean[][][]{{{new TypedBeanImpl().init(),null},null},null}).append(null)));
 		assertObjectEquals("{foo:[[[[{_type:'TypedBeanImpl',a:1,b:'foo'},null],null],null],null]}", proxy.getTypedBean1d3dListMap());
-		assertEquals("{foo:[[[[{_type:'TypedBeanImpl',a:1,b:'foo'},null],null],null],null]}", cf.get("A", "typedBean1d3dListMap"));
+		assertEquals("{foo:[[[[{_type:'TypedBeanImpl',a:1,b:'foo'},null],null],null],null]}", cf.get("A/typedBean1d3dListMap"));
 		assertType(TypedBeanImpl.class, proxy.getTypedBean1d3dListMap().get("foo").get(0)[0][0][0]);
 	}
 
@@ -287,7 +287,7 @@ public class ConfigFileInterfaceTest {
 	public void testTypedBeanListMapIntegerKeys() throws Exception {
 		proxy.setTypedBeanListMapIntegerKeys(new AMap<Integer,List<TypedBean>>().append(1,Arrays.asList((TypedBean)new TypedBeanImpl().init())));
 		assertObjectEquals("{'1':[{_type:'TypedBeanImpl',a:1,b:'foo'}]}", proxy.getTypedBeanListMapIntegerKeys());
-		assertEquals("{'1':[{_type:'TypedBeanImpl',a:1,b:'foo'}]}", cf.get("A", "typedBeanListMapIntegerKeys"));
+		assertEquals("{'1':[{_type:'TypedBeanImpl',a:1,b:'foo'}]}", cf.get("A/typedBeanListMapIntegerKeys"));
 		assertType(TypedBeanImpl.class, proxy.getTypedBeanListMapIntegerKeys().get(1).get(0));
 	}
 
@@ -297,7 +297,7 @@ public class ConfigFileInterfaceTest {
 	public void testSwappedPojo() throws Exception {
 		proxy.setSwappedPojo(new SwappedPojo());
 		assertObjectEquals("'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/'", proxy.getSwappedPojo());
-		assertEquals("swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/", cf.get("A", "swappedPojo"));
+		assertEquals("swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/", cf.get("A/swappedPojo"));
 		assertType(SwappedPojo.class, proxy.getSwappedPojo());
 	}
 
@@ -305,7 +305,7 @@ public class ConfigFileInterfaceTest {
 	public void testSwappedPojo3dArray() throws Exception {
 		proxy.setSwappedPojo3dArray(new SwappedPojo[][][]{{{new SwappedPojo(),null},null},null});
 		assertObjectEquals("[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]", proxy.getSwappedPojo3dArray());
-		assertEquals("[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]", cf.get("A", "swappedPojo3dArray"));
+		assertEquals("[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]", cf.get("A/swappedPojo3dArray"));
 		assertType(SwappedPojo.class, proxy.getSwappedPojo3dArray()[0][0][0]);
 	}
 
@@ -313,7 +313,7 @@ public class ConfigFileInterfaceTest {
 	public void testSwappedPojoMap() throws Exception {
 		proxy.setSwappedPojoMap(new AMap<SwappedPojo,SwappedPojo>().append(new SwappedPojo(), new SwappedPojo()));
 		assertObjectEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/'}", proxy.getSwappedPojoMap());
-		assertEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/'}", cf.get("A", "swappedPojoMap"));
+		assertEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/'}", cf.get("A/swappedPojoMap"));
 		assertType(SwappedPojo.class, proxy.getSwappedPojoMap().keySet().iterator().next());
 		assertType(SwappedPojo.class, proxy.getSwappedPojoMap().values().iterator().next());
 	}
@@ -322,7 +322,7 @@ public class ConfigFileInterfaceTest {
 	public void testSwappedPojo3dMap() throws Exception {
 		proxy.setSwappedPojo3dMap(new AMap<SwappedPojo,SwappedPojo[][][]>().append(new SwappedPojo(), new SwappedPojo[][][]{{{new SwappedPojo(),null},null},null}));
 		assertObjectEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]}", proxy.getSwappedPojo3dMap());
-		assertEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]}", cf.get("A", "swappedPojo3dMap"));
+		assertEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]}", cf.get("A/swappedPojo3dMap"));
 		assertType(SwappedPojo.class, proxy.getSwappedPojo3dMap().keySet().iterator().next());
 		assertType(SwappedPojo.class, proxy.getSwappedPojo3dMap().values().iterator().next()[0][0][0]);
 	}
@@ -333,7 +333,7 @@ public class ConfigFileInterfaceTest {
 	public void testImplicitSwappedPojo() throws Exception {
 		proxy.setImplicitSwappedPojo(new ImplicitSwappedPojo());
 		assertObjectEquals("'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/'", proxy.getImplicitSwappedPojo());
-		assertEquals("swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/", cf.get("A", "implicitSwappedPojo"));
+		assertEquals("swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/", cf.get("A/implicitSwappedPojo"));
 		assertType(ImplicitSwappedPojo.class, proxy.getImplicitSwappedPojo());
 	}
 
@@ -341,7 +341,7 @@ public class ConfigFileInterfaceTest {
 	public void testImplicitSwappedPojo3dArray() throws Exception {
 		proxy.setImplicitSwappedPojo3dArray(new ImplicitSwappedPojo[][][]{{{new ImplicitSwappedPojo(),null},null},null});
 		assertObjectEquals("[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]", proxy.getImplicitSwappedPojo3dArray());
-		assertEquals("[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]", cf.get("A", "implicitSwappedPojo3dArray"));
+		assertEquals("[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]", cf.get("A/implicitSwappedPojo3dArray"));
 		assertType(ImplicitSwappedPojo.class, proxy.getImplicitSwappedPojo3dArray()[0][0][0]);
 	}
 
@@ -349,7 +349,7 @@ public class ConfigFileInterfaceTest {
 	public void testImplicitSwappedPojoMap() throws Exception {
 		proxy.setImplicitSwappedPojoMap(new AMap<ImplicitSwappedPojo,ImplicitSwappedPojo>().append(new ImplicitSwappedPojo(), new ImplicitSwappedPojo()));
 		assertObjectEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/'}", proxy.getImplicitSwappedPojoMap());
-		assertEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/'}", cf.get("A", "implicitSwappedPojoMap"));
+		assertEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/'}", cf.get("A/implicitSwappedPojoMap"));
 		assertType(ImplicitSwappedPojo.class, proxy.getImplicitSwappedPojoMap().keySet().iterator().next());
 		assertType(ImplicitSwappedPojo.class, proxy.getImplicitSwappedPojoMap().values().iterator().next());
 	}
@@ -358,7 +358,7 @@ public class ConfigFileInterfaceTest {
 	public void testImplicitSwappedPojo3dMap() throws Exception {
 		proxy.setImplicitSwappedPojo3dMap(new AMap<ImplicitSwappedPojo,ImplicitSwappedPojo[][][]>().append(new ImplicitSwappedPojo(), new ImplicitSwappedPojo[][][]{{{new ImplicitSwappedPojo(),null},null},null}));
 		assertObjectEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]}", proxy.getImplicitSwappedPojo3dMap());
-		assertEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]}", cf.get("A", "implicitSwappedPojo3dMap"));
+		assertEquals("{'swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/':[[['swap-~!@#$%^&*()_+`-={}[]|:;\"<,>.?/',null],null],null]}", cf.get("A/implicitSwappedPojo3dMap"));
 		assertType(ImplicitSwappedPojo.class, proxy.getImplicitSwappedPojo3dMap().keySet().iterator().next());
 		assertType(ImplicitSwappedPojo.class, proxy.getImplicitSwappedPojo3dMap().values().iterator().next()[0][0][0]);
 	}
@@ -369,7 +369,7 @@ public class ConfigFileInterfaceTest {
 	public void testEnum() throws Exception {
 		proxy.setEnum(TestEnum.TWO);
 		assertObjectEquals("'TWO'", proxy.getEnum());
-		assertEquals("TWO", cf.get("A", "enum"));
+		assertEquals("TWO", cf.get("A/enum"));
 		assertType(TestEnum.class, proxy.getEnum());
 	}
 
@@ -377,7 +377,7 @@ public class ConfigFileInterfaceTest {
 	public void testEnum3d() throws Exception {
 		proxy.setEnum3d(new TestEnum[][][]{{{TestEnum.TWO,null},null},null});
 		assertObjectEquals("[[['TWO',null],null],null]", proxy.getEnum3d());
-		assertEquals("[[['TWO',null],null],null]", cf.get("A", "enum3d"));
+		assertEquals("[[['TWO',null],null],null]", cf.get("A/enum3d"));
 		assertType(TestEnum.class, proxy.getEnum3d()[0][0][0]);
 	}
 
@@ -385,7 +385,7 @@ public class ConfigFileInterfaceTest {
 	public void testEnumList() throws Exception {
 		proxy.setEnumList(new AList<TestEnum>().append(TestEnum.TWO).append(null));
 		assertObjectEquals("['TWO',null]", proxy.getEnumList());
-		assertEquals("['TWO',null]", cf.get("A", "enumList"));
+		assertEquals("['TWO',null]", cf.get("A/enumList"));
 		assertType(TestEnum.class, proxy.getEnumList().get(0));
 	}
 
@@ -403,7 +403,7 @@ public class ConfigFileInterfaceTest {
 			)
 		);
 		assertObjectEquals("[[['TWO',null],null,null]]", proxy.getEnum3dList());
-		assertEquals("[[['TWO',null],null,null]]", cf.get("A", "enum3dList"));
+		assertEquals("[[['TWO',null],null,null]]", cf.get("A/enum3dList"));
 		assertType(TestEnum.class, proxy.getEnum3dList().get(0).get(0).get(0));
 	}
 
@@ -411,7 +411,7 @@ public class ConfigFileInterfaceTest {
 	public void testEnum1d3dList() throws Exception {
 		proxy.setEnum1d3dList(new AList<TestEnum[][][]>().append(new TestEnum[][][]{{{TestEnum.TWO,null},null},null}).append(null));
 		assertObjectEquals("[[[['TWO',null],null],null],null]", proxy.getEnum1d3dList());
-		assertEquals("[[[['TWO',null],null],null],null]", cf.get("A", "enum1d3dList"));
+		assertEquals("[[[['TWO',null],null],null],null]", cf.get("A/enum1d3dList"));
 		assertType(TestEnum.class, proxy.getEnum1d3dList().get(0)[0][0][0]);
 	}
 
@@ -419,7 +419,7 @@ public class ConfigFileInterfaceTest {
 	public void testEnumMap() throws Exception {
 		proxy.setEnumMap(new AMap<TestEnum,TestEnum>().append(TestEnum.ONE,TestEnum.TWO));
 		assertObjectEquals("{ONE:'TWO'}", proxy.getEnumMap());
-		assertEquals("{ONE:'TWO'}", cf.get("A", "enumMap"));
+		assertEquals("{ONE:'TWO'}", cf.get("A/enumMap"));
 		assertType(TestEnum.class, proxy.getEnumMap().keySet().iterator().next());
 		assertType(TestEnum.class, proxy.getEnumMap().values().iterator().next());
 	}
@@ -428,7 +428,7 @@ public class ConfigFileInterfaceTest {
 	public void testEnum3dArrayMap() throws Exception {
 		proxy.setEnum3dArrayMap(new AMap<TestEnum,TestEnum[][][]>().append(TestEnum.ONE, new TestEnum[][][]{{{TestEnum.TWO,null},null},null}));
 		assertObjectEquals("{ONE:[[['TWO',null],null],null]}", proxy.getEnum3dArrayMap());
-		assertEquals("{ONE:[[['TWO',null],null],null]}", cf.get("A", "enum3dArrayMap"));
+		assertEquals("{ONE:[[['TWO',null],null],null]}", cf.get("A/enum3dArrayMap"));
 		assertType(TestEnum.class, proxy.getEnum3dArrayMap().keySet().iterator().next());
 		assertType(TestEnum.class, proxy.getEnum3dArrayMap().values().iterator().next()[0][0][0]);
 	}
@@ -437,7 +437,7 @@ public class ConfigFileInterfaceTest {
 	public void testEnum1d3dListMap() throws Exception {
 		proxy.setEnum1d3dListMap(new AMap<TestEnum,List<TestEnum[][][]>>().append(TestEnum.ONE, new AList<TestEnum[][][]>().append(new TestEnum[][][]{{{TestEnum.TWO,null},null},null}).append(null)));
 		assertObjectEquals("{ONE:[[[['TWO',null],null],null],null]}", proxy.getEnum1d3dListMap());
-		assertEquals("{ONE:[[[['TWO',null],null],null],null]}", cf.get("A", "enum1d3dListMap"));
+		assertEquals("{ONE:[[[['TWO',null],null],null],null]}", cf.get("A/enum1d3dListMap"));
 		assertType(TestEnum.class, proxy.getEnum1d3dListMap().keySet().iterator().next());
 		assertType(TestEnum.class, proxy.getEnum1d3dListMap().values().iterator().next().get(0)[0][0][0]);
 	}
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/ConfigMapListenerTest.java
similarity index 80%
rename from juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapListenerTest.java
rename to juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/ConfigMapListenerTest.java
index 5e575d3..ee2fbca 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/ConfigMapListenerTest.java
@@ -10,7 +10,7 @@
 // * "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;
+package org.apache.juneau.config;
 
 import static org.junit.Assert.*;
 import static org.apache.juneau.TestUtils.*;
@@ -31,7 +31,7 @@ public class ConfigMapListenerTest {
 	
 	@Test
 	public void testBasicDefaultSection() throws Exception {
-		Store s = initStore("Foo", 
+		ConfigStore s = initStore("Foo.cfg", 
 			"foo=bar"
 		);		
 		
@@ -39,14 +39,14 @@ public class ConfigMapListenerTest {
 		
 		LatchedListener l = new LatchedListener(latch) {
 			@Override
-			public void check(List<ChangeEvent> events) throws Exception {
+			public void check(List<ConfigEvent> events) throws Exception {
 				assertObjectEquals("['SET(foo = baz)']", events);
 			}
 		};
 		
-		ConfigMap cm = s.getMap("Foo");
+		ConfigMap cm = s.getMap("Foo.cfg");
 		cm.register(l);
-		cm.setValue("default", "foo", "baz");
+		cm.setEntry("default", "foo", "baz", null, null, null);
 		cm.save();
 		wait(latch);
 		assertNull(l.error);
@@ -57,7 +57,7 @@ public class ConfigMapListenerTest {
 	
 	@Test
 	public void testBasicNormalSection() throws Exception {
-		Store s = initStore("Foo", 
+		ConfigStore s = initStore("Foo.cfg", 
 			"[S1]",
 			"foo=bar"
 		);		
@@ -66,14 +66,14 @@ public class ConfigMapListenerTest {
 		
 		LatchedListener l = new LatchedListener(latch) {
 			@Override
-			public void check(List<ChangeEvent> events) throws Exception {
+			public void check(List<ConfigEvent> events) throws Exception {
 				assertObjectEquals("['SET(foo = baz)']", events);
 			}
 		};
 		
-		ConfigMap cm = s.getMap("Foo");
+		ConfigMap cm = s.getMap("Foo.cfg");
 		cm.register(l);
-		cm.setValue("S1", "foo", "baz");
+		cm.setEntry("S1", "foo", "baz", null, null, null);
 		cm.save();
 		wait(latch);
 		assertNull(l.error);
@@ -88,22 +88,22 @@ public class ConfigMapListenerTest {
 	
 	@Test
 	public void testAddNewEntries() throws Exception {
-		Store s = initStore("Foo"
+		ConfigStore s = initStore("Foo.cfg"
 		);		
 		
 		final CountDownLatch latch = new CountDownLatch(2);
 
 		LatchedListener l = new LatchedListener(latch) {
 			@Override
-			public void check(List<ChangeEvent> events) throws Exception {
+			public void check(List<ConfigEvent> events) throws Exception {
 				assertObjectEquals("['SET(k = vb)','SET(k1 = v1b)']", events);
 			}
 		};
 
-		ConfigMap cm = s.getMap("Foo");
+		ConfigMap cm = s.getMap("Foo.cfg");
 		cm.register(l);
-		cm.setValue("default", "k", "vb");
-		cm.setValue("S1", "k1", "v1b");
+		cm.setEntry("default", "k", "vb", null, null, null);
+		cm.setEntry("S1", "k1", "v1b", null, null, null);
 		cm.save();
 		wait(latch);
 		assertNull(l.error);
@@ -114,19 +114,19 @@ public class ConfigMapListenerTest {
 	
 	@Test
 	public void testAddNewEntriesWithAttributes() throws Exception {
-		Store s = initStore("Foo"
+		ConfigStore s = initStore("Foo.cfg"
 		);		
 		
 		final CountDownLatch latch = new CountDownLatch(2);
 
 		LatchedListener l = new LatchedListener(latch) {
 			@Override
-			public void check(List<ChangeEvent> events) throws Exception {
+			public void check(List<ConfigEvent> events) throws Exception {
 				assertObjectEquals("['SET(k^* = kb # C)','SET(k1^* = k1b # C1)']", events);
 			}
 		};
 
-		ConfigMap cm = s.getMap("Foo");
+		ConfigMap cm = s.getMap("Foo.cfg");
 		cm.register(l);
 		cm.setEntry("default", "k", "kb", "^*", "C", Arrays.asList("#k"));
 		cm.setEntry("S1", "k1", "k1b", "^*", "C1", Arrays.asList("#k1"));
@@ -140,7 +140,7 @@ public class ConfigMapListenerTest {
 
 	@Test
 	public void testAddExistingEntriesWithAttributes() throws Exception {
-		Store s = initStore("Foo",
+		ConfigStore s = initStore("Foo.cfg",
 			"#ka",
 			"k=va # Ca",
 			"#S1",
@@ -153,12 +153,12 @@ public class ConfigMapListenerTest {
 
 		LatchedListener l = new LatchedListener(latch) {
 			@Override
-			public void check(List<ChangeEvent> events) throws Exception {
+			public void check(List<ConfigEvent> events) throws Exception {
 				assertObjectEquals("['SET(k^* = kb # Cb)','SET(k1^* = k1b # Cb1)']", events);
 			}
 		};
 
-		ConfigMap cm = s.getMap("Foo");
+		ConfigMap cm = s.getMap("Foo.cfg");
 		cm.register(l);
 		cm.setEntry("default", "k", "kb", "^*", "Cb", Arrays.asList("#kb"));
 		cm.setEntry("S1", "k1", "k1b", "^*", "Cb1", Arrays.asList("#k1b"));
@@ -176,7 +176,7 @@ public class ConfigMapListenerTest {
 
 	@Test
 	public void testRemoveExistingEntries() throws Exception {
-		Store s = initStore("Foo",
+		ConfigStore s = initStore("Foo.cfg",
 			"k=v",
 			"[S1]",
 			"k1=v1"
@@ -186,15 +186,15 @@ public class ConfigMapListenerTest {
 
 		LatchedListener l = new LatchedListener(latch) {
 			@Override
-			public void check(List<ChangeEvent> events) throws Exception {
-				assertObjectEquals("['SET(k = null)','SET(k1 = null)']", events);
+			public void check(List<ConfigEvent> events) throws Exception {
+				assertObjectEquals("['REMOVE_ENTRY(default/k)','REMOVE_ENTRY(S1/k1)']", events);
 			}
 		};
 
-		ConfigMap cm = s.getMap("Foo");
+		ConfigMap cm = s.getMap("Foo.cfg");
 		cm.register(l);
-		cm.setValue("default", "k", null);
-		cm.setValue("S1", "k1", null);
+		cm.removeEntry("default", "k");
+		cm.removeEntry("S1", "k1");
 		cm.save();
 		wait(latch);
 		assertNull(l.error);
@@ -205,7 +205,7 @@ public class ConfigMapListenerTest {
 	
 	@Test
 	public void testRemoveExistingEntriesWithAttributes() throws Exception {
-		Store s = initStore("Foo",
+		ConfigStore s = initStore("Foo.cfg",
 			"#ka",
 			"k=va # Ca",
 			"#S1",
@@ -218,15 +218,15 @@ public class ConfigMapListenerTest {
 
 		LatchedListener l = new LatchedListener(latch) {
 			@Override
-			public void check(List<ChangeEvent> events) throws Exception {
-				assertObjectEquals("['SET(k = null)','SET(k1 = null)']", events);
+			public void check(List<ConfigEvent> events) throws Exception {
+				assertObjectEquals("['REMOVE_ENTRY(default/k)','REMOVE_ENTRY(S1/k1)']", events);
 			}
 		};
 
-		ConfigMap cm = s.getMap("Foo");
+		ConfigMap cm = s.getMap("Foo.cfg");
 		cm.register(l);
-		cm.setValue("default", "k", null);
-		cm.setValue("S1", "k1", null);
+		cm.removeEntry("default", "k");
+		cm.removeEntry("S1", "k1");
 		cm.save();
 		wait(latch);
 		assertNull(l.error);
@@ -241,25 +241,25 @@ public class ConfigMapListenerTest {
 	
 	@Test
 	public void testAddNewSections() throws Exception {
-		Store s = initStore("Foo"
+		ConfigStore s = initStore("Foo.cfg"
 		);		
 		
 		final CountDownLatch latch = new CountDownLatch(1);
 
 		LatchedListener l = new LatchedListener(latch) {
 			@Override
-			public void check(List<ChangeEvent> events) throws Exception {
+			public void check(List<ConfigEvent> events) throws Exception {
 				assertObjectEquals("['SET(k3 = v3)']", events);
 			}
 		};
 
-		ConfigMap cm = s.getMap("Foo");
+		ConfigMap cm = s.getMap("Foo.cfg");
 		cm.register(l);
 		cm.setSection("default", Arrays.asList("#D1"));
 		cm.setSection("S1", Arrays.asList("#S1"));
 		cm.setSection("S2", null);
 		cm.setSection("S3", Collections.<String>emptyList());
-		cm.setValue("S3", "k3", "v3");
+		cm.setEntry("S3", "k3", "v3", null, null, null);
 		cm.save();
 		wait(latch);
 		assertNull(l.error);
@@ -270,7 +270,7 @@ public class ConfigMapListenerTest {
 
 	@Test
 	public void testModifyExistingSections() throws Exception {
-		Store s = initStore("Foo",
+		ConfigStore s = initStore("Foo.cfg",
 			"#Da",
 			"",
 			"#S1a",
@@ -283,18 +283,18 @@ public class ConfigMapListenerTest {
 
 		LatchedListener l = new LatchedListener(latch) {
 			@Override
-			public void check(List<ChangeEvent> events) throws Exception {
+			public void check(List<ConfigEvent> events) throws Exception {
 				assertObjectEquals("['SET(k3 = v3)']", events);
 			}
 		};
 
-		ConfigMap cm = s.getMap("Foo");
+		ConfigMap cm = s.getMap("Foo.cfg");
 		cm.register(l);
 		cm.setSection("default", Arrays.asList("#Db"));
 		cm.setSection("S1", Arrays.asList("#S1b"));
 		cm.setSection("S2", null);
 		cm.setSection("S3", Collections.<String>emptyList());
-		cm.setValue("S3", "k3", "v3");
+		cm.setEntry("S3", "k3", "v3", null, null, null);
 		cm.save();
 		wait(latch);
 		assertNull(l.error);
@@ -309,7 +309,7 @@ public class ConfigMapListenerTest {
 	
 	@Test
 	public void testRemoveSections() throws Exception {
-		Store s = initStore("Foo",
+		ConfigStore s = initStore("Foo.cfg",
 			"#Da",
 			"",
 			"k = v",
@@ -328,12 +328,12 @@ public class ConfigMapListenerTest {
 
 		LatchedListener l = new LatchedListener(latch) {
 			@Override
... 5176 lines suppressed ...

-- 
To stop receiving notification emails like this one, please contact
jamesbognar@apache.org.

Mime
View raw message