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 Mon, 19 Feb 2018 19:06:49 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 39d0e81  Config API refactoring.
39d0e81 is described below

commit 39d0e818e5db497ff7dfa3a27e0dc0c90c7b02aa
Author: JamesBognar <jamesbognar@apache.org>
AuthorDate: Mon Feb 19 14:06:27 2018 -0500

    Config API refactoring.
---
 .../org/apache/juneau/config/ConfigFileImpl.java   |    3 +-
 .../java/org/apache/juneau/config/ConfigUtils.java |   16 +-
 .../java/org/apache/juneau/config/Section.java     |    3 +-
 .../apache/juneau/config/event/ChangeEvent.java    |  225 ++++
 .../ChangeEventListener.java}                      |  103 +-
 .../ChangeEventType.java}                          |   52 +-
 .../juneau/config/listener/package-info.java       |    2 +-
 .../apache/juneau/config/proto/ConfigEntry.java    |  142 +++
 .../org/apache/juneau/config/proto/ConfigMap.java  |  683 +++++++++++
 .../org/apache/juneau/config/store/FileStore.java  |   14 +-
 .../apache/juneau/config/store/MemoryStore.java    |   15 +-
 .../java/org/apache/juneau/config/store/Store.java |   54 +-
 .../juneau/config/proto/ConfigMapListenerTest.java |  596 ++++++++++
 .../apache/juneau/config/proto/ConfigMapTest.java  | 1217 ++++++++++++++++++++
 .../apache/juneau/config/store/FileStoreTest.java  |   12 +-
 .../juneau/config/store/MemoryStoreTest.java       |   10 +-
 .../java/org/apache/juneau/serializer/TestURI.java |    4 +-
 .../org/apache/juneau/utils/StringUtilsTest.java   |   41 +
 .../java/org/apache/juneau/jena/RdfSerializer.java |    3 +-
 .../main/java/org/apache/juneau/BeanContext.java   |    5 +-
 .../src/main/java/org/apache/juneau/BeanMap.java   |    6 +-
 .../src/main/java/org/apache/juneau/BeanMeta.java  |    9 +-
 .../main/java/org/apache/juneau/BeanRegistry.java  |    4 +-
 .../src/main/java/org/apache/juneau/ObjectMap.java |    2 +-
 .../main/java/org/apache/juneau/PropertyStore.java |    5 +-
 .../main/java/org/apache/juneau/UriResolver.java   |    6 +-
 .../org/apache/juneau/encoders/EncoderGroup.java   |    6 +-
 .../apache/juneau/html/HtmlSerializerSession.java  |    6 +-
 .../main/java/org/apache/juneau/http/Accept.java   |    3 +-
 .../org/apache/juneau/http/HeaderRangeArray.java   |    4 +-
 .../java/org/apache/juneau/http/MediaType.java     |    5 +-
 .../org/apache/juneau/http/MediaTypeRange.java     |    4 +-
 .../java/org/apache/juneau/http/StringRange.java   |    4 +-
 .../org/apache/juneau/internal/ArrayUtils.java     |    3 +-
 .../java/org/apache/juneau/internal/AsciiSet.java  |   18 +
 .../apache/juneau/internal/BeanPropertyUtils.java  |    2 +-
 .../apache/juneau/internal/CollectionUtils.java    |   65 ++
 .../org/apache/juneau/internal/StringUtils.java    |   62 +-
 .../java/org/apache/juneau/parser/ParserGroup.java |    6 +-
 .../apache/juneau/remoteable/RemoteableMeta.java   |    3 +-
 .../apache/juneau/serializer/SerializerGroup.java  |    6 +-
 .../java/org/apache/juneau/xml/XmlBeanMeta.java    |    7 +-
 .../main/java/org/apache/juneau/xml/XmlUtils.java  |   29 -
 .../main/java/org/apache/juneau/svl/MapVar.java    |    5 +-
 .../org/apache/juneau/svl/VarResolverContext.java  |    5 +-
 juneau-doc/src/main/javadoc/overview.html          |    6 +-
 .../apache/juneau/microservice/Microservice.java   |    3 +-
 .../microservice/resources/LogEntryFormatter.java  |    3 +-
 .../apache/juneau/rest/client/NameValuePairs.java  |    5 +-
 .../juneau/rest/client/RestClientBuilder.java      |    5 +-
 .../org/apache/juneau/rest/HtmlDocBuilder.java     |    8 +-
 .../org/apache/juneau/rest/ReaderResource.java     |    3 +-
 .../org/apache/juneau/rest/RequestFormData.java    |    5 +-
 .../org/apache/juneau/rest/RequestHeaders.java     |    4 +-
 .../java/org/apache/juneau/rest/RequestQuery.java  |    8 +-
 .../java/org/apache/juneau/rest/RestContext.java   |   12 +-
 .../org/apache/juneau/rest/RestContextBuilder.java |    1 -
 .../org/apache/juneau/rest/RestJavaMethod.java     |    7 +-
 .../java/org/apache/juneau/rest/RestRequest.java   |    1 -
 .../java/org/apache/juneau/rest/RestResponse.java  |    5 +-
 .../org/apache/juneau/rest/StaticFileMapping.java  |    6 +-
 .../org/apache/juneau/rest/StreamResource.java     |    3 +-
 .../apache/juneau/rest/annotation/RestMethod.java  |    2 +-
 .../juneau/rest/annotation/RestResource.java       |    2 +-
 .../juneau/rest/response/StreamableHandler.java    |    5 +-
 .../juneau/rest/response/WritableHandler.java      |    5 +-
 .../juneau/rest/vars/RequestAttributeVar.java      |    5 +-
 67 files changed, 3321 insertions(+), 258 deletions(-)

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
index 47d5c31..e54283d 100644
--- 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
@@ -15,6 +15,7 @@ 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.*;
@@ -101,7 +102,7 @@ public final class ConfigFileImpl extends ConfigFile {
 		load();
 		this.readOnly = readOnly;
 		if (readOnly) {
-			this.sections = Collections.unmodifiableMap(this.sections);
+			this.sections = immutableMap(this.sections);
 			for (Section s : sections.values())
 				s.setReadOnly();
 		}
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
index d79f612..0844371 100644
--- 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
@@ -17,14 +17,26 @@ package org.apache.juneau.config;
  */
 public class ConfigUtils {
 
-	static final String getSectionName(String key) {
+	/**
+	 * 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);
 	}
 
-	static final String getSectionKey(String key) {
+	/**
+	 * 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;
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
index d77f78f..b01396f 100644
--- 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
@@ -15,6 +15,7 @@ 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.*;
@@ -59,7 +60,7 @@ public final class Section implements Map<String,String> {
 	Section setReadOnly() {
 		// This method is only called once from ConfigFileImpl constructor.
 		this.readOnly = true;
-		this.entries = Collections.unmodifiableMap(entries);
+		this.entries = immutableMap(entries);
 		return this;
 	}
 
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/ChangeEvent.java
new file mode 100644
index 0000000..2b87b98
--- /dev/null
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEvent.java
@@ -0,0 +1,225 @@
+// ***************************************************************************************************************************
+// * 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.event;
+
+import static org.apache.juneau.config.event.ChangeEventType.*;
+
+import java.util.*;
+
+import org.apache.juneau.annotation.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Represents a change to a config.
+ */
+@BeanIgnore
+public class ChangeEvent {
+	
+	private final ChangeEventType 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) {
+		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);
+	}
+	
+	//---------------------------------------------------------------------------------------------
+	// Static creators.
+	//---------------------------------------------------------------------------------------------
+	
+	/**
+	 * Sets or replaces a value in a configuration.
+	 * 
+	 * @param section 
+	 * 	The section name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * @param key
+	 * 	The entry name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * @param value
+	 * 	The entry value.
+	 * 	<br>Can be <jk>null</jk> to remove an entry.
+	 * @param comment
+	 * 	Optional comment string to add on the same line as the entry.
+	 * @param modifiers
+	 * 	Optional entry modifiers.
+	 * @param prelines 
+	 * 	Comment lines that occur before this entry.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * @return
+	 * 	A new {@link ChangeEvent} 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);
+	}
+	
+	/**
+	 * Removes a value from a configuration.
+	 * 
+	 * @param section 
+	 * 	The section name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * @param key
+	 * 	The entry name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * @return
+	 * 	A new {@link ChangeEvent} object.
+	 */
+	public static ChangeEvent removeEntry(String section, String key) {
+		return new ChangeEvent(SET_ENTRY, section, key, null, null, null, null);
+	}
+	
+	
+	/**
+	 * Adds a new empty section to the config.
+	 * 
+	 * @param section
+	 * 	The section name.
+	 * @param prelines 
+	 * 	Comment lines that occur before this section.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * @return
+	 * 	A new {@link ChangeEvent} object.
+	 */
+	public static ChangeEvent setSection(String section, List<String> prelines) {
+		return new ChangeEvent(SET_SECTION, section, null, null, null, null, prelines);
+	}
+	
+	/**
+	 * Removes a section from the config.
+	 * 
+	 * @param section
+	 * 	The section name.
+	 * @return
+	 * 	A new {@link ChangeEvent} object.
+	 */
+	public static ChangeEvent removeSection(String section) {
+		return new ChangeEvent(REMOVE_SECTION, section, null, null, null, null, null);
+	}
+	
+	
+	//---------------------------------------------------------------------------------------------
+	// Instance methods.
+	//---------------------------------------------------------------------------------------------
+	
+	/**
+	 * Returns the event type.
+	 *
+	 * @return The event type.
+	 */
+	public ChangeEventType getType() {
+		return type;
+	}
+
+	/**
+	 * Returns the section name.
+	 *
+	 * @return The section name.
+	 */
+	public String getSection() {
+		return section;
+	}
+
+	/**
+	 * Returns the entry name.
+	 *
+	 * @return The entry name.
+	 */
+	public String getKey() {
+		return key;
+	}
+
+	/**
+	 * Returns the entry value.
+	 *
+	 * @return The entry value.
+	 */
+	public String getValue() {
+		return value;
+	}
+
+	/**
+	 * Returns the entry comment.
+	 *
+	 * @return The entry comment.
+	 */
+	public String getComment() {
+		return comment;
+	}
+
+	/**
+	 * Returns the section or entry lines.
+	 *
+	 * @return The section or entry lines.
+	 */
+	public List<String> getPreLines() {
+		return preLines;
+	}
+
+	/**
+	 * 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 
+	 * 	The modifier characters.
+	 * 	<br>Never <jk>null</jk>.
+	 */
+	public String getModifiers() {
+		return modifiers;
+	}
+	
+	@Override /* Object */
+	public String toString() {
+		switch(type) {
+			case REMOVE_SECTION:
+				return "REMOVE_SECTION("+section+")";
+			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);
+				out.append(" = ");
+				String val = value == null ? "null" : value;
+				if (val.indexOf('\n') != -1)
+					val = val.replaceAll("(\\r?\\n)", "$1\t");
+				if (val.indexOf('#') != -1)
+					val = val.replaceAll("#", "\\\\#");
+				out.append(val);
+				if (comment != null) 
+					out.append(" # ").append(comment);
+				out.append(')');
+				return out.toString();
+			default:
+				return null;
+		}
+	}
+}
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileContext.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java
similarity index 59%
rename from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileContext.java
rename to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java
index 303ab25..546f330 100644
--- a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/ConfigFileContext.java
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventListener.java
@@ -1,75 +1,28 @@
-// ***************************************************************************************************************************
-// * 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 org.apache.juneau.*;
-
-/**
- * TODO
- */
-public class ConfigFileContext extends Context {
-
-	/**
-	 * TODO
-	 * 
-	 * @param ps
-	 */
-	public ConfigFileContext(PropertyStore ps) {
-		super(ps);
-	}
-
-	/**
-	 * TODO
-	 */
-	public static final String CONFIGFILE_serializer = "ConfigFile.serializer";
-
-	/**
-	 * TODO
-	 */
-	public static final String CONFIGFILE_parser = "ConfigFile.parser";
-
-	/**
-	 * TODO
-	 */
-	public static final String CONFIGFILE_encoder = "ConfigFile.encoder";
-
-	/**
-	 * TODO
-	 */
-	public static final String CONFIGFILE_readonly = "ConfigFile.readonly";
-
-	/**
-	 * TODO
-	 */
-	public static final String CONFIGFILE_createIfNotExists = "ConfigFile.createIfNotExists";
-
-	/**
-	 * TODO
-	 */
-	public static final String CONFIGFILE_wsDepth = "ConfigFile.wsDepth";
-
-	@Override
-	public ContextBuilder builder() {
-		throw new NoSuchMethodError();
-	}
-
-	@Override
-	public Session createSession(SessionArgs args) {
-		throw new NoSuchMethodError();
-	}
-
-	@Override
-	public SessionArgs createDefaultSessionArgs() {
-		throw new NoSuchMethodError();
-	}
-}
+// ***************************************************************************************************************************
+// * 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.event;
+
+import java.util.*;
+
+/**
+ * Listener that can be used to listen for change events in config maps.
+ */
+public interface ChangeEventListener {
+
+	/**
+	 * Gets called immediately after a config file has been loaded.
+	 * 
+	 * @param events The change events.
+	 */
+	void onEvents(List<ChangeEvent> events);
+}
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/ChangeEventType.java
old mode 100755
new mode 100644
similarity index 78%
copy from juneau-core/juneau-config/src/main/java/org/apache/juneau/config/listener/package-info.java
copy to juneau-core/juneau-config/src/main/java/org/apache/juneau/config/event/ChangeEventType.java
index 77edd53..3ecca35
--- 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/ChangeEventType.java
@@ -1,18 +1,34 @@
-// ***************************************************************************************************************************
-// * 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.                                              *
-// ***************************************************************************************************************************
-
-/**
- * Config Listener Support
- */
-package org.apache.juneau.config.listener;
-
+// ***************************************************************************************************************************
+// * 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.event;
+
+/**
+ * Possible event types for the {@link ChangeEvent} class.
+ */
+public enum ChangeEventType {
+
+	/**
+	 * Set an individual entry value in a config.
+	 */
+	SET_ENTRY,
+	
+	/**
+	 * Adds or replaces a section in a config.
+	 */
+	SET_SECTION,
+	
+	/**
+	 * Removes a section from a config.
+	 */
+	REMOVE_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/listener/package-info.java
index 77edd53..e79c195 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/listener/package-info.java
@@ -12,7 +12,7 @@
 // ***************************************************************************************************************************
 
 /**
- * Config Listener Support
+ * Config Event and Listener Support
  */
 package org.apache.juneau.config.listener;
 
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigEntry.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigEntry.java
new file mode 100644
index 0000000..9a570fb
--- /dev/null
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigEntry.java
@@ -0,0 +1,142 @@
+// ***************************************************************************************************************************
+// * 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.CollectionUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.internal.*;
+
+/**
+ * Represents a single entry in a configuration.
+ * 
+ * This is a read-only object.
+ */
+public class ConfigEntry {
+	final String rawLine;
+	final String key, value, comment;
+	final String modifiers;
+	final List<String> preLines;
+	
+	private final static AsciiSet MOD_CHARS = new AsciiSet("#$%&*+^@~");
+
+	ConfigEntry(String line, List<String> preLines) {
+		this.rawLine = line;
+		int i = line.indexOf('=');
+		String key = line.substring(0, i).trim();
+		
+		int modIndex = key.length();
+		for (int j = key.length()-1; j > 0; j--) 
+			if (MOD_CHARS.contains(key.charAt(j)))
+				modIndex--;
+
+		this.modifiers = key.substring(modIndex);
+		this.key = key.substring(0, modIndex);
+		
+		line = line.substring(i+1);
+	
+		i = line.indexOf('#');
+		if (i != -1) {
+			String[] l2 = StringUtils.split(line, '#', 2);
+			line = l2[0];
+			if (l2.length == 2)
+				this.comment = l2[1].trim();
+			else
+				this.comment = null;
+		} else {
+			this.comment = null;
+		}
+	
+		this.value = line.trim();
+
+		this.preLines = immutableList(preLines);
+	}
+	
+	ConfigEntry(String key, String value, String modifiers, String comment, List<String> preLines) {
+		this.rawLine = null;
+		this.key = key;
+		this.value = value;
+		this.comment = comment;
+		this.modifiers = modifiers;
+		this.preLines = immutableList(preLines);
+	}
+	
+	/**
+	 * Returns the raw value of this entry.
+	 * 
+	 * @return The raw value of this entry.
+	 */
+	public String getValue() {
+		return value;
+	}
+	
+	/**
+	 * Returns the same-line comment of this entry.
+	 * 
+	 * @return The same-line comment of this entry.
+	 */
+	public String getComment() {
+		return comment;
+	}
+
+	/**
+	 * Returns the pre-lines of this entry.
+	 * 
+	 * @return The pre-lines of this entry as an unmodifiable list.
+	 */
+	public List<String> getPreLines() {
+		return preLines;
+	}
+
+	/**
+	 * Returns whether this entry has the specified modifier.
+	 * 
+	 * @param m The modifier character.
+	 * @return <jk>true</jk> if this entry is encoded.
+	 */
+	public boolean hasModifier(char m) {
+		return modifiers.indexOf(m) != -1;
+	}
+	
+	Writer writeTo(Writer out) throws IOException {
+		if (value == null)
+			return out;
+		for (String pl : preLines)
+			out.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');
+		} else {
+			out.append(key);
+			out.append(modifiers);
+			out.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);
+				
+			if (comment != null) 
+				out.append(" # ").append(comment);
+
+			out.append('\n');
+		}
+		return out;
+	}
+}
\ No newline at end of file
diff --git a/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigMap.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigMap.java
new file mode 100644
index 0000000..3b7957d
--- /dev/null
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/proto/ConfigMap.java
@@ -0,0 +1,683 @@
+// ***************************************************************************************************************************
+// * 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.config.event.ChangeEventType.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.locks.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.config.event.*;
+import org.apache.juneau.config.store.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Represents the parsed contents of a configuration.
+ */
+public class ConfigMap implements StoreListener {
+
+	private final Store 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>());
+	
+	// Registered listeners listening for changes during saves or reloads.
+	private final Set<ChangeEventListener> listeners = Collections.synchronizedSet(new HashSet<ChangeEventListener>());
+
+	// The parsed entries of this map with all changes applied.
+	final Map<String,ConfigSection> entries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigSection>());
+
+	// The original entries of this map before any changes were applied.
+	final Map<String,ConfigSection> oentries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigSection>());
+	
+	private final ReadWriteLock lock = new ReentrantReadWriteLock();
+	
+	/**
+	 * Constructor.
+	 * 
+	 * @param store The config store.
+	 * @param name The config name.
+	 * @throws IOException
+	 */
+	public ConfigMap(Store store, String name) throws IOException {
+		this.store = store;
+		this.name = name;
+		load(store.read(name));
+	}
+
+	ConfigMap(String contents) {
+		this.store = null;
+		this.name = null;
+		load(contents);
+	}
+	
+	private ConfigMap load(String contents) {
+		if (contents == null)
+			contents = "";
+		this.contents = contents;
+		
+		entries.clear();
+		oentries.clear();
+		
+		List<String> lines = new LinkedList<>();
+		try (Scanner scanner = new Scanner(contents)) {
+			while (scanner.hasNextLine()) {
+				String line = scanner.nextLine();
+				char c = StringUtils.firstNonWhitespaceChar(line);
+				if (c != 0 || c != '#') {
+					if (c == '[') {
+						int c2 = StringUtils.lastNonWhitespaceChar(line);
+						String l = line.trim();
+						if (c2 != ']' || ! isValidNewSectionName(l.substring(1, l.length()-1)))
+							throw new ConfigException("Invalid section name found in configuration:  {0}", line) ;
+					}
+				}
+				lines.add(line);
+			}
+		}
+		
+		// Add [default] section.
+		boolean inserted = false;
+		boolean foundComment = false;
+		for (ListIterator<String> li = lines.listIterator(); li.hasNext();) {
+			String l = li.next();
+			char c = StringUtils.firstNonWhitespaceChar(l);
+			if (c != '#') {
+				if (c == 0 && foundComment) {
+					li.set("[default]");
+					inserted = true;
+				}
+				break;
+			} 
+			foundComment = true;
+		}
+		if (! inserted)
+			lines.add(0, "[default]");
+		
+		// Collapse any multi-lines.
+		ListIterator<String> li = lines.listIterator(lines.size());
+		String accumulator = null;
+		while (li.hasPrevious()) {
+			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();
+			} else if (accumulator != null) {
+				li.set(l + "\n" + accumulator);
+				accumulator = null;
+			}
+		}
+		
+		lines = new ArrayList<>(lines);
+		int last = lines.size()-1;
+		int S1 = 1; // Looking for section.
+		int S2 = 2; // Found section, looking for start.
+		int state = S1;
+		
+		List<ConfigSection> sections = new ArrayList<>();
+		
+		for (int i = last; i >= 0; i--) {
+			String l = lines.get(i);
+			char c = StringUtils.firstNonWhitespaceChar(l);
+			
+			if (state == S1) {
+				if (c == '[') {
+					state = S2;
+				}
+			} else {
+				if (c != '#' && (c == '[' || l.indexOf('=') != -1)) {
+					sections.add(new ConfigSection(lines.subList(i+1, last+1)));
+					last = i + 1;// (c == '[' ? i+1 : i);
+					state = (c == '[' ? S2 : S1);
+				}
+			}
+		}
+
+		sections.add(new ConfigSection(lines.subList(0, last+1)));
+		
+		for (int i = sections.size() - 1; i >= 0; i--) {
+			ConfigSection cs = sections.get(i);
+			if (entries.containsKey(cs.name))
+				throw new ConfigException("Duplicate section found in configuration:  [{0}]", cs.name);
+			entries.put(cs.name, cs);
+		 }
+
+		oentries.putAll(entries);
+		return this;
+	}
+	
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Getters
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	/**
+	 * Reads an entry from this map.
+	 * 
+	 * @param section The section name.
+	 * @param key The entry key.
+	 * @return The entry, or <jk>null</jk> if the entry doesn't exist.
+	 */
+	public ConfigEntry getEntry(String section, String key) {
+		readLock();
+		try {
+			ConfigSection cs = entries.get(section);
+			return cs == null ? null : cs.entries.get(key);
+		} finally {
+			readUnlock();
+		}
+	}
+
+	/**
+	 * Returns the pre-lines on the specified section.
+	 * 
+	 * <p>
+	 * The pre-lines are all lines such as blank lines and comments that preceed a section.
+	 * 
+	 * @param section 
+	 * 	The section name.
+	 * 	<br>Must not be <jk>null</jk>.
+	 * @return
+	 * 	An unmodifiable list of lines, or <jk>null</jk> if the section doesn't exist.
+	 */
+	public List<String> getPreLines(String section) {
+		readLock();
+		try {
+			ConfigSection cs = entries.get(section);
+			return cs == null ? null : cs.preLines;
+		} finally {
+			readUnlock();
+		}
+	}
+
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Setters
+	//-----------------------------------------------------------------------------------------------------------------
+
+	/**
+	 * Adds a new section or replaces the pre-lines on an existing section.
+	 * 
+	 * @param section The section name.
+	 * @param preLines
+	 * 	The pre-lines on the section.
+	 * 	<br>Can be <jk>null</jk>.
+	 * @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));
+	}
+	
+	/**
+	 * 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.
+	 * 
+	 * @param section The section name.
+	 * @param key The entry key.
+	 * @param preLines 
+	 * 	The new pre-lines.
+	 * 	<br>Can be <jk>null</jk>.
+	 * @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);
+	}
+	
+	/**
+	 * Sets the value on an entry without modifying any other attributes.
+	 * 
+	 * @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.
+	 * 
+	 * @param section The section name.
+	 * @param key The entry key.
+	 * @param comment 
+	 * 	The new comment.
+	 * 	<br>Can be <jk>null</jk>.
+	 * @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);
+	}
+	
+	/**
+	 * Adds or overwrites an existing 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>.
+	 * @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));
+	}
+	
+	private ConfigMap applyChange(boolean addToChangeList, ChangeEvent ce) {
+		if (ce == null)
+			return this;
+		writeLock();
+		try {
+			String section = ce.getSection();
+			ConfigSection cs = entries.get(section);
+			if (ce.getType() == SET_ENTRY) {
+				if (cs == null) {
+					cs = new ConfigSection(section);
+					entries.put(section, cs);
+				}
+				cs.addEntry(ce.getKey(), ce.getValue(), ce.getModifiers(), ce.getComment(), 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 (cs != null)
+					entries.remove(section);
+			}
+			if (addToChangeList)
+				changes.add(ce);
+		} finally {
+			writeUnlock();
+		}
+		return this;	
+	}	
+	
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Lifecycle events
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	/**
+	 * Persist any changes made to this map and signal all listeners.
+	 * 
+	 * <p>
+	 * If the underlying contents of the file have changed, this will reload it and apply the changes
+	 * on top of the modified file.
+	 * 
+	 * <p>
+	 * Subsequent changes made to the underlying file will also be signaled to all listeners.
+	 * 
+	 * <p>
+	 * We try saving the file up to 10 times.
+	 * <br>If the file keeps changing on the file system, we throw an exception.
+	 * 
+	 * @return This object (for method chaining).
+	 * @throws IOException
+	 */
+	public ConfigMap save() throws IOException {
+		writeLock();
+		try {
+			String newContents = asString();
+			for (int i = 0; i <= 10; i++) {
+				if (i == 10)
+					throw new ConfigException("Unable to store contents of config to store.");
+				String currentContents = store.write(name, contents, newContents);
+				if (currentContents == null) 
+					break;
+				onChange(name, currentContents);
+			}
+			this.changes.clear();
+		} finally {
+			writeUnlock();
+		}
+		return this;
+	}
+
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Listeners
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	/**
+	 * Registers an event listener on this map.
+	 * 
+	 * @param listener The new listener.
+	 * @return This object (for method chaining).
+	 */
+	public ConfigMap registerListener(ChangeEventListener listener) {
+		listeners.add(listener);
+		return this;
+	}
+	
+	/**
+	 * Unregisters an event listener from this map.
+	 * 
+	 * @param listener The listener to remove.
+	 * @return This object (for method chaining).
+	 */
+	public ConfigMap unregisterListener(ChangeEventListener listener) {
+		listeners.remove(listener);
+		return this;
+	}
+	
+	@Override /* StoreListener */
+	public void onChange(String name, String newContents) {
+		List<ChangeEvent> changes = null;
+		writeLock();
+		try {
+			if (! StringUtils.isEquals(contents, newContents)) {
+				changes = findDiffs(newContents);
+				load(newContents);
+				
+				// Reapply our changes on top of the modifications.
+				for (ChangeEvent ce : this.changes)
+					applyChange(false, ce);
+			}
+		} finally {
+			writeUnlock();
+		}
+		if (changes != null && ! changes.isEmpty())
+			signal(changes);
+	}
+	
+	private void signal(List<ChangeEvent> changes) {
+		for (ChangeEventListener l : listeners)
+			l.onEvents(changes);
+	}
+
+	private List<ChangeEvent> findDiffs(String updatedContents) {
+		List<ChangeEvent> 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));
+				for (ConfigEntry ne : ns.entries.values()) {
+					changes.add(ChangeEvent.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));
+					}
+				}
+				for (ConfigEntry e : s.oentries.values()) {
+					ConfigEntry ne = ns.oentries.get(e.key);
+					if (ne == null) {
+						changes.add(ChangeEvent.removeEntry(s.name, e.key));
+					}
+				}
+			}
+		}
+		for (ConfigSection s : oentries.values()) {
+			ConfigSection ns = newMap.oentries.get(s.name);
+			if (ns == null) {
+				//changes.add(ChangeEvent.removeSection(s.name));
+				for (ConfigEntry e : s.oentries.values())
+					changes.add(ChangeEvent.removeEntry(s.name, e.key));
+			}
+		}
+		return changes;
+	}
+
+	
+	//---------------------------------------------------------------------------------------------
+	// ConfigSection
+	//---------------------------------------------------------------------------------------------
+	
+	class ConfigSection {
+
+		final String name;   // The config section name, or "default" if the default section.  Never null.
+
+		final List<String> preLines = Collections.synchronizedList(new ArrayList<String>());
+		private final String rawLine;
+		
+		final Map<String,ConfigEntry> oentries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigEntry>());
+		final Map<String,ConfigEntry> entries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigEntry>());
+
+		/**
+		 * Constructor.
+		 */
+		ConfigSection(String name) {
+			this.name = name;
+			this.rawLine = "[" + name + "]";
+		}
+		
+		/**
+		 * Constructor.
+		 */
+		ConfigSection(List<String> lines) {
+			
+			String name = null, rawLine = null;
+			
+			int S1 = 1; // Looking for section.
+			int S2 = 2; // Found section, looking for end.
+			int state = S1;
+			int start = 0;
+			
+			for (int i = 0; i < lines.size(); i++) {
+				String l = lines.get(i);
+				char c = StringUtils.firstNonWhitespaceChar(l);
+				if (state == S1) {
+					if (c == '[') {
+						int i1 = l.indexOf('['), i2 = l.indexOf(']');
+						name = l.substring(i1+1, i2).trim();
+						rawLine = l;
+						state = S2;
+						start = i+1;
+					} else {
+						preLines.add(l);
+					}
+				} else {
+					if (c != '#' && l.indexOf('=') != -1) {
+						ConfigEntry e = new ConfigEntry(l, lines.subList(start, i));
+						if (entries.containsKey(e.key))
+							throw new ConfigException("Duplicate entry found in section [{0}] of configuration:  {1}", name, e.key);
+						entries.put(e.key, e);
+						start = i+1;
+					} 
+				}
+			}
+			
+			this.name = name;
+			this.rawLine = rawLine;
+			this.oentries.putAll(entries);
+		}
+		
+		ConfigSection addEntry(String key, String value, String modifiers, String comment, List<String> preLines) {
+			ConfigEntry e = new ConfigEntry(key, value, modifiers, comment, preLines);
+			this.entries.put(e.key, e);
+			return this;
+		}
+
+		ConfigSection setPreLines(List<String> preLines) {
+			this.preLines.clear();
+			this.preLines.addAll(preLines);
+			return this;
+		}
+		
+		Writer writeTo(Writer out) throws IOException {
+			for (String s : preLines)
+				out.append(s).append('\n');
+			
+			if (! name.equals("default"))
+				out.append(rawLine).append('\n');
+			else {
+				// Need separation between default prelines and first-entry prelines.
+				if (! preLines.isEmpty())
+					out.append('\n');
+			}
+
+			for (ConfigEntry e : entries.values()) 
+				e.writeTo(out);
+			
+			return out;
+		}
+	}
+
+	@Override /* Object */
+	public String toString() {
+		readLock();
+		try {
+			return asString();
+		} finally {
+			readUnlock();
+		}
+	}
+
+	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-config/src/main/java/org/apache/juneau/config/store/FileStore.java b/juneau-core/juneau-config/src/main/java/org/apache/juneau/config/store/FileStore.java
index 75f50ec..b3063d5 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/FileStore.java
@@ -209,7 +209,7 @@ public class FileStore extends Store {
 	}
 	
 	@Override /* Store */
