http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileContext.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileContext.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileContext.java
new file mode 100644
index 0000000..593a7e9
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileContext.java
@@ -0,0 +1,60 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import org.apache.juneau.*;
+
+/**
+ * TODO
+ */
+public class ConfigFileContext extends Context {
+
+       /**
+        * TODO
+        *
+        * @param propertyStore
+        */
+       public ConfigFileContext(PropertyStore propertyStore) {
+               super(propertyStore);
+       }
+
+       /**
+        * TODO
+        */
+       public static final String CONFIGFILE_serializer = 
"ConfigFile.serializer";
+
+       /**
+        * TODO
+        */
+       public static final String CONFIGFILE_parser = "ConfigFile.parser";
+
+       /**
+        * TODO
+        */
+       public static final String CONFIGFILE_encoder = "ConfigFile.encoder";
+
+       /**
+        * TODO
+        */
+       public static final String CONFIGFILE_readonly = "ConfigFile.readonly";
+
+       /**
+        * TODO
+        */
+       public static final String CONFIGFILE_createIfNotExists = 
"ConfigFile.createIfNotExists";
+
+       /**
+        * TODO
+        */
+       public static final String CONFIGFILE_wsDepth = "ConfigFile.wsDepth";
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java
new file mode 100644
index 0000000..3000a66
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileFormat.java
@@ -0,0 +1,30 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import java.io.*;
+
+/**
+ * Valid formats that can be passed to the {@link 
ConfigFile#serializeTo(Writer, ConfigFileFormat)} method.
+ */
+public enum ConfigFileFormat {
+
+       /** Normal INI file format*/
+       INI,
+
+       /** Batch file with "set X" commands */
+       BATCH,
+
+       /** Shell script file with "export X" commands */
+       SHELL;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java
new file mode 100644
index 0000000..5986ebc
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileImpl.java
@@ -0,0 +1,821 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import static org.apache.juneau.ini.ConfigUtils.*;
+import static org.apache.juneau.internal.ThrowableUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.nio.charset.*;
+import java.util.*;
+import java.util.concurrent.locks.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.internal.*;
+import org.apache.juneau.json.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+import org.apache.juneau.svl.vars.*;
+
+/**
+ * Implementation class for {@link ConfigFile}.
+ */
+public final class ConfigFileImpl extends ConfigFile {
+
+       private final File file;
+       private final Encoder encoder;
+       private final WriterSerializer serializer;
+       private final ReaderParser parser;
+       private final BeanSession pBeanSession;
+       private final Charset charset;
+       final List<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(String,Class)} method.
+        *      If <jk>null</jk>, defaults to {@link JsonParser#DEFAULT}.
+        * @param charset
+        *      The charset on the files.
+        *      If <jk>null</jk>, defaults to {@link Charset#defaultCharset()}.
+        * @throws IOException
+        */
+       public ConfigFileImpl(File file, boolean readOnly, Encoder encoder, 
WriterSerializer serializer, ReaderParser parser,
+                       Charset charset) throws IOException {
+               this.file = file;
+               this.encoder = encoder == null ? XorEncoder.INSTANCE : encoder;
+               this.serializer = serializer == null ? JsonSerializer.DEFAULT : 
serializer;
+               this.parser = parser == null ? JsonParser.DEFAULT : parser;
+               this.charset = charset == null ? Charset.defaultCharset() : 
charset;
+               load();
+               this.readOnly = readOnly;
+               if (readOnly) {
+                       this.sections = 
Collections.unmodifiableMap(this.sections);
+                       for (Section s : sections.values())
+                               s.setReadOnly();
+               }
+               this.pBeanSession = 
this.parser.getBeanContext().createSession();
+       }
+
+       /**
+        * Constructor.
+        *
+        * <p>
+        * Shortcut for calling <code><jk>new</jk> ConfigFileImpl(file, 
<jk>false</jk>, <jk>null</jk>, <jk>null</jk>,
+        * <jk>null</jk>, <jk>null</jk>);</code>
+        *
+        * @param file The config file.  Does not need to exist.
+        * @throws IOException
+        */
+       public ConfigFileImpl(File file) throws IOException {
+               this(file, false, null, null, null, null);
+       }
+
+       /**
+        * Constructor.
+        *
+        * <p>
+        * Shortcut for calling <code><jk>new</jk> 
ConfigFileImpl(<jk>null</jk>, <jk>false</jk>, <jk>null</jk>,
+        * <jk>null</jk>, <jk>null</jk>, <jk>null</jk>);</code>
+        *
+        * @throws IOException
+        */
+       public ConfigFileImpl() throws IOException {
+               this(null);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFileImpl loadIfModified() throws IOException {
+               if (file == null)
+                       return this;
+               writeLock();
+               try {
+                       if (file.lastModified() > modifiedTimestamp)
+                               load();
+               } finally {
+                       writeUnlock();
+               }
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFileImpl load() throws IOException {
+               Reader r = null;
+               if (file != null && file.exists())
+                       r = new InputStreamReader(new FileInputStream(file), 
charset);
+               else
+                       r = new StringReader("");
+               try {
+                       load(r);
+               } finally {
+                       r.close();
+               }
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFileImpl load(Reader r) throws IOException {
+               assertFieldNotNull(r, "r");
+               writeLock();
+               try {
+                       this.sections = Collections.synchronizedMap(new 
LinkedHashMap<String,Section>());
+                       BufferedReader in = new BufferedReader(r);
+                       try {
+                               writeLock();
+                               hasBeenModified = false;
+                               try {
+                                       sections.clear();
+                                       String line = null;
+                                       Section section = getSection(null, 
true);
+                                       ArrayList<String> lines = new 
ArrayList<String>();
+                                       boolean canAppend = false;
+                                       while ((line = in.readLine()) != null) {
+                                               if (isSection(line)) {
+                                                       section.addLines(null, 
lines.toArray(new String[lines.size()]));
+                                                       lines.clear();
+                                                       canAppend = false;
+                                                       String sn = 
replaceUnicodeSequences(line.substring(line.indexOf('[')+1, 
line.indexOf(']')).trim());
+                                                       section = 
getSection(sn, true).addHeaderComments(section.removeTrailingComments());
+                                               } else {
+                                                       char c = line.isEmpty() 
? 0 : line.charAt(0);
+                                                       if ((c == ' ' || c == 
'\t') && canAppend && ! (isComment(line) || isAssignment(line)))
+                                                               
lines.add(lines.remove(lines.size()-1) + '\n' + line.substring(1));
+                                                       else {
+                                                               lines.add(line);
+                                                               if 
(isAssignment(line))
+                                                                       
canAppend = true;
+                                                               else
+                                                                       
canAppend = canAppend && ! (StringUtils.isEmpty(line) || isComment(line));
+                                                       }
+                                               }
+                                       }
+                                       section.addLines(null, 
lines.toArray(new String[lines.size()]));
+                                       in.close();
+                                       if (hasBeenModified)  // Set when 
values need to be encoded.
+                                               save();
+                                       if (file != null)
+                                               modifiedTimestamp = 
file.lastModified();
+                               } finally {
+                                       writeUnlock();
+                               }
+                       } finally {
+                               in.close();
+                       }
+               } finally {
+                       writeUnlock();
+               }
+               for (ConfigFileListener l : listeners)
+                       l.onLoad(this);
+               return this;
+       }
+
+       @SuppressWarnings("hiding")
+       @Override /* ConfigFile */
+       protected String serialize(Object value, Serializer serializer, boolean 
newline) throws SerializeException {
+               if (value == null)
+                       return "";
+               if (serializer == null)
+                       serializer = this.serializer;
+               Class<?> c = value.getClass();
+               if (isSimpleType(c))
+                       return value.toString();
+
+               String r = null;
+               if (newline)
+                       r = "\n" + (String)serializer.serialize(value);
+               else
+                       r = (String)serializer.serialize(value);
+
+               if (r.startsWith("'"))
+                       return r.substring(1, r.length()-1);
+               return r;
+       }
+
+       @Override /* ConfigFile */
+       @SuppressWarnings({ "unchecked", "hiding" })
+       protected <T> T parse(String s, Parser parser, Type type, Type...args) 
throws ParseException {
+
+               if (StringUtils.isEmpty(s))
+                       return null;
+
+               if (isSimpleType(type))
+                       return (T)pBeanSession.convertToType(s, (Class<?>)type);
+
+               char s1 = firstNonWhitespaceChar(s);
+               if (s1 != '[' && s1 != '{' && ! "null".equals(s))
+                       s = '\'' + s + '\'';
+
+               if (parser == null)
+                       parser = this.parser;
+
+               return parser.parse(s, type, args);
+       }
+
+       private static boolean isSimpleType(Type t) {
+               if (! (t instanceof Class))
+                       return false;
+               Class<?> c = (Class<?>)t;
+               return (c == String.class || c.isPrimitive() || 
c.isAssignableFrom(Number.class) || c == Boolean.class || c.isEnum());
+       }
+
+
+       
//--------------------------------------------------------------------------------
+       // 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());
+       }
+
+       @SuppressWarnings("hiding")
+       @Override /* ConfigFile */
+       public String put(String sectionName, String sectionKey, Object value, 
Serializer serializer, boolean encoded,
+                       boolean newline) throws SerializeException {
+               assertFieldNotNull(sectionKey, "sectionKey");
+               Section s = getSection(sectionName, true);
+               return s.put(sectionKey, serialize(value, serializer, newline), 
encoded);
+       }
+
+       @Override /* ConfigFile */
+       public String put(String sectionName, String sectionKey, String value, 
boolean encoded) {
+               assertFieldNotNull(sectionKey, "sectionKey");
+               Section s = getSection(sectionName, true);
+               return s.put(sectionKey, value, encoded);
+       }
+
+       @Override /* ConfigFile */
+       public String remove(String sectionName, String sectionKey) {
+               assertFieldNotNull(sectionKey, "sectionKey");
+               Section s = getSection(sectionName, false);
+               if (s == null)
+                       return null;
+               return s.remove(sectionKey);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFileImpl addLines(String section, String...lines) {
+               Set<String> changes = createChanges();
+               writeLock();
+               try {
+                       getSection(section, true).addLines(changes, lines);
+               } finally {
+                       writeUnlock();
+               }
+               signalChanges(changes);
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFileImpl addHeaderComments(String section, 
String...headerComments) {
+               writeLock();
+               try {
+                       if (headerComments == null)
+                               headerComments = new String[0];
+                       getSection(section, 
true).addHeaderComments(Arrays.asList(headerComments));
+               } finally {
+                       writeUnlock();
+               }
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFileImpl clearHeaderComments(String section) {
+               writeLock();
+               try {
+                       Section s = getSection(section, false);
+                       if (s != null)
+                               s.clearHeaderComments();
+               } finally {
+                       writeUnlock();
+               }
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public Section getSection(String name) {
+               return getSection(name, false);
+       }
+
+       @Override /* ConfigFile */
+       public Section getSection(String name, boolean create) {
+               if (StringUtils.isEmpty(name))
+                       name = DEFAULT;
+               Section s = sections.get(name);
+               if (s != null)
+                       return s;
+               if (create) {
+                       s = new Section().setParent(this).setName(name);
+                       sections.put(name, s);
+                       return s;
+               }
+               return null;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFileImpl addSection(String name) {
+               writeLock();
+               try {
+                       getSection(name, true);
+               } finally {
+                       writeUnlock();
+               }
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile setSection(String name, Map<String,String> contents) {
+               writeLock();
+               try {
+                       put(name, new 
Section(contents).setParent(this).setName(name));
+               } finally {
+                       writeUnlock();
+               }
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFileImpl removeSection(String name) {
+               Set<String> changes = createChanges();
+               writeLock();
+               try {
+                       Section prev = sections.remove(name);
+                       if (changes != null && prev != null)
+                               findChanges(changes, prev, null);
+               } finally {
+                       writeUnlock();
+               }
+               signalChanges(changes);
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public Set<String> getSectionKeys(String sectionName) {
+               Section s = get(sectionName);
+               if (s == null)
+                       return null;
+               return s.keySet();
+       }
+
+       @Override /* ConfigFile */
+       public boolean isEncoded(String key) {
+               assertFieldNotNull(key, "key");
+               String section = getSectionName(key);
+               Section s = getSection(section, false);
+               if (s == null)
+                       return false;
+               return s.isEncoded(getSectionKey(key));
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFileImpl save() throws IOException {
+               writeLock();
+               try {
+                       if (file == null)
+                               throw new UnsupportedOperationException("No 
backing file specified for config file.");
+                       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 BeanSession getBeanSession() {
+               return pBeanSession;
+       }
+
+       @Override /* ConfigFile */
+       protected void readLock() {
+               lock.readLock().lock();
+       }
+
+       @Override /* ConfigFile */
+       protected void readUnlock() {
+               lock.readLock().unlock();
+       }
+
+       private void writeLock() {
+               if (readOnly)
+                       throw new UnsupportedOperationException("Cannot modify 
read-only ConfigFile.");
+               lock.writeLock().lock();
+               hasBeenModified = true;
+       }
+
+       private void writeUnlock() {
+               lock.writeLock().unlock();
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile getResolving(VarResolver vr) {
+               assertFieldNotNull(vr, "vr");
+               return new ConfigFileWrapped(this, vr);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile getResolving(VarResolverSession vs) {
+               assertFieldNotNull(vs, "vs");
+               return new ConfigFileWrapped(this, vs);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile getResolving() {
+               return getResolving(
+                       new VarResolverBuilder()
+                               .vars(SystemPropertiesVar.class, 
EnvVariablesVar.class, SwitchVar.class, IfVar.class, ConfigFileVar.class,
+                                       IfVar.class, SwitchVar.class)
+                               .contextObject(ConfigFileVar.SESSION_config, 
this)
+                               .build()
+               );
+       }
+
+       /*
+        * Finds the keys that are different between the two sections and adds 
it to
+        * the specified set.
+        */
+       private static 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 static void addChange(Set<String> changes, String section, 
String key, String oldVal, String newVal) {
+               if (! 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/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileListener.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileListener.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileListener.java
new file mode 100644
index 0000000..cfed21a
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileListener.java
@@ -0,0 +1,46 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import java.util.*;
+
+/**
+ * Listener that can be used to listen for change events in config files.
+ *
+ * <p>
+ * Use the {@link ConfigFile#addListener(ConfigFileListener)} method to 
register listeners.
+ */
+public class ConfigFileListener {
+
+       /**
+        * Gets called immediately after a config file has been loaded.
+        *
+        * @param cf The config file being loaded.
+        */
+       public void onLoad(ConfigFile cf) {}
+
+       /**
+        * Gets called immediately after a config file has been saved.
+        *
+        * @param cf The config file being saved.
+        */
+       public void onSave(ConfigFile cf) {}
+
+       /**
+        * Signifies that the specified values have changed.
+        *
+        * @param cf The config file being modified.
+        * @param changes The full keys (e.g. <js>"Section/key"</js>) of 
entries that have changed in the config file.
+        */
+       public void onChange(ConfigFile cf, Set<String> changes) {}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileVar.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileVar.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileVar.java
new file mode 100644
index 0000000..52fe0ea
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileVar.java
@@ -0,0 +1,69 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import org.apache.juneau.svl.*;
+
+/**
+ * Config file variable resolver.
+ *
+ * <p>
+ * The format for this var is <js>"$C{key[,defaultValue]}"</js>.
+ * See {@link ConfigFile#getString(String)} for the format of the key.
+ *
+ * <p>
+ * This variable resolver requires that a {@link ConfigFile} object be set as 
a context object on the resolver or a
+ * session object on the resolver session.
+ *
+ * <h5 class='section'>Example:</h5>
+ * <p class='bcode'>
+ *     <jc>// Create a config file object.</jc>
+ *     ConfigFile configFile = new 
ConfigFileBuilder().build(<js>"MyConfig.cfg"</js>);
+ *
+ *     <jc>// Create a variable resolver that resolves config file entries 
(e.g. "$C{MySection/myKey}")</jc>
+ *     VarResolver r = <jk>new</jk> 
VarResolver().addVars(ConfigVar.<js>class</js>)
+ *             .addContextObject(<jsf>SESSION_config</jsf>, configFile);
+ *
+ *     <jc>// Use it!</jc>
+ *     System.<jsf>out</jsf>.println(r.resolve(<js>"Value for myKey in section 
MySection is $C{MySection/myKey}"</js>));
+ * </p>
+ *
+ * <p>
+ * Since this is a {@link SimpleVar}, any variables contained in the result 
will be recursively resolved.
+ * Likewise, if the arguments contain any variables, those will be resolved 
before they are passed to this var.
+ *
+ * @see org.apache.juneau.ini.ConfigFile
+ * @see org.apache.juneau.svl
+ */
+public class ConfigFileVar extends DefaultingVar {
+
+       /**
+        * The name of the session or context object that identifies the {@link 
ConfigFile} object.
+        */
+       public static final String SESSION_config = "config";
+
+       /** The name of this variable. */
+       public static final String NAME = "C";
+
+       /**
+        * Constructor.
+        */
+       public ConfigFileVar() {
+               super(NAME);
+       }
+
+       @Override /* Var */
+       public String resolve(VarResolverSession session, String key) {
+               return session.getSessionObject(ConfigFile.class, 
SESSION_config).getString(key);
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java
new file mode 100644
index 0000000..da8e83b
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWrapped.java
@@ -0,0 +1,290 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import static org.apache.juneau.internal.ThrowableUtils.*;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.serializer.*;
+import org.apache.juneau.svl.*;
+
+/**
+ * Wraps an instance of {@link ConfigFileImpl} in an interface that will 
automatically replace {@link VarResolver}
+ * variables.
+ *
+ * <p>
+ * The {@link ConfigFile#getResolving(VarResolver)} returns an instance of 
this class.
+ *
+ * <p>
+ * This class overrides the {@link #getString(String, String)} to resolve 
string variables.
+ * All other method calls are passed through to the inner config file.
+ */
+public final class ConfigFileWrapped extends ConfigFile {
+
+       private final ConfigFileImpl cf;
+       private final VarResolverSession vs;
+
+       ConfigFileWrapped(ConfigFileImpl cf, VarResolver vr) {
+               this.cf = cf;
+               this.vs = vr.builder()
+                       .vars(ConfigFileVar.class)
+                       .contextObject(ConfigFileVar.SESSION_config, cf)
+                       .build()
+                       .createSession();
+       }
+
+       ConfigFileWrapped(ConfigFileImpl cf, VarResolverSession vs) {
+               this.cf = cf;
+               this.vs = vs;
+       }
+
+       @Override /* ConfigFile */
+       public void clear() {
+               cf.clear();
+       }
+
+       @Override /* ConfigFile */
+       public boolean containsKey(Object key) {
+               return cf.containsKey(key);
+       }
+
+       @Override /* ConfigFile */
+       public boolean containsValue(Object value) {
+               return cf.containsValue(value);
+       }
+
+       @Override /* ConfigFile */
+       public Set<java.util.Map.Entry<String,Section>> entrySet() {
+               return cf.entrySet();
+       }
+
+       @Override /* ConfigFile */
+       public Section get(Object key) {
+               return cf.get(key);
+       }
+
+       @Override /* ConfigFile */
+       public boolean isEmpty() {
+               return cf.isEmpty();
+       }
+
+       @Override /* ConfigFile */
+       public Set<String> keySet() {
+               return cf.keySet();
+       }
+
+       @Override /* ConfigFile */
+       public Section put(String key, Section value) {
+               return cf.put(key, value);
+       }
+
+       @Override /* ConfigFile */
+       public void putAll(Map<? extends String,? extends Section> map) {
+               cf.putAll(map);
+       }
+
+       @Override /* ConfigFile */
+       public Section remove(Object key) {
+               return cf.remove(key);
+       }
+
+       @Override /* ConfigFile */
+       public int size() {
+               return cf.size();
+       }
+
+       @Override /* ConfigFile */
+       public Collection<Section> values() {
+               return cf.values();
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile loadIfModified() throws IOException {
+               cf.loadIfModified();
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile load() throws IOException {
+               cf.load();
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile load(Reader r) throws IOException {
+               cf.load(r);
+               return this;
+       }
+
+
+       @Override /* ConfigFile */
+       public boolean isEncoded(String key) {
+               return cf.isEncoded(key);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile addLines(String section, String... lines) {
+               cf.addLines(section, lines);
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile addHeaderComments(String section, String... 
headerComments) {
+               cf.addHeaderComments(section, headerComments);
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile clearHeaderComments(String section) {
+               cf.clearHeaderComments(section);
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public Section getSection(String name) {
+               return cf.getSection(name);
+       }
+
+       @Override /* ConfigFile */
+       public Section getSection(String name, boolean create) {
+               return cf.getSection(name, create);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile addSection(String name) {
+               cf.addSection(name);
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile setSection(String name, Map<String,String> contents) {
+               cf.setSection(name, contents);
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile removeSection(String name) {
+               cf.removeSection(name);
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile save() throws IOException {
+               cf.save();
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile serializeTo(Writer out, ConfigFileFormat format) 
throws IOException {
+               cf.serializeTo(out, format);
+               return this;
+       }
+
+       @Override /* ConfigFile */
+       public String toString() {
+               return cf.toString();
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile getResolving(VarResolver varResolver) {
+               assertFieldNotNull(varResolver, "vr");
+               return new ConfigFileWrapped(cf, varResolver);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile getResolving(VarResolverSession varSession) {
+               assertFieldNotNull(varSession, "vs");
+               return new ConfigFileWrapped(cf, varSession);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile getResolving() {
+               return new ConfigFileWrapped(cf, VarResolver.DEFAULT);
+       }
+
+       @Override /* ConfigFile */
+       public ConfigFile addListener(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 BeanSession getBeanSession() {
+               return cf.getBeanSession();
+       }
+
+       @Override /* ConfigFile */
+       public String get(String sectionName, String sectionKey) {
+               String s = cf.get(sectionName, sectionKey);
+               if (s == null)
+                       return null;
+               return vs.resolve(s);
+       }
+
+       @Override /* ConfigFile */
+       public String put(String sectionName, String sectionKey, String value, 
boolean encoded) {
+               return cf.put(sectionName, sectionKey, value, encoded);
+       }
+
+       @Override /* ConfigFile */
+       public String put(String sectionName, String sectionKey, Object value, 
Serializer serializer, boolean encoded,
+                       boolean newline) throws SerializeException {
+               return cf.put(sectionName, sectionKey, value, serializer, 
encoded, newline);
+       }
+
+       @Override /* ConfigFile */
+       public String remove(String sectionName, String sectionKey) {
+               return cf.remove(sectionName, sectionKey);
+       }
+
+       @Override /* ConfigFile */
+       public Set<String> getSectionKeys(String sectionName) {
+               return cf.getSectionKeys(sectionName);
+       }
+
+       @Override /* ConfigFile */
+       protected void readLock() {
+               cf.readLock();
+       }
+
+       @Override /* ConfigFile */
+       protected void readUnlock() {
+               cf.readUnlock();
+       }
+
+       @Override /* ConfigFile */
+       protected String serialize(Object o, Serializer s, boolean newline) 
throws SerializeException {
+               return cf.serialize(o, s, newline);
+       }
+
+       @Override /* ConfigFile */
+       protected <T> T parse(String s, Parser parser, Type type, Type... args) 
throws ParseException {
+               return cf.parse(s, parser, type, args);
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
new file mode 100644
index 0000000..b8c504c
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigFileWritable.java
@@ -0,0 +1,45 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import java.io.*;
+
+import org.apache.juneau.*;
+import org.apache.juneau.http.*;
+
+/**
+ * Wraps a {@link ConfigFile} in a {@link Writable} to be rendered as plain 
text.
+ */
+class ConfigFileWritable implements Writable {
+
+       private ConfigFileImpl cf;
+
+       protected ConfigFileWritable(ConfigFileImpl cf) {
+               this.cf = cf;
+       }
+
+       @Override /* Writable */
+       public void writeTo(Writer out) throws IOException {
+               cf.readLock();
+               try {
+                       cf.serializeTo(out);
+               } finally {
+                       cf.readUnlock();
+               }
+       }
+
+       @Override /* Writable */
+       public MediaType getMediaType() {
+               return MediaType.PLAIN;
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigUtils.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigUtils.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigUtils.java
new file mode 100644
index 0000000..9e635f4
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/ConfigUtils.java
@@ -0,0 +1,92 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+/**
+ * Internal utility methods.
+ */
+public class ConfigUtils {
+
+       static final String getSectionName(String key) {
+               int i = key.indexOf('/');
+               if (i == -1)
+                       return "default";
+               return key.substring(0, i);
+       }
+
+       static final String getSectionKey(String key) {
+               int i = key.indexOf('/');
+               if (i == -1)
+                       return key;
+               return key.substring(i+1);
+       }
+
+       static final String getFullKey(String section, String key) {
+               if (section.equals("default"))
+                       return key;
+               return section + '/' + key;
+       }
+
+       static final boolean isComment(String line) {
+               for (int i = 0; i < line.length(); i++) {
+                       char c = line.charAt(i);
+                       if (! Character.isWhitespace(c))
+                               return c == '#';
+               }
+               return false;
+       }
+
+       static final boolean isAssignment(String line) {
+               int S1 = 1; // Looking for char;
+               int S2 = 2; // Found char, looking for whitespace or =
+               int S3 = 3; // Found whitespace, looking for =
+               int state = S1;
+               for (int i = 0; i < line.length(); i++) {
+                       char c = line.charAt(i);
+                       if (state == S1) {
+                               if (! Character.isWhitespace(c))
+                                       state = S2;
+                       } else if (state == S2) {
+                               if (c == '=')
+                                       return true;
+                               if (Character.isWhitespace(c))
+                                       state = S3;
+                       } else if (state == S3) {
+                               if (c == '=')
+                                       return true;
+                       }
+               }
+               return false;
+       }
+
+       static final boolean isSection(String line) {
+               int S1 = 1; // Looking for [;
+               int S2 = 2; // Found [, looking for ]
+               int state = S1;
+               for (int i = 0; i < line.length(); i++) {
+                       char c = line.charAt(i);
+                       if (state == S1) {
+                               if (! Character.isWhitespace(c)) {
+                                       if (c == '[')
+                                               state = S2;
+                                       else
+                                               return false;
+                               }
+                       } else if (state == S2) {
+                               if (c == ']')
+                                       return true;
+                       }
+               }
+               return false;
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Encoder.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Encoder.java 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Encoder.java
new file mode 100644
index 0000000..9b73b25
--- /dev/null
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Encoder.java
@@ -0,0 +1,37 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+/**
+ * API for defining a string encoding/decoding mechanism for entries in {@link 
ConfigFile}.
+ */
+public interface Encoder {
+
+       /**
+        * Encode a string.
+        *
+        * @param fieldName The field name being encoded.
+        * @param in The unencoded input string.
+        * @return The encoded output string.
+        */
+       public String encode(String fieldName, String in);
+
+       /**
+        * Decode a string.
+        *
+        * @param fieldName The field name being decoded.
+        * @param in The encoded input string.
+        * @return The decoded output string.
+        */
+       public String decode(String fieldName, String in);
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/EntryListener.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/EntryListener.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/EntryListener.java
new file mode 100644
index 0000000..87526c1
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/EntryListener.java
@@ -0,0 +1,48 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import java.util.*;
+
+/**
+ * Listener that can be used to listen for change events for a specific entry 
in a config file.
+ *
+ * <p>
+ * Use the {@link ConfigFile#addListener(ConfigFileListener)} method to 
register listeners.
+ */
+public class EntryListener extends ConfigFileListener {
+
+       private String fullKey;
+
+       /**
+        * Constructor.
+        *
+        * @param fullKey The key in the config file to listen for changes on.
+        */
+       public EntryListener(String fullKey) {
+               this.fullKey = fullKey;
+       }
+
+       @Override /* ConfigFileListener */
+       public void onChange(ConfigFile cf, Set<String> changes) {
+               if (changes.contains(fullKey))
+                       onChange(cf);
+       }
+
+       /**
+        * Signifies that the config file entry changed.
+        *
+        * @param cf The config file being changed.
+        */
+       public void onChange(ConfigFile cf) {}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Section.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Section.java 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Section.java
new file mode 100644
index 0000000..39108a3
--- /dev/null
+++ b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/Section.java
@@ -0,0 +1,575 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import static org.apache.juneau.ini.ConfigFileFormat.*;
+import static org.apache.juneau.ini.ConfigUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.locks.*;
+
+import org.apache.juneau.annotation.*;
+
+/**
+ * Defines a section in a config file.
+ */
+public class Section implements Map<String,String> {
+
+       private ConfigFileImpl configFile;
+       String name;   // The config section name, or "default" if the default 
section.  Never null.
+
+       // The data structures that make up this object.
+       // These must be kept synchronized.
+       private LinkedList<String> lines = new LinkedList<String>();
+       private List<String> headerComments = new LinkedList<String>();
+       private Map<String,String> entries;
+
+       private ReadWriteLock lock = new ReentrantReadWriteLock();
+       private boolean readOnly;
+
+       /**
+        * Constructor.
+        */
+       public Section() {
+               this.entries = new LinkedHashMap<String,String>();
+       }
+
+       /**
+        * Constructor with predefined contents.
+        *
+        * @param contents Predefined contents to copy into this section.
+        */
+       public Section(Map<String,String> contents) {
+               this.entries = new LinkedHashMap<String,String>(contents);
+       }
+
+       Section setReadOnly() {
+               // This method is only called once from ConfigFileImpl 
constructor.
+               this.readOnly = true;
+               this.entries = Collections.unmodifiableMap(entries);
+               return this;
+       }
+
+       /**
+        * Sets the config file that this section belongs to.
+        *
+        * @param configFile The config file that this section belongs to.
+        * @return This object (for method chaining).
+        */
+       @ParentProperty
+       public Section setParent(ConfigFileImpl configFile) {
+               this.configFile = configFile;
+               return this;
+       }
+
+       /**
+        * Sets the section name
+        *
+        * @param name The section name.
+        * @return This object (for method chaining).
+        */
+       @NameProperty
+       public Section setName(String name) {
+               this.name = name;
+               return this;
+       }
+
+
+       
//--------------------------------------------------------------------------------
+       // Map methods
+       
//--------------------------------------------------------------------------------
+
+       @Override /* Map */
+       public void clear() {
+               Set<String> changes = createChanges();
+               writeLock();
+               try {
+                       if (changes != null)
+                               for (String k : keySet())
+                                       changes.add(getFullKey(name, k));
+                       entries.clear();
+                       lines.clear();
+                       headerComments.clear();
+               } finally {
+                       writeUnlock();
+               }
+               signalChanges(changes);
+       }
+
+       @Override /* Map */
+       public boolean containsKey(Object key) {
+               return entries.containsKey(key);
+       }
+
+       @Override /* Map */
+       public boolean containsValue(Object value) {
+               return entries.containsValue(value);
+       }
+
+       @Override /* Map */
+       public Set<Map.Entry<String,String>> entrySet() {
+
+               // We need to create our own set so that entries are removed 
correctly.
+               return new AbstractSet<Map.Entry<String,String>>() {
+                       @Override /* Set */
+                       public Iterator<Map.Entry<String,String>> iterator() {
+                               return new Iterator<Map.Entry<String,String>>() 
{
+                                       Iterator<Map.Entry<String,String>> i = 
entries.entrySet().iterator();
+                                       Map.Entry<String,String> i2;
+
+                                       @Override /* Iterator */
+                                       public boolean hasNext() {
+                                               return i.hasNext();
+                                       }
+
+                                       @Override /* Iterator */
+                                       public Map.Entry<String,String> next() {
+                                               i2 = i.next();
+                                               return i2;
+                                       }
+
+                                       @Override /* Iterator */
+                                       public void remove() {
+                                               Set<String> changes = 
createChanges();
+                                               String key = i2.getKey(), val = 
i2.getValue();
+                                               addChange(changes, key, val, 
null);
+                                               writeLock();
+                                               try {
+                                                       i.remove();
+                                                       removeLine(key);
+                                               } finally {
+                                                       writeUnlock();
+                                               }
+                                               signalChanges(changes);
+                                       }
+                               };
+                       }
+
+                       @Override /* Set */
+                       public int size() {
+                               return entries.size();
+                       }
+               };
+       }
+
+       @Override /* Map */
+       public String get(Object key) {
+               String s = entries.get(key);
+               return s;
+       }
+
+       @Override /* Map */
+       public boolean isEmpty() {
+               return entries.isEmpty();
+       }
+
+       @Override /* Map */
+       public Set<String> keySet() {
+
+               // We need to create our own set so that sections are removed 
correctly.
+               return new AbstractSet<String>() {
+                       @Override /* Set */
+                       public Iterator<String> iterator() {
+                               return new Iterator<String>() {
+                                       Iterator<String> i = 
entries.keySet().iterator();
+                                       String i2;
+
+                                       @Override /* Iterator */
+                                       public boolean hasNext() {
+                                               return i.hasNext();
+                                       }
+
+                                       @Override /* Iterator */
+                                       public String next() {
+                                               i2 = i.next();
+                                               return i2;
+                                       }
+
+                                       @Override /* Iterator */
+                                       public void remove() {
+                                               Set<String> changes = 
createChanges();
+                                               String key = i2;
+                                               String val = entries.get(key);
+                                               addChange(changes, key, val, 
null);
+                                               writeLock();
+                                               try {
+                                                       i.remove();
+                                                       removeLine(key);
+                                               } finally {
+                                                       writeUnlock();
+                                               }
+                                               signalChanges(changes);
+                                       }
+                               };
+                       }
+
+                       @Override /* Set */
+                       public int size() {
+                               return entries.size();
+                       }
+               };
+       }
+
+       @Override /* Map */
+       public String put(String key, String value) {
+               return put(key, value, false);
+       }
+
+       /**
+        * Sets the specified value in this section.
+        *
+        * @param key The section key.
+        * @param value The new value.
+        * @param encoded Whether this value should be encoded during save.
+        * @return The previous value.
+        */
+       public String put(String key, String value, boolean encoded) {
+               Set<String> changes = createChanges();
+               String s = put(key, value, encoded, changes);
+               signalChanges(changes);
+               return s;
+       }
+
+       String put(String key, String value, boolean encoded, Set<String> 
changes) {
+               writeLock();
+               try {
+                       addLine(key, encoded);
+                       String prev = entries.put(key, value);
+                       addChange(changes, key, prev, value);
+                       return prev;
+               } finally {
+                       writeUnlock();
+               }
+       }
+
+       @Override /* Map */
+       public void putAll(Map<? extends String,? extends String> map) {
+               Set<String> changes = createChanges();
+               for (Map.Entry<? extends String,? extends String> e : 
map.entrySet())
+                       put(e.getKey(), e.getValue(), false, changes);
+               signalChanges(changes);
+       }
+
+       @Override /* Map */
+       public String remove(Object key) {
+               Set<String> changes = createChanges();
+               String old = remove(key, changes);
+               signalChanges(changes);
+               return old;
+       }
+
+       String remove(Object key, Set<String> changes) {
+               writeLock();
+               try {
+                       String prev = entries.remove(key);
+                       addChange(changes, key.toString(), prev, null);
+                       removeLine(key.toString());
+                       return prev;
+               } finally {
+                       writeUnlock();
+               }
+       }
+
+       private void removeLine(String key) {
+               for (Iterator<String> i = lines.iterator(); i.hasNext();) {
+                       String k = i.next();
+                       if (k.startsWith("*") || k.startsWith(">")) {
+                               if (k.substring(1).equals(key)) {
+                                       i.remove();
+                                       break;
+                               }
+                       }
+               }
+       }
+
+       @Override /* Map */
+       public int size() {
+               return entries.size();
+       }
+
+       @Override /* Map */
+       public Collection<String> values() {
+               return Collections.unmodifiableCollection(entries.values());
+       }
+
+
+       
//--------------------------------------------------------------------------------
+       // API methods
+       
//--------------------------------------------------------------------------------
+
+       /**
+        * Returns <jk>true</jk> if the specified entry is encoded.
+        *
+        * @param key The key.
+        * @return <jk>true</jk> if the specified entry is encoded.
+        */
+       public boolean isEncoded(String key) {
+               readLock();
+               try {
+                       for (String s : lines)
+                               if (s.length() > 1)
+                                       if (s.substring(1).equals(key))
+                                               return s.charAt(0) == '*';
+                       return false;
+               } finally {
+                       readUnlock();
+               }
+       }
+
+       /**
+        * Adds header comments to this section.
+        *
+        * @see ConfigFile#addHeaderComments(String, String...) for a 
description.
+        * @param comments The comment lines to add to this section.
+        * @return This object (for method chaining).
+        */
+       public Section addHeaderComments(List<String> comments) {
+               writeLock();
+               try {
+                       for (String c : comments) {
+                               if (c == null)
+                                       c = "";
+                               if (! c.startsWith("#"))
+                                       c = "#" + c;
+                               this.headerComments.add(c);
+                       }
+                       return this;
+               } finally {
+                       writeUnlock();
+               }
+       }
+
+       /**
+        * Removes all header comments from this section.
+        */
+       public void clearHeaderComments() {
+               writeLock();
+               try {
+                       this.headerComments.clear();
+               } finally {
+                       writeUnlock();
+               }
+       }
+
+       /**
+        * Serialize this section.
+        *
+        * @param out What to serialize to.
+        * @param format The format (e.g. INI, BATCH, SHELL).
+        */
+       public void writeTo(PrintWriter out, ConfigFileFormat format) {
+               readLock();
+               try {
+                       if (format == INI) {
+                               for (String s : headerComments)
+                                       out.append(s).println();
+                               if (! name.equals("default"))
+                                       
out.append('[').append(name).append(']').println();
+                               for (String l : lines) {
+                                       char c = (l.length() > 0 ? l.charAt(0) 
: 0);
+                                       if (c == '>' || c == '*'){
+                                               boolean encode = c == '*';
+                                               String key = l.substring(1);
+                                               String val = entries.get(key);
+                                               if (val.indexOf('\n') != -1)
+                                                       val = 
val.replaceAll("(\\r?\\n)", "$1\t");
+                                               if (val.indexOf('=') != -1)
+                                                       val = val.replace("=", 
"\\u003D");
+                                               if (val.indexOf('#') != -1)
+                                                       val = val.replace("#", 
"\\u0023");
+                                               out.append(key);
+                                               if (encode)
+                                                       out.append('*');
+                                               out.append(" = ");
+                                               if (encode)
+                                                       
out.append('{').append(configFile.getEncoder().encode(key, val)).append('}');
+                                               else
+                                                       out.append(val);
+                                               out.println();
+                                       } else {
+                                               out.append(l).println();
+                                       }
+                               }
+
+                       } else if (format == BATCH) {
+                               String section = name.replaceAll("\\.\\/", "_");
+                               for (String l : headerComments) {
+                                       l = trimComment(l);
+                                       if (! l.isEmpty())
+                                               out.append("rem ").append(l);
+                                       out.println();
+                               }
+                               for (String l : lines) {
+                                       char c = (l.length() > 0 ? l.charAt(0) 
: 0);
+                                       if (c == '>' || c == '*') {
+                                               String key = l.substring(1);
+                                               String val = entries.get(key);
+                                               out.append("set ");
+                                               if (! name.equals("default"))
+                                                       
out.append(section).append('_');
+                                               
out.append(key.replaceAll("\\.\\/", "_")).append(" = ").append(val).println();
+                                       } else {
+                                               l = trimComment(l);
+                                               if (! l.isEmpty())
+                                                       out.append("rem 
").append(l);
+                                               out.println();
+                                       }
+                               }
+
+                       } else if (format == SHELL) {
+                               String section = name.replaceAll("\\.\\/", "_");
+                               for (String l : headerComments) {
+                                       l = trimComment(l);
+                                       if (! l.isEmpty())
+                                               out.append("# ").append(l);
+                                       out.println();
+                               }
+                               for (String l : lines) {
+                                       char c = (l.length() > 0 ? l.charAt(0) 
: 0);
+                                       if (c == '>' || c == '*'){
+                                               String key = l.substring(1);
+                                               String val = 
entries.get(key).replaceAll("\\\\", "\\\\\\\\");
+                                               out.append("export ");
+                                               if (! name.equals("default"))
+                                                       
out.append(section).append('_');
+                                               
out.append(key.replaceAll("\\.\\/", 
"_")).append('=').append('"').append(val).append('"').println();
+                                       } else {
+                                               l = trimComment(l);
+                                               if (! l.isEmpty())
+                                                       out.append("# 
").append(l);
+                                               out.println();
+                                       }
+                               }
+                       }
+               } finally {
+                       readUnlock();
+               }
+       }
+
+
+       
//--------------------------------------------------------------------------------
+       // Protected methods used by ConfigFile
+       
//--------------------------------------------------------------------------------
+
+       /*
+        * Add lines to this section.
+        */
+       Section addLines(Set<String> changes, String...l) {
+               writeLock();
+               try {
+                       if (l == null)
+                               l = new String[0];
+                       for (int i = 0; i < l.length; i++) {
+                               String line = l[i];
+                               if (line == null)
+                                       line = "";
+                               if (isComment(line))
+                                       this.lines.add(line);
+                               else if (isAssignment(line)) {
+                                       // Key/value pairs are stored as either 
">key" or "*key";
+                                       String key = 
replaceUnicodeSequences(line.substring(0, line.indexOf('=')).trim());
+                                       String val = 
replaceUnicodeSequences(line.substring(line.indexOf('=')+1).trim());
+                                       boolean encoded = key.length() > 1 && 
key.endsWith("*");
+                                       if (encoded) {
+                                               key = key.substring(0, 
key.lastIndexOf('*'));
+                                               String v = 
val.toString().trim();
+                                               if (v.startsWith("{") && 
v.endsWith("}"))
+                                                       val = 
configFile.getEncoder().decode(key, v.substring(1, v.length()-1));
+                                               else
+                                                       
configFile.setHasBeenModified();
+                                       }
+                                       if (containsKey(key)) {
+                                               entries.remove(key);
+                                               lines.remove('*' + key);
+                                               lines.remove('>' + key);
+                                       }
+                                       lines.add((encoded ? '*' : '>') + key);
+                                       addChange(changes, key, 
entries.put(key, val), val);
+                               } else {
+                                       this.lines.add(line);
+                               }
+                       }
+                       return this;
+               } finally {
+                       writeUnlock();
+               }
+       }
+
+       /*
+        * Remove all "#*" lines at the end of this section so they can
+        * be associated with the next section.
+        */
+       List<String> removeTrailingComments() {
+               LinkedList<String> l = new LinkedList<String>();
+               while ((! lines.isEmpty()) && lines.getLast().startsWith("#"))
+                       l.addFirst(lines.removeLast());
+               return l;
+       }
+
+
+       
//--------------------------------------------------------------------------------
+       // Private methods
+       
//--------------------------------------------------------------------------------
+
+       private void addLine(String key, boolean encoded) {
+               for (Iterator<String> i = lines.iterator(); i.hasNext();) {
+                       String k = i.next();
+                       if ((k.startsWith("*") || k.startsWith(">")) && 
k.substring(1).equals(key)) {
+                               if (k.startsWith("*") && encoded || 
k.startsWith(">") && ! encoded)
+                                       return;
+                               i.remove();
+                       }
+               }
+               lines.add((encoded ? "*" : ">") + key);
+       }
+
+       private void readLock() {
+               lock.readLock().lock();
+       }
+
+       private void readUnlock() {
+               lock.readLock().unlock();
+       }
+
+       private void writeLock() {
+               if (readOnly)
+                       throw new UnsupportedOperationException("Cannot modify 
read-only ConfigFile.");
+               lock.writeLock().lock();
+       }
+
+       private void writeUnlock() {
+               lock.writeLock().unlock();
+       }
+
+       private static String trimComment(String s) {
+               return s.replaceAll("^\\s*\\#\\s*", "").trim();
+       }
+
+       private Set<String> createChanges() {
+               return (configFile != null && configFile.getListeners().size() 
> 0 ? new LinkedHashSet<String>() : null);
+       }
+
+       private void signalChanges(Set<String> changes) {
+               if (changes != null && ! changes.isEmpty())
+                       for (ConfigFileListener l : configFile.getListeners())
+                               l.onChange(configFile, changes);
+       }
+
+       private void addChange(Set<String> changes, String key, String oldVal, 
String newVal) {
+               if (changes != null)
+                       if (! isEquals(oldVal, newVal))
+                               changes.add(getFullKey(name, key));
+       }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/SectionListener.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/SectionListener.java
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/SectionListener.java
new file mode 100644
index 0000000..aec28c1
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/SectionListener.java
@@ -0,0 +1,63 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.util.*;
+
+/**
+ * Listener that can be used to listen for change events for a specific 
section in a config file.
+ *
+ * <p>
+ * Use the {@link ConfigFile#addListener(ConfigFileListener)} method to 
register listeners.
+ */
+public class SectionListener extends ConfigFileListener {
+
+       private boolean isDefault;
+       private String prefix;
+
+       /**
+        * Constructor.
+        *
+        * @param section The name of the section in the config file to listen 
to.
+        */
+       public SectionListener(String section) {
+               isDefault = isEmpty(section);
+               prefix = isDefault ? null : (section + '/');
+       }
+
+       @Override /* ConfigFileListener */
+       public void onChange(ConfigFile cf, Set<String> changes) {
+               for (String c : changes) {
+                       if (isDefault) {
+                               if (c.indexOf('/') == -1) {
+                                       onChange(cf);
+                                       return;
+                               }
+                       } else {
+                               if (c.startsWith(prefix)) {
+                                       onChange(cf);
+                                       return;
+                               }
+                       }
+               }
+       }
+
+       /**
+        * Signifies that the config file entry changed.
+        *
+        * @param cf The config file being modified.
+        */
+       public void onChange(ConfigFile cf) {}
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/XorEncoder.java
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/XorEncoder.java 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/XorEncoder.java
new file mode 100644
index 0000000..bbd6b3d
--- /dev/null
+++ 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/XorEncoder.java
@@ -0,0 +1,51 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.ini;
+
+import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.internal.IOUtils.*;
+
+/**
+ * Simply XOR+Base64 encoder for obscuring passwords and other sensitive data 
in INI config files.
+ *
+ * <p>
+ * This is not intended to be used as strong encryption.
+ */
+public final class XorEncoder implements Encoder {
+
+       /** Reusable XOR-Encoder instance. */
+       public static final XorEncoder INSTANCE = new XorEncoder();
+
+       private static final String key = 
System.getProperty("org.apache.juneau.ini.XorEncoder.key",
+               "nuy7og796Vh6G9O6bG230SHK0cc8QYkH");    // The 
super-duper-secret key
+
+       @Override /* Encoder */
+       public String encode(String fieldName, String in) {
+               byte[] b = in.getBytes(UTF8);
+               for (int i = 0; i < b.length; i++) {
+                               int j = i % key.length();
+                       b[i] = (byte)(b[i] ^ key.charAt(j));
+               }
+               return base64Encode(b);
+       }
+
+       @Override /* Encoder */
+       public String decode(String fieldName, String in) {
+               byte[] b = base64Decode(in);
+               for (int i = 0; i < b.length; i++) {
+                       int j = i % key.length();
+                       b[i] = (byte)(b[i] ^ key.charAt(j));
+       }
+               return new String(b, UTF8);
+       }
+}

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config1.png
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config1.png
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config1.png
new file mode 100644
index 0000000..531f280
Binary files /dev/null and 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config1.png
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config2.png
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config2.png
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config2.png
new file mode 100644
index 0000000..7f5a4b3
Binary files /dev/null and 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config2.png
 differ

http://git-wip-us.apache.org/repos/asf/incubator-juneau/blob/ab15d45b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config3.png
----------------------------------------------------------------------
diff --git 
a/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config3.png
 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config3.png
new file mode 100644
index 0000000..749da14
Binary files /dev/null and 
b/juneau-core/juneau-config/src/main/java/org/apache/juneau/ini/doc-files/config3.png
 differ

Reply via email to