blob: 39108a32c4fe7e8dfd169e45c3cc5fe9d51aa556 [file] [log] [blame]
// ***************************************************************************************************************************
// * 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));
}
}