-	public synchronized String read(String name) throws Exception {
+	public synchronized String read(String name) throws IOException {
 		String s = cache.get(name);
 		if (s != null)
 			return s;
@@ -235,12 +235,12 @@ public class FileStore extends Store {
 	}
 
 	@Override /* Store */
-	public synchronized boolean write(String name, String oldContents, String newContents) throws Exception {
+	public synchronized String write(String name, String expectedContents, String newContents) throws IOException {
 		dir.mkdirs();
 		Path p = dir.toPath().resolve(name + '.' + ext);
 		boolean exists = Files.exists(p);
-		if (oldContents != null && ! exists)
-			return false;
+		if (expectedContents != null && ! exists)
+			return "";
 		try (FileChannel fc = FileChannel.open(p, READ, WRITE, CREATE)) {
 			try (FileLock lock = fc.lock()) {
 				String currentContents = null;
@@ -253,19 +253,19 @@ public class FileStore extends Store {
 					}
 					currentContents = sb.toString();
 				}
-				if (! isEquals(oldContents, currentContents)) {
+				if (! isEquals(expectedContents, currentContents)) {
 					if (currentContents == null)
 						cache.remove(name);
 					else
 						cache.put(name, currentContents);
-					return false;
+					return currentContents;
 				}
 				fc.position(0);
 				fc.write(charset.encode(newContents));
 				cache.put(name, newContents);
 			}
 		}
-		return true;
+		return null;
 	}
 		
 	@Override /* Store */
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/MemoryStore.java
index 8dde322..6cbcddc 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/MemoryStore.java
@@ -18,6 +18,7 @@ import java.io.*;
 import java.util.concurrent.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
 
 /**
  * Filesystem-based storage location for configuration files.
@@ -65,23 +66,23 @@ public class MemoryStore extends Store {
 	}
 	
 	@Override /* Store */
-	public synchronized String read(String name) throws Exception {
+	public synchronized String read(String name) {
 		return cache.get(name);
 	}
 
 	@Override /* Store */
-	public synchronized boolean write(String name, String oldContents, String newContents) throws Exception {
-		String s = cache.get(name);
+	public synchronized String write(String name, String expectedContents, String newContents) {
+		String currentContents = read(name);
 		
-		if (! isEquals(s, oldContents)) 
-			return false;
+		if (! isEquals(currentContents, expectedContents)) 
+			return StringUtils.emptyIfNull(currentContents);
 		
-		if (! isEquals(s, newContents)) {
+		if (! isEquals(currentContents, newContents)) {
 			cache.put(name, newContents);
 			update(name, newContents);
 		}
 		
-		return true;
+		return null;
 	}
 
 	
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/Store.java
index 6b8488c..9cb7986 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/Store.java
@@ -14,8 +14,10 @@ package org.apache.juneau.config.store;
 
 import java.io.*;
 import java.util.*;
+import java.util.concurrent.*;
 
 import org.apache.juneau.*;
+import org.apache.juneau.config.proto.*;
 
 /**
  * Represents a storage location for configuration files.
@@ -31,6 +33,8 @@ public abstract class Store extends Context implements Closeable {
 	
 	private final List<StoreListener> listeners = new LinkedList<>();
 	
+	private final ConcurrentHashMap<String,ConfigMap> configMaps = new ConcurrentHashMap<>();
+	
 	/**
 	 * Constructor.
 	 * 
@@ -45,21 +49,22 @@ public abstract class Store extends Context implements Closeable {
 	 * 
 	 * @param name The config file name.
 	 * @return The contents of the configuration file.
-	 * @throws Exception
+	 * @throws IOException
 	 */
-	public abstract String read(String name) throws Exception;
+	public abstract String read(String name) throws IOException;
 
 	/**
 	 * Saves the contents of the configuration file if the underlying storage hasn't been modified.
 	 * 
 	 * @param name The config file name.
-	 * @param oldContents The old contents.
+	 * @param expectedContents The expected contents of the file.
 	 * @param newContents The new contents.
-	 * @return <jk>true</jk> if we successfully saved the new configuration file contents, or <jk>false</jk> if the
-	 * 	underlying storage changed since the last time the {@link #read(String)} method was called.
-	 * @throws Exception
+	 * @return 
+	 * 	If <jk>null</jk>, then we successfully stored the contents of the file.
+	 * 	<br>Otherwise the contents of the file have changed and we return the new contents of the file.
+	 * @throws IOException
 	 */
-	public abstract boolean write(String name, String oldContents, String newContents) throws Exception;
+	public abstract String write(String name, String expectedContents, String newContents) throws IOException;
 
 	/**
 	 * Registers a new listener on this store.
@@ -84,6 +89,27 @@ public abstract class Store extends Context implements Closeable {
 	}
 
 	/**
+	 * Returns a map file containing the parsed contents of a configuration.
+	 * 
+	 * @param name The configuration name.
+	 * @return 
+	 * 	The parsed configuration.
+	 * 	<br>Never <jk>null</jk>.
+	 * @throws IOException
+	 */
+	public ConfigMap getMap(String name) throws IOException {
+		ConfigMap cm = configMaps.get(name);
+		if (cm != null)
+			return cm;
+		cm = new ConfigMap(this, name);
+		ConfigMap cm2 = configMaps.putIfAbsent(name, cm);
+		if (cm2 != null)
+			return cm2;
+		listeners.add(cm);
+		return cm;
+	}
+	
+	/**
 	 * Called when the physical contents of a config file have changed.
 	 * 
 	 * <p>
@@ -98,6 +124,20 @@ public abstract class Store extends Context implements Closeable {
 			l.onChange(name, contents);
 		return this;
 	}
+
+	/**
+	 * Convenience method for updating the contents of a file with lines.
+	 * 
+	 * @param name The config name (e.g. the filename without the extension).
+	 * @param contentLines The new contents.
+	 * @return This object (for method chaining).
+	 */
+	public Store update(String name, String...contentLines) {
+		StringBuilder sb = new StringBuilder();
+		for (String l : contentLines)
+			sb.append(l).append('\n');
+		return update(name, sb.toString());
+	}
 	
 	/**
 	 * Unused.
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
new file mode 100644
index 0000000..bec142e
--- /dev/null
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapListenerTest.java
@@ -0,0 +1,596 @@
+// ***************************************************************************************************************************
+// * 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.junit.Assert.*;
+import static org.apache.juneau.TestUtils.*;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.config.event.*;
+import org.apache.juneau.config.store.*;
+import org.junit.*;
+
+public class ConfigMapListenerTest {
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Sanity tests.
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Test
+	public void testBasicDefaultSection() throws Exception {
+		Store s = initStore("Foo", 
+			"foo=bar"
+		);		
+		
+		final CountDownLatch latch = new CountDownLatch(1);
+		
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(foo = baz)']", events);
+			}
+		};
+		
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(l);
+		cm.setValue("default", "foo", "baz");
+		cm.save();
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("foo = baz|", cm.toString());
+	}
+	
+	@Test
+	public void testBasicNormalSection() throws Exception {
+		Store s = initStore("Foo", 
+			"[S1]",
+			"foo=bar"
+		);		
+		
+		final CountDownLatch latch = new CountDownLatch(1);
+		
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(foo = baz)']", events);
+			}
+		};
+		
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(l);
+		cm.setValue("S1", "foo", "baz");
+		cm.save();
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("[S1]|foo = baz|", cm.toString());
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Add new entries.
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Test
+	public void testAddNewEntries() throws Exception {
+		Store s = initStore("Foo"
+		);		
+		
+		final CountDownLatch latch = new CountDownLatch(2);
+
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(k = vb)','SET(k1 = v1b)']", events);
+			}
+		};
+
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(l);
+		cm.setValue("default", "k", "vb");
+		cm.setValue("S1", "k1", "v1b");
+		cm.save();
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("k = vb|[S1]|k1 = v1b|", cm.toString());
+	}
+	
+	@Test
+	public void testAddNewEntriesWithAttributes() throws Exception {
+		Store s = initStore("Foo"
+		);		
+		
+		final CountDownLatch latch = new CountDownLatch(2);
+
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(k^* = kb # C)','SET(k1^* = k1b # C1)']", events);
+			}
+		};
+
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(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);
+		
+		assertTextEquals("#k|k^* = kb # C|[S1]|#k1|k1^* = k1b # C1|", cm.toString());
+	}
+
+	@Test
+	public void testAddExistingEntriesWithAttributes() throws Exception {
+		Store s = initStore("Foo",
+			"#ka",
+			"k=va # Ca",
+			"#S1",
+			"[S1]",
+			"#k1a",
+			"k1=v1a # Cb"
+		);		
+		
+		final CountDownLatch latch = new CountDownLatch(2);
+
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(k^* = kb # Cb)','SET(k1^* = k1b # Cb1)']", events);
+			}
+		};
+
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(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);
+		
+		assertTextEquals("#kb|k^* = kb # Cb|#S1|[S1]|#k1b|k1^* = k1b # Cb1|", cm.toString());
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Remove existing entries.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void testRemoveExistingEntries() throws Exception {
+		Store s = initStore("Foo",
+			"k=v",
+			"[S1]",
+			"k1=v1"
+		);		
+		
+		final CountDownLatch latch = new CountDownLatch(2);
+
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(k = null)','SET(k1 = null)']", events);
+			}
+		};
+
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(l);
+		cm.setValue("default", "k", null);
+		cm.setValue("S1", "k1", null);
+		cm.save();
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("[S1]|", cm.toString());
+	}
+	
+	@Test
+	public void testRemoveExistingEntriesWithAttributes() throws Exception {
+		Store s = initStore("Foo",
+			"#ka",
+			"k=va # Ca",
+			"#S1",
+			"[S1]",
+			"#k1a",
+			"k1=v1a # Cb"
+		);		
+		
+		final CountDownLatch latch = new CountDownLatch(2);
+
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(k = null)','SET(k1 = null)']", events);
+			}
+		};
+
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(l);
+		cm.setValue("default", "k", null);
+		cm.setValue("S1", "k1", null);
+		cm.save();
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("#S1|[S1]|", cm.toString());
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Add new sections.
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Test
+	public void testAddNewSections() throws Exception {
+		Store s = initStore("Foo"
+		);		
+		
+		final CountDownLatch latch = new CountDownLatch(1);
+
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(k3 = v3)']", events);
+			}
+		};
+
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(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.save();
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("#D1||#S1|[S1]|[S2]|[S3]|k3 = v3|", cm.toString());
+	}
+
+	@Test
+	public void testModifyExistingSections() throws Exception {
+		Store s = initStore("Foo",
+			"#Da",
+			"",
+			"#S1a",
+			"[S1]",
+			"[S2]",
+			"[S3]"
+		);		
+		
+		final CountDownLatch latch = new CountDownLatch(1);
+
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(k3 = v3)']", events);
+			}
+		};
+
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(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.save();
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("#Db||#S1b|[S1]|[S2]|[S3]|k3 = v3|", cm.toString());
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Remove sections.
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Test
+	public void testRemoveSections() throws Exception {
+		Store s = initStore("Foo",
+			"#Da",
+			"",
+			"k = v",
+			"",
+			"#S1",
+			"[S1]",
+			"#k1",
+			"k1 = v1",
+			"[S2]",
+			"#k2",
+			"k2 = v2",
+			"[S3]"
+		);		
+		
+		final CountDownLatch latch = new CountDownLatch(3);
+
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(k = null)','SET(k1 = null)','SET(k2 = null)']", events);
+			}
+		};
+
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(l);
+		cm.removeSection("default");
+		cm.removeSection("S1");
+		cm.removeSection("S2");
+		cm.removeSection("S3");
+		cm.save();
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("", cm.toString());
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Update from store.
+	//-----------------------------------------------------------------------------------------------------------------
+	
+	@Test
+	public void testUpdateFromStore() throws Exception {
+		Store s = initStore("Foo");
+
+		final CountDownLatch latch = new CountDownLatch(3);
+
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals("['SET(k = v # cv)','SET(k1 = v1 # cv1)','SET(k2 = v2 # cv2)']", events);
+			}
+		};
+		
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(l);
+		s.update("Foo",
+			"#Da",
+			"",
+			"k = v # cv",
+			"",
+			"#S1",
+			"[S1]",
+			"#k1",
+			"k1 = v1 # cv1",
+			"[S2]",
+			"#k2",
+			"k2 = v2 # cv2",
+			"[S3]"
+		);
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("#Da||k = v # cv||#S1|[S1]|#k1|k1 = v1 # cv1|[S2]|#k2|k2 = v2 # cv2|[S3]|", cm.toString());
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Merges.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void testMergeNoOverwrite() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1a"
+		);
+
+		final CountDownLatch latch = new CountDownLatch(2);
+		final Queue<String> eventList = new ConcurrentLinkedQueue<String>();
+		eventList.add("['SET(k1 = v1b)']");
+		eventList.add("['SET(k2 = v2b)']");
+		
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals(eventList.poll(), events);
+			}
+		};
+		
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(l);
+		cm.setValue("S2", "k2", "v2b");
+		s.update("Foo",
+			"[S1]",
+			"k1 = v1b"
+		);
+		cm.save();
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("[S1]|k1 = v1b|[S2]|k2 = v2b|", cm.toString());
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// If we're modifying an entry and it changes on the file system, we should overwrite the change on save().
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void testMergeWithOverwrite() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1a"
+		);
+
+		final CountDownLatch latch = new CountDownLatch(2);
+		final Queue<String> eventList = new ConcurrentLinkedQueue<String>();
+		eventList.add("['SET(k1 = v1b)']");
+		eventList.add("['SET(k1 = v1c)']");
+		
+		LatchedListener l = new LatchedListener(latch) {
+			@Override
+			public void check(List<ChangeEvent> events) throws Exception {
+				assertObjectEquals(eventList.poll(), events);
+			}
+		};
+		
+		ConfigMap cm = s.getMap("Foo");
+		cm.registerListener(l);
+		cm.setValue("S1", "k1", "v1c");
+		s.update("Foo",
+			"[S1]",
+			"k1 = v1b"
+		);
+		cm.save();
+		wait(latch);
+		assertNull(l.error);
+		cm.unregisterListener(l);
+		
+		assertTextEquals("[S1]|k1 = v1c|", cm.toString());
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// If the contents of a file have been modified on the file system before a signal has been received.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	@Test
+	public void testMergeWithOverwriteNoSignal() throws Exception {
+		
+		final Queue<String> contents = new ConcurrentLinkedQueue<String>();
+		contents.add("[S1]\nk1 = v1a");
+		contents.add("[S1]\nk1 = v1b");
+		contents.add("[S1]\nk1 = v1c");
+		contents.add("[S1]\nk1 = v1c");
+		
+		MemoryStore s = new MemoryStore(null) {
+			public synchronized String read(String name) {
+				return contents.poll();
+			}
+		};
+		try {
+			final CountDownLatch latch = new CountDownLatch(2);
+			final Queue<String> eventList = new ConcurrentLinkedQueue<String>();
+			eventList.add("['SET(k1 = v1b)']");
+			eventList.add("['SET(k1 = v1c)']");
+			
+			LatchedListener l = new LatchedListener(latch) {
+				@Override
+				public void check(List<ChangeEvent> events) throws Exception {
+					assertObjectEquals(eventList.poll(), events);
+				}
+			};
+			
+			ConfigMap cm = s.getMap("Foo");
+			cm.registerListener(l);
+			cm.setValue("S1", "k1", "v1c");
+			cm.save();
+			wait(latch);
+			assertNull(l.error);
+			cm.unregisterListener(l);
+			
+			assertTextEquals("[S1]|k1 = v1c|", cm.toString());
+			
+		} finally {
+			s.close();
+		}
+	}
+	
+	@Test
+	public void testMergeWithConstantlyUpdatingFile() throws Exception {
+		
+		MemoryStore s = new MemoryStore(null) {
+			char c = 'a';
+			public synchronized String read(String name) {
+				return "[S1]\nk1 = v1" + (c++);
+			}
+		};
+		try {
+			final CountDownLatch latch = new CountDownLatch(10);
+			final Queue<String> eventList = new ConcurrentLinkedQueue<String>();
+			eventList.add("['SET(k1 = v1b)']");
+			eventList.add("['SET(k1 = v1c)']");
+			eventList.add("['SET(k1 = v1d)']");
+			eventList.add("['SET(k1 = v1e)']");
+			eventList.add("['SET(k1 = v1f)']");
+			eventList.add("['SET(k1 = v1g)']");
+			eventList.add("['SET(k1 = v1h)']");
+			eventList.add("['SET(k1 = v1i)']");
+			eventList.add("['SET(k1 = v1j)']");
+			eventList.add("['SET(k1 = v1k)']");
+			
+			LatchedListener l = new LatchedListener(latch) {
+				@Override
+				public void check(List<ChangeEvent> events) throws Exception {
+					assertObjectEquals(eventList.poll(), events);
+				}
+			};
+			
+			ConfigMap cm = s.getMap("Foo");
+			cm.registerListener(l);
+			cm.setValue("S1", "k1", "v1c");
+			try {
+				cm.save();
+				fail("Exception expected.");
+			} catch (ConfigException e) {
+				assertEquals("Unable to store contents of config to store.", e.getMessage());
+			}
+			wait(latch);
+			assertNull(l.error);
+			cm.unregisterListener(l);
+			
+			assertTextEquals("[S1]|k1 = v1c|", cm.toString());
+			
+		} finally {
+			s.close();
+		}
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Utilities.
+	//-----------------------------------------------------------------------------------------------------------------
+
+	private static Store initStore(String name, String...contents) {
+		return MemoryStore.create().build().update(name, contents);
+	}
+	
+	public static class LatchedListener implements ChangeEventListener {
+		private final CountDownLatch latch;
+		private volatile String error = null;
+		public LatchedListener(CountDownLatch latch) {
+			this.latch = latch;
+		}
+		
+		@Override
+		public void onEvents(List<ChangeEvent> events) {
+			try {
+				check(events);
+			} catch (Exception e) {
+				error = e.getLocalizedMessage();
+			}
+			for (int i = 0; i < events.size(); i++)
+				latch.countDown();
+		}
+		
+		public void check(List<ChangeEvent> events) throws Exception {
+		}
+	}
+	
+	private static void wait(CountDownLatch latch) throws InterruptedException {
+		if (! latch.await(10, TimeUnit.SECONDS))
+			throw new RuntimeException("Latch failed.");
+	}
+}
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapTest.java
new file mode 100644
index 0000000..2625d7f
--- /dev/null
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/proto/ConfigMapTest.java
@@ -0,0 +1,1217 @@
+// ***************************************************************************************************************************
+// * 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.junit.Assert.*;
+
+import java.util.*;
+
+import static org.apache.juneau.TestUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.config.store.*;
+import org.junit.*;
+
+public class ConfigMapTest {
+	
+	final static String ENCODED = "*";
+	final static String BASE64 = "^";
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Should be able to read non-existent files without errors.
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testNonExistentConfig() throws Exception {
+		Store s = MemoryStore.create().build();		
+		ConfigMap cm = s.getMap("Foo");
+		assertEquals("", cm.toString());
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Should be able to read blank files without errors.
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testBlankConfig() throws Exception {
+		
+		Store s = initStore("Foo", "");		
+		ConfigMap cm = s.getMap("Foo");
+		assertEquals("", cm.toString());
+		
+		s.update("Foo", "   \n   \n   ");
+		cm = s.getMap("Foo");
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Simple one-line file.
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSimpleOneLine() throws Exception {
+		Store s = initStore("Foo", 
+			"foo=bar"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		assertTextEquals("foo=bar|", cm);
+		
+		assertEquals("", join(cm.getPreLines("default"), '|'));
+		assertEquals("", join(cm.getEntry("default", "foo").getPreLines(), '|'));
+		
+		assertEquals("bar", cm.getEntry("default", "foo").getValue());
+		
+		// Round trip.
+		s.update("Foo", cm.toString());
+		cm = s.getMap("Foo");
+		assertTextEquals("foo=bar|", cm);
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Simple one-line file with leading comments.
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSimpleOneLineWithComments() throws Exception {
+		Store s = initStore("Foo", 
+			"#comment",
+			"foo=bar"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		assertTextEquals("#comment|foo=bar|", cm);
+
+		assertEquals("", join(cm.getPreLines("default"), '|'));
+		assertEquals("#comment", join(cm.getEntry("default", "foo").getPreLines(), '|'));
+
+		assertEquals("bar", cm.getEntry("default", "foo").getValue());
+		
+		// Round trip.
+		s.update("Foo", cm.toString());
+		cm = s.getMap("Foo");
+		assertTextEquals("#comment|foo=bar|", cm);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Simple section.
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSimpleSection() throws Exception {
+		Store s = initStore("Foo", 
+			"[MySection]",
+			"foo=bar"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		assertTextEquals("[MySection]|foo=bar|", cm);
+		
+		assertEquals("", join(cm.getPreLines("default"), '|'));
+		assertEquals("", join(cm.getPreLines("MySection"), '|'));
+		assertEquals("", join(cm.getEntry("MySection", "foo").getPreLines(), '|'));
+		
+		assertEquals("bar", cm.getEntry("MySection", "foo").getValue());
+		
+		// Round trip.
+		s.update("Foo", cm.toString());
+		cm = s.getMap("Foo");
+		assertTextEquals("[MySection]|foo=bar|", cm);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Non-existent values should not throw exceptions.
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testNonExistentValues() throws Exception {
+		Store s = initStore("Foo", 
+			"[MySection]",
+			"foo=bar"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		assertTextEquals("[MySection]|foo=bar|", cm);
+		
+		assertEquals("", join(cm.getPreLines("default"), '|'));
+		
+		assertNull(cm.getPreLines("XXX"));
+
+		assertNull(cm.getEntry("XXX", "yyy"));
+		assertNull(cm.getEntry("MySection", "yyy"));
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// testSimpleSectionWithComments
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSimpleSectionWithComments() throws Exception {
+		Store s = initStore("Foo", 
+			"#S1",
+			"[S1]",
+			"#k1",
+			"k1=v1",
+			"#S2",
+			"[S2]",
+			"#k2",
+			"k2=v2"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		assertTextEquals("#S1|[S1]|#k1|k1=v1|#S2|[S2]|#k2|k2=v2|", cm);
+		
+		assertEquals("", join(cm.getPreLines("default"), '|'));
+		assertEquals("#S1", join(cm.getPreLines("S1"), '|'));
+		assertEquals("#k1", join(cm.getEntry("S1", "k1").getPreLines(), '|'));
+		assertEquals("#S2", join(cm.getPreLines("S2"), '|'));
+		assertEquals("#k2", join(cm.getEntry("S2", "k2").getPreLines(), '|'));
+		
+		assertEquals("v1", cm.getEntry("S1", "k1").getValue());
+		assertEquals("v2", cm.getEntry("S2", "k2").getValue());
+
+		// Round trip.
+		s.update("Foo", cm.toString());
+		cm = s.getMap("Foo");
+		assertTextEquals("#S1|[S1]|#k1|k1=v1|#S2|[S2]|#k2|k2=v2|", cm);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// testSimpleAndDefaultSectionsWithComments
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSimpleAndDefaultSectionsWithComments() throws Exception {
+		Store s = initStore("Foo", 
+			"#D",
+			"",
+			"#k",
+			"k=v",
+			"#S1",
+			"[S1]",
+			"#k1",
+			"k1=v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		assertTextEquals("#D||#k|k=v|#S1|[S1]|#k1|k1=v1|", cm);
+
+		assertEquals("#D", join(cm.getPreLines("default"), '|'));
+		assertEquals("#k", join(cm.getEntry("default", "k").getPreLines(), '|'));
+		assertEquals("#S1", join(cm.getPreLines("S1"), '|'));
+		assertEquals("#k1", join(cm.getEntry("S1", "k1").getPreLines(), '|'));
+		
+		assertEquals("v", cm.getEntry("default", "k").getValue());
+		assertEquals("v1", cm.getEntry("S1", "k1").getValue());
+		
+		// Round trip.
+		s.update("Foo", cm.toString());
+		cm = s.getMap("Foo");
+		assertTextEquals("#D||#k|k=v|#S1|[S1]|#k1|k1=v1|", cm);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// testSimpleAndDefaultSectionsWithCommentsAndExtraSpaces
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSimpleAndDefaultSectionsWithCommentsAndExtraSpaces() throws Exception {
+		Store s = initStore("Foo", 
+			"#Da",
+			"#Db",
+			"",
+			"#ka",
+			"",
+			"#kb",
+			"",
+			"k=v",
+			"",
+			"#S1a",
+			"",
+			"#S1b",
+			"",
+			"[S1]",
+			"",
+			"#k1a",
+			"",
+			"#k1b",
+			"",
+			"k1=v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		assertTextEquals("#Da|#Db||#ka||#kb||k=v||#S1a||#S1b||[S1]||#k1a||#k1b||k1=v1|", cm);
+
+		assertEquals("#Da|#Db", join(cm.getPreLines("default"), '|'));
+		assertEquals("#ka||#kb|", join(cm.getEntry("default", "k").getPreLines(), '|'));
+		assertEquals("|#S1a||#S1b|", join(cm.getPreLines("S1"), '|'));
+		assertEquals("|#k1a||#k1b|", join(cm.getEntry("S1", "k1").getPreLines(), '|'));
+		
+		assertEquals("v", cm.getEntry("default", "k").getValue());
+		assertEquals("v1", cm.getEntry("S1", "k1").getValue());
+		
+		// Round trip.
+		s.update("Foo", cm.toString());
+		cm = s.getMap("Foo");
+		assertTextEquals("#Da|#Db||#ka||#kb||k=v||#S1a||#S1b||[S1]||#k1a||#k1b||k1=v1|", cm);
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Error conditions.
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testMalformedSectionHeaders() throws Exception {
+		
+		String[] test = {
+			"[default]", "[ default ]", " [ default ] ", "\t[\tdefault\t]\t",
+			"[/]", "[[]", "[]]", "[\\]", 
+			"[foo/bar]", "[foo[bar]", "[foo]bar]", "[foo\\bar]", 
+			"[]", "[ ]", "[\t]", " [] ",
+		};
+		
+		for (String t : test) {
+			Store s = initStore("Foo", t);		
+			try {
+				s.getMap("Foo");
+				fail("Exception expected.");
+			} catch (ConfigException e) {
+				assertTrue(e.getLocalizedMessage().startsWith("Invalid section name"));
+			}
+		}
+	}
+	
+	@Test
+	public void testDuplicateSectionNames() throws Exception {
+		Store s = initStore("Foo", "[S1]", "[S1]");		
+		try {
+			s.getMap("Foo");
+			fail("Exception expected.");
+		} catch (ConfigException e) {
+			assertEquals("Duplicate section found in configuration:  [S1]", e.getLocalizedMessage());
+		}
+	}	
+
+	@Test
+	public void testDuplicateEntryNames() throws Exception {
+		Store s = initStore("Foo", "[S1]", "foo=v1", "foo=v2");		
+		try {
+			s.getMap("Foo");
+			fail("Exception expected.");
+		} catch (ConfigException e) {
+			assertEquals("Duplicate entry found in section [S1] of configuration:  foo", e.getLocalizedMessage());
+		}
+	}	
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Lines can be split up.
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testMultipleLines() throws Exception {
+		Store s = initStore("Foo", 
+			"k1 = v1a,",
+			"\tv1b,",
+			"\tv1c",
+			"k2 = v2a,",
+			"\tv2b,",
+			"\tv2c"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		assertEquals("", join(cm.getEntry("default", "k1").getPreLines(), '|'));
+		assertEquals("", join(cm.getEntry("default", "k2").getPreLines(), '|'));
+
+		assertTextEquals("k1 = v1a,|\tv1b,|\tv1c|k2 = v2a,|\tv2b,|\tv2c|", cm);
+
+		assertEquals("v1a,\nv1b,\nv1c", cm.getEntry("default", "k1").getValue());
+		assertEquals("v2a,\nv2b,\nv2c", cm.getEntry("default", "k2").getValue());
+		
+		// Round trip.
+		s.update("Foo", cm.toString());
+		cm = s.getMap("Foo");
+		assertTextEquals("k1 = v1a,|\tv1b,|\tv1c|k2 = v2a,|\tv2b,|\tv2c|", cm);
+	}
+
+	@Test
+	public void testMultipleLinesWithSpacesAndComments() throws Exception {
+		Store s = initStore("Foo", 
+			"",
+			"#k1",
+			"",
+			"k1 = v1a,",
+			"\tv1b,",
+			"\tv1c",
+			"",
+			"#k2",
+			"",
+			"k2 = v2a,",
+			"\tv2b,",
+			"\tv2c"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		assertEquals("|#k1|", join(cm.getEntry("default", "k1").getPreLines(), '|'));
+		assertEquals("|#k2|", join(cm.getEntry("default", "k2").getPreLines(), '|'));
+
+		assertTextEquals("|#k1||k1 = v1a,|	v1b,|	v1c||#k2||k2 = v2a,|	v2b,|	v2c|", cm);
+
+		assertEquals("v1a,\nv1b,\nv1c", cm.getEntry("default", "k1").getValue());
+		assertEquals("v2a,\nv2b,\nv2c", cm.getEntry("default", "k2").getValue());
+		
+		// Round trip.
+		s.update("Foo", cm.toString());
+		cm = s.getMap("Foo");
+		assertTextEquals("|#k1||k1 = v1a,|	v1b,|	v1c||#k2||k2 = v2a,|	v2b,|	v2c|", cm);
+	}
+
+	@Test
+	public void testMultipleLinesInSection() throws Exception {
+		Store s = initStore("Foo", 
+			"[S1]",
+			"k1 = v1a,",
+			"\tv1b,",
+			"\tv1c",
+			"k2 = v2a,",
+			"\tv2b,",
+			"\tv2c"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		assertEquals("", join(cm.getEntry("S1", "k1").getPreLines(), '|'));
+		assertEquals("", join(cm.getEntry("S1", "k2").getPreLines(), '|'));
+
+		assertTextEquals("[S1]|k1 = v1a,|\tv1b,|\tv1c|k2 = v2a,|\tv2b,|\tv2c|", cm);
+
+		assertEquals("v1a,\nv1b,\nv1c", cm.getEntry("S1", "k1").getValue());
+		assertEquals("v2a,\nv2b,\nv2c", cm.getEntry("S1", "k2").getValue());
+		
+		// Round trip.
+		s.update("Foo", cm.toString());
+		cm = s.getMap("Foo");
+		assertTextEquals("[S1]|k1 = v1a,|\tv1b,|\tv1c|k2 = v2a,|\tv2b,|\tv2c|", cm);
+	}
+
+	@Test
+	public void testMultipleLinesInSectionWithSpacesAndPrelines() throws Exception {
+		Store s = initStore("Foo",
+			"",
+			"#S1",
+			"",
+			"[S1]",
+			"",
+			"#k1",
+			"",
+			"k1 = v1a,",
+			"\tv1b,",
+			"\tv1c",
+			"",
+			"#k2",
+			"",
+			"k2 = v2a,",
+			"\tv2b,",
+			"\tv2c"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		assertEquals("|#S1|", join(cm.getPreLines("S1"), '|'));
+		assertEquals("|#k1|", join(cm.getEntry("S1", "k1").getPreLines(), '|'));
+		assertEquals("|#k2|", join(cm.getEntry("S1", "k2").getPreLines(), '|'));
+
+		assertTextEquals("|#S1||[S1]||#k1||k1 = v1a,|	v1b,|	v1c||#k2||k2 = v2a,|	v2b,|	v2c|", cm);
+
+		assertEquals("v1a,\nv1b,\nv1c", cm.getEntry("S1", "k1").getValue());
+		assertEquals("v2a,\nv2b,\nv2c", cm.getEntry("S1", "k2").getValue());
+		
+		// Round trip.
+		s.update("Foo", cm.toString());
+		cm = s.getMap("Foo");
+		assertTextEquals("|#S1||[S1]||#k1||k1 = v1a,|	v1b,|	v1c||#k2||k2 = v2a,|	v2b,|	v2c|", cm);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Entry lines can have trailing comments.
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testEntriesWithComments() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = foo # comment"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		assertTextEquals("[S1]|k1 = foo # comment|", cm);
+		assertEquals("foo", cm.getEntry("S1", "k1").getValue());
+		assertEquals("comment", cm.getEntry("S1", "k1").getComment());
+		
+		cm.setComment("S1", "k1", "newcomment");
+		assertTextEquals("[S1]|k1 = foo # newcomment|", cm);
+		assertEquals("foo", cm.getEntry("S1", "k1").getValue());
+		assertEquals("newcomment", cm.getEntry("S1", "k1").getComment());
+		
+		cm.setComment("S1", "k1", "");
+		assertTextEquals("[S1]|k1 = foo # |", cm);
+		assertEquals("foo", cm.getEntry("S1", "k1").getValue());
+		assertEquals("", cm.getEntry("S1", "k1").getComment());
+		
+		cm.setComment("S1", "k1", null);
+		assertTextEquals("[S1]|k1 = foo|", cm);
+		assertEquals("foo", cm.getEntry("S1", "k1").getValue());
+		assertEquals(null, cm.getEntry("S1", "k1").getComment());
+	}
+	
+	@Test
+	public void testEntriesWithOddComments() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = foo#",
+			"k2 = foo # "
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		assertTextEquals("[S1]|k1 = foo#|k2 = foo # |", cm);
+		assertEquals("", cm.getEntry("S1", "k1").getComment());
+		assertEquals("", cm.getEntry("S1", "k2").getComment());
+	}
+	
+	@Test
+	public void testEntriesWithEscapedComments() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = foo\\#bar",
+			"k2 = foo \\# bar",
+			"k3 = foo \\# bar # real-comment"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		assertTextEquals("[S1]|k1 = foo\\#bar|k2 = foo \\# bar|k3 = foo \\# bar # real-comment|", cm);
+		
+		assertEquals(null, cm.getEntry("S1", "k1").getComment());
+		assertEquals(null, cm.getEntry("S1", "k2").getComment());
+		assertEquals("real-comment", cm.getEntry("S1", "k3").getComment());
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// Test setting entries.
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSettingEntries() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1a",
+			"k2 = v2a"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setValue("S1", "k1", "v1b");
+		cm.setValue("S1", "k2", null);
+		cm.setValue("S1", "k3", "v3b");
+		
+		assertTextEquals("[S1]|k1 = v1b|k3 = v3b|", cm);
+		
+		cm.save();
+		assertTextEquals("[S1]|k1 = v1b|k3 = v3b|", s.read("Foo"));
+		
+		// Round trip.
+		cm = s.getMap("Foo");
+		assertTextEquals("[S1]|k1 = v1b|k3 = v3b|", cm);
+	}
+	
+	@Test
+	public void testSettingEntriesWithPreLines() throws Exception {
+		Store s = initStore("Foo",
+			"",
+			"#S1",
+			"",
+			"[S1]",
+			"",
+			"#k1",
+			"",
+			"k1 = v1a",
+			"",
+			"#k2",
+			"",
+			"k2 = v2a"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setValue("S1", "k1", "v1b");
+		cm.setValue("S1", "k2", null);
+		cm.setValue("S1", "k3", "v3b");
+		cm.setEntry("S1", "k4", "v4b", null, null, Arrays.asList("","#k4",""));
+		
+		assertTextEquals("|#S1||[S1]||#k1||k1 = v1b|k3 = v3b||#k4||k4 = v4b|", cm);
+		
+		cm.save();
+		assertTextEquals("|#S1||[S1]||#k1||k1 = v1b|k3 = v3b||#k4||k4 = v4b|", s.read("Foo"));
+		
+		// Round trip.
+		cm = s.getMap("Foo");
+		assertTextEquals("|#S1||[S1]||#k1||k1 = v1b|k3 = v3b||#k4||k4 = v4b|", cm);
+	}
+	
+	@Test
+	public void testSettingEntriesWithNewlines() throws Exception {
+		Store s = initStore("Foo");		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setValue("default", "k", "v1\nv2\nv3");
+		cm.setValue("S1", "k1", "v1\nv2\nv3");
+		
+		assertTextEquals("k = v1|	v2|	v3|[S1]|k1 = v1|	v2|	v3|", cm);
+		
+		assertEquals("v1\nv2\nv3", cm.getEntry("default", "k").getValue());
+		assertEquals("v1\nv2\nv3", cm.getEntry("S1", "k1").getValue());
+		cm.save();
+		assertTextEquals("k = v1|	v2|	v3|[S1]|k1 = v1|	v2|	v3|", cm);
+		
+		// Round trip.
+		cm = s.getMap("Foo");
+		assertTextEquals("k = v1|	v2|	v3|[S1]|k1 = v1|	v2|	v3|", cm);
+	}
+	
+	@Test
+	public void testSettingEntriesWithNewlinesAndSpaces() throws Exception {
+		Store s = initStore("Foo");		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setValue("default", "k", "v1 \n v2 \n v3");
+		cm.setValue("S1", "k1", "v1\t\n\tv2\t\n\tv3");
+		
+		assertTextEquals("k = v1 |	 v2 |	 v3|[S1]|k1 = v1	|		v2	|		v3|", cm);
+		
+		assertEquals("v1 \n v2 \n v3", cm.getEntry("default", "k").getValue());
+		assertEquals("v1\t\n\tv2\t\n\tv3", cm.getEntry("S1", "k1").getValue());
+		cm.save();
+		assertTextEquals("k = v1 |	 v2 |	 v3|[S1]|k1 = v1	|		v2	|		v3|", cm);
+		
+		// Round trip.
+		cm = s.getMap("Foo");
+		assertTextEquals("k = v1 |	 v2 |	 v3|[S1]|k1 = v1	|		v2	|		v3|", cm);
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// setSection()
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSetSectionOnExistingSection() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setSection("S1", Arrays.asList("#S1"));
+		assertTextEquals("#S1|[S1]|k1 = v1|", cm);
+		cm.setSection("S1", Collections.<String>emptyList());
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setSection("S1", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+	}
+	
+	@Test
+	public void testSetSectionOnDefaultSection() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setSection("default", Arrays.asList("#D"));
+		assertTextEquals("#D||[S1]|k1 = v1|", cm);
+		cm.setSection("default", Collections.<String>emptyList());
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setSection("default", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+	}
+
+	@Test
+	public void testSetSectionOnNewSection() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setSection("S2", Arrays.asList("#S2"));
+		assertTextEquals("[S1]|k1 = v1|#S2|[S2]|", cm);
+		cm.setSection("S3", Collections.<String>emptyList());
+		assertTextEquals("[S1]|k1 = v1|#S2|[S2]|[S3]|", cm);
+		cm.setSection("S4", null);
+		assertTextEquals("[S1]|k1 = v1|#S2|[S2]|[S3]|[S4]|", cm);
+	}
+
+	@Test
+	public void testSetSectionBadNames() throws Exception {
+		Store s = initStore("Foo");		
+		ConfigMap cm = s.getMap("Foo");
+		
+		String[] test = {
+			"/", "[", "]",
+			"foo/bar", "foo[bar", "foo]bar", 
+			"", " ",
+			null
+		};
+		
+		for (String t : test) {
+			try {
+				cm.setSection(t, null);
+				fail("Exception expected.");
+			} catch (ConfigException e) {
+				assertTrue(e.getLocalizedMessage().startsWith("Invalid section name"));
+			}
+		}
+	}
+	
+	@Test
+	public void testSetSectionOkNames() throws Exception {
+		Store s = initStore("Foo");		
+		ConfigMap cm = s.getMap("Foo");
+
+		// These are all okay characters to use in section names.
+		String validChars = "~`!@#$%^&*()_-+={}|:;\"\'<,>.?";
+		
+		for (char c : validChars.toCharArray()) {
+			String test = ""+c;
+			cm.setSection(test, Arrays.asList("test"));
+			cm.save();
+			assertEquals("test", cm.getPreLines(test).get(0));
+			
+			test = "foo"+c+"bar";
+			cm.setSection(test, Arrays.asList("test"));
+			cm.save();
+			assertEquals("test", cm.getPreLines(test).get(0));
+		}
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// removeSection()
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testRemoveSectionOnExistingSection() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1",
+			"[S2]",
+			"k2 = v2"
+			
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.removeSection("S1");
+		assertTextEquals("[S2]|k2 = v2|", cm);
+	}
+	
+	@Test
+	public void testRemoveSectionOnNonExistingSection() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1",
+			"[S2]",
+			"k2 = v2"
+			
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.removeSection("S3");
+		cm.removeSection("");
+		cm.removeSection(null);
+		assertTextEquals("[S1]|k1 = v1|[S2]|k2 = v2|", cm);
+	}
+
+	@Test
+	public void testRemoveDefaultSection() throws Exception {
+		Store s = initStore("Foo",
+			"k = v",
+			"[S1]",
+			"k1 = v1",
+			"[S2]",
+			"k2 = v2"
+			
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.removeSection("default");
+		assertTextEquals("[S1]|k1 = v1|[S2]|k2 = v2|", cm);
+	}
+	
+	@Test
+	public void testRemoveDefaultSectionWithComments() throws Exception {
+		Store s = initStore("Foo",
+			"#D",
+			"",
+			"#k",
+			"k = v",
+			"[S1]",
+			"k1 = v1",
+			"[S2]",
+			"k2 = v2"
+			
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.removeSection("default");
+		assertTextEquals("[S1]|k1 = v1|[S2]|k2 = v2|", cm);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// setPreLines()
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSetPrelinesOnExistingEntry() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setPreLines("S1", "k1", Arrays.asList("#k1"));
+		assertTextEquals("[S1]|#k1|k1 = v1|", cm);
+		cm.setPreLines("S1", "k1", Collections.<String>emptyList());
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setPreLines("S1", "k1", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+	}
+	
+	@Test
+	public void testSetPrelinesOnExistingEntryWithAtrributes() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"#k1a",
+			"k1 = v1 # comment"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setPreLines("S1", "k1", Arrays.asList("#k1b"));
+		assertTextEquals("[S1]|#k1b|k1 = v1 # comment|", cm);
+	}
+
+	@Test
+	public void testSetPrelinesOnNonExistingEntry() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setPreLines("S1", "k2", Arrays.asList("#k2"));
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setPreLines("S1", "k2", Collections.<String>emptyList());
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setPreLines("S1", "k2", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		
+		cm.setPreLines("S2", "k2", Arrays.asList("#k2"));
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setPreLines("S2", "k2", Collections.<String>emptyList());
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setPreLines("S2", "k2", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+
+		cm.setPreLines("S1", null, Arrays.asList("#k2"));
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setPreLines("S1", null, Collections.<String>emptyList());
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setPreLines("S1", null, null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+
+		cm.setPreLines(null, "k2", Arrays.asList("#k2"));
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setPreLines(null, "k2", Collections.<String>emptyList());
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setPreLines(null, "k2", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+	}
+
+	//-----------------------------------------------------------------------------------------------------------------
+	// setValue()
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSetValueOnExistingEntry() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setValue("S1", "k1", "v2");
+		assertTextEquals("[S1]|k1 = v2|", cm);
+	}
+	
+	@Test
+	public void testSetValueOnExistingEntryWithAttributes() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"#k1",
+			"k1 = v1 # comment"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setValue("S1", "k1", "v2");
+		assertTextEquals("[S1]|#k1|k1 = v2 # comment|", cm);
+	}
+
+	@Test
+	public void testSetValueToNullOnExistingEntry() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setValue("S1", "k1", null);
+		assertTextEquals("[S1]|", cm);
+	}
+
+	@Test
+	public void testSetValueOnNonExistingEntry() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setValue("S1", "k2", "v2");
+		assertTextEquals("[S1]|k1 = v1|k2 = v2|", cm);
+		cm.setValue("S1", "k2", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setValue("S1", "k2", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+	}
+	
+	@Test
+	public void testSetValueOnNonExistingEntryOnNonExistentSection() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setValue("S2", "k2", "v2");
+		assertTextEquals("[S1]|k1 = v1|[S2]|k2 = v2|", cm);
+	}
+
+	@Test
+	public void testSetValueInvalidSectionNames() throws Exception {
+		Store s = initStore("Foo");		
+		ConfigMap cm = s.getMap("Foo");
+		
+		String[] test = {
+			"/", "[", "]",
+			"foo/bar", "foo[bar", "foo]bar", 
+			"", " ",
+			null
+		};
+		
+		for (String t : test) {
+			try {
+				cm.setValue(t, "k1", "foo");
+				fail("Exception expected.");
+			} catch (ConfigException e) {
+				assertTrue(e.getLocalizedMessage().startsWith("Invalid section name"));
+			}
+		}
+	}
+
+	@Test
+	public void testSetValueInvalidKeyNames() throws Exception {
+		Store s = initStore("Foo");		
+		ConfigMap cm = s.getMap("Foo");
+		
+		String[] test = {
+			"", " ", "\t",
+			"foo=bar", "=",
+			"foo/bar", "/",
+			"foo[bar", "]",
+			"foo]bar", "]",
+			"foo\\bar", "\\",
+			"foo#bar", "#",
+			null
+		};
+		
+		for (String t : test) {
+			try {
+				cm.setValue("S1", t, "foo");
+				fail("Exception expected.");
+			} catch (ConfigException e) {
+				assertTrue(e.getLocalizedMessage().startsWith("Invalid key name"));
+			}
+		}
+	}
+	
+	@Test
+	public void testSetValueWithCommentChars() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		// If value has # in it, it should get escaped.
+		cm.setValue("S1", "k1", "v1 # foo");
+		assertTextEquals("[S1]|k1 = v1 \\# foo|", cm);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// setComment()
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSetCommentOnExistingEntry() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setComment("S1", "k1", "c1");
+		assertTextEquals("[S1]|k1 = v1 # c1|", cm);
+
+		cm.setComment("S1", "k1", "");
+		assertTextEquals("[S1]|k1 = v1 # |", cm);
+		cm.save();
+		assertTextEquals("[S1]|k1 = v1 # |", cm);
+		
+		cm.setComment("S1", "k1", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+	}
+	
+	@Test
+	public void testSetCommentOnExistingEntryWithAttributes() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"#k1a",
+			"k1 = v1 # c1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setComment("S1", "k1", "c2");
+		assertTextEquals("[S1]|#k1a|k1 = v1 # c2|", cm);
+	}
+
+	@Test
+	public void testSetCommentOnNonExistingEntry() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setComment("S1", "k2", "foo");
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setComment("S1", "k2", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		
+		cm.setComment("S2", "k2", "foo");
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setComment("S2", "k2", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+
+		cm.setComment("S1", null, "foo");
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setComment("S1", null, null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+
+		cm.setComment(null, "k2", "foo");
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setComment(null, "k2", null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// setValue()
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testSetEntryOnExistingEntry() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setEntry("S1", "k1", "v2", null, null, null);
+		assertTextEquals("[S1]|k1 = v2|", cm);
+
+		cm.setEntry("S1", "k1", "v3", ENCODED, "c3", Arrays.asList("#k1a"));
+		assertTextEquals("[S1]|#k1a|k1* = v3 # c3|", cm);
+
+		cm.setEntry("S1", "k1", "v4", BASE64, "c4", Arrays.asList("#k1b"));
+		assertTextEquals("[S1]|#k1b|k1^ = v4 # c4|", cm);
+	}
+	
+	@Test
+	public void testSetEntryOnExistingEntryWithAttributes() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"#k1",
+			"k1 = v1 # comment"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setEntry("S1", "k1", "v2", null, null, null);
+		assertTextEquals("[S1]|k1 = v2|", cm);
+
+		cm.setEntry("S1", "k1", "v3", ENCODED, "c3", Arrays.asList("#k1a"));
+		assertTextEquals("[S1]|#k1a|k1* = v3 # c3|", cm);
+
+		cm.setEntry("S1", "k1", "v4", BASE64, "c4", Arrays.asList("#k1b"));
+		assertTextEquals("[S1]|#k1b|k1^ = v4 # c4|", cm);
+	}
+
+	@Test
+	public void testSetEntryToNullOnExistingEntry() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setEntry("S1", "k1", null, null, null, null);
+		assertTextEquals("[S1]|", cm);
+
+		cm.setEntry("S1", "k1", null, ENCODED, "c3", Arrays.asList("#k1a"));
+		assertTextEquals("[S1]|", cm);
+
+		cm.setEntry("S1", "k1", null, BASE64, "c4", Arrays.asList("#k1b"));
+		assertTextEquals("[S1]|", cm);
+	}
+	
+	@Test
+	public void testSetEntryOnNonExistingEntry() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setEntry("S1", "k2", "v2", null, null, null);
+		assertTextEquals("[S1]|k1 = v1|k2 = v2|", cm);
+		cm.setEntry("S1", "k2", null, null, null, null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+		cm.setEntry("S1", "k2", null, null, null, null);
+		assertTextEquals("[S1]|k1 = v1|", cm);
+	}
+	
+	@Test
+	public void testSetEntryOnNonExistingEntryOnNonExistentSection() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		cm.setEntry("S2", "k2", "v2", null, null, null);
+		assertTextEquals("[S1]|k1 = v1|[S2]|k2 = v2|", cm);
+	}
+
+	@Test
+	public void testSetEntryInvalidSectionNames() throws Exception {
+		Store s = initStore("Foo");		
+		ConfigMap cm = s.getMap("Foo");
+		
+		String[] test = {
+			"/", "[", "]",
+			"foo/bar", "foo[bar", "foo]bar", 
+			"", " ",
+			null
+		};
+		
+		for (String t : test) {
+			try {
+				cm.setEntry(t, "k1", "foo", null, null, null);
+				fail("Exception expected.");
+			} catch (ConfigException e) {
+				assertTrue(e.getLocalizedMessage().startsWith("Invalid section name"));
+			}
+		}
+	}
+
+	@Test
+	public void testSetEntryInvalidKeyNames() throws Exception {
+		Store s = initStore("Foo");		
+		ConfigMap cm = s.getMap("Foo");
+		
+		String[] test = {
+			"", " ", "\t",
+			"foo=bar", "=",
+			"foo/bar", "/",
+			"foo[bar", "]",
+			"foo]bar", "]",
+			"foo\\bar", "\\",
+			"foo#bar", "#",
+			null
+		};
+		
+		for (String t : test) {
+			try {
+				cm.setEntry("S1", t, "foo", null, null, null);
+				fail("Exception expected.");
+			} catch (ConfigException e) {
+				assertTrue(e.getLocalizedMessage().startsWith("Invalid key name"));
+			}
+		}
+	}
+	
+	@Test
+	public void testSetEntryWithCommentChars() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1 = v1"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		// If value has # in it, it should get escaped.
+		cm.setEntry("S1", "k1", "v1 # foo", null, null, null);
+		assertTextEquals("[S1]|k1 = v1 \\# foo|", cm);
+	}
+	
+	//-----------------------------------------------------------------------------------------------------------------
+	// Modifiers
+	//-----------------------------------------------------------------------------------------------------------------
+	@Test
+	public void testModifiers() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1^ = v1",
+			"k2* = v2",
+			"k3*^ = v3"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		assertTextEquals("[S1]|k1^ = v1|k2* = v2|k3*^ = v3|", cm);
+		assertTrue(cm.getEntry("S1", "k1").hasModifier('^'));
+		assertFalse(cm.getEntry("S1", "k1").hasModifier('*'));
+		assertFalse(cm.getEntry("S1", "k2").hasModifier('^'));
+		assertTrue(cm.getEntry("S1", "k2").hasModifier('*'));
+		assertTrue(cm.getEntry("S1", "k3").hasModifier('^'));
+		assertTrue(cm.getEntry("S1", "k3").hasModifier('*'));
+		
+		cm.setEntry("S1", "k1", "v1", "#$%&*+^@~", null, null);
+		assertTextEquals("[S1]|k1#$%&*+^@~ = v1|k2* = v2|k3*^ = v3|", cm);
+	}
+	
+	@Test
+	public void testInvalidModifier() throws Exception {
+		Store s = initStore("Foo",
+			"[S1]",
+			"k1^ = v1",
+			"k2* = v2",
+			"k3*^ = v3"
+		);		
+		ConfigMap cm = s.getMap("Foo");
+		
+		// This is okay.
+		cm.setEntry("S1", "k1", "v1", "", null, null);
+
+		try {
+			cm.setEntry("S1", "k1", "v1", "X", null, null);
+			fail("Exception expected.");
+		} catch (ConfigException e) {
+			assertEquals("Invalid modifiers: X", e.getLocalizedMessage());
+		}
+
+		try {
+			cm.setEntry("S1", "k1", "v1", " ", null, null);
+			fail("Exception expected.");
+		} catch (ConfigException e) {
+			assertEquals("Invalid modifiers:  ", e.getLocalizedMessage());
+		}
+	}
+
+	private static Store initStore(String name, String...contents) {
+		return MemoryStore.create().build().update(name, contents);
+	}
+}
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/store/FileStoreTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/store/FileStoreTest.java
index 97b76e9..2e2afbf 100644
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/store/FileStoreTest.java
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/store/FileStoreTest.java
@@ -46,7 +46,7 @@ public class FileStoreTest {
 	@Test
 	public void testSimpleCreate() throws Exception {
 		FileStore fs = FileStore.create().directory(DIR).build();
-		assertTrue(fs.write("X", null, "foo"));
+		assertNull(fs.write("X", null, "foo"));
 		assertEquals("foo", fs.read("X"));
 		assertFileExists("X.cfg");
 	}
@@ -54,21 +54,21 @@ public class FileStoreTest {
 	@Test
 	public void testFailOnMismatch() throws Exception {
 		FileStore fs = FileStore.create().directory(DIR).build();
-		assertFalse(fs.write("X", "xxx", "foo"));
+		assertNotNull(fs.write("X", "xxx", "foo"));
 		assertEquals(null, fs.read("X"));
 		assertFileNotExists("X.cfg");
-		assertTrue(fs.write("X", null, "foo"));
+		assertNull(fs.write("X", null, "foo"));
 		assertEquals("foo", fs.read("X"));
-		assertFalse(fs.write("X", "xxx", "foo"));
+		assertNotNull(fs.write("X", "xxx", "foo"));
 		assertEquals("foo", fs.read("X"));
-		assertTrue(fs.write("X", "foo", "bar"));
+		assertNull(fs.write("X", "foo", "bar"));
 		assertEquals("bar", fs.read("X"));
 	}
 	
 	@Test
 	public void testCharset() throws Exception {
 		FileStore fs = FileStore.create().directory(DIR).charset("UTF-8").build();
-		assertTrue(fs.write("X", null, "foo"));
+		assertNull(fs.write("X", null, "foo"));
 		assertEquals("foo", fs.read("X"));
 	}		
 	
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/store/MemoryStoreTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/store/MemoryStoreTest.java
index ac38dff..990c5c9 100644
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/store/MemoryStoreTest.java
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/config/store/MemoryStoreTest.java
@@ -29,20 +29,20 @@ public class MemoryStoreTest {
 	@Test
 	public void testSimpleCreate() throws Exception {
 		MemoryStore fs = MemoryStore.create().build();
-		assertTrue(fs.write("X", null, "foo"));
+		assertNull(fs.write("X", null, "foo"));
 		assertEquals("foo", fs.read("X"));
 	}
 
 	@Test
 	public void testFailOnMismatch() throws Exception {
 		MemoryStore fs = MemoryStore.create().build();
-		assertFalse(fs.write("X", "xxx", "foo"));
+		assertNotNull(fs.write("X", "xxx", "foo"));
 		assertEquals(null, fs.read("X"));
-		assertTrue(fs.write("X", null, "foo"));
+		assertNull(fs.write("X", null, "foo"));
 		assertEquals("foo", fs.read("X"));
-		assertFalse(fs.write("X", "xxx", "foo"));
+		assertNotNull(fs.write("X", "xxx", "foo"));
 		assertEquals("foo", fs.read("X"));
-		assertTrue(fs.write("X", "foo", "bar"));
+		assertNull(fs.write("X", "foo", "bar"));
 		assertEquals("bar", fs.read("X"));
 	}
 	
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/serializer/TestURI.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/serializer/TestURI.java
index afea736..801bded 100755
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/serializer/TestURI.java
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/serializer/TestURI.java
@@ -15,8 +15,8 @@ package org.apache.juneau.serializer;
 import java.net.URI;
 
 import org.apache.juneau.annotation.*;
+import org.apache.juneau.internal.*;
 import org.apache.juneau.jena.annotation.*;
-import org.apache.juneau.xml.*;
 import org.apache.juneau.xml.annotation.*;
 
 @Bean(sort=true)
@@ -69,7 +69,7 @@ public class TestURI {
 	@org.apache.juneau.annotation.URI
 	public String
 		f3a = "http://www.apache.org/f3a/x?label=MY_LABEL&foo=bar",
-		f3b = XmlUtils.urlEncode("<>&'\""),
+		f3b = StringUtils.urlEncode("<>&'\""),
 		f3c = "<>&'\"";  // Invalid URI, but should produce parsable output.
 
 	// @URI on bean
diff --git a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/utils/StringUtilsTest.java b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
index d3b2650..c670c75 100755
--- a/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
+++ b/juneau-core/juneau-core-test/src/test/java/org/apache/juneau/utils/StringUtilsTest.java
@@ -799,4 +799,45 @@ public class StringUtilsTest {
 		assertObjectEquals("['\"foo\"']", splitQuoted("'\"foo\"'"));
 		assertObjectEquals("['\\'foo\\'']", splitQuoted("\"'foo'\""));
 	}
+	
+	//====================================================================================================
+	// firstNonWhitespaceChar(String)
+	//====================================================================================================
+	@Test
+	public void testFirstNonWhitespaceChar() {
+		assertEquals('f', firstNonWhitespaceChar("foo"));
+		assertEquals('f', firstNonWhitespaceChar(" foo"));
+		assertEquals('f', firstNonWhitespaceChar("\tfoo"));
+		assertEquals(0, firstNonWhitespaceChar(""));
+		assertEquals(0, firstNonWhitespaceChar(" "));
+		assertEquals(0, firstNonWhitespaceChar("\t"));
+		assertEquals(0, firstNonWhitespaceChar(null));
+	}
+
+	//====================================================================================================
+	// lastNonWhitespaceChar(String)
+	//====================================================================================================
+	@Test
+	public void testLastNonWhitespaceChar() {
+		assertEquals('r', lastNonWhitespaceChar("bar"));
+		assertEquals('r', lastNonWhitespaceChar(" bar "));
+		assertEquals('r', lastNonWhitespaceChar("\tbar\t"));
+		assertEquals(0, lastNonWhitespaceChar(""));
+		assertEquals(0, lastNonWhitespaceChar(" "));
+		assertEquals(0, lastNonWhitespaceChar("\t"));
+		assertEquals(0, lastNonWhitespaceChar(null));
+	}
+
+	//====================================================================================================
+	// testSplitEqually(String,int)
+	//====================================================================================================
+	@Test
+	public void testSplitEqually() {
+		assertNull(null, splitEqually(null, 3));
+		assertEquals("", join(splitEqually("", 3), '|'));
+		assertEquals("a", join(splitEqually("a", 3), '|'));
+		assertEquals("ab", join(splitEqually("ab", 3), '|'));
+		assertEquals("abc", join(splitEqually("abc", 3), '|'));
+		assertEquals("abc|d", join(splitEqually("abcd", 3), '|'));
+	}
 }
diff --git a/juneau-core/juneau-marshall-rdf/src/main/java/org/apache/juneau/jena/RdfSerializer.java b/juneau-core/juneau-marshall-rdf/src/main/java/org/apache/juneau/jena/RdfSerializer.java
index a0addab..c3ed0ff 100644
--- a/juneau-core/juneau-marshall-rdf/src/main/java/org/apache/juneau/jena/RdfSerializer.java
+++ b/juneau-core/juneau-marshall-rdf/src/main/java/org/apache/juneau/jena/RdfSerializer.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.jena;
 
 import static org.apache.juneau.jena.Constants.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 
 import java.util.*;
 
@@ -352,7 +353,7 @@ public class RdfSerializer extends WriterSerializer implements RdfCommon {
 		for (String k : getPropertyKeys("RdfCommon")) 
 			if (k.startsWith("jena."))
 				m.put(k.substring(5), getProperty(k));
-		jenaSettings = Collections.unmodifiableMap(m);
+		jenaSettings = unmodifiableMap(m);
 	}
 	
 	/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanContext.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanContext.java
index 36952be..c5e9fbc 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanContext.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanContext.java
@@ -14,6 +14,7 @@ package org.apache.juneau;
 
 import static org.apache.juneau.Visibility.*;
 import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
 
 import java.io.*;
@@ -1850,12 +1851,12 @@ public class BeanContext extends Context {
 		Map<String,String[]> m2 = new HashMap<>();
 		for (Map.Entry<String,String> e : getMapProperty(BEAN_includeProperties, String.class).entrySet())
 			m2.put(e.getKey(), StringUtils.split(e.getValue()));
-		includeProperties = Collections.unmodifiableMap(m2);
+		includeProperties = unmodifiableMap(m2);
 
 		m2 = new HashMap<>();
 		for (Map.Entry<String,String> e : getMapProperty(BEAN_excludeProperties, String.class).entrySet())
 			m2.put(e.getKey(), StringUtils.split(e.getValue()));
-		excludeProperties = Collections.unmodifiableMap(m2);
+		excludeProperties = unmodifiableMap(m2);
 
 		locale = getInstanceProperty(BEAN_locale, Locale.class, null);
 		timeZone = getInstanceProperty(BEAN_timeZone, TimeZone.class, null);
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
index 89239ef..309f669 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMap.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau;
 
+import static org.apache.juneau.internal.StringUtils.*;
+
 import java.io.*;
 import java.lang.reflect.*;
 import java.util.*;
@@ -290,7 +292,7 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T
 	 */
 	@Override /* Map */
 	public Object get(Object property) {
-		String pName = StringUtils.toString(property);
+		String pName = asString(property);
 		BeanPropertyMeta p = getPropertyMeta(pName);
 		if (p == null)
 			return null;
@@ -307,7 +309,7 @@ public class BeanMap<T> extends AbstractMap<String,Object> implements Delegate<T
 	 * @return The raw property value.
 	 */
 	public Object getRaw(Object property) {
-		String pName = StringUtils.toString(property);
+		String pName = asString(property);
 		BeanPropertyMeta p = getPropertyMeta(pName);
 		if (p == null)
 			return null;
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
index c93f741..1bf3d5d 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanMeta.java
@@ -14,6 +14,7 @@ package org.apache.juneau;
 
 import static org.apache.juneau.Visibility.*;
 import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.internal.ReflectionUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
 
@@ -118,11 +119,11 @@ public class BeanMeta<T> {
 
 		this.beanFilter = beanFilter;
 		this.dictionaryName = b.dictionaryName;
-		this.properties = b.properties == null ? null : Collections.unmodifiableMap(b.properties);
-		this.getterProps = Collections.unmodifiableMap(b.getterProps);
-		this.setterProps = Collections.unmodifiableMap(b.setterProps);
+		this.properties = unmodifiableMap(b.properties);
+		this.getterProps = unmodifiableMap(b.getterProps);
+		this.setterProps = unmodifiableMap(b.setterProps);
 		this.dynaProperty = b.dynaProperty;
-		this.typeVarImpls = b.typeVarImpls == null ? null : Collections.unmodifiableMap(b.typeVarImpls);
+		this.typeVarImpls = unmodifiableMap(b.typeVarImpls);
 		this.constructor = b.constructor;
 		this.constructorArgs = b.constructorArgs;
 		this.extMeta = b.extMeta;
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanRegistry.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanRegistry.java
index 99fdc36..aa8765b 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanRegistry.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/BeanRegistry.java
@@ -13,13 +13,13 @@
 package org.apache.juneau;
 
 import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
 
 import java.lang.reflect.*;
 import java.util.*;
 import java.util.concurrent.*;
 
 import org.apache.juneau.annotation.*;
-import org.apache.juneau.internal.*;
 
 /**
  * A lookup table for resolving bean types by name.
@@ -75,7 +75,7 @@ public class BeanRegistry {
 				} else if (isParentClass(Map.class, c)) {
 					Map<?,?> m = beanContext.newInstance(Map.class, c);
 					for (Map.Entry<?,?> e : m.entrySet()) {
-						String typeName = StringUtils.toString(e.getKey());
+						String typeName = asString(e.getKey());
 						Object v = e.getValue();
 						ClassMeta<?> val = null;
 						if (v instanceof Type)
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ObjectMap.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ObjectMap.java
index a5aa0aa..a41239c 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ObjectMap.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/ObjectMap.java
@@ -722,7 +722,7 @@ public class ObjectMap extends LinkedHashMap<String,Object> {
 		else if (s instanceof Object[])
 			r = ArrayUtils.toStringArray(Arrays.asList((Object[])s));
 		else
-			r = split(StringUtils.toString(s));
+			r = split(asString(s));
 		return (r.length == 0 ? def : r);
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore.java
index b861c91..7513f8f 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/PropertyStore.java
@@ -13,6 +13,7 @@
 package org.apache.juneau;
 
 import static org.apache.juneau.PropertyType.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 
 import java.lang.reflect.*;
 import java.util.*;
@@ -836,7 +837,7 @@ public final class PropertyStore {
 						throw new ConfigException("Invalid property conversion ''{0}'' to ''List<{1}>'' on property ''{2}''", type, eType, name);
 					l.add(t);
 				}
-				return Collections.unmodifiableList(l); 
+				return unmodifiableList(l); 
 			} else {
 				throw new ConfigException("Invalid property conversion ''{0}'' to ''List<{1}>'' on property ''{2}''", type, eType, name);
 			}
@@ -857,7 +858,7 @@ public final class PropertyStore {
 						throw new ConfigException("Invalid property conversion ''{0}'' to ''Map<String,{1}>'' on property ''{2}''", type, eType, name);
 					m.put(e.getKey(), t);
 				}
-				return Collections.unmodifiableMap(m); 
+				return unmodifiableMap(m); 
 			} else {
 				throw new ConfigException("Invalid property conversion ''{0}'' to ''Map<String,{1}>'' on property ''{2}''", type, eType, name);
 			}
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriResolver.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriResolver.java
index 98c53a4..3edf117 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriResolver.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/UriResolver.java
@@ -19,8 +19,6 @@ import static org.apache.juneau.internal.StringUtils.*;
 import java.io.*;
 import java.net.*;
 
-import org.apache.juneau.internal.*;
-
 /**
  * Class used to create absolute and root-relative URIs based on your current URI 'location' and rules about how to
  * make such resolutions.
@@ -105,7 +103,7 @@ public class UriResolver {
 	}
 
 	private String resolve(Object uri, UriResolution res) {
-		String s = StringUtils.toString(uri);
+		String s = asString(uri);
 		if (isAbsoluteUri(s))
 			return hasDotSegments(s) && res != NONE ? normalize(s) : s;
 		if (res == ROOT_RELATIVE && startsWith(s, '/'))
@@ -149,7 +147,7 @@ public class UriResolver {
 	public Appendable append(Appendable a, Object o) {
 
 		try {
-			String uri = StringUtils.toString(o);
+			String uri = asString(o);
 			uri = nullIfEmpty(uri);
 			boolean needsNormalize = hasDotSegments(uri) && resolution != null;
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/encoders/EncoderGroup.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/encoders/EncoderGroup.java
index 1fae662..baa2f20 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/encoders/EncoderGroup.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/encoders/EncoderGroup.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.encoders;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
+
 import java.util.*;
 import java.util.concurrent.*;
 
@@ -89,7 +91,7 @@ public final class EncoderGroup {
 	 * @param encoders The encoders to add to this group.
 	 */
 	public EncoderGroup(Encoder[] encoders) {
-		this.encoders = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(encoders)));
+		this.encoders = immutableList(encoders);
 
 		List<String> lc = new ArrayList<>();
 		List<Encoder> l = new ArrayList<>();
@@ -101,7 +103,7 @@ public final class EncoderGroup {
 		}
 
 		this.encodings = lc.toArray(new String[lc.size()]);
-		this.encodingsList = Collections.unmodifiableList(lc);
+		this.encodingsList = unmodifiableList(lc);
 		this.encodingsEncoders = l.toArray(new Encoder[l.size()]);
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
index 912b6f5..e574a0c 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/html/HtmlSerializerSession.java
@@ -14,7 +14,7 @@ package org.apache.juneau.html;
 
 import static org.apache.juneau.html.HtmlSerializer.*;
 import static org.apache.juneau.html.HtmlSerializerSession.ContentResult.*;
-import static org.apache.juneau.xml.XmlUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
 
 import java.io.*;
 import java.util.*;
@@ -427,7 +427,7 @@ public class HtmlSerializerSession extends XmlSerializerSession {
 				out.attr("style", style);
 			out.cTag();
 			if (link != null)
-				out.oTag(i+3, "a").attrUri("href", link.replace("{#}", StringUtils.toString(value))).cTag();
+				out.oTag(i+3, "a").attrUri("href", link.replace("{#}", asString(value))).cTag();
 			ContentResult cr = serializeAnything(out, key, keyType, null, 2, null, false);
 			if (link != null)
 				out.eTag("a");
@@ -646,7 +646,7 @@ public class HtmlSerializerSession extends XmlSerializerSession {
 					out.attr("style", style);
 				out.cTag();
 				if (link != null)
-					out.oTag(i+2, "a").attrUri("href", link.replace("{#}", StringUtils.toString(o))).cTag();
+					out.oTag(i+2, "a").attrUri("href", link.replace("{#}", asString(o))).cTag();
 				ContentResult cr = serializeAnything(out, o, eType.getElementType(), name, 1, null, false);
 				if (link != null)
 					out.eTag("a");
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/Accept.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/Accept.java
index 9feafa7..ca94279 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/Accept.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/Accept.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.http;
 
 import static org.apache.juneau.http.Constants.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
 
 import java.util.*;
@@ -167,7 +168,7 @@ public final class Accept {
 
 	private Accept(String value) {
 		this.mediaRanges = MediaTypeRange.parse(value);
-		this.mediaRangesList = Collections.unmodifiableList(Arrays.asList(mediaRanges));
+		this.mediaRangesList = immutableList(mediaRanges);
 	}
 
 	/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/HeaderRangeArray.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/HeaderRangeArray.java
index 87b82de..59ff680 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/HeaderRangeArray.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/HeaderRangeArray.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.http;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
+
 import java.util.*;
 
 import org.apache.juneau.internal.*;
@@ -42,7 +44,7 @@ public class HeaderRangeArray {
 	 */
 	protected HeaderRangeArray(String value) {
 		this.typeRanges = StringRange.parse(value);
-		this.typeRangesList = Collections.unmodifiableList(Arrays.asList(typeRanges));
+		this.typeRangesList = immutableList(typeRanges);
 	}
 
 	/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/MediaType.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/MediaType.java
index f320d49..f09eeba 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/MediaType.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/MediaType.java
@@ -12,6 +12,7 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.http;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
 
 import java.util.*;
@@ -125,8 +126,8 @@ public class MediaType implements Comparable<MediaType> {
 		this.subType = b.subType;
 		this.subTypes = b.subTypes;
 		this.subTypesSorted = b.subTypesSorted;
-		this.subTypesList = Collections.unmodifiableList(Arrays.asList(subTypes));
-		this.parameters = (b.parameters == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(b.parameters));
+		this.subTypesList = immutableList(subTypes);
+		this.parameters = unmodifiableMap(b.parameters);
 		this.hasSubtypeMeta = b.hasSubtypeMeta;
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/MediaTypeRange.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/MediaTypeRange.java
index c304a0c..ccbfd01 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/MediaTypeRange.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/MediaTypeRange.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.http;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
+
 import java.util.*;
 import java.util.Map.*;
 
@@ -92,7 +94,7 @@ public final class MediaTypeRange implements Comparable<MediaTypeRange>  {
 		Builder b = new Builder(token);
 		this.mediaType = b.mediaType;
 		this.qValue = b.qValue;
-		this.extensions = (b.extensions == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(b.extensions));
+		this.extensions = unmodifiableMap(b.extensions);
 	}
 
 	static final class Builder {
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/StringRange.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/StringRange.java
index fa0e282..e7d473f 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/StringRange.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/http/StringRange.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.http;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
+
 import java.util.*;
 import java.util.Map.*;
 
@@ -99,7 +101,7 @@ public final class StringRange implements Comparable<StringRange>  {
 		Builder b = new Builder(token);
 		this.type = b.type;
 		this.qValue = b.qValue;
-		this.extensions = (b.extensions == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(b.extensions));
+		this.extensions = unmodifiableMap(b.extensions);
 	}
 
 	static final class Builder {
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ArrayUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ArrayUtils.java
index 7c60001..dbcac53 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ArrayUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ArrayUtils.java
@@ -12,6 +12,7 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.internal;
 
+import static org.apache.juneau.internal.StringUtils.*;
 import static org.apache.juneau.internal.ThrowableUtils.*;
 
 import java.lang.reflect.*;
@@ -432,7 +433,7 @@ public final class ArrayUtils {
 		String[] r = new String[c.size()];
 		int i = 0;
 		for (Object o : c)
-			r[i++] = StringUtils.toString(o);
+			r[i++] = asString(o);
 		return r;
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/AsciiSet.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/AsciiSet.java
index 87439bd..f5a4717 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/AsciiSet.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/AsciiSet.java
@@ -69,4 +69,22 @@ public final class AsciiSet {
 				return true;
 		return false;
 	}
+
+	/**
+	 * Returns <jk>true</jk> if the specified string contains only characters in this set.
+	 * 
+	 * @param s The string to test.
+	 * @return 
+	 * 	<jk>true</jk> if the string contains only characters in this set.
+	 * 	<br>Nulls always return <jk>false</jk>.
+	 * 	<br>Blanks always return <jk>true</jk>.
+	 */
+	public boolean containsOnly(String s) {
+		if (s == null)
+			return false;
+		for (int i = 0; i < s.length(); i++)
+			if (! contains(s.charAt(i)))
+				return false;
+		return true;
+	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/BeanPropertyUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/BeanPropertyUtils.java
index 63b481c..f0219b6 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/BeanPropertyUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/BeanPropertyUtils.java
@@ -32,7 +32,7 @@ public final class BeanPropertyUtils {
 	 * @return The converted value, or <jk>null</jk> if the input was null.
 	 */
 	public static String toStringVal(Object o) {
-		return StringUtils.toString(o);
+		return StringUtils.asString(o);
 	}
 
 	/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/CollectionUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/CollectionUtils.java
index a71ea97..6d2d507 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/CollectionUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/CollectionUtils.java
@@ -174,6 +174,71 @@ public final class CollectionUtils {
 	}
 	
 	/**
+	 * Creates an immutable list from the specified collection.
+	 * 
+	 * @param l The collection to copy from.
+	 * @return An unmodifiable {@link ArrayList} copy of the collection, or a {@link Collections#emptyList()}
+	 * 	if the collection was empty or <jk>null</jk>.
+	 */
+	public static <T> List<T> immutableList(Collection<T> l) {
+		if (l == null || l.isEmpty())
+			return Collections.emptyList();
+		return Collections.unmodifiableList(new ArrayList<>(l));
+	}
+	
+	/**
+	 * Creates an unmodifiable list from the specified collection.
+	 * 
+	 * @param l The collection to copy from.
+	 * @return An unmodifiable view of the list, or a {@link Collections#emptyList()}
+	 * 	if the list was empty or <jk>null</jk>.
+	 */
+	public static <T> List<T> unmodifiableList(List<T> l) {
+		if (l == null || l.isEmpty())
+			return Collections.emptyList();
+		return Collections.unmodifiableList(l);
+	}
+
+	/**
+	 * Creates an immutable list from the specified array.
+	 * 
+	 * @param l The array to copy from.
+	 * @return An unmodifiable {@link ArrayList} copy of the collection, or a {@link Collections#emptyList()}
+	 * 	if the collection was empty or <jk>null</jk>.
+	 */
+	public static <T> List<T> immutableList(T[] l) {
+		if (l == null || l.length == 0)
+			return Collections.emptyList();
+		return Collections.unmodifiableList(new ArrayList<>(Arrays.asList(l)));
+	}
+	
+	/**
+	 * Creates an immutable map from the specified map.
+	 * 
+	 * @param m The map to copy from.
+	 * @return An unmodifiable {@link LinkedHashMap} copy of the collection, or a {@link Collections#emptyMap()}
+	 * 	if the collection was empty or <jk>null</jk>.
+	 */
+	public static <K,V> Map<K,V> immutableMap(Map<K,V> m) {
+		if (m == null || m.isEmpty())
+			return Collections.emptyMap();
+		return Collections.unmodifiableMap(new LinkedHashMap<>(m));
+	}
+	
+	/**
+	 * Creates an unmodifiable map from the specified map.
+	 * 
+	 * @param m The map to copy from.
+	 * @return An unmodifiable view of the collection, or a {@link Collections#emptyMap()}
+	 * 	if the collection was empty or <jk>null</jk>.
+	 */
+	public static <K,V> Map<K,V> unmodifiableMap(Map<K,V> m) {
+		if (m == null || m.isEmpty())
+			return Collections.emptyMap();
+		return Collections.unmodifiableMap(m);
+	}
+
+	/**
 	 * Asserts that all entries in the list are either instances or subclasses of at least one of the specified classes.
 	 * 
 	 * @param l The list to check.
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 d20ff95..b71a6a7 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
@@ -1297,24 +1297,24 @@ public final class StringUtils {
 	 * @param o The object to convert to a string.
 	 * @return The object converted to a string, or <jk>null</jk> if the object was null.
 	 */
-	public static String toString(Object o) {
+	public static String asString(Object o) {
 		return (o == null ? null : o.toString());
 	}
-
+	
 	/**
 	 * Converts an array of objects to an array of strings.
 	 * 
 	 * @param o The array of objects to convert to strings.
 	 * @return A new array of objects converted to strings.
 	 */
-	public static String[] toStrings(Object[] o) {
+	public static String[] asStrings(Object...o) {
 		if (o == null)
 			return null;
 		if (o instanceof String[])
 			return (String[])o;
 		String[] s = new String[o.length];
 		for (int i = 0; i < o.length; i++)
-			s[i] = toString(o[i]);
+			s[i] = asString(o[i]);
 		return s;
 	}
 
@@ -1513,6 +1513,20 @@ public final class StringUtils {
 	}
 
 	/**
+	 * Shortcut for calling <code>URLEncoder.<jsm>encode</jsm>(o.toString(), <js>"UTF-8"</js>)</code>.
+	 * 
+	 * @param o The object to encode.
+	 * @return The URL encoded string, or <jk>null</jk> if the object was null.
+	 */
+	public static String urlEncode(Object o) {
+		try {
+			if (o != null)
+				return URLEncoder.encode(o.toString(), "UTF-8");
+		} catch (UnsupportedEncodingException e) {}
+		return null;
+	}
+
+	/**
 	 * Decodes a <code>application/x-www-form-urlencoded</code> string using <code>UTF-8</code> encoding scheme.
 	 * 
 	 * @param s The string to decode.
@@ -1527,10 +1541,11 @@ public final class StringUtils {
 			if (c == '+' || c == '%')
 				needsDecode = true;
 		}
-		if (needsDecode)
-		try {
+		if (needsDecode) {
+			try {
 				return URLDecoder.decode(s, "UTF-8");
 			} catch (UnsupportedEncodingException e) {/* Won't happen */}
+		}
 		return s;
 	}
 
@@ -1553,6 +1568,25 @@ public final class StringUtils {
 		}
 		return s;
 	}
+	
+	/**
+	 * Splits a string into equally-sized parts.
+	 * 
+	 * @param s The string to split.
+	 * @param size The token sizes.
+	 * @return The tokens, or <jk>null</jk> if the input was <jk>null</jk>.
+	 */
+	public static List<String> splitEqually(String s, int size) {
+		if (s == null)
+			return null;
+		
+		List<String> l = new ArrayList<>((s.length() + size - 1) / size);
+
+		for (int i = 0; i < s.length(); i += size) 
+			l.add(s.substring(i, Math.min(s.length(), i + size)));
+
+		return l;
+	}
 
 	/**
 	 * Returns the first non-whitespace character in the string.
@@ -1571,6 +1605,22 @@ public final class StringUtils {
 	}
 
 	/**
+	 * Returns the last non-whitespace character in the string.
+	 * 
+	 * @param s The string to check.
+	 * @return
+	 * 	The last non-whitespace character, or <code>0</code> if the string is <jk>null</jk>, empty, or composed
+	 * 	of only whitespace.
+	 */
+	public static char lastNonWhitespaceChar(String s) {
+		if (s != null)
+			for (int i = s.length()-1; i >= 0; i--)
+				if (! Character.isWhitespace(s.charAt(i)))
+					return s.charAt(i);
+		return 0;
+	}
+
+	/**
 	 * Returns the character at the specified index in the string without throwing exceptions.
 	 * 
 	 * @param s The string.
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserGroup.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserGroup.java
index 39ea4b3..b44e535 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserGroup.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/parser/ParserGroup.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.parser;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
+
 import java.util.*;
 import java.util.concurrent.*;
 
@@ -114,7 +116,7 @@ public final class ParserGroup extends BeanContext {
 	 */
 	public ParserGroup(PropertyStore ps, Parser[] parsers) {
 		super(ps);
-		this.parsers = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(parsers)));
+		this.parsers = immutableList(parsers);
 
 		List<MediaType> lmt = new ArrayList<>();
 		List<Parser> l = new ArrayList<>();
@@ -126,7 +128,7 @@ public final class ParserGroup extends BeanContext {
 		}
 
 		this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]);
-		this.mediaTypesList = Collections.unmodifiableList(lmt);
+		this.mediaTypesList = unmodifiableList(lmt);
 		this.mediaTypeParsers = l.toArray(new Parser[l.size()]);
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteableMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteableMeta.java
index a56725f..7d9fea5 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteableMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/remoteable/RemoteableMeta.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.remoteable;
 
 import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.internal.ReflectionUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
 
@@ -57,7 +58,7 @@ public class RemoteableMeta {
 			}
 		}
 
-		this.methods = Collections.unmodifiableMap(_methods);
+		this.methods = unmodifiableMap(_methods);
 	}
 
 	/**
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerGroup.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerGroup.java
index 77ebab7..98fb82b 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerGroup.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/serializer/SerializerGroup.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.serializer;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
+
 import java.util.*;
 import java.util.concurrent.*;
 
@@ -106,7 +108,7 @@ public final class SerializerGroup extends BeanContext {
 	 */
 	public SerializerGroup(PropertyStore ps, Serializer[] serializers) {
 		super(ps);
-		this.serializers = Collections.unmodifiableList(new ArrayList<>(Arrays.asList(serializers)));
+		this.serializers = immutableList(serializers);
 
 		List<MediaType> lmt = new ArrayList<>();
 		List<Serializer> l = new ArrayList<>();
@@ -118,7 +120,7 @@ public final class SerializerGroup extends BeanContext {
 		}
 
 		this.mediaTypes = lmt.toArray(new MediaType[lmt.size()]);
-		this.mediaTypesList = Collections.unmodifiableList(lmt);
+		this.mediaTypesList = unmodifiableList(lmt);
 		this.mediaTypeSerializers = l.toArray(new Serializer[l.size()]);
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlBeanMeta.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlBeanMeta.java
index 91e75e3..b38ffd0 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlBeanMeta.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlBeanMeta.java
@@ -12,6 +12,7 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.xml;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.xml.annotation.XmlFormat.*;
 
 import java.util.*;
@@ -44,10 +45,10 @@ public class XmlBeanMeta extends BeanMetaExtended {
 		Class<?> c = beanMeta.getClassMeta().getInnerClass();
 		XmlBeanMetaBuilder b = new XmlBeanMetaBuilder(beanMeta);
 
-		attrs = Collections.unmodifiableMap(b.attrs);
-		elements = Collections.unmodifiableMap(b.elements);
+		attrs = unmodifiableMap(b.attrs);
+		elements = unmodifiableMap(b.elements);
 		attrsProperty = b.attrsProperty;
-		collapsedProperties = Collections.unmodifiableMap(b.collapsedProperties);
+		collapsedProperties = unmodifiableMap(b.collapsedProperties);
 		contentProperty = b.contentProperty;
 		contentFormat = b.contentFormat;
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlUtils.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlUtils.java
index 88d9270..96fcb3e 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlUtils.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/xml/XmlUtils.java
@@ -15,7 +15,6 @@ package org.apache.juneau.xml;
 import static org.apache.juneau.internal.StringUtils.*;
 
 import java.io.*;
-import java.net.*;
 import java.util.*;
 
 import javax.xml.stream.*;
@@ -586,32 +585,4 @@ public final class XmlUtils {
 			return "ENTITY_DECLARATION";
 		return "UNKNOWN";
 	}
-
-	/**
-	 * Shortcut for calling <code>URLEncoder.<jsm>encode</jsm>(o.toString(), <js>"UTF-8"</js>)</code>.
-	 * 
-	 * @param o The object to encode.
-	 * @return The URL encoded string, or <jk>null</jk> if the object was null.
-	 */
-	public static String urlEncode(Object o) {
-		try {
-			if (o != null)
-				return URLEncoder.encode(o.toString(), "UTF-8");
-		} catch (UnsupportedEncodingException e) {}
-		return null;
-	}
-
-	/**
-	 * Shortcut for calling <code>URLEncoder.<jsm>decode</jsm>(o.toString(), <js>"UTF-8"</js>)</code>.
-	 * 
-	 * @param s The string to decode.
-	 * @return The decoded string, or <jk>null</jk> if the string was null.
-	 */
-	public static String urlDecode(String s) {
-		try {
-			if (s != null)
-				return URLDecoder.decode(s, "UTF-8");
-		} catch (UnsupportedEncodingException e) {}
-		return null;
-	}
 }
diff --git a/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/MapVar.java b/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/MapVar.java
index f8ee406..cd8b669 100644
--- a/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/MapVar.java
+++ b/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/MapVar.java
@@ -12,12 +12,11 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.svl;
 
+import static org.apache.juneau.internal.StringUtils.*;
 import static org.apache.juneau.internal.ThrowableUtils.*;
 
 import java.util.*;
 
-import org.apache.juneau.internal.*;
-
 /**
  * A subclass of {@link DefaultingVar} that simply pulls values from a {@link Map}.
  * 
@@ -46,6 +45,6 @@ public abstract class MapVar extends DefaultingVar {
 
 	@Override /* Var */
 	public String resolve(VarResolverSession session, String varVal) {
-		return StringUtils.toString(m.get(varVal));
+		return asString(m.get(varVal));
 	}
 }
diff --git a/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/VarResolverContext.java b/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/VarResolverContext.java
index be59001..358756c 100644
--- a/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/VarResolverContext.java
+++ b/juneau-core/juneau-svl/src/main/java/org/apache/juneau/svl/VarResolverContext.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.svl;
 
 import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 
 import java.util.*;
 import java.util.concurrent.*;
@@ -52,8 +53,8 @@ public class VarResolverContext {
 			m.put(v.getName(), v);
 		}
 
-		this.varMap = Collections.unmodifiableMap(m);
-		this.contextObjects = contextObjects == null ? null : Collections.unmodifiableMap(new ConcurrentHashMap<>(contextObjects));
+		this.varMap = unmodifiableMap(m);
+		this.contextObjects = immutableMap(contextObjects);
 	}
 
 	/**
diff --git a/juneau-doc/src/main/javadoc/overview.html b/juneau-doc/src/main/javadoc/overview.html
index 089c8e0..5e2ce7a 100644
--- a/juneau-doc/src/main/javadoc/overview.html
+++ b/juneau-doc/src/main/javadoc/overview.html
@@ -5031,11 +5031,11 @@
 		</p>
 		<ul class='spaced-list'>
 			<li>
-				{@link org.apache.juneau.config.listener.ConfigListener} - Config file is saved, loaded, or modified.
+				{@link org.apache.juneau.config.event.ConfigListener} - Config file is saved, loaded, or modified.
 			<li>
-				{@link org.apache.juneau.config.listener.SectionListener} - One or more entries in a section are modified.
+				{@link org.apache.juneau.config.event.SectionListener} - One or more entries in a section are modified.
 			<li>
-				{@link org.apache.juneau.config.listener.EntryListener} - An individual entry is modified.
+				{@link org.apache.juneau.config.event.EntryListener} - An individual entry is modified.
 		</ul>
 		
 		<h5 class="topic">Example:</h5>
diff --git a/juneau-microservice/juneau-microservice-server/src/main/java/org/apache/juneau/microservice/Microservice.java b/juneau-microservice/juneau-microservice-server/src/main/java/org/apache/juneau/microservice/Microservice.java
index 8ee55f1..c53b49c 100755
--- a/juneau-microservice/juneau-microservice-server/src/main/java/org/apache/juneau/microservice/Microservice.java
+++ b/juneau-microservice/juneau-microservice-server/src/main/java/org/apache/juneau/microservice/Microservice.java
@@ -15,6 +15,7 @@ package org.apache.juneau.microservice;
 import static org.apache.juneau.internal.FileUtils.*;
 import static org.apache.juneau.internal.IOUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 
 import java.io.*;
 import java.net.*;
@@ -622,7 +623,7 @@ public abstract class Microservice {
 		consoleCommands = new LinkedHashMap<>();
 		for (ConsoleCommand cc : createConsoleCommands())
 			consoleCommands.put(cc.getName(), cc);
-		consoleCommands = Collections.unmodifiableMap(consoleCommands);
+		consoleCommands = unmodifiableMap(consoleCommands);
 		
 		final Map<String,ConsoleCommand> commands = consoleCommands;
 		final MessageBundle mb2 = mb;
diff --git a/juneau-microservice/juneau-microservice-server/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java b/juneau-microservice/juneau-microservice-server/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java
index 91c36d6..230008d 100644
--- a/juneau-microservice/juneau-microservice-server/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java
+++ b/juneau-microservice/juneau-microservice-server/src/main/java/org/apache/juneau/microservice/resources/LogEntryFormatter.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.microservice.resources;
 
 import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 
 import java.text.*;
 import java.util.*;
@@ -177,7 +178,7 @@ public class LogEntryFormatter extends Formatter {
 		sre = sre.replaceAll("\\\\%n", "\\\\n");
 
 		rePattern = Pattern.compile(sre);
-		fieldIndexes = Collections.unmodifiableMap(fieldIndexes);
+		fieldIndexes = unmodifiableMap(fieldIndexes);
 	}
 
 	/**
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/NameValuePairs.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/NameValuePairs.java
index 2fe804b..0fc7067 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/NameValuePairs.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/NameValuePairs.java
@@ -12,13 +12,14 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest.client;
 
+import static org.apache.juneau.internal.StringUtils.*;
+
 import java.util.*;
 
 import org.apache.http.*;
 import org.apache.http.client.entity.*;
 import org.apache.http.message.*;
 import org.apache.juneau.httppart.*;
-import org.apache.juneau.internal.*;
 import org.apache.juneau.urlencoding.*;
 
 /**
@@ -63,7 +64,7 @@ public final class NameValuePairs extends LinkedList<NameValuePair> {
 	 * @return This object (for method chaining).
 	 */
 	public NameValuePairs append(String name, Object value) {
-		super.add(new BasicNameValuePair(name, StringUtils.toString(value)));
+		super.add(new BasicNameValuePair(name, asString(value)));
 		return this;
 	}
 
diff --git a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClientBuilder.java b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClientBuilder.java
index e02a1f1..e94b631 100644
--- a/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClientBuilder.java
+++ b/juneau-rest/juneau-rest-client/src/main/java/org/apache/juneau/rest/client/RestClientBuilder.java
@@ -13,14 +13,13 @@
 package org.apache.juneau.rest.client;
 
 import static org.apache.juneau.internal.StringUtils.*;
-import static org.apache.juneau.json.JsonSerializer.*;
 import static org.apache.juneau.parser.Parser.*;
-import static org.apache.juneau.uon.UonSerializer.*;
 import static org.apache.juneau.rest.client.RestClient.*;
+import static org.apache.juneau.serializer.Serializer.*;
+import static org.apache.juneau.uon.UonSerializer.*;
 
 import java.lang.reflect.*;
 import java.net.*;
-import java.net.URI;
 import java.security.*;
 import java.util.*;
 import java.util.concurrent.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/HtmlDocBuilder.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/HtmlDocBuilder.java
index 54ebe99..27818fc 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/HtmlDocBuilder.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/HtmlDocBuilder.java
@@ -13,13 +13,13 @@
 package org.apache.juneau.rest;
 
 import static org.apache.juneau.html.HtmlDocSerializer.*;
+import static org.apache.juneau.internal.StringUtils.*;
 
 import java.util.*;
 import java.util.regex.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.html.*;
-import org.apache.juneau.internal.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.utils.*;
 
@@ -484,7 +484,7 @@ public class HtmlDocBuilder {
 	private static String[] resolveLinks(Object[] value, String[] prev) {
 		List<String> list = new ArrayList<>();
 		for (Object v : value) {
-			String s = StringUtils.toString(v);
+			String s = asString(v);
 			if ("INHERIT".equals(s)) {
 				list.addAll(Arrays.asList(prev));
 			} else if (s.indexOf('[') != -1 && INDEXED_LINK_PATTERN.matcher(s).matches()) {
@@ -504,7 +504,7 @@ public class HtmlDocBuilder {
 	private static String[] resolveSet(Object[] value, String[] prev) {
 		Set<String> set = new HashSet<>();
 		for (Object v : value) {
-			String s = StringUtils.toString(v);
+			String s = asString(v);
 			if ("INHERIT".equals(s)) {
 				if (prev != null)
 					set.addAll(Arrays.asList(prev));
@@ -520,7 +520,7 @@ public class HtmlDocBuilder {
 	private static String[] resolveList(Object[] value, String[] prev) {
 		Set<String> set = new LinkedHashSet<>();
 		for (Object v : value) {
-			String s = StringUtils.toString(v);
+			String s = asString(v);
 			if ("INHERIT".equals(s)) {
 				if (prev != null)
 					set.addAll(Arrays.asList(prev));
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 dc3cdc9..22275ad 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
@@ -12,6 +12,7 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.internal.IOUtils.*;
 
 import java.io.*;
@@ -80,7 +81,7 @@ public class ReaderResource implements Writable {
 		this.mediaType = mediaType;
 		this.varSession = varSession;
 
-		this.headers = headers == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(new LinkedHashMap<>(headers));
+		this.headers = immutableMap(headers);
 
 		this.contents = new String[contents.length];
 		for (int i = 0; i < contents.length; i++) {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestFormData.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestFormData.java
index 76a514e..1374bde 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestFormData.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestFormData.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.rest;
 
 import static org.apache.juneau.internal.ArrayUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
 
 import java.lang.reflect.*;
 import java.util.*;
@@ -85,7 +86,7 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 				Object value = e.getValue();
 				String[] v = get(key);
 				if (v == null || v.length == 0 || StringUtils.isEmpty(v[0]))
-					put(key, new String[]{StringUtils.toString(value)});
+					put(key, asStrings(value));
 			}
 		}
 		return this;
@@ -116,7 +117,7 @@ public class RequestFormData extends LinkedHashMap<String,String[]> {
 	 * @param value The parameter value.
 	 */
 	public void put(String name, Object value) {
-		super.put(name, new String[]{StringUtils.toString(value)});
+		super.put(name, asStrings(value));
 	}
 
 	/**
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeaders.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeaders.java
index ae89577..44f780e 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeaders.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestHeaders.java
@@ -81,7 +81,7 @@ public class RequestHeaders extends TreeMap<String,String[]> {
 				Object value = e.getValue();
 				String[] v = get(key);
 				if (v == null || v.length == 0 || StringUtils.isEmpty(v[0]))
-					put(key, new String[]{StringUtils.toString(value)});
+					put(key, asStrings(value));
 			}
 		}
 		return this;
@@ -219,7 +219,7 @@ public class RequestHeaders extends TreeMap<String,String[]> {
 	 * @param value The header value.
 	 */
 	public void put(String name, Object value) {
-		super.put(name, new String[]{StringUtils.toString(value)});
+		super.put(name, asStrings(value));
 	}
 
 	/**
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestQuery.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestQuery.java
index d0e889c..ff949fa 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestQuery.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RequestQuery.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.rest;
 
 import static org.apache.juneau.internal.ArrayUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
 
 import java.lang.reflect.*;
 import java.util.*;
@@ -25,7 +26,6 @@ import org.apache.juneau.internal.*;
 import org.apache.juneau.json.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.utils.*;
-import org.apache.juneau.xml.*;
 
 /**
  * Represents the query parameters in an HTTP request.
@@ -84,7 +84,7 @@ public final class RequestQuery extends LinkedHashMap<String,String[]> {
 				Object value = e.getValue();
 				String[] v = get(key);
 				if (v == null || v.length == 0 || StringUtils.isEmpty(v[0]))
-					put(key, new String[]{StringUtils.toString(value)});
+					put(key, asStrings(value));
 			}
 		}
 		return this;
@@ -123,7 +123,7 @@ public final class RequestQuery extends LinkedHashMap<String,String[]> {
 		if (value == null)
 			put(name, null);
 		else
-			put(name, new String[]{StringUtils.toString(value)});
+			put(name, asStrings(value));
 	}
 
 	/**
@@ -629,7 +629,7 @@ public final class RequestQuery extends LinkedHashMap<String,String[]> {
 			for (int i = 0; i < e.getValue().length; i++) {
 				if (sb.length() > 0)
 					sb.append("&");
-				sb.append(XmlUtils.urlEncode(e.getKey())).append('=').append(XmlUtils.urlEncode(e.getValue()[i]));
+				sb.append(urlEncode(e.getKey())).append('=').append(urlEncode(e.getValue()[i]));
 			}
 		}
 		return sb.toString();
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index a0a1640..ab550f5 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -14,6 +14,7 @@ package org.apache.juneau.rest;
 
 import static javax.servlet.http.HttpServletResponse.*;
 import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.internal.IOUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
 
@@ -33,7 +34,6 @@ import javax.servlet.http.*;
 import org.apache.juneau.*;
 import org.apache.juneau.config.*;
 import org.apache.juneau.encoders.*;
-import org.apache.juneau.encoders.Encoder;
 import org.apache.juneau.html.*;
 import org.apache.juneau.http.*;
 import org.apache.juneau.httppart.*;
@@ -2851,11 +2851,11 @@ public final class RestContext extends BeanContext {
 			Map<Class<?>,RestParam> _paramResolvers = new HashMap<>();
 			for (RestParam rp : getInstanceArrayProperty(REST_paramResolvers, RestParam.class, new RestParam[0], true, this)) 
 				_paramResolvers.put(rp.forClass(), rp);
-			paramResolvers = Collections.unmodifiableMap(_paramResolvers);
+			paramResolvers = unmodifiableMap(_paramResolvers);
 			
 			Map<String,Object> _defaultRequestHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
 			_defaultRequestHeaders.putAll(getMapProperty(REST_defaultRequestHeaders, String.class));
-			defaultRequestHeaders = Collections.unmodifiableMap(new LinkedHashMap<>(_defaultRequestHeaders));
+			defaultRequestHeaders = unmodifiableMap(new LinkedHashMap<>(_defaultRequestHeaders));
 			
 			defaultResponseHeaders = getMapProperty(REST_defaultResponseHeaders, Object.class);
 			staticFileResponseHeaders = getMapProperty(REST_staticFileResponseHeaders, Object.class);	
@@ -2925,7 +2925,7 @@ public final class RestContext extends BeanContext {
 			Map<String,Widget> _widgets = new LinkedHashMap<>();
 			for (Widget w : getInstanceArrayProperty(REST_widgets, resource, Widget.class, new Widget[0], true, ps))
 				_widgets.put(w.getName(), w);
-			this.widgets = Collections.unmodifiableMap(_widgets);
+			this.widgets = unmodifiableMap(_widgets);
 
 			//----------------------------------------------------------------------------------------------------
 			// Initialize the child resources.
@@ -3096,7 +3096,7 @@ public final class RestContext extends BeanContext {
 				}
 			}
 
-			this.callMethods = Collections.unmodifiableMap(_javaRestMethods);
+			this.callMethods = unmodifiableMap(_javaRestMethods);
 			this.preCallMethods = _preCallMethods.values().toArray(new Method[_preCallMethods.size()]);
 			this.postCallMethods = _postCallMethods.values().toArray(new Method[_postCallMethods.size()]);
 			this.startCallMethods = _startCallMethods.values().toArray(new Method[_startCallMethods.size()]);
@@ -3115,7 +3115,7 @@ public final class RestContext extends BeanContext {
 			Map<String,RestCallRouter> _callRouters = new LinkedHashMap<>();
 			for (RestCallRouter.Builder crb : routers.values())
 				_callRouters.put(crb.getHttpMethodName(), crb.build());
-			this.callRouters = Collections.unmodifiableMap(_callRouters);
+			this.callRouters = unmodifiableMap(_callRouters);
 
 			// Initialize our child resources.
 			resourceResolver = getInstanceProperty(REST_resourceResolver, resource, RestResourceResolver.class, parentContext == null ? RestResourceResolverDefault.class : parentContext.resourceResolver, true, this);
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java
index 83f09f6..4038545 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContextBuilder.java
@@ -29,7 +29,6 @@ import org.apache.juneau.*;
 import org.apache.juneau.config.*;
 import org.apache.juneau.config.vars.*;
 import org.apache.juneau.encoders.*;
-import org.apache.juneau.encoders.Encoder;
 import org.apache.juneau.http.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.internal.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java
index 9f579e8..e36bad5 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestJavaMethod.java
@@ -15,6 +15,7 @@ package org.apache.juneau.rest;
 import static javax.servlet.http.HttpServletResponse.*;
 import static org.apache.juneau.BeanContext.*;
 import static org.apache.juneau.internal.ClassUtils.*;
+import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.internal.StringUtils.*;
 import static org.apache.juneau.internal.Utils.*;
 import static org.apache.juneau.rest.RestContext.*;
@@ -95,7 +96,7 @@ public class RestJavaMethod implements Comparable<RestJavaMethod>  {
 		this.priority = b.priority;
 		this.supportedAcceptTypes = b.supportedAcceptTypes;
 		this.supportedContentTypes = b.supportedContentTypes;
-		this.widgets = Collections.unmodifiableMap(b.widgets);
+		this.widgets = unmodifiableMap(b.widgets);
 	}
 
 	private static final class Builder  {
@@ -381,11 +382,11 @@ public class RestJavaMethod implements Comparable<RestJavaMethod>  {
 
 				supportedAcceptTypes = 
 					m.produces().length > 0 
-					? Collections.unmodifiableList(new ArrayList<>(Arrays.asList(MediaType.forStrings(resolveVars(vr, m.produces()))))) 
+					? immutableList(MediaType.forStrings(resolveVars(vr, m.produces()))) 
 					: serializers.getSupportedMediaTypes();
 				supportedContentTypes =
 					m.consumes().length > 0 
-					? Collections.unmodifiableList(new ArrayList<>(Arrays.asList(MediaType.forStrings(resolveVars(vr, m.consumes()))))) 
+					? immutableList(MediaType.forStrings(resolveVars(vr, m.consumes()))) 
 					: parsers.getSupportedMediaTypes();
 					
 				params = context.findParams(method, pathPattern, false);
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
index 5205527..99ac375 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
@@ -20,7 +20,6 @@ import static org.apache.juneau.internal.IOUtils.*;
 import static org.apache.juneau.serializer.Serializer.*;
 
 import java.io.*;
-import java.lang.reflect.*;
 import java.lang.reflect.Method;
 import java.net.*;
 import java.nio.charset.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
index 1228145..fcdc854 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestResponse.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest;
 
+import static org.apache.juneau.internal.StringUtils.*;
+
 import java.io.*;
 import java.nio.charset.*;
 import java.util.*;
@@ -23,7 +25,6 @@ import org.apache.juneau.*;
 import org.apache.juneau.encoders.*;
 import org.apache.juneau.http.*;
 import org.apache.juneau.httppart.*;
-import org.apache.juneau.internal.*;
 import org.apache.juneau.rest.annotation.*;
 import org.apache.juneau.serializer.*;
 
@@ -71,7 +72,7 @@ public final class RestResponse extends HttpServletResponseWrapper {
 		this.request = req;
 
 		for (Map.Entry<String,Object> e : context.getDefaultResponseHeaders().entrySet())
-			setHeader(e.getKey(), StringUtils.toString(e.getValue()));
+			setHeader(e.getKey(), asString(e.getValue()));
 
 		try {
 			String passThroughHeaders = req.getHeader("x-response-headers");
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java
index 230365f..74b61d6 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFileMapping.java
@@ -12,6 +12,8 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
+
 import java.util.*;
 
 import org.apache.juneau.*;
@@ -88,7 +90,7 @@ public class StaticFileMapping {
 		this.resourceClass = resourceClass;
 		this.path = StringUtils.trimSlashes(path);
 		this.location = StringUtils.trimSlashes(location);
-		this.responseHeaders = responseHeaders == null ? null : Collections.unmodifiableMap(new LinkedHashMap<>(responseHeaders));
+		this.responseHeaders = immutableMap(responseHeaders);
 	}
 	
 	/**
@@ -116,7 +118,7 @@ public class StaticFileMapping {
 		this.location = StringUtils.trimSlashes(parts[1]); 
 		if (parts.length == 3) {
 			try {
-				responseHeaders = Collections.unmodifiableMap(new ObjectMap(parts[2]));
+				responseHeaders = unmodifiableMap(new ObjectMap(parts[2]));
 			} catch (ParseException e) {
 				throw new FormattedRuntimeException(e, "Invalid mapping string format: ''{0}''", mappingString);
 			}
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StreamResource.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StreamResource.java
index a62cb17..999c80e 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StreamResource.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StreamResource.java
@@ -12,6 +12,7 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest;
 
+import static org.apache.juneau.internal.CollectionUtils.*;
 import static org.apache.juneau.internal.IOUtils.*;
 
 import java.io.*;
@@ -76,7 +77,7 @@ public class StreamResource implements Streamable {
 	public StreamResource(MediaType mediaType, Map<String,Object> headers, Object...contents) throws IOException {
 		this.mediaType = mediaType;
 
-		this.headers = headers == null ? Collections.EMPTY_MAP : Collections.unmodifiableMap(new LinkedHashMap<>(headers));
+		this.headers = immutableMap(headers);
 
 		this.contents = new byte[contents.length][];
 		for (int i = 0; i < contents.length; i++) {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethod.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethod.java
index b181583..bef4796 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethod.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestMethod.java
@@ -18,7 +18,7 @@ import static java.lang.annotation.RetentionPolicy.*;
 import java.lang.annotation.*;
 
 import org.apache.juneau.*;
-import org.apache.juneau.encoders.Encoder;
+import org.apache.juneau.encoders.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.remoteable.*;
 import org.apache.juneau.rest.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
index d708a51..284bd6b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/annotation/RestResource.java
@@ -19,7 +19,7 @@ import java.lang.annotation.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.config.*;
-import org.apache.juneau.encoders.Encoder;
+import org.apache.juneau.encoders.*;
 import org.apache.juneau.httppart.*;
 import org.apache.juneau.parser.*;
 import org.apache.juneau.rest.*;
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/StreamableHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/StreamableHandler.java
index c99cc8d..766016b 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/StreamableHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/StreamableHandler.java
@@ -12,12 +12,13 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest.response;
 
+import static org.apache.juneau.internal.StringUtils.*;
+
 import java.io.*;
 import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.http.*;
-import org.apache.juneau.internal.*;
 import org.apache.juneau.rest.*;
 
 /**
@@ -43,7 +44,7 @@ public final class StreamableHandler implements ResponseHandler {
 				if (mediaType != null)
 					res.setContentType(mediaType.toString());
 				for (Map.Entry<String,Object> h : r.getHeaders().entrySet())
-					res.setHeader(h.getKey(), StringUtils.toString(h.getValue()));
+					res.setHeader(h.getKey(), asString(h.getValue()));
 			}
 			try (OutputStream os = res.getOutputStream()) {
 				((Streamable)output).streamTo(os);
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/WritableHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/WritableHandler.java
index 436ee1c..dad3bec 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/WritableHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/response/WritableHandler.java
@@ -12,12 +12,13 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest.response;
 
+import static org.apache.juneau.internal.StringUtils.*;
+
 import java.io.*;
 import java.util.*;
 
 import org.apache.juneau.*;
 import org.apache.juneau.http.*;
-import org.apache.juneau.internal.*;
 import org.apache.juneau.rest.*;
 
 /**
@@ -42,7 +43,7 @@ public final class WritableHandler implements ResponseHandler {
 				if (mediaType != null)
 					res.setContentType(mediaType.toString());
 				for (Map.Entry<String,Object> h : r.getHeaders().entrySet())
-					res.setHeader(h.getKey(), StringUtils.toString(h.getValue()));
+					res.setHeader(h.getKey(), asString(h.getValue()));
 			}
 			try (Writer w = res.getNegotiatedWriter()) {
 				((Writable)output).writeTo(w);
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/RequestAttributeVar.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/RequestAttributeVar.java
index 408dcbf..d5934be 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/RequestAttributeVar.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/vars/RequestAttributeVar.java
@@ -12,9 +12,10 @@
 // ***************************************************************************************************************************
 package org.apache.juneau.rest.vars;
 
+import static org.apache.juneau.internal.StringUtils.*;
+
 import javax.servlet.http.*;
 
-import org.apache.juneau.internal.*;
 import org.apache.juneau.rest.*;
 import org.apache.juneau.svl.*;
 
@@ -79,6 +80,6 @@ public class RequestAttributeVar extends MultipartResolvingVar {
 	@Override /* Parameter */
 	public String resolve(VarResolverSession session, String key) {
 		RestRequest req = session.getSessionObject(RestRequest.class, SESSION_req);
-		return StringUtils.toString(req.getAttribute(key));
+		return asString(req.getAttribute(key));
 	}
 }
\ No newline at end of file

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

Mime
View raw message