You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2016/08/01 17:29:53 UTC

[04/53] [partial] incubator-juneau git commit: Merge changes from GitHub repo.

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/html/dto/temp.txt
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/html/dto/temp.txt b/org.apache.juneau/src/main/java/org/apache/juneau/html/dto/temp.txt
new file mode 100644
index 0000000..cd769ed
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/html/dto/temp.txt
@@ -0,0 +1,142 @@
+\u24d8 a \u2013 hyperlink CHANGED
+\u24d8 abbr \u2013 abbreviation
+\u24d8 address \u2013 contact information
+\u24d8 area \u2013 image-map hyperlink
+\u24d8 article \u2013 article NEW
+\u24d8 aside \u2013 tangential content NEW
+\u24d8 audio \u2013 audio stream NEW
+\u24d8 b \u2013 offset text conventionally styled in bold CHANGED
+\u24d8 base \u2013 base URL
+\u24d8 bdi \u2013 BiDi isolate NEW
+\u24d8 bdo \u2013 BiDi override
+\u24d8 blockquote \u2013 block quotation
+\u24d8 body \u2013 document body
+\u24d8 br \u2013 line break
+\u24d8 button \u2013 button
+\u24d8 button type=submit \u2013 submit button
+\u24d8 button type=reset \u2013 reset button
+\u24d8 button type=button \u2013 button with no additional semantics
+\u24d8 canvas \u2013 canvas for dynamic graphics NEW
+\u24d8 caption \u2013 table title
+\u24d8 cite \u2013 cited title of a work CHANGED
+\u24d8 code \u2013 code fragment
+\u24d8 col \u2013 table column
+\u24d8 colgroup \u2013 table column group
+\u24d8 command \u2013 command NEW
+\u24d8 command type=command \u2013 command with an associated action NEW
+\u24d8 command type=radio \u2013 selection of one item from a list of items NEW
+\u24d8 command type=checkbox \u2013 state or option that can be toggled NEW
+\u24d8 datalist \u2013 predefined options for other controls NEW
+\u24d8 dd \u2013 description or value
+\u24d8 del \u2013 deleted text
+\u24d8 details \u2013 control for additional on-demand information NEW
+\u24d8 dfn \u2013 defining instance
+\u24d8 div \u2013 generic flow container
+\u24d8 dl \u2013 description list
+\u24d8 dt \u2013 term or name
+\u24d8 em \u2013 emphatic stress
+\u24d8 embed \u2013 integration point for plugins NEW
+\u24d8 fieldset \u2013 set of related form controls
+\u24d8 figcaption \u2013 figure caption NEW
+\u24d8 figure \u2013 figure with optional caption NEW
+\u24d8 footer \u2013 footer NEW
+\u24d8 form \u2013 user-submittable form
+\u24d8 h1 \u2013 heading
+\u24d8 h2 \u2013 heading
+\u24d8 h3 \u2013 heading
+\u24d8 h4 \u2013 heading
+\u24d8 h5 \u2013 heading
+\u24d8 h6 \u2013 heading
+\u24d8 head \u2013 document metadata container
+\u24d8 header \u2013 header NEW
+\u24d8 hgroup \u2013 heading group NEW
+\u24d8 hr \u2013 thematic break CHANGED
+\u24d8 html \u2013 root element
+\u24d8 i \u2013 offset text conventionally styled in italic CHANGED
+\u24d8 iframe \u2013 nested browsing context (inline frame)
+\u24d8 img \u2013 image
+\u24d8 input \u2013 input control CHANGED
+\u24d8 input type=text \u2013 text-input field
+\u24d8 input type=password \u2013 password-input field
+\u24d8 input type=checkbox \u2013 checkbox
+\u24d8 input type=radio \u2013 radio button
+\u24d8 input type=button \u2013 button
+\u24d8 input type=submit \u2013 submit button
+\u24d8 input type=reset \u2013 reset button
+\u24d8 input type=file \u2013 file upload control
+\u24d8 input type=hidden \u2013 hidden input control
+\u24d8 input type=image \u2013 image-coordinates input control
+\u24d8 input type=datetime \u2013 global date-and-time input control NEW
+\u24d8 input type=datetime-local \u2013 local date-and-time input control NEW
+\u24d8 input type=date \u2013 date input control NEW
+\u24d8 input type=month \u2013 year-and-month input control NEW
+\u24d8 input type=time \u2013 time input control NEW
+\u24d8 input type=week \u2013 year-and-week input control NEW
+\u24d8 input type=number \u2013 number input control NEW
+\u24d8 input type=range \u2013 imprecise number-input control NEW
+\u24d8 input type=email \u2013 e-mail address input control NEW
+\u24d8 input type=url \u2013 URL input control NEW
+\u24d8 input type=search \u2013 search field NEW
+\u24d8 input type=tel \u2013 telephone-number-input field NEW
+\u24d8 input type=color \u2013 color-well control NEW
+\u24d8 ins \u2013 inserted text
+\u24d8 kbd \u2013 user input
+\u24d8 keygen \u2013 key-pair generator/input control NEW
+\u24d8 label \u2013 caption for a form control
+\u24d8 legend \u2013 title or explanatory caption
+\u24d8 li \u2013 list item
+\u24d8 link \u2013 inter-document relationship metadata
+\u24d8 map \u2013 image-map definition
+\u24d8 mark \u2013 marked (highlighted) text NEW
+\u24d8 menu \u2013 list of commands CHANGED
+\u24d8 meta \u2013 metadata CHANGED
+\u24d8 meta name \u2013 name-value metadata
+\u24d8 meta http-equiv=refresh \u2013 \u201crefresh\u201d pragma directive
+\u24d8 meta http-equiv=default-style \u2013 \u201cpreferred stylesheet\u201d pragma directive
+\u24d8 meta charset \u2013 document character-encoding declaration NEW
+\u24d8 meta http-equiv=content-type \u2013 document character-encoding declaration
+\u24d8 meter \u2013 scalar gauge NEW
+\u24d8 nav \u2013 group of navigational links NEW
+\u24d8 noscript \u2013 fallback content for script
+\u24d8 object \u2013 generic external content
+\u24d8 ol \u2013 ordered list
+\u24d8 optgroup \u2013 group of options
+\u24d8 option \u2013 option
+\u24d8 output \u2013 result of a calculation in a form NEW
+\u24d8 p \u2013 paragraph
+\u24d8 param \u2013 initialization parameters for plugins
+\u24d8 pre \u2013 preformatted text
+\u24d8 progress \u2013 progress indicator NEW
+\u24d8 q \u2013 quoted text
+\u24d8 rp \u2013 ruby parenthesis NEW
+\u24d8 rt \u2013 ruby text NEW
+\u24d8 ruby \u2013 ruby annotation NEW
+\u24d8 s \u2013 struck text CHANGED
+\u24d8 samp \u2013 (sample) output
+\u24d8 script \u2013 embedded script
+\u24d8 section \u2013 section NEW
+\u24d8 select \u2013 option-selection form control
+\u24d8 small \u2013 small print CHANGED
+\u24d8 source \u2013 media source NEW
+\u24d8 span \u2013 generic span
+\u24d8 strong \u2013 strong importance
+\u24d8 style \u2013 style (presentation) information
+\u24d8 sub \u2013 subscript
+\u24d8 summary \u2013 summary, caption, or legend for a details control NEW
+\u24d8 sup \u2013 superscript
+\u24d8 table \u2013 table
+\u24d8 tbody \u2013 table row group
+\u24d8 td \u2013 table cell
+\u24d8 textarea \u2013 text input area
+\u24d8 tfoot \u2013 table footer row group
+\u24d8 th \u2013 table header cell
+\u24d8 thead \u2013 table heading group
+\u24d8 time \u2013 date and/or time NEW
+\u24d8 title \u2013 document title
+\u24d8 tr \u2013 table row
+\u24d8 track \u2013 supplementary media track NEW
+\u24d8 u \u2013 offset text conventionally styled with an underline CHANGED
+\u24d8 ul \u2013 unordered list
+\u24d8 var \u2013 variable or placeholder text
+\u24d8 video \u2013 video NEW
+\u24d8 wbr \u2013 line-break opportunity NEW
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/html/package.html
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/html/package.html b/org.apache.juneau/src/main/java/org/apache/juneau/html/package.html
new file mode 100644
index 0000000..371c8c4
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/html/package.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<!--
+/***************************************************************************************************************************
+ * 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.
+ *
+ ***************************************************************************************************************************/
+ -->
+<html>
+<head>
+	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+	<style type="text/css">
+		/* For viewing in Page Designer */
+		@IMPORT url("../../../../../../javadoc.css");
+
+		/* For viewing in REST interface */
+		@IMPORT url("../htdocs/javadoc.css");
+		body { 
+			margin: 20px; 
+		}	
+	</style>
+	<script>
+		/* Replace all @code and @link tags. */	
+		window.onload = function() {
+			document.body.innerHTML = document.body.innerHTML.replace(/\{\@code ([^\}]+)\}/g, '<code>$1</code>');
+			document.body.innerHTML = document.body.innerHTML.replace(/\{\@link (([^\}]+)\.)?([^\.\}]+)\}/g, '<code>$3</code>');
+		}
+	</script>
+</head>
+<body>
+<p>HTML serialization and parsing support</p>
+<script>
+	function toggle(x) {
+		var div = x.nextSibling;
+		while (div != null && div.nodeType != 1)
+			div = div.nextSibling;
+		if (div != null) {
+			var d = div.style.display;
+			if (d == 'block' || d == '') {
+				div.style.display = 'none';
+				x.className += " closed";
+			} else {
+				div.style.display = 'block';
+				x.className = x.className.replace(/(?:^|\s)closed(?!\S)/g , '' );
+			}
+		}
+	}
+</script>
+
+<a id='TOC'></a><h5 class='toc'>Table of Contents</h5>
+<ol class='toc'>
+	<li><p><a class='doclink' href='#HtmlSerializer'>HTML serialization support</a></p> 
+	<li><p><a class='doclink' href='#HtmlParser'>HTML parsing support</a></p> 
+</ol>
+
+<!-- ======================================================================================================== -->
+<a id="HtmlSerializer"></a>
+<h2 class='topic' onclick='toggle(this)'>1 - HTML serialization support</h2>
+<div class='topic'>
+	TODO
+</div>
+
+<!-- ======================================================================================================== -->
+<a id="HtmlParser"></a>
+<h2 class='topic' onclick='toggle(this)'>2 - HTML parsing support</h2>
+<div class='topic'>
+	TODO
+</div>
+
+</body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFile.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFile.java b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFile.java
new file mode 100644
index 0000000..08e78ec
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFile.java
@@ -0,0 +1,766 @@
+/***************************************************************************************************************************
+ * 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.ini;
+
+import static java.lang.reflect.Modifier.*;
+import static org.apache.juneau.ini.ConfigFileFormat.*;
+import static org.apache.juneau.ini.ConfigUtils.*;
+import static org.apache.juneau.internal.ThrowableUtils.*;
+
+import java.beans.*;
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Implements the API for accessing the contents of a config file.
+ * <p>
+ * Refer to {@link org.apache.juneau.ini} for more information.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+public abstract class ConfigFile implements Map<String,Section> {
+
+	//--------------------------------------------------------------------------------
+	// Abstract methods
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Retrieves an entry value from this config file.
+	 *
+	 * @param sectionName The section name.  Must not be <jk>null</jk>.
+	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
+	 * @return The value, or the default value if the section or value doesn't exist.
+	 */
+	public abstract String get(String sectionName, String sectionKey);
+
+	/**
+	 * Sets an entry value in this config file.
+	 *
+	 * @param sectionName The section name.  Must not be <jk>null</jk>.
+	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
+	 * @param value The new value.
+	 * @param encoded
+	 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract String put(String sectionName, String sectionKey, Object value, boolean encoded);
+
+	/**
+	 * Removes an antry from this config file.
+	 *
+	 * @param sectionName The section name.  Must not be <jk>null</jk>.
+	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
+	 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract String remove(String sectionName, String sectionKey);
+
+	/**
+	 * Returns the current set of keys in the specified section.
+	 *
+	 * @param sectionName The section name.  Must not be <jk>null</jk>.
+	 * @return The list of keys in the specified section, or <jk>null</jk> if section does not exist.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract Set<String> getSectionKeys(String sectionName);
+
+	/**
+	 * Reloads ths config file object from the persisted file contents if the modified timestamp on the file has changed.
+	 *
+	 * @return This object (for method chaining).
+	 * @throws IOException If file could not be read, or file is not associated with this object.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile loadIfModified() throws IOException;
+
+	/**
+	 * Loads ths config file object from the persisted file contents.
+	 *
+	 * @return This object (for method chaining).
+	 * @throws IOException If file could not be read, or file is not associated with this object.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile load() throws IOException;
+
+	/**
+	 * Loads ths config file object from the specified reader.
+	 *
+	 * @param r The reader to read from.
+	 * @return This object (for method chaining).
+	 * @throws IOException If file could not be read, or file is not associated with this object.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile load(Reader r) throws IOException;
+
+	/**
+	 * Adds arbitrary lines to the specified config file section.
+	 * <p>
+	 * The lines can be any of the following....
+	 * <ul class='spaced-list'>
+	 * 	<li><js>"# comment"</js> - A comment line.
+	 * 	<li><js>"key=val"</js> - A key/value pair (equivalent to calling {@link #put(String,Object)}.
+	 * 	<li><js>" foobar "</js> - Anything else (interpreted as a comment).
+	 * </ul>
+	 * <p>
+	 * If the section does not exist, it will automatically be created.
+	 *
+	 * @param section The name of the section to add lines to, or <jk>null</jk> to add to the beginning unnamed section.
+	 * @param lines The lines to add to the section.
+	 * @return This object (for method chaining).
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile addLines(String section, String...lines);
+
+	/**
+	 * Adds header comments to the specified section.
+	 * <p>
+	 * Header comments are defined as lines that start with <jk>"#"</jk> immediately preceding a section header <jk>"[section]"</jk>.
+	 * These are handled as part of the section itself instead of being interpreted as comments in the previous section.
+	 * <p>
+	 * Header comments can be of the following formats...
+	 * <ul class='spaced-list'>
+	 * 	<li><js>"# comment"</js> - A comment line.
+	 * 	<li><js>"comment"</js> - Anything else (will automatically be prefixed with <js>"# "</js>).
+	 * </ul>
+	 * <p>
+	 * If the section does not exist, it will automatically be created.
+	 *
+	 * @param section The name of the section to add lines to, or <jk>null</jk> to add to the default section.
+	 * @param headerComments The comment lines to add to the section.
+	 * @return This object (for method chaining).
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile addHeaderComments(String section, String...headerComments);
+
+	/**
+	 * Removes any header comments from the specified section.
+	 *
+	 * @param section The name of the section to remove header comments from.
+	 * @return This object (for method chaining).
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile clearHeaderComments(String section);
+
+	/**
+	 * Returns the serializer in use for this config file.
+	 *
+	 * @return This object (for method chaining).
+	 * @throws SerializeException If no serializer is defined on this config file.
+	 */
+	protected abstract WriterSerializer getSerializer() throws SerializeException;
+
+	/**
+	 * Returns the parser in use for this config file.
+	 *
+	 * @return This object (for method chaining).
+	 * @throws ParseException If no parser is defined on this config file.
+	 */
+	protected abstract ReaderParser getParser() throws ParseException;
+
+	/**
+	 * Places a read lock on this config file.
+	 */
+	protected abstract void readLock();
+
+	/**
+	 * Removes the read lock on this config file.
+	 */
+	protected abstract void readUnlock();
+
+
+	//--------------------------------------------------------------------------------
+	// API methods
+	//--------------------------------------------------------------------------------
+
+	/**
+	 * Returns the specified value as a string from the config file.
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @param def The default value if the section or value does not exist.
+	 * @return The value, or the default value if the section or value doesn't exist.
+	 */
+	public final String getString(String key, String def) {
+		assertFieldNotNull(key, "key");
+		String s = get(getSectionName(key), getSectionKey(key));
+		return (StringUtils.isEmpty(s) && def != null ? def : s);
+	}
+
+	/**
+	 * Removes an entry with the specified key.
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public final String removeString(String key) {
+		assertFieldNotNull(key, "key");
+		return remove(getSectionName(key), getSectionKey(key));
+	}
+
+	/**
+	 * Gets the entry with the specified key and converts it to the specified value.
+	 * <p>
+	 * The key can be in one of the following formats...
+	 * <ul class='spaced-list'>
+	 * 	<li><js>"key"</js> - A value in the default section (i.e. defined above any <code>[section]</code> header).
+	 * 	<li><js>"section/key"</js> - A value from the specified section.
+	 * </ul>
+	 * <p>
+	 * If the class type is an array, the value is split on commas and converted individually.
+	 * <p>
+	 * If you specify a primitive element type using this method (e.g. <code><jk>int</jk>.<jk>class</jk></code>,
+	 * 	you will get an array of wrapped objects (e.g. <code>Integer[].<jk>class</jk></code>.
+	 *
+	 * @param c The class to convert the value to.
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file.
+	 * @return The value, or <jk>null</jk> if the section or key does not exist.
+	 */
+	@SuppressWarnings("unchecked")
+	public final <T> T getObject(Class<T> c, String key) throws ParseException {
+		assertFieldNotNull(c, "c");
+		return getObject(c, key, c.isArray() ? (T)Array.newInstance(c.getComponentType(), 0) : null);
+	}
+
+	/**
+	 * Gets the entry with the specified key and converts it to the specified value..
+	 * <p>
+	 * The key can be in one of the following formats...
+	 * <ul class='spaced-list'>
+	 * 	<li><js>"key"</js> - A value in the default section (i.e. defined above any <code>[section]</code> header).
+	 * 	<li><js>"section/key"</js> - A value from the specified section.
+	 * </ul>
+	 *
+	 * @param c The class to convert the value to.
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @param def The default value if section or key does not exist.
+	 * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file.
+	 * @return The value, or <jk>null</jk> if the section or key does not exist.
+	 */
+	public final <T> T getObject(Class<T> c, String key, T def) throws ParseException {
+		assertFieldNotNull(c, "c");
+		assertFieldNotNull(key, "key");
+		return getObject(c, getSectionName(key), getSectionKey(key), def);
+	}
+
+	/**
+	 * Same as {@link #getObject(Class, String, Object)}, but value is referenced through section name and key instead of full key.
+	 *
+	 * @param c The class to convert the value to.
+	 * @param sectionName The section name.  Must not be <jk>null</jk>.
+	 * @param sectionKey The section key.  Must not be <jk>null</jk>.
+	 * @param def The default value if section or key does not exist.
+	 * @throws ParseException If parser could not parse the value or if a parser is not registered with this config file.
+	 * @return The value, or the default value if the section or value doesn't exist.
+	 */
+	@SuppressWarnings("unchecked")
+	public <T> T getObject(Class<T> c, String sectionName, String sectionKey, T def) throws ParseException {
+		String s = get(sectionName, sectionKey);
+		if (s == null)
+			return def;
+		if (c == String.class)
+			return (T)s;
+		if (c == Integer.class || c == int.class)
+			return (T)(StringUtils.isEmpty(s) ? def : Integer.valueOf(parseIntWithSuffix(s)));
+		if (c == Boolean.class || c == boolean.class)
+			return (T)(StringUtils.isEmpty(s) ? def : Boolean.valueOf(Boolean.parseBoolean(s)));
+		if (c == String[].class) {
+			String[] r = StringUtils.isEmpty(s) ? new String[0] : StringUtils.split(s, ',');
+			return (T)(r.length == 0 ? def : r);
+		}
+		if (c.isArray()) {
+			Class<?> ce = c.getComponentType();
+			if (StringUtils.isEmpty(s))
+				return def;
+			String[] r = StringUtils.split(s, ',');
+			Object o = Array.newInstance(ce, r.length);
+			for (int i = 0; i < r.length; i++)
+				Array.set(o, i, getParser().parse(r[i], ce));
+			return (T)o;
+		}
+		if (StringUtils.isEmpty(s))
+			return def;
+		return getParser().parse(s, c);
+	}
+
+	/**
+	 * Gets the entry with the specified key.
+	 * <p>
+	 * The key can be in one of the following formats...
+	 * <ul class='spaced-list'>
+	 * 	<li><js>"key"</js> - A value in the default section (i.e. defined above any <code>[section]</code> header).
+	 * 	<li><js>"section/key"</js> - A value from the specified section.
+	 * </ul>
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @return The value, or <jk>null</jk> if the section or key does not exist.
+	 */
+	public final String getString(String key) {
+		return getString(key, null);
+	}
+
+	/**
+	 * Gets the entry with the specified key, splits the value on commas, and returns the values as trimmed strings.
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @return The value, or an empty list if the section or key does not exist.
+	 */
+	public final String[] getStringArray(String key) {
+		return getStringArray(key, new String[0]);
+	}
+
+	/**
+	 * Same as {@link #getStringArray(String)} but returns a default value if the value cannot be found.
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @param def The default value if section or key does not exist.
+	 * @return The value, or an empty list if the section or key does not exist.
+	 */
+	public final String[] getStringArray(String key, String[] def) {
+		String s = getString(key);
+		if (s == null)
+			return def;
+		String[] r = StringUtils.isEmpty(s) ? new String[0] : StringUtils.split(s, ',');
+		return r.length == 0 ? def : r;
+	}
+
+	/**
+	 * Convenience method for getting int config values.
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @return The value, or <code>0</code> if the section or key does not exist or cannot be parsed as an integer.
+	 */
+	public final int getInt(String key) {
+		return getInt(key, 0);
+	}
+
+	/**
+	 * Convenience method for getting int config values.
+	 * <p>
+	 * <js>"M"</js> and <js>"K"</js> can be used to identify millions and thousands.
+	 *
+	 * <dl>
+	 * 	<dt>Example:</dt>
+	 * 	<dd>
+	 * 		<ul class='spaced-list'>
+	 * 			<li><code><js>"100K"</js> => 1024000</code>
+	 * 			<li><code><js>"100M"</js> => 104857600</code>
+	 * 		</ul>
+	 * 	</dd>
+	 * </dl>
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @param def The default value if config file or value does not exist.
+	 * @return The value, or the default value if the section or key does not exist or cannot be parsed as an integer.
+	 */
+	public final int getInt(String key, int def) {
+		String s = getString(key);
+		if (StringUtils.isEmpty(s))
+			return def;
+		return parseIntWithSuffix(s);
+	}
+
+	/**
+	 * Convenience method for getting boolean config values.
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @return The value, or <jk>false</jk> if the section or key does not exist or cannot be parsed as a boolean.
+	 */
+	public final boolean getBoolean(String key) {
+		return getBoolean(key, false);
+	}
+
+	/**
+	 * Convenience method for getting boolean config values.
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @param def The default value if config file or value does not exist.
+	 * @return The value, or the default value if the section or key does not exist or cannot be parsed as a boolean.
+	 */
+	public final boolean getBoolean(String key, boolean def) {
+		String s = getString(key);
+		return StringUtils.isEmpty(s) ? def : Boolean.parseBoolean(s);
+	}
+
+	/**
+	 * Adds or replaces an entry with the specified key with a POJO serialized to a string using the registered serializer.
+	 *	<p>
+	 *	Equivalent to calling <code>put(key, value, isEncoded(key))</code>.
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @param value The new value POJO.
+	 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
+	 * @throws SerializeException If serializer could not serialize the value or if a serializer is not registered with this config file.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public final String put(String key, Object value) throws SerializeException {
+		return put(key, value, isEncoded(key));
+	}
+
+	/**
+	 * Adds or replaces an entry with the specified key with the specified value.
+	 * <p>
+	 * The format of the entry depends on the data type of the value.
+	 * <ul class='spaced-list'>
+	 * 	<li>Simple types (<code>String</code>, <code>Number</code>, <code>Boolean</code>, primitives)
+	 * 		are serialized as plain strings.
+	 * 	<li>Arrays and collections of simple types are serialized as comma-delimited lists of plain strings.
+	 * 	<li>Other types (e.g. beans) are serialized using the serializer registered with this config file.
+	 * 	<li>Arrays and collections of other types are serialized as comma-delimited lists of serialized strings of each entry.
+	 * </ul>
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @param value The new value.
+	 *	@param encoded If <jk>true</jk>, value is encoded by the registered encoder when the config file is persisted to disk.
+	 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist.
+	 * @throws SerializeException If serializer could not serialize the value or if a serializer is not registered with this config file.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public final String put(String key, Object value, boolean encoded) throws SerializeException {
+		assertFieldNotNull(key, "key");
+		if (value == null)
+			value = "";
+		Class<?> c = value.getClass();
+		if (isSimpleType(c))
+			return put(getSectionName(key), getSectionKey(key), value.toString(), encoded);
+		if (c.isAssignableFrom(Collection.class)) {
+			Collection<?> c2 = (Collection<?>)value;
+			String[] r = new String[c2.size()];
+			int i = 0;
+			for (Object o2 : c2) {
+				boolean isSimpleType = o2 == null ? true : isSimpleType(o2.getClass());
+				r[i++] = (isSimpleType ? Array.get(value, i).toString() : getSerializer().toString(Array.get(value, i)));
+			}
+			return put(getSectionName(key), getSectionKey(key), StringUtils.join(r, ','), encoded);
+		} else if (c.isArray()) {
+			boolean isSimpleType = isSimpleType(c.getComponentType());
+			String[] r = new String[Array.getLength(value)];
+			for (int i = 0; i < r.length; i++) {
+				r[i] = (isSimpleType ? Array.get(value, i).toString() : getSerializer().toString(Array.get(value, i)));
+			}
+			return put(getSectionName(key), getSectionKey(key), StringUtils.join(r, ','), encoded);
+		}
+		return put(getSectionName(key), getSectionKey(key), getSerializer().toString(value), encoded);
+	}
+
+	private final boolean isSimpleType(Class<?> c) {
+		return (c == String.class || c.isPrimitive() || c.isAssignableFrom(Number.class) || c == Boolean.class);
+	}
+
+	/**
+	 * Returns the specified section as a map of key/value pairs.
+	 *
+	 * @param sectionName The section name to retrieve.
+	 * @return A map of the section, or <jk>null</jk> if the section was not found.
+	 */
+	public final ObjectMap getSectionMap(String sectionName) {
+		readLock();
+		try {
+			Set<String> keys = getSectionKeys(sectionName);
+			if (keys == null)
+				return null;
+			ObjectMap m = new ObjectMap();
+			for (String key : keys)
+				m.put(key, get(sectionName, key));
+			return m;
+		} finally {
+			readUnlock();
+		}
+	}
+
+	/**
+	 * Copies the entries in a section to the specified bean by calling the public setters on that bean.
+	 *
+	 *	@param sectionName The section name to write from.
+	 * @param bean The bean to set the properties on.
+	 * @param ignoreUnknownProperties If <jk>true</jk>, don't throw an {@link IllegalArgumentException} if this section
+	 * 	contains a key that doesn't correspond to a setter method.
+	 * @param permittedPropertyTypes If specified, only look for setters whose property types
+	 * 	are those listed.  If not specified, use all setters.
+	 * @return An object map of the changes made to the bean.
+	 * @throws ParseException If parser was not set on this config file or invalid properties were found in the section.
+	 * @throws IllegalArgumentException
+	 * @throws IllegalAccessException
+	 * @throws InvocationTargetException
+	 */
+	public final ObjectMap writeProperties(String sectionName, Object bean, boolean ignoreUnknownProperties, Class<?>...permittedPropertyTypes) throws ParseException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
+		assertFieldNotNull(bean, "bean");
+		ObjectMap om = new ObjectMap();
+		readLock();
+		try {
+			Set<String> keys = getSectionKeys(sectionName);
+			if (keys == null)
+				throw new IllegalArgumentException("Section not found");
+			keys = new LinkedHashSet<String>(keys);
+			for (Method m : bean.getClass().getMethods()) {
+				int mod = m.getModifiers();
+				if (isPublic(mod) && (!isStatic(mod)) && m.getName().startsWith("set") && m.getParameterTypes().length == 1) {
+					Class<?> pt = m.getParameterTypes()[0];
+					if (permittedPropertyTypes == null || permittedPropertyTypes.length == 0 || ArrayUtils.contains(pt, permittedPropertyTypes)) {
+						String propName = Introspector.decapitalize(m.getName().substring(3));
+						Object value = getObject(pt, sectionName, propName, null);
+						if (value != null) {
+							m.invoke(bean, value);
+							om.put(propName, value);
+							keys.remove(propName);
+						}
+					}
+				}
+			}
+			if (! (ignoreUnknownProperties || keys.isEmpty()))
+				throw new ParseException("Invalid properties found in config file section ["+sectionName+"]: " + JsonSerializer.DEFAULT_LAX.toString(keys));
+			return om;
+		} finally {
+			readUnlock();
+		}
+	}
+
+	/**
+	 * Shortcut for calling <code>asBean(sectionName, c, <jk>false</jk>)</code>.
+	 *
+	 * @param sectionName The section name to write from.
+	 * @param c The bean class to create.
+	 * @return A new bean instance.
+	 * @throws ParseException
+	 */
+	public final <T> T getSectionAsBean(String sectionName, Class<T>c) throws ParseException {
+		return getSectionAsBean(sectionName, c, false);
+	}
+
+	/**
+	 * Converts this config file section to the specified bean instance.
+	 *
+	 *	@param sectionName The section name to write from.
+	 * @param c The bean class to create.
+	 * @param ignoreUnknownProperties If <jk>false</jk>, throws a {@link ParseException} if
+	 * 	the section contains an entry that isn't a bean property name.
+	 * @return A new bean instance.
+	 * @throws ParseException
+	 */
+	public final <T> T getSectionAsBean(String sectionName, Class<T> c, boolean ignoreUnknownProperties) throws ParseException {
+		assertFieldNotNull(c, "c");
+		readLock();
+		try {
+			BeanMap<T> bm = getParser().getBeanContext().newBeanMap(c);
+			for (String k : getSectionKeys(sectionName)) {
+				BeanPropertyMeta<?> bpm = bm.getPropertyMeta(k);
+				if (bpm == null) {
+					if (! ignoreUnknownProperties)
+						throw new ParseException("Unknown property {0} encountered", k);
+				} else {
+					bm.put(k, getObject(bpm.getClassMeta().getInnerClass(), sectionName + '/' + k));
+				}
+			}
+			return bm.getBean();
+		} finally {
+			readUnlock();
+		}
+	}
+
+	/**
+	 * Returns <jk>true</jk> if this section contains the specified key and the key has a non-blank value.
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @return <jk>true</jk> if this section contains the specified key and the key has a non-blank value.
+	 */
+	public final boolean containsNonEmptyValue(String key) {
+		return ! StringUtils.isEmpty(getString(key, null));
+	}
+
+	/**
+	 * Gets the section with the specified name.
+	 *
+	 * @param name The section name.
+	 * @return The section, or <jk>null</jk> if section does not exist.
+	 */
+	protected abstract Section getSection(String name);
+
+	/**
+	 * Gets the section with the specified name and optionally creates it if it's not there.
+	 *
+	 * @param name The section name.
+	 * @param create Create the section if it's not there.
+	 * @return The section, or <jk>null</jk> if section does not exist.
+	 * @throws UnsupportedOperationException If config file is read only and section doesn't exist and <code>create</code> is <jk>true</jk>.
+	 */
+	protected abstract Section getSection(String name, boolean create);
+
+	/**
+	 * Appends a section to this config file if it does not already exist.
+	 * <p>
+	 * Returns the existing section if it already exists.
+	 *
+	 * @param name The section name, or <jk>null</jk> for the default section.
+	 * @return The appended or existing section.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile addSection(String name);
+
+	/**
+	 * Creates or overwrites the specified section.
+	 *
+	 * @param name The section name, or <jk>null</jk> for the default section.
+	 * @param contents The contents of the new section.
+	 * @return The appended or existing section.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile setSection(String name, Map<String,String> contents);
+
+	/**
+	 * Removes the section with the specified name.
+	 *
+	 * @param name The name of the section to remove, or <jk>null</jk> for the default section.
+	 * @return The removed section, or <jk>null</jk> if named section does not exist.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile removeSection(String name);
+
+	/**
+	 * Returns <jk>true</jk> if the encoding flag is set on the specified entry.
+	 *
+	 * @param key The key.  See {@link #getString(String)} for a description of the key.
+	 * @return <jk>true</jk> if the encoding flag is set on the specified entry.
+	 */
+	public abstract boolean isEncoded(String key);
+
+	/**
+	 * Saves this config file to disk.
+	 *
+	 * @return This object (for method chaining).
+	 * @throws IOException If a problem occurred trying to save file to disk, or file is not associated with this object.
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile save() throws IOException;
+
+	/**
+	 * Saves this config file to the specified writer as an INI file.
+	 * <p>
+	 * The writer will automatically be closed.
+	 *
+	 * @param out The writer to send the output to.
+	 * @return This object (for method chaining).
+	 * @throws IOException If a problem occurred trying to send contents to the writer.
+	 */
+	public final ConfigFile serializeTo(Writer out) throws IOException {
+		return serializeTo(out, INI);
+	}
+
+	/**
+	 * Same as {@link #serializeTo(Writer)}, except allows you to explicitely specify a format.
+	 *
+	 * @param out The writer to send the output to.
+	 * @param format The {@link ConfigFileFormat} of the output.
+	 * @return This object (for method chaining).
+	 * @throws IOException If a problem occurred trying to send contents to the writer.
+	 */
+	public abstract ConfigFile serializeTo(Writer out, ConfigFileFormat format) throws IOException;
+
+	/**
+	 * Add a listener to this config file to react to modification events.
+	 *
+	 * @param listener The new listener to add.
+	 * @return This object (for method chaining).
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile addListener(ConfigFileListener listener);
+
+	/**
+	 * Merges the contents of the specified config file into this config file.
+	 * <p>
+	 * Pretty much identical to just replacing this config file, but
+	 * 	causes the {@link ConfigFileListener#onChange(ConfigFile, Set)} method to be invoked
+	 * 	on differences between the file.
+	 * @param cf The config file whose values should be copied into this config file.
+	 * @return This object (for method chaining).
+	 * @throws UnsupportedOperationException If config file is read only.
+	 */
+	public abstract ConfigFile merge(ConfigFile cf);
+
+	/**
+	 * Returns the config file contents as a string.
+	 * <p>
+	 * The contents of the string are the same as the contents that would be serialized to disk.
+	 */
+	@Override /* Object */
+	public abstract String toString();
+
+	/**
+	 * Returns a wrapped instance of this config file where calls to getters
+	 * 	have their values first resolved by the specified {@link VarResolver}.
+	 *
+	 * @param vr The {@link VarResolver} for resolving variables in values.
+	 * @return This config file wrapped in an instance of {@link ConfigFileWrapped}.
+	 */
+	public abstract ConfigFile getResolving(VarResolver vr);
+
+	/**
+	 * Returns a wrapped instance of this config file where calls to getters
+	 * 	have their values first resolved by the specified {@link VarResolverSession}.
+	 *
+	 * @param vs The {@link VarResolverSession} for resolving variables in values.
+	 * @return This config file wrapped in an instance of {@link ConfigFileWrapped}.
+	 */
+	public abstract ConfigFile getResolving(VarResolverSession vs);
+
+	/**
+	 * Returns a wrapped instance of this config file where calls to getters have their values
+	 * 	first resolved by a default {@link VarResolver}.
+	 *
+	 *  The default {@link VarResolver} is registered with the following {@link Var StringVars}:
+	 * <ul class='spaced-list'>
+	 * 	<li><code>$S{key}</code>,<code>$S{key,default}</code> - System properties.
+	 * 	<li><code>$E{key}</code>,<code>$E{key,default}</code> - Environment variables.
+	 * 	<li><code>$C{key}</code>,<code>$C{key,default}</code> - Values in this configuration file.
+	 * </ul>
+	 *
+	 * @return A new config file that resolves string variables.
+	 */
+	public abstract ConfigFile getResolving();
+
+	/**
+	 * Wraps this config file in a {@link Writable} interface that renders it as plain text.
+	 *
+	 * @return This config file wrapped in a {@link Writable}.
+	 */
+	public abstract Writable toWritable();
+
+	/**
+	 * @return The string var resolver associated with this config file.
+	 */
+	protected VarResolver getVarResolver() {
+		// Only ConfigFileWrapped returns a value.
+		return null;
+	}
+
+
+	private int parseIntWithSuffix(String s) {
+		assertFieldNotNull(s, "s");
+		int m = 1;
+		if (s.endsWith("M")) {
+			m = 1024*1024;
+			s = s.substring(0, s.length()-1).trim();
+		} else if (s.endsWith("K")) {
+			m = 1024;
+			s = s.substring(0, s.length()-1).trim();
+		}
+		return Integer.parseInt(s) * m;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java
new file mode 100644
index 0000000..821f7d3
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java
@@ -0,0 +1,29 @@
+/***************************************************************************************************************************
+ * 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.ini;
+
+import java.io.*;
+
+/**
+ * Valid formats that can be passed to the {@link ConfigFile#serializeTo(Writer, ConfigFileFormat)} method.
+ */
+public enum ConfigFileFormat {
+	/** Normal INI file format*/
+	INI,
+
+	/** Batch file with "set X" commands */
+	BATCH,
+
+	/** Shell script file with "export X" commands */
+	SHELL;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java
new file mode 100644
index 0000000..4aaaa46
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java
@@ -0,0 +1,747 @@
+/***************************************************************************************************************************
+ * 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.ini;
+
+import static org.apache.juneau.ini.ConfigUtils.*;
+import static org.apache.juneau.internal.ThrowableUtils.*;
+
+import java.io.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.concurrent.locks.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+import org.apache.juneau.svl.vars.*;
+
+/**
+ * Implementation class for {@link ConfigFile}.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+public final class ConfigFileImpl extends ConfigFile {
+
+	private final File file;
+	private final Encoder encoder;
+	private final WriterSerializer serializer;
+	private final ReaderParser parser;
+	private final Charset charset;
+	final List<ConfigFileListener> listeners = Collections.synchronizedList(new ArrayList<ConfigFileListener>());
+
+	private Map<String,Section> sections;  // The actual data.
+
+	private static final String DEFAULT = "default";
+
+	private final boolean readOnly;
+
+	volatile boolean hasBeenModified = false;
+	private ReadWriteLock lock = new ReentrantReadWriteLock();
+
+	long modifiedTimestamp;
+
+	/**
+	 * Constructor.
+	 * <p>
+	 * Loads the contents of the specified file into this config file.
+	 * <p>
+	 * If file does not initially exist, this object will start off empty.
+	 *
+	 * @param file The INI file on disk.
+	 * 	If <jk>null</jk>, create an in-memory config file.
+	 * @param readOnly Make this configuration file read-only.
+	 *		Attempting to set any values on this config file will cause {@link UnsupportedOperationException} to be thrown.
+	 *	@param encoder The encoder to use for encoding sensitive values in this configuration file.
+	 * 	If <jk>null</jk>, defaults to {@link XorEncoder#INSTANCE}.
+	 *	@param serializer The serializer to use for serializing POJOs in the {@link #put(String, Object)} method.
+	 * 	If <jk>null</jk>, defaults to {@link JsonSerializer#DEFAULT}.
+	 *	@param parser The parser to use for parsing POJOs in the {@link #getObject(Class,String)} method.
+	 * 	If <jk>null</jk>, defaults to {@link JsonParser#DEFAULT}.
+	 * @param charset The charset on the files.
+	 * 	If <jk>null</jk>, defaults to {@link Charset#defaultCharset()}.
+	 * @throws IOException
+	 */
+	public ConfigFileImpl(File file, boolean readOnly, Encoder encoder, WriterSerializer serializer, ReaderParser parser, Charset charset) throws IOException {
+		this.file = file;
+		this.encoder = encoder == null ? XorEncoder.INSTANCE : encoder;
+		this.serializer = serializer == null ? JsonSerializer.DEFAULT : serializer;
+		this.parser = parser == null ? JsonParser.DEFAULT : parser;
+		this.charset = charset == null ? Charset.defaultCharset() : charset;
+		load();
+		this.readOnly = readOnly;
+		if (readOnly) {
+			this.sections = Collections.unmodifiableMap(this.sections);
+			for (Section s : sections.values())
+				s.setReadOnly();
+		}
+	}
+
+	/**
+	 * Constructor.
+	 * Shortcut for calling <code><jk>new</jk> ConfigFileImpl(file, <jk>false</jk>, <jk>null</jk>, <jk>null</jk>, <jk>null</jk>, <jk>null</jk>);</code>
+	 *
+	 * @param file The config file.  Does not need to exist.
+	 * @throws IOException
+	 */
+	public ConfigFileImpl(File file) throws IOException {
+		this(file, false, null, null, null, null);
+	}
+
+	/**
+	 * Constructor.
+	 * Shortcut for calling <code><jk>new</jk> ConfigFileImpl(<jk>null</jk>, <jk>false</jk>, <jk>null</jk>, <jk>null</jk>, <jk>null</jk>, <jk>null</jk>);</code>
+	 *
+	 * @throws IOException
+	 */
+	public ConfigFileImpl() throws IOException {
+		this(null);
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFileImpl loadIfModified() throws IOException {
+		if (file == null)
+			return this;
+		writeLock();
+		try {
+			if (file.lastModified() > modifiedTimestamp)
+				load();
+		} finally {
+			writeUnlock();
+		}
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFileImpl load() throws IOException {
+		Reader r = null;
+		if (file != null && file.exists())
+			r = new InputStreamReader(new FileInputStream(file), charset);
+		else
+			r = new StringReader("");
+		try {
+			load(r);
+		} finally {
+			r.close();
+		}
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFileImpl load(Reader r) throws IOException {
+		assertFieldNotNull(r, "r");
+		writeLock();
+		try {
+			this.sections = Collections.synchronizedMap(new LinkedHashMap<String,Section>());
+			BufferedReader in = new BufferedReader(r);
+			try {
+				writeLock();
+				hasBeenModified = false;
+				try {
+					sections.clear();
+					String line = null;
+					Section section = getSection(null, true);
+					ArrayList<String> lines = new ArrayList<String>();
+					boolean canAppend = false;
+					while ((line = in.readLine()) != null) {
+						if (isSection(line)) {
+							section.addLines(null, lines.toArray(new String[lines.size()]));
+							lines.clear();
+							canAppend = false;
+							String sn = StringUtils.replaceUnicodeSequences(line.substring(line.indexOf('[')+1, line.indexOf(']')).trim());
+							section = getSection(sn, true).addHeaderComments(section.removeTrailingComments());
+						} else {
+							char c = line.isEmpty() ? 0 : line.charAt(0);
+							if ((c == ' ' || c == '\t') && canAppend && ! (isComment(line) || isAssignment(line)))
+								lines.add(lines.remove(lines.size()-1) + '\n' + line.substring(1));
+							else {
+								lines.add(line);
+								if (isAssignment(line))
+									canAppend = true;
+								else
+									canAppend = canAppend && ! (StringUtils.isEmpty(line) || isComment(line));
+							}
+						}
+					}
+					section.addLines(null, lines.toArray(new String[lines.size()]));
+					in.close();
+					if (hasBeenModified)  // Set when values need to be encoded.
+						save();
+					if (file != null)
+						modifiedTimestamp = file.lastModified();
+				} finally {
+					writeUnlock();
+				}
+			} finally {
+				in.close();
+			}
+		} finally {
+			writeUnlock();
+		}
+		for (ConfigFileListener l : listeners)
+			l.onLoad(this);
+		return this;
+	}
+
+	//--------------------------------------------------------------------------------
+	// Map methods
+	//--------------------------------------------------------------------------------
+
+	@Override /* Map */
+	public Section get(Object key) {
+		if (StringUtils.isEmpty(key))
+			key = DEFAULT;
+		readLock();
+		try {
+			return sections.get(key);
+		} finally {
+			readUnlock();
+		}
+	}
+
+	@Override /* Map */
+	public Section put(String key, Section section) {
+		Set<String> changes = createChanges();
+		Section old = put(key, section, changes);
+		signalChanges(changes);
+		return old;
+	}
+
+	private Section put(String key, Section section, Set<String> changes) {
+		if (StringUtils.isEmpty(key))
+			key = DEFAULT;
+		writeLock();
+		try {
+			Section prev = sections.put(key, section);
+			findChanges(changes, prev, section);
+			return prev;
+		} finally {
+			writeUnlock();
+		}
+	}
+
+	@Override /* Map */
+	public void putAll(Map<? extends String,? extends Section> map) {
+		Set<String> changes = createChanges();
+		writeLock();
+		try {
+			for (Map.Entry<? extends String,? extends Section> e : map.entrySet())
+				put(e.getKey(), e.getValue(), changes);
+		} finally {
+			writeUnlock();
+		}
+		signalChanges(changes);
+	}
+
+	@Override /* Map */
+	public void clear() {
+		Set<String> changes = createChanges();
+		writeLock();
+		try {
+			for (Section s : values())
+				findChanges(changes, s, null);
+			sections.clear();
+		} finally {
+			writeUnlock();
+		}
+		signalChanges(changes);
+	}
+
+	@Override /* Map */
+	public boolean containsKey(Object key) {
+		if (StringUtils.isEmpty(key))
+			key = DEFAULT;
+		return sections.containsKey(key);
+	}
+
+	@Override /* Map */
+	public boolean containsValue(Object value) {
+		return sections.containsValue(value);
+	}
+
+	@Override /* Map */
+	public Set<Map.Entry<String,Section>> entrySet() {
+
+		// We need to create our own set so that entries are removed correctly.
+		return new AbstractSet<Map.Entry<String,Section>>() {
+			@Override /* Map */
+			public Iterator<Map.Entry<String,Section>> iterator() {
+				return new Iterator<Map.Entry<String,Section>>() {
+					Iterator<Map.Entry<String,Section>> i = sections.entrySet().iterator();
+					Map.Entry<String,Section> i2;
+
+					@Override /* Iterator */
+					public boolean hasNext() {
+						return i.hasNext();
+					}
+
+					@Override /* Iterator */
+					public Map.Entry<String,Section> next() {
+						i2 = i.next();
+						return i2;
+					}
+
+					@Override /* Iterator */
+					public void remove() {
+						Set<String> changes = createChanges();
+						findChanges(changes, i2.getValue(), null);
+						i.remove();
+						signalChanges(changes);
+					}
+				};
+			}
+
+			@Override /* Map */
+			public int size() {
+				return sections.size();
+			}
+		};
+	}
+
+	@Override /* Map */
+	public boolean isEmpty() {
+		return sections.isEmpty();
+	}
+
+	@Override /* Map */
+	public Set<String> keySet() {
+
+		// We need to create our own set so that sections are removed correctly.
+		return new AbstractSet<String>() {
+			@Override /* Set */
+			public Iterator<String> iterator() {
+				return new Iterator<String>() {
+					Iterator<String> i = sections.keySet().iterator();
+					String i2;
+
+					@Override /* Iterator */
+					public boolean hasNext() {
+						return i.hasNext();
+					}
+
+					@Override /* Iterator */
+					public String next() {
+						i2 = i.next();
+						return i2;
+					}
+
+					@Override /* Iterator */
+					public void remove() {
+						Set<String> changes = createChanges();
+						findChanges(changes, sections.get(i2), null);
+						i.remove();
+						signalChanges(changes);
+					}
+				};
+			}
+
+			@Override /* Set */
+			public int size() {
+				return sections.size();
+			}
+		};
+	}
+
+	@Override /* Map */
+	public int size() {
+		return sections.size();
+	}
+
+	@Override /* Map */
+	public Collection<Section> values() {
+		return new AbstractCollection<Section>() {
+			@Override /* Collection */
+			public Iterator<Section> iterator() {
+				return new Iterator<Section>() {
+					Iterator<Section> i = sections.values().iterator();
+					Section i2;
+
+					@Override /* Iterator */
+					public boolean hasNext() {
+						return i.hasNext();
+					}
+
+					@Override /* Iterator */
+					public Section next() {
+						i2 = i.next();
+						return i2;
+					}
+
+					@Override /* Iterator */
+					public void remove() {
+						Set<String> changes = createChanges();
+						findChanges(changes, i2, null);
+						i.remove();
+						signalChanges(changes);
+					}
+				};
+			}
+			@Override /* Collection */
+			public int size() {
+				return sections.size();
+			}
+		};
+	}
+
+	@Override /* Map */
+	public Section remove(Object key) {
+		Set<String> changes = createChanges();
+		Section prev = remove(key, changes);
+		signalChanges(changes);
+		return prev;
+	}
+
+	private Section remove(Object key, Set<String> changes) {
+		writeLock();
+		try {
+			Section prev = sections.remove(key);
+			findChanges(changes, prev, null);
+			return prev;
+		} finally {
+			writeUnlock();
+		}
+	}
+
+	//--------------------------------------------------------------------------------
+	// API methods
+	//--------------------------------------------------------------------------------
+
+	@Override /* ConfigFile */
+	public String get(String sectionName, String sectionKey) {
+		assertFieldNotNull(sectionKey, "sectionKey");
+		Section s = get(sectionName);
+		if (s == null)
+			return null;
+		Object s2 = s.get(sectionKey);
+		return (s2 == null ? null : s2.toString());
+	}
+
+	@Override /* ConfigFile */
+	public String put(String sectionName, String sectionKey, Object value, boolean encoded) {
+		assertFieldNotNull(sectionKey, "sectionKey");
+		Section s = getSection(sectionName, true);
+		return s.put(sectionKey, value.toString(), encoded);
+	}
+
+	@Override /* ConfigFile */
+	public String remove(String sectionName, String sectionKey) {
+		assertFieldNotNull(sectionKey, "sectionKey");
+		Section s = getSection(sectionName, false);
+		if (s == null)
+			return null;
+		return s.remove(sectionKey);
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFileImpl addLines(String section, String...lines) {
+		Set<String> changes = createChanges();
+		writeLock();
+		try {
+			getSection(section, true).addLines(changes, lines);
+		} finally {
+			writeUnlock();
+		}
+		signalChanges(changes);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFileImpl addHeaderComments(String section, String...headerComments) {
+		writeLock();
+		try {
+			if (headerComments == null)
+				headerComments = new String[0];
+			getSection(section, true).addHeaderComments(Arrays.asList(headerComments));
+		} finally {
+			writeUnlock();
+		}
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFileImpl clearHeaderComments(String section) {
+		writeLock();
+		try {
+			Section s = getSection(section, false);
+			if (s != null)
+				s.clearHeaderComments();
+		} finally {
+			writeUnlock();
+		}
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public Section getSection(String name) {
+		return getSection(name, false);
+	}
+
+	@Override /* ConfigFile */
+	public Section getSection(String name, boolean create) {
+		if (StringUtils.isEmpty(name))
+			name = DEFAULT;
+		Section s = sections.get(name);
+		if (s != null)
+			return s;
+		if (create) {
+			s = new Section().setParent(this).setName(name);
+			sections.put(name, s);
+			return s;
+		}
+		return null;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFileImpl addSection(String name) {
+		writeLock();
+		try {
+			getSection(name, true);
+		} finally {
+			writeUnlock();
+		}
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile setSection(String name, Map<String,String> contents) {
+		writeLock();
+		try {
+			put(name, new Section(contents).setParent(this).setName(name));
+		} finally {
+			writeUnlock();
+		}
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFileImpl removeSection(String name) {
+		Set<String> changes = createChanges();
+		writeLock();
+		try {
+			Section prev = sections.remove(name);
+			if (changes != null && prev != null)
+				findChanges(changes, prev, null);
+		} finally {
+			writeUnlock();
+		}
+		signalChanges(changes);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public Set<String> getSectionKeys(String sectionName) {
+		Section s = get(sectionName);
+		if (s == null)
+			return null;
+		return s.keySet();
+	}
+
+	@Override /* ConfigFile */
+	public boolean isEncoded(String key) {
+		assertFieldNotNull(key, "key");
+		String section = getSectionName(key);
+		Section s = getSection(section, false);
+		if (s == null)
+			return false;
+		return s.isEncoded(getSectionKey(key));
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFileImpl save() throws IOException {
+		writeLock();
+		try {
+			if (file == null)
+				throw new UnsupportedOperationException("No backing file specified for config file.");
+			Writer out = new OutputStreamWriter(new FileOutputStream(file), charset);
+			try {
+				serializeTo(out);
+				hasBeenModified = false;
+				modifiedTimestamp = file.lastModified();
+			} finally {
+				out.close();
+			}
+			for (ConfigFileListener l : listeners)
+				l.onSave(this);
+			return this;
+		} finally {
+			writeUnlock();
+		}
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFileImpl serializeTo(Writer out, ConfigFileFormat format) throws IOException {
+		readLock();
+		try {
+			PrintWriter pw = (out instanceof PrintWriter ? (PrintWriter)out : new PrintWriter(out));
+			for (Section s : sections.values())
+				s.writeTo(pw, format);
+			pw.flush();
+			pw.close();
+			out.close();
+		} finally {
+			readUnlock();
+		}
+		return this;
+	}
+
+	void setHasBeenModified() {
+		hasBeenModified = true;
+	}
+
+	@Override /* ConfigFile */
+	public String toString() {
+		try {
+			StringWriter sw = new StringWriter();
+			toWritable().writeTo(sw);
+			return sw.toString();
+		} catch (IOException e) {
+			return e.getLocalizedMessage();
+		}
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile addListener(ConfigFileListener listener) {
+		assertFieldNotNull(listener, "listener");
+		writeLock();
+		try {
+			this.listeners.add(listener);
+			return this;
+		} finally {
+			writeUnlock();
+		}
+	}
+
+	List<ConfigFileListener> getListeners() {
+		return listeners;
+	}
+
+	@Override /* ConfigFile */
+	public Writable toWritable() {
+		return new ConfigFileWritable(this);
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile merge(ConfigFile cf) {
+		assertFieldNotNull(cf, "cf");
+		Set<String> changes = createChanges();
+		writeLock();
+		try {
+			for (String sectionName : this.keySet())
+				if (! cf.containsKey(sectionName))
+					remove(sectionName, changes);
+
+			for (Map.Entry<String,Section> e : cf.entrySet())
+				put(e.getKey(), e.getValue(), changes);
+
+		} finally {
+			writeUnlock();
+		}
+		signalChanges(changes);
+		return this;
+	}
+
+	Encoder getEncoder() {
+		return encoder;
+	}
+
+	@Override /* ConfigFile */
+	protected WriterSerializer getSerializer() throws SerializeException {
+		if (serializer == null)
+			throw new SerializeException("Serializer not defined on config file.");
+		return serializer;
+	}
+
+	@Override /* ConfigFile */
+	protected ReaderParser getParser() throws ParseException {
+		if (parser == null)
+			throw new ParseException("Parser not defined on config file.");
+		return parser;
+	}
+
+	@Override /* ConfigFile */
+	protected void readLock() {
+		lock.readLock().lock();
+	}
+
+	@Override /* ConfigFile */
+	protected void readUnlock() {
+		lock.readLock().unlock();
+	}
+
+	private void writeLock() {
+		if (readOnly)
+			throw new UnsupportedOperationException("Cannot modify read-only ConfigFile.");
+		lock.writeLock().lock();
+		hasBeenModified = true;
+	}
+
+	private void writeUnlock() {
+		lock.writeLock().unlock();
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile getResolving(VarResolver vr) {
+		assertFieldNotNull(vr, "vr");
+		return new ConfigFileWrapped(this, vr);
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile getResolving(VarResolverSession vs) {
+		assertFieldNotNull(vs, "vs");
+		return new ConfigFileWrapped(this, vs);
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile getResolving() {
+		return getResolving(VarResolver.DEFAULT.clone().addVars(ConfigFileVar.class).setContextObject(ConfigFileVar.SESSION_config, this));
+	}
+
+	/*
+	 * Finds the keys that are different between the two sections and adds it to
+	 * the specified set.
+	 */
+	private void findChanges(Set<String> s, Section a, Section b) {
+		if (s == null)
+			return;
+		String sname = (a == null ? b.name : a.name);
+		if (a == null) {
+			for (String k : b.keySet())
+				s.add(getFullKey(sname, k));
+		} else if (b == null) {
+			for (String k : a.keySet())
+				s.add(getFullKey(sname, k));
+		} else {
+			for (String k : a.keySet())
+				addChange(s, sname, k, a.get(k), b.get(k));
+			for (String k : b.keySet())
+				addChange(s, sname, k, a.get(k), b.get(k));
+		}
+	}
+
+	private void addChange(Set<String> changes, String section, String key, String oldVal, String newVal) {
+		if (! StringUtils.isEquals(oldVal, newVal))
+			changes.add(getFullKey(section, key));
+	}
+
+	private Set<String> createChanges() {
+		return (listeners.size() > 0 ? new LinkedHashSet<String>() : null);
+	}
+
+	private void signalChanges(Set<String> changes) {
+		if (changes != null && ! changes.isEmpty())
+			for (ConfigFileListener l : listeners)
+				l.onChange(this, changes);
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileListener.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileListener.java b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileListener.java
new file mode 100644
index 0000000..3a1d37a
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileListener.java
@@ -0,0 +1,46 @@
+/***************************************************************************************************************************
+ * 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.ini;
+
+import java.util.*;
+
+
+/**
+ * Listener that can be used to listen for change events in config files.
+ * <p>
+ * Use the {@link ConfigFile#addListener(ConfigFileListener)} method to register listeners.
+ */
+public class ConfigFileListener {
+
+	/**
+	 * Gets called immediately after a config file has been loaded.
+	 *
+	 * @param cf The config file being loaded.
+	 */
+	public void onLoad(ConfigFile cf) {}
+
+	/**
+	 * Gets called immediately after a config file has been saved.
+	 *
+	 * @param cf The config file being saved.
+	 */
+	public void onSave(ConfigFile cf) {}
+
+	/**
+	 * Signifies that the specified values have changed.
+	 *
+	 * @param cf The config file being modified.
+	 * @param changes The full keys (e.g. <js>"Section/key"</js>) of entries that have changed in the config file.
+	 */
+	public void onChange(ConfigFile cf, Set<String> changes) {}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java
new file mode 100644
index 0000000..c9175d4
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java
@@ -0,0 +1,278 @@
+/***************************************************************************************************************************
+ * 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.ini;
+
+import static org.apache.juneau.internal.ThrowableUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+import org.apache.juneau.svl.vars.*;
+
+/**
+ * Wraps an instance of {@link ConfigFileImpl} in an interface that will
+ * 	automatically replace {@link VarResolver} variables.
+ * <p>
+ * The {@link ConfigFile#getResolving(VarResolver)} returns an instance of this class.
+ * <p>
+ * This class overrides the {@link #getString(String, String)} to resolve string variables.
+ * All other method calls are passed through to the inner config file.
+ *
+ * @author James Bognar (james.bognar@salesforce.com)
+ */
+public final class ConfigFileWrapped extends ConfigFile {
+
+	private final ConfigFileImpl cf;
+	private final VarResolverSession vs;
+
+	ConfigFileWrapped(ConfigFileImpl cf, VarResolver vr) {
+		this.cf = cf;
+		this.vs = vr.clone()
+			.addVars(ConfigFileVar.class)
+			.setContextObject(ConfigFileVar.SESSION_config, cf)
+			.createSession();
+	}
+
+	ConfigFileWrapped(ConfigFileImpl cf, VarResolverSession vs) {
+		this.cf = cf;
+		this.vs = vs;
+	}
+
+	@Override /* ConfigFile */
+	public void clear() {
+		cf.clear();
+	}
+
+	@Override /* ConfigFile */
+	public boolean containsKey(Object key) {
+		return cf.containsKey(key);
+	}
+
+	@Override /* ConfigFile */
+	public boolean containsValue(Object value) {
+		return cf.containsValue(value);
+	}
+
+	@Override /* ConfigFile */
+	public Set<java.util.Map.Entry<String,Section>> entrySet() {
+		return cf.entrySet();
+	}
+
+	@Override /* ConfigFile */
+	public Section get(Object key) {
+		return cf.get(key);
+	}
+
+	@Override /* ConfigFile */
+	public boolean isEmpty() {
+		return cf.isEmpty();
+	}
+
+	@Override /* ConfigFile */
+	public Set<String> keySet() {
+		return cf.keySet();
+	}
+
+	@Override /* ConfigFile */
+	public Section put(String key, Section value) {
+		return cf.put(key, value);
+	}
+
+	@Override /* ConfigFile */
+	public void putAll(Map<? extends String,? extends Section> map) {
+		cf.putAll(map);
+	}
+
+	@Override /* ConfigFile */
+	public Section remove(Object key) {
+		return cf.remove(key);
+	}
+
+	@Override /* ConfigFile */
+	public int size() {
+		return cf.size();
+	}
+
+	@Override /* ConfigFile */
+	public Collection<Section> values() {
+		return cf.values();
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile loadIfModified() throws IOException {
+		cf.loadIfModified();
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile load() throws IOException {
+		cf.load();
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile load(Reader r) throws IOException {
+		cf.load(r);
+		return this;
+	}
+
+
+	@Override /* ConfigFile */
+	public boolean isEncoded(String key) {
+		return cf.isEncoded(key);
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile addLines(String section, String... lines) {
+		cf.addLines(section, lines);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile addHeaderComments(String section, String... headerComments) {
+		cf.addHeaderComments(section, headerComments);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile clearHeaderComments(String section) {
+		cf.clearHeaderComments(section);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public Section getSection(String name) {
+		return cf.getSection(name);
+	}
+
+	@Override /* ConfigFile */
+	public Section getSection(String name, boolean create) {
+		return cf.getSection(name, create);
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile addSection(String name) {
+		cf.addSection(name);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile setSection(String name, Map<String,String> contents) {
+		cf.setSection(name, contents);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile removeSection(String name) {
+		cf.removeSection(name);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile save() throws IOException {
+		cf.save();
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile serializeTo(Writer out, ConfigFileFormat format) throws IOException {
+		cf.serializeTo(out, format);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public String toString() {
+		return cf.toString();
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile getResolving(VarResolver varResolver) {
+		assertFieldNotNull(varResolver, "vr");
+		return new ConfigFileWrapped(cf, varResolver);
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile getResolving(VarResolverSession varSession) {
+		assertFieldNotNull(varSession, "vs");
+		return new ConfigFileWrapped(cf, varSession);
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile getResolving() {
+		return new ConfigFileWrapped(cf, VarResolver.DEFAULT);
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile addListener(ConfigFileListener listener) {
+		cf.addListener(listener);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	public Writable toWritable() {
+		return cf.toWritable();
+	}
+
+	@Override /* ConfigFile */
+	public ConfigFile merge(ConfigFile newCf) {
+		cf.merge(newCf);
+		return this;
+	}
+
+	@Override /* ConfigFile */
+	protected WriterSerializer getSerializer() throws SerializeException {
+		return cf.getSerializer();
+	}
+
+	@Override /* ConfigFile */
+	protected ReaderParser getParser() throws ParseException {
+		return cf.getParser();
+	}
+
+	@Override /* ConfigFile */
+	public String get(String sectionName, String sectionKey) {
+		String s = cf.get(sectionName, sectionKey);
+		if (s == null)
+			return null;
+		return vs.resolve(s);
+	}
+
+	@Override /* ConfigFile */
+	public String put(String sectionName, String sectionKey, Object value, boolean encoded) {
+		return cf.put(sectionName, sectionKey, value, encoded);
+	}
+
+	@Override /* ConfigFile */
+	public String remove(String sectionName, String sectionKey) {
+		return cf.remove(sectionName, sectionKey);
+	}
+
+	@Override /* ConfigFile */
+	public Set<String> getSectionKeys(String sectionName) {
+		return cf.getSectionKeys(sectionName);
+	}
+
+	@Override /* ConfigFile */
+	protected void readLock() {
+		cf.readLock();
+	}
+
+	@Override /* ConfigFile */
+	protected void readUnlock() {
+		cf.readUnlock();
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/1b4f98a0/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
----------------------------------------------------------------------
diff --git a/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
new file mode 100644
index 0000000..1d076fa
--- /dev/null
+++ b/org.apache.juneau/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
@@ -0,0 +1,44 @@
+/***************************************************************************************************************************
+ * 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.ini;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+
+/**
+ * Wraps a {@link ConfigFile} in a {@link Writable} to be rendered as plain text.
+ */
+class ConfigFileWritable implements Writable {
+
+	private ConfigFileImpl cf;
+
+	protected ConfigFileWritable(ConfigFileImpl cf) {
+		this.cf = cf;
+	}
+
+	@Override /* Writable */
+	public void writeTo(Writer out) throws IOException {
+		cf.readLock();
+		try {
+			cf.serializeTo(out);
+		} finally {
+			cf.readUnlock();
+		}
+	}
+
+	@Override /* Writable */
+	public String getMediaType() {
+		return "text/plain";
+	}
+}