http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/HtmlStrippedDocSerializer.java
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/HtmlStrippedDocSerializer.java
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/HtmlStrippedDocSerializer.java
new file mode 100755
index 0000000..a03c32c
--- /dev/null
+++ 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/HtmlStrippedDocSerializer.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2011, 2015. All Rights Reserved.
+ *
+ *  The source code for this program is not published or otherwise
+ *  divested of its trade secrets, irrespective of what has been
+ *  deposited with the U.S. Copyright Office.
+ 
*******************************************************************************/
+package com.ibm.juno.core.html;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import com.ibm.juno.core.annotation.*;
+import com.ibm.juno.core.serializer.*;
+
+/**
+ * Serializes POJOs to HTTP responses as stripped HTML.
+ *
+ *
+ * <h6 class='topic'>Media types</h6>
+ * <p>
+ *     Handles <code>Accept</code> types: <code>text/html+stripped</code>
+ * <p>
+ *     Produces <code>Content-Type</code> types: <code>text/html</code>
+ *
+ *
+ * <h6 class='topic'>Description</h6>
+ * <p>
+ *     Produces the same output as {@link HtmlDocSerializer}, but without the 
header and body tags and page title and description.
+ *     Used primarily for JUnit testing the {@link HtmlDocSerializer} class.
+ *
+ *
+ * @author James Bognar ([email protected])
+ */
+@Produces(value="text/html+stripped",contentType="text/html")
+public class HtmlStrippedDocSerializer extends HtmlSerializer {
+
+       
//---------------------------------------------------------------------------
+       // Overridden methods
+       
//---------------------------------------------------------------------------
+
+       @Override /* Serializer */
+       protected void doSerialize(Object o, Writer out, SerializerContext ctx) 
throws IOException, SerializeException {
+               HtmlSerializerContext hctx = (HtmlSerializerContext)ctx;
+               HtmlSerializerWriter w = hctx.getWriter(out);
+               if (o == null
+                       || (o instanceof Collection && 
((Collection<?>)o).size() == 0)
+                       || (o.getClass().isArray() && Array.getLength(o) == 0))
+                       w.sTag(1, "p").append("No Results").eTag("p").nl();
+               else
+                       super.doSerialize(o, w, hctx);
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/SimpleHtmlWriter.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/SimpleHtmlWriter.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/SimpleHtmlWriter.class
new file mode 100755
index 0000000..3d86ffb
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/SimpleHtmlWriter.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/SimpleHtmlWriter.java
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/SimpleHtmlWriter.java
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/SimpleHtmlWriter.java
new file mode 100755
index 0000000..5c10459
--- /dev/null
+++ 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/SimpleHtmlWriter.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2015. All Rights Reserved.
+ *
+ *  The source code for this program is not published or otherwise
+ *  divested of its trade secrets, irrespective of what has been
+ *  deposited with the U.S. Copyright Office.
+ 
*******************************************************************************/
+package com.ibm.juno.core.html;
+
+import java.io.*;
+
+/**
+ * Utility class for creating custom HTML.
+ * <p>
+ * Example:
+ * <p class='bcode'>
+ *     String table = <jk>new</jk> 
SimpleHtmlWriter().sTag(<js>"table"</js>).sTag(<js>"tr"</js>).sTag(<js>"td"</js>).append(<js>"hello"</js>).eTag(<js>"td"</js>).eTag(<js>"tr"</js>).eTag(<js>"table"</js>).toString();
+ * </p>
+ *
+ * @author James Bognar ([email protected])
+ */
+public class SimpleHtmlWriter extends HtmlSerializerWriter {
+
+       /**
+        * Constructor.
+        */
+       public SimpleHtmlWriter() {
+               super(new StringWriter(), true, '\'', null, null);
+       }
+
+       @Override /* Object */
+       public String toString() {
+               return out.toString();
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/Html.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/Html.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/Html.class
new file mode 100755
index 0000000..0f0e970
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/Html.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/Html.java
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/Html.java 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/Html.java
new file mode 100755
index 0000000..4d9829b
--- /dev/null
+++ 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/Html.java
@@ -0,0 +1,54 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2014, 2015. All Rights Reserved.
+ *
+ *  The source code for this program is not published or otherwise
+ *  divested of its trade secrets, irrespective of what has been
+ *  deposited with the U.S. Copyright Office.
+ 
*******************************************************************************/
+package com.ibm.juno.core.html.annotation;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.*;
+
+import java.lang.annotation.*;
+
+import com.ibm.juno.core.html.*;
+
+/**
+ * Annotation that can be applied to classes, fields, and methods to tweak how
+ * they are handled by {@link HtmlSerializer}.
+ *
+ * @author James Bognar ([email protected])
+ */
+@Documented
+@Target({TYPE,FIELD,METHOD})
+@Retention(RUNTIME)
+@Inherited
+public @interface Html {
+
+       /**
+        * Treat as XML.
+        * Useful when creating beans that model HTML elements.
+        */
+       boolean asXml() default false;
+
+       /**
+        * Treat as plain text.
+        * Object is serialized to a String using the <code>toString()</code> 
method and written directly to output.
+        * Useful when you want to serialize custom HTML.
+        */
+       boolean asPlainText() default false;
+
+       /**
+        * When <jk>true</jk>, collections of beans should be rendered as trees 
instead of tables.
+        * Default is <jk>false</jk>.
+        */
+       boolean noTables() default false;
+
+       /**
+        * When <jk>true</jk>, don't add headers to tables.
+        * Default is <jk>false</jk>.
+        */
+       boolean noTableHeaders() default false;
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/package.html
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/package.html
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/package.html
new file mode 100755
index 0000000..082a694
--- /dev/null
+++ 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/annotation/package.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<!--
+    Licensed Materials - Property of IBM
+    (c) Copyright IBM Corporation 2014. All Rights Reserved.
+   
+    Note to U.S. Government Users Restricted Rights:  
+    Use, duplication or disclosure restricted by GSA ADP Schedule 
+    Contract with IBM Corp. 
+ -->
+<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 annotations</p>
+</body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_DESCRIPTION.png
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_DESCRIPTION.png
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_DESCRIPTION.png
new file mode 100755
index 0000000..621721b
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_DESCRIPTION.png
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_LINKS.png
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_LINKS.png
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_LINKS.png
new file mode 100755
index 0000000..3c07fe6
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_LINKS.png
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_TITLE.png
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_TITLE.png
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_TITLE.png
new file mode 100755
index 0000000..5365735
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/doc-files/HTML_TITLE.png
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/HtmlElement.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/HtmlElement.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/HtmlElement.class
new file mode 100755
index 0000000..4a7103e
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/HtmlElement.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/HtmlElement.java
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/HtmlElement.java 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/HtmlElement.java
new file mode 100755
index 0000000..986e4e9
--- /dev/null
+++ 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/HtmlElement.java
@@ -0,0 +1,23 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2014. All Rights Reserved.
+ *
+ * Note to U.S. Government Users Restricted Rights:  Use,
+ * duplication or disclosure restricted by GSA ADP Schedule
+ * Contract with IBM Corp.
+ 
*******************************************************************************/
+package com.ibm.juno.core.html.dto;
+
+import com.ibm.juno.core.html.*;
+import com.ibm.juno.core.html.annotation.*;
+
+/**
+ * Superclass for all HTML elements.
+ * <p>
+ * These are beans that when serialized using {@link HtmlSerializer} generate
+ * valid XHTML elements.
+ *
+ * @author James Bognar ([email protected])
+ */
+@Html(asXml=true)
+public abstract class HtmlElement {}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/Img.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/Img.class 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/Img.class
new file mode 100755
index 0000000..70226c9
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/Img.class differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/Img.java
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/Img.java 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/Img.java
new file mode 100755
index 0000000..f8e1672
--- /dev/null
+++ b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/Img.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2014, 2015. All Rights Reserved.
+ *
+ * Note to U.S. Government Users Restricted Rights:  Use,
+ * duplication or disclosure restricted by GSA ADP Schedule
+ * Contract with IBM Corp.
+ 
*******************************************************************************/
+package com.ibm.juno.core.html.dto;
+
+import static com.ibm.juno.core.xml.annotation.XmlFormat.*;
+
+import com.ibm.juno.core.xml.annotation.*;
+
+/**
+ * Represents an HTML IMG element.
+ *
+ * @author James Bognar ([email protected])
+ */
+@Xml(name="img")
+public class Img extends HtmlElement {
+
+       /** <code>src</code> attribute */
+       @Xml(format=ATTR)
+       public String src;
+
+       /**
+        * Constructor
+        *
+        * @param src <code>src</code> attribute
+        */
+       public Img(String src) {
+               this.src = src;
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/package.html
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/package.html 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/package.html
new file mode 100755
index 0000000..d3e9ed5
--- /dev/null
+++ b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/dto/package.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<!--
+    Licensed Materials - Property of IBM
+    (c) Copyright IBM Corporation 2014. All Rights Reserved.
+   
+    Note to U.S. Government Users Restricted Rights:  
+    Use, duplication or disclosure restricted by GSA ADP Schedule 
+    Contract with IBM Corp. 
+ -->
+<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 Data Transfer Objects</p>
+</body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/package.html
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/package.html 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/package.html
new file mode 100755
index 0000000..b06a2fc
--- /dev/null
+++ b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/html/package.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<!--
+    Licensed Materials - Property of IBM
+    (c) Copyright IBM Corporation 2014. All Rights Reserved.
+   
+    Note to U.S. Government Users Restricted Rights:  
+    Use, duplication or disclosure restricted by GSA ADP Schedule 
+    Contract with IBM Corp. 
+ -->
+<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/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFile.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFile.class 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFile.class
new file mode 100755
index 0000000..fbb891c
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFile.class 
differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFile.java
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFile.java 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFile.java
new file mode 100755
index 0000000..4280133
--- /dev/null
+++ b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFile.java
@@ -0,0 +1,743 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2014, 2015. All Rights Reserved.
+ *
+ *  The source code for this program is not published or otherwise
+ *  divested of its trade secrets, irrespective of what has been
+ *  deposited with the U.S. Copyright Office.
+ 
*******************************************************************************/
+package com.ibm.juno.core.ini;
+
+import static com.ibm.juno.core.ini.ConfigFileFormat.*;
+import static com.ibm.juno.core.ini.ConfigUtils.*;
+import static com.ibm.juno.core.utils.ThrowableUtils.*;
+import static java.lang.reflect.Modifier.*;
+
+import java.beans.*;
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import com.ibm.juno.core.*;
+import com.ibm.juno.core.json.*;
+import com.ibm.juno.core.parser.*;
+import com.ibm.juno.core.serializer.*;
+import com.ibm.juno.core.utils.*;
+
+/**
+ * Implements the API for accessing the contents of a config file.
+ * <p>
+ * Refer to {@link com.ibm.juno.core.ini} for more information.
+ *
+ * @author James Bognar ([email protected])
+ */
+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>
+        *      <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>
+        *      <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 (s == 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>
+        *      <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>
+        *      <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>
+        *      <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>
+        *      <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>
+        *      <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 
StringVarResolver}.
+        * <p>
+        * @param vr
+        * @return This config file wrapped in an instance of {@link 
ConfigFileWrapped}.
+        */
+       public abstract ConfigFile getResolving(StringVarResolver vr);
+
+       /**
+        * Returns a wrapped instance of this config file where calls to 
getters have their values
+        *      first resolved by a default {@link StringVarResolver}.
+        *
+        *  The default {@link StringVarResolver} is registered with the 
following {@link StringVar StringVars}:
+        * <ul>
+        *      <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();
+
+       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/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileFormat.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileFormat.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileFormat.class
new file mode 100755
index 0000000..2af9062
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileFormat.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileFormat.java
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileFormat.java 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileFormat.java
new file mode 100755
index 0000000..ae991e7
--- /dev/null
+++ 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileFormat.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2015. All Rights Reserved.
+ *
+ *  The source code for this program is not published or otherwise
+ *  divested of its trade secrets, irrespective of what has been
+ *  deposited with the U.S. Copyright Office.
+ 
*******************************************************************************/
+package com.ibm.juno.core.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/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$1$1.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$1$1.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$1$1.class
new file mode 100755
index 0000000..df4b978
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$1$1.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$1.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$1.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$1.class
new file mode 100755
index 0000000..248452a
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$1.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$2$1.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$2$1.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$2$1.class
new file mode 100755
index 0000000..9dcb412
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$2$1.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$2.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$2.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$2.class
new file mode 100755
index 0000000..a533afa
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$2.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$3$1.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$3$1.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$3$1.class
new file mode 100755
index 0000000..b09b475
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$3$1.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$3.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$3.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$3.class
new file mode 100755
index 0000000..d132e6a
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$3.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$4.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$4.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$4.class
new file mode 100755
index 0000000..3f9c85a
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl$4.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl.class 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl.class
new file mode 100755
index 0000000..55cd0f5
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl.class 
differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl.java
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl.java 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl.java
new file mode 100755
index 0000000..e9a0a64
--- /dev/null
+++ 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileImpl.java
@@ -0,0 +1,729 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2015. All Rights Reserved.
+ *
+ *  The source code for this program is not published or otherwise
+ *  divested of its trade secrets, irrespective of what has been
+ *  deposited with the U.S. Copyright Office.
+ 
*******************************************************************************/
+package com.ibm.juno.core.ini;
+
+import static com.ibm.juno.core.ini.ConfigUtils.*;
+import static com.ibm.juno.core.utils.ThrowableUtils.*;
+
+import java.io.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.concurrent.locks.*;
+
+import com.ibm.juno.core.*;
+import com.ibm.juno.core.json.*;
+import com.ibm.juno.core.parser.*;
+import com.ibm.juno.core.serializer.*;
+import com.ibm.juno.core.utils.*;
+
+/**
+ * Implementation class for {@link ConfigFile}.
+ *
+ * @author James Bognar ([email protected])
+ */
+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);
+                                       while ((line = in.readLine()) != null) {
+                                               if 
(line.matches("\\s*\\[.*\\].*")) {
+                                                       String sn = 
line.substring(line.indexOf('[')+1, line.indexOf(']')).trim();
+                                                       section = 
getSection(sn, true).addHeaderComments(section.removeTrailingComments());
+                                               } else {
+                                                       section.addLines(null, 
line);
+                                               }
+                                       }
+                                       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(StringVarResolver vr) {
+               assertFieldNotNull(vr, "vr");
+               return new ConfigFileWrapped(this, vr);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile getResolving() {
+               return getResolving(
+                       new StringVarResolver(StringVarResolver.DEFAULT)
+                               .addVar("C", new StringVarWithDefault() {
+                                       @Override /* StringVar */
+                                       public String resolve(String varVal) {
+                                               return getString(varVal);
+                                       }
+                               }
+                       )
+               );
+       }
+
+       /*
+        * 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/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileListener.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileListener.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileListener.class
new file mode 100755
index 0000000..2e64bfd
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileListener.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileListener.java
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileListener.java
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileListener.java
new file mode 100755
index 0000000..a813356
--- /dev/null
+++ 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileListener.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2015. All Rights Reserved.
+ *
+ *  The source code for this program is not published or otherwise
+ *  divested of its trade secrets, irrespective of what has been
+ *  deposited with the U.S. Copyright Office.
+ 
*******************************************************************************/
+package com.ibm.juno.core.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/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWrapped.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWrapped.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWrapped.class
new file mode 100755
index 0000000..bd1a0af
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWrapped.class
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWrapped.java
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWrapped.java
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWrapped.java
new file mode 100755
index 0000000..a0bbfd1
--- /dev/null
+++ 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWrapped.java
@@ -0,0 +1,259 @@
+/*******************************************************************************
+ * Licensed Materials - Property of IBM
+ * (c) Copyright IBM Corporation 2014, 2015. All Rights Reserved.
+ *
+ *  The source code for this program is not published or otherwise
+ *  divested of its trade secrets, irrespective of what has been
+ *  deposited with the U.S. Copyright Office.
+ 
*******************************************************************************/
+package com.ibm.juno.core.ini;
+import static com.ibm.juno.core.utils.ThrowableUtils.*;
+
+import java.io.*;
+import java.util.*;
+
+import com.ibm.juno.core.*;
+import com.ibm.juno.core.parser.*;
+import com.ibm.juno.core.serializer.*;
+import com.ibm.juno.core.utils.*;
+
+/**
+ * Wraps an instance of {@link ConfigFileImpl} in an interface that will
+ *     automatically replace {@link StringVarResolver} variables.
+ * <p>
+ * The {@link ConfigFile#getResolving(StringVarResolver)} 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 ([email protected])
+ */
+public final class ConfigFileWrapped extends ConfigFile {
+
+       private final ConfigFileImpl cf;
+       private final StringVarResolver vr;
+
+       ConfigFileWrapped(ConfigFileImpl cf, StringVarResolver vr) {
+               this.cf = cf;
+               this.vr = vr;
+       }
+
+       @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 */
+       @SuppressWarnings("hiding")
+       public ConfigFile getResolving(StringVarResolver vr) {
+               assertFieldNotNull(vr, "vr");
+               return new ConfigFileWrapped(cf, vr);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile getResolving() {
+               return new ConfigFileWrapped(cf, StringVarResolver.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 vr.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/7e4f63e6/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWritable.class
----------------------------------------------------------------------
diff --git 
a/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWritable.class
 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWritable.class
new file mode 100755
index 0000000..ff6dfa8
Binary files /dev/null and 
b/com.ibm.team.juno.releng/bin/core/com/ibm/juno/core/ini/ConfigFileWritable.class
 differ

Reply via email to