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 refactoring.
Date Wed, 21 Feb 2018 02:38:09 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 8611b24  Config API refactoring.
8611b24 is described below

commit 8611b246f9a0f5e11f6060197260f0107eef85dd
Author: JamesBognar <jamesbognar@apache.org>
AuthorDate: Tue Feb 20 21:38:07 2018 -0500

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

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

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

Mime
View raw message