blob: 5986ebcd8b48c736fdaab7e3bb0d745a2635b757 [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.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);
}
}