org.apache.juneau.config.Config Maven / Gradle / Ivy
// ***************************************************************************************************************************
// * 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.config;
import static org.apache.juneau.config.ConfigMod.*;
import static org.apache.juneau.internal.StringUtils.*;
import static org.apache.juneau.internal.ThrowableUtils.*;
import java.beans.*;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import org.apache.juneau.*;
import org.apache.juneau.config.encode.*;
import org.apache.juneau.config.encode.ConfigEncoder;
import org.apache.juneau.config.event.*;
import org.apache.juneau.config.internal.*;
import org.apache.juneau.config.store.*;
import org.apache.juneau.config.vars.*;
import org.apache.juneau.http.*;
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.*;
/**
* Main configuration API class.
*
* See Also:
*
* - {@doc juneau-config}
*
*/
public final class Config extends Context implements ConfigEventListener, Writable {
//-------------------------------------------------------------------------------------------------------------------
// Configurable properties
//-------------------------------------------------------------------------------------------------------------------
private static final String PREFIX = "Config.";
/**
* Configuration property: Configuration name.
*
* Property:
*
* - Name:
"Config.name.s"
* - Data type:
String
* - Default:
"Configuration.cfg"
* - Methods:
*
* - {@link ConfigBuilder#name(String)}
*
*
*
* Description:
*
* Specifies the configuration name.
*
This is typically the configuration file name, although
* the name can be anything identifiable by the {@link ConfigStore} used for retrieving and storing the configuration.
*/
public static final String CONFIG_name = PREFIX + "name.s";
/**
* Configuration property: Configuration store.
*
*
Property:
*
* - Name:
"Config.store.o"
* - Data type: {@link ConfigStore}
*
- Default: {@link ConfigFileStore#DEFAULT}
*
- Methods:
*
* - {@link ConfigBuilder#store(ConfigStore)}
*
*
*
* Description:
*
* The configuration store used for retrieving and storing configurations.
*/
public static final String CONFIG_store = PREFIX + "store.o";
/**
* Configuration property: POJO serializer.
*
*
Property:
*
* - Name:
"Config.serializer.o"
* - Data type: {@link WriterSerializer}
*
- Default: {@link SimpleJsonSerializer#DEFAULT}
*
- Methods:
*
* - {@link ConfigBuilder#serializer(Class)}
*
- {@link ConfigBuilder#serializer(WriterSerializer)}
*
*
*
* Description:
*
* The serializer to use for serializing POJO values.
*/
public static final String CONFIG_serializer = PREFIX + "serializer.o";
/**
* Configuration property: POJO parser.
*
*
Property:
*
* - Name:
"Config.parser.o"
* - Data type: {@link ReaderParser}
*
- Default: {@link JsonParser#DEFAULT}
*
- Methods:
*
* - {@link ConfigBuilder#parser(Class)}
*
- {@link ConfigBuilder#parser(ReaderParser)}
*
*
*
* Description:
*
* The parser to use for parsing values to POJOs.
*/
public static final String CONFIG_parser = PREFIX + "parser.o";
/**
* Configuration property: Value encoder.
*
*
Property:
*
* - Name:
"Config.encoder.o"
* - Data type: {@link ConfigEncoder}
*
- Default: {@link ConfigXorEncoder#INSTANCE}
*
- Methods:
*
* - {@link ConfigBuilder#encoder(Class)}
*
- {@link ConfigBuilder#encoder(ConfigEncoder)}
*
*
*
* Description:
*
* The encoder to use for encoding encoded configuration values.
*/
public static final String CONFIG_encoder = PREFIX + "encoder.o";
/**
* Configuration property: SVL variable resolver.
*
*
Property:
*
* - Name:
"Config.varResolver.o"
* - Data type: {@link VarResolver}
*
- Default: {@link VarResolver#DEFAULT}
*
- Methods:
*
* - {@link ConfigBuilder#varResolver(Class)}
*
- {@link ConfigBuilder#varResolver(VarResolver)}
*
*
*
* Description:
*
* The resolver to use for resolving SVL variables.
*/
public static final String CONFIG_varResolver = PREFIX + "varResolver.o";
/**
* Configuration property: Binary value line length.
*
*
Property:
*
* - Name:
"Config.binaryLineLength.i"
* - Data type:
Integer
* - Default:
-1
* - Methods:
*
* - {@link ConfigBuilder#binaryLineLength(int)}
*
*
*
* Description:
*
* When serializing binary values, lines will be split after this many characters.
*
Use -1
to represent no line splitting.
*/
public static final String CONFIG_binaryLineLength = PREFIX + "binaryLineLength.i";
/**
* Configuration property: Binary value format.
*
*
Property:
*
* - Name:
"Config.binaryFormat.s"
* - Data type: {@link BinaryFormat}
*
- Default: {@link BinaryFormat#BASE64}
*
- Methods:
*
* - {@link ConfigBuilder#binaryFormat(BinaryFormat)}
*
*
*
* Description:
*
* The format to use when persisting byte arrays.
*
*
* Possible values:
*
* - {@link BinaryFormat#BASE64} - BASE64-encoded string.
*
- {@link BinaryFormat#HEX} - Hexadecimal.
*
- {@link BinaryFormat#SPACED_HEX} - Hexadecimal with spaces between bytes.
*
*/
public static final String CONFIG_binaryFormat = PREFIX + "binaryFormat.s";
/**
* Configuration property: Multi-line values should always be on separate lines.
*
* Property:
*
* - Name:
"Config.multiLineValuesOnSeparateLines.b"
* - Data type:
Boolean
* - Default:
false
* - Methods:
*
* - {@link ConfigBuilder#multiLineValuesOnSeparateLines()}
*
*
*
* Description:
*
* When enabled, multi-line values will always be placed on a separate line from the key.
*/
public static final String CONFIG_multiLineValuesOnSeparateLines = PREFIX + "multiLineValuesOnSeparateLines.b";
/**
* Configuration property: Read-only.
*
*
Property:
*
* - Name:
"Config.readOnly.b"
* - Data type:
Boolean
* - Default:
false
* - Methods:
*
* - {@link ConfigBuilder#readOnly()}
*
*
*
* Description:
*
* When enabled, attempts to call any setters on this object will throw an {@link UnsupportedOperationException}.
*/
public static final String CONFIG_readOnly = PREFIX + "readOnly.b";
//-------------------------------------------------------------------------------------------------------------------
// Instance
//-------------------------------------------------------------------------------------------------------------------
private final String name;
private final ConfigStore store;
private final WriterSerializer serializer;
private final ReaderParser parser;
private final ConfigEncoder encoder;
private final VarResolverSession varSession;
private final int binaryLineLength;
private final BinaryFormat binaryFormat;
private final boolean multiLineValuesOnSeparateLines, readOnly;
private final ConfigMap configMap;
private final BeanSession beanSession;
private final List listeners = Collections.synchronizedList(new LinkedList());
/**
* Instantiates a new clean-slate {@link ConfigBuilder} object.
*
*
* This is equivalent to simply calling new ConfigBuilder()
.
*
* @return A new {@link ConfigBuilder} object.
*/
public static ConfigBuilder create() {
return new ConfigBuilder();
}
/**
* Same as {@link #create()} but initializes the builder with the specified config name.
*
*
* This is equivalent to simply calling new ConfigBuilder().name(name)
.
*
* @param name The configuration name.
* @return A new {@link ConfigBuilder} object.
*/
public static ConfigBuilder create(String name) {
return new ConfigBuilder().name(name);
}
@Override /* Context */
public ConfigBuilder builder() {
return new ConfigBuilder(getPropertyStore());
}
/**
* Constructor.
*
* @param ps
* The property store containing all the settings for this object.
* @throws IOException
*/
public Config(PropertyStore ps) throws IOException {
super(ps);
name = getStringProperty(CONFIG_name, "Configuration.cfg");
store = getInstanceProperty(CONFIG_store, ConfigStore.class, ConfigFileStore.DEFAULT);
configMap = store.getMap(name);
configMap.register(this);
serializer = getInstanceProperty(CONFIG_serializer, WriterSerializer.class, SimpleJsonSerializer.DEFAULT);
parser = getInstanceProperty(CONFIG_parser, ReaderParser.class, JsonParser.DEFAULT);
beanSession = parser.createBeanSession();
encoder = getInstanceProperty(CONFIG_encoder, ConfigEncoder.class, ConfigXorEncoder.INSTANCE);
varSession = getInstanceProperty(CONFIG_varResolver, VarResolver.class, VarResolver.DEFAULT)
.builder()
.vars(ConfigVar.class)
.contextObject(ConfigVar.SESSION_config, this)
.build()
.createSession();
binaryLineLength = getIntegerProperty(CONFIG_binaryLineLength, -1);
binaryFormat = getProperty(CONFIG_binaryFormat, BinaryFormat.class, BinaryFormat.BASE64);
multiLineValuesOnSeparateLines = getBooleanProperty(CONFIG_multiLineValuesOnSeparateLines, false);
readOnly = getBooleanProperty(CONFIG_readOnly, false);
}
Config(Config copyFrom, VarResolverSession varSession) {
super(null);
name = copyFrom.name;
store = copyFrom.store;
configMap = copyFrom.configMap;
configMap.register(this);
serializer = copyFrom.serializer;
parser = copyFrom.parser;
encoder = copyFrom.encoder;
this.varSession = varSession;
binaryLineLength = copyFrom.binaryLineLength;
binaryFormat = copyFrom.binaryFormat;
multiLineValuesOnSeparateLines = copyFrom.multiLineValuesOnSeparateLines;
readOnly = copyFrom.readOnly;
beanSession = copyFrom.beanSession;
}
/**
* Creates a copy of this config using the specified var session for resolving variables.
*
*
* This creates a shallow copy of the config but replacing the variable resolver.
*
* @param varSession The var session used for resolving string variables.
* @return A new config object.
*/
public Config resolving(VarResolverSession varSession) {
return new Config(this, varSession);
}
//-----------------------------------------------------------------------------------------------------------------
// Workhorse getters
//-----------------------------------------------------------------------------------------------------------------
/**
* Returns the specified value as a string from the config file.
*
*
* Unlike {@link #getString(String)}, this method doesn't replace SVL variables.
*
* @param key The key.
* @return The value, or null if the section or value doesn't exist.
*/
public String get(String key) {
String sname = sname(key);
String skey = skey(key);
ConfigEntry ce = configMap.getEntry(sname, skey);
if (ce == null || ce.getValue() == null)
return null;
String val = ce.getValue();
for (ConfigMod m : ConfigMod.asModifiersReverse(ce.getModifiers())) {
if (m == ENCODED) {
if (encoder.isEncoded(val))
val = encoder.decode(key, val);
}
}
return val;
}
//-----------------------------------------------------------------------------------------------------------------
// Workhorse setters
//-----------------------------------------------------------------------------------------------------------------
/**
* Sets a value in this config.
*
* @param key The key.
* @param value The value.
* @return This object (for method chaining).
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config set(String key, String value) {
checkWrite();
assertFieldNotNull(key, "key");
String sname = sname(key);
String skey = skey(key);
ConfigEntry ce = configMap.getEntry(sname, skey);
if (ce == null && value == null)
return this;
String mod = ce == null ? "" : ce.getModifiers();
String s = asString(value);
for (ConfigMod m : ConfigMod.asModifiers(mod)) {
if (m == ENCODED) {
s = encoder.encode(key, s);
}
}
configMap.setEntry(sname, skey, s, null, null, null);
return this;
}
/**
* Adds or replaces an entry with the specified key with a POJO serialized to a string using the registered
* serializer.
*
*
* Equivalent to calling put(key, value, isEncoded(key))
.
*
* @param key The key.
* @param value The new value POJO.
* @return The previous value, or null if the section or key did not previously exist.
* @throws SerializeException
* If serializer could not serialize the value or if a serializer is not registered with this config file.
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config set(String key, Object value) throws SerializeException {
return set(key, value, null);
}
/**
* Same as {@link #set(String, Object)} but allows you to specify the serializer to use to serialize the
* value.
*
* @param key The key.
* @param value The new value.
* @param serializer
* The serializer to use for serializing the object.
* If null , then uses the predefined serializer on the config file.
* @return The previous value, or null if the section or key did not previously exist.
* @throws SerializeException
* If serializer could not serialize the value or if a serializer is not registered with this config file.
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config set(String key, Object value, Serializer serializer) throws SerializeException {
return set(key, serialize(value, serializer));
}
/**
* Same as {@link #set(String, Object)} but allows you to specify all aspects of a value.
*
* @param key The key.
* @param value The new value.
* @param serializer
* The serializer to use for serializing the object.
* If null , then uses the predefined serializer on the config file.
* @param modifier
* Optional modifier to apply to the value.
*
If null , then previous value will not be replaced.
* @param comment
* Optional same-line comment to add to this value.
*
If null , then previous value will not be replaced.
* @param preLines
* Optional comment or blank lines to add before this entry.
*
If null , then previous value will not be replaced.
* @return The previous value, or null if the section or key did not previously exist.
* @throws SerializeException
* If serializer could not serialize the value or if a serializer is not registered with this config file.
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config set(String key, Object value, Serializer serializer, ConfigMod modifier, String comment, List preLines) throws SerializeException {
return set(key, value, serializer, modifier == null ? null : new ConfigMod[]{modifier}, comment, preLines);
}
/**
* Same as {@link #set(String, Object)} but allows you to specify all aspects of a value.
*
* @param key The key.
* @param value The new value.
* @param serializer
* The serializer to use for serializing the object.
* If null , then uses the predefined serializer on the config file.
* @param modifiers
* Optional modifiers to apply to the value.
*
If null , then previous value will not be replaced.
* @param comment
* Optional same-line comment to add to this value.
*
If null , then previous value will not be replaced.
* @param preLines
* Optional comment or blank lines to add before this entry.
*
If null , then previous value will not be replaced.
* @return The previous value, or null if the section or key did not previously exist.
* @throws SerializeException
* If serializer could not serialize the value or if a serializer is not registered with this config file.
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config set(String key, Object value, Serializer serializer, ConfigMod[] modifiers, String comment, List preLines) throws SerializeException {
checkWrite();
assertFieldNotNull(key, "key");
String sname = sname(key);
String skey = skey(key);
String s = serialize(value, serializer);
if (modifiers != null) {
for (ConfigMod m : modifiers) {
if (m == ENCODED) {
s = encoder.encode(key, s);
}
}
}
configMap.setEntry(sname, skey, s, modifiers == null ? null : ConfigMod.asString(modifiers), comment, preLines);
return this;
}
/**
* Removes an entry with the specified key.
*
* @param key The key.
* @return The previous value, or null if the section or key did not previously exist.
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config remove(String key) {
checkWrite();
String sname = sname(key);
String skey = skey(key);
configMap.removeEntry(sname, skey);
return this;
}
/**
* Encodes and unencoded entries in this config.
*
*
* If any entries in the config are marked as encoded but not actually encoded,
* this will encode them.
*
* @return This object (for method chaining).
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config encodeEntries() {
checkWrite();
for (String section : configMap.getSections()) {
for (String key : configMap.getKeys(section)) {
ConfigEntry ce = configMap.getEntry(section, key);
if (ce != null && ce.hasModifier('*') && ! encoder.isEncoded(ce.getValue())) {
configMap.setEntry(section, key, encoder.encode(section + '/' + key, ce.getValue()), null, null, null);
}
}
}
return this;
}
//-----------------------------------------------------------------------------------------------------------------
// API methods
//-----------------------------------------------------------------------------------------------------------------
/**
* Gets the entry with the specified key.
*
*
* The key can be in one of the following formats...
*
* -
*
"key" - A value in the default section (i.e. defined above any [section]
header).
* -
*
"section/key" - A value from the specified section.
*
*
* @param key The key.
* @return The value, or null if the section or key does not exist.
*/
public String getString(String key) {
String s = get(key);
if (s == null)
return null;
if (varSession != null)
s = varSession.resolve(s);
return s;
}
/**
* Gets the entry with the specified key.
*
*
* The key can be in one of the following formats...
*
* -
*
"key" - A value in the default section (i.e. defined above any [section]
header).
* -
*
"section/key" - A value from the specified section.
*
*
* @param key The key.
* @param def The default value if the value does not exist.
* @return The value, or the default value if the section or key does not exist.
*/
public String getString(String key, String def) {
String s = get(key);
if (isEmpty(s))
return def;
if (varSession != null)
s = varSession.resolve(s);
return s;
}
/**
* Gets the entry with the specified key, splits the value on commas, and returns the values as trimmed strings.
*
* @param key The key.
* @return The value, or an empty array if the section or key does not exist.
*/
public String[] getStringArray(String key) {
return getStringArray(key, new String[0]);
}
/**
* Same as {@link #getStringArray(String)} but returns a default value if the value cannot be found.
*
* @param key The key.
* @param def The default value if the value does not exist.
* @return The value, or the default value if the section or key does not exist or is blank.
*/
public String[] getStringArray(String key, String[] def) {
String s = getString(key);
if (isEmpty(s))
return def;
String[] r = split(s);
return r.length == 0 ? def : r;
}
/**
* Convenience method for getting int config values.
*
*
* "K" , "M" , and "G" can be used to identify kilo, mega, and giga.
*
*
Example:
*
* -
*
"100K" => 1024000
* -
*
"100M" => 104857600
*
*
*
* Uses {@link Integer#decode(String)} underneath, so any of the following integer formats are supported:
*
* "0x..."
* "0X..."
* "#..."
* "0..."
*
*
* @param key The key.
* @return The value, or 0
if the value does not exist or the value is empty.
*/
public int getInt(String key) {
return getInt(key, 0);
}
/**
* Same as {@link #getInt(String)} but returns a default value if not set.
*
* @param key The key.
* @param def The default value if the value does not exist.
* @return The value, or the default value if the value does not exist or the value is empty.
*/
public int getInt(String key, int def) {
String s = getString(key);
if (isEmpty(s))
return def;
return parseIntWithSuffix(s);
}
/**
* Convenience method for getting boolean config values.
*
* @param key The key.
* @return The value, or false if the section or key does not exist or cannot be parsed as a boolean.
*/
public boolean getBoolean(String key) {
return getBoolean(key, false);
}
/**
* Convenience method for getting boolean config values.
*
* @param key The key.
* @param def The default value if the value does not exist.
* @return The value, or the default value if the section or key does not exist or cannot be parsed as a boolean.
*/
public boolean getBoolean(String key, boolean def) {
String s = getString(key);
return isEmpty(s) ? def : Boolean.parseBoolean(s);
}
/**
* Convenience method for getting long config values.
*
*
* "K" , "M" , and "G" can be used to identify kilo, mega, and giga.
*
*
Example:
*
* -
*
"100K" => 1024000
* -
*
"100M" => 104857600
*
*
*
* Uses {@link Long#decode(String)} underneath, so any of the following number formats are supported:
*
* "0x..."
* "0X..."
* "#..."
* "0..."
*
*
* @param key The key.
* @return The value, or 0
if the value does not exist or the value is empty.
*/
public long getLong(String key) {
return getLong(key, 0);
}
/**
* Same as {@link #getLong(String)} but returns a default value if not set.
*
* @param key The key.
* @param def The default value if the value does not exist.
* @return The value, or the default value if the value does not exist or the value is empty.
*/
public long getLong(String key, long def) {
String s = getString(key);
if (isEmpty(s))
return def;
return parseLongWithSuffix(s);
}
/**
* Convenience method for getting double config values.
*
*
* Uses {@link Double#valueOf(String)} underneath, so any of the following number formats are supported:
*
* "0x..."
* "0X..."
* "#..."
* "0..."
*
*
* @param key The key.
* @return The value, or 0
if the value does not exist or the value is empty.
*/
public double getDouble(String key) {
return getDouble(key, 0);
}
/**
* Same as {@link #getDouble(String)} but returns a default value if not set.
*
* @param key The key.
* @param def The default value if the value does not exist.
* @return The value, or the default value if the value does not exist or the value is empty.
*/
public double getDouble(String key, double def) {
String s = getString(key);
if (isEmpty(s))
return def;
return Double.valueOf(s);
}
/**
* Convenience method for getting float config values.
*
*
* Uses {@link Float#valueOf(String)} underneath, so any of the following number formats are supported:
*
* "0x..."
* "0X..."
* "#..."
* "0..."
*
*
* @param key The key.
* @return The value, or 0
if the value does not exist or the value is empty.
*/
public float getFloat(String key) {
return getFloat(key, 0);
}
/**
* Same as {@link #getFloat(String)} but returns a default value if not set.
*
* @param key The key.
* @param def The default value if the value does not exist.
* @return The value, or the default value if the value does not exist or the value is empty.
*/
public float getFloat(String key, float def) {
String s = getString(key);
if (isEmpty(s))
return def;
return Float.valueOf(s);
}
/**
* Convenience method for getting byte array config values.
*
*
* This is equivalent to calling the following:
*
* byte [] b = config.getObject(key, byte [].class );
*
*
* Byte arrays are stored as encoded strings, typically BASE64, but dependent on the {@link #CONFIG_binaryFormat} setting.
*
* @param key The key.
* @return The value, or null if the section or key does not exist.
* @throws ParseException If value could not be converted to a byte array.
*/
public byte[] getBytes(String key) throws ParseException {
String s = get(key);
if (s == null)
return null;
if (s.isEmpty())
return new byte[0];
return getObject(key, byte[].class);
}
/**
* Same as {@link #getBytes(String)} but with a default value if the entry doesn't exist.
*
* @param key The key.
* @param def The default value if the value does not exist.
* @return The value, or the default value if the section or key does not exist.
* @throws ParseException If value could not be converted to a byte array.
*/
public byte[] getBytes(String key, byte[] def) throws ParseException {
String s = get(key);
if (s == null)
return def;
if (s.isEmpty())
return def;
return getObjectWithDefault(key, def, byte[].class);
}
/**
* Gets the entry with the specified key and converts it to the specified value.
*
*
* The key can be in one of the following formats...
*
* -
*
"key" - A value in the default section (i.e. defined above any [section]
header).
* -
*
"section/key" - A value from the specified section.
*
*
*
* The type can be a simple type (e.g. beans, strings, numbers) or parameterized type (collections/maps).
*
*
Examples:
*
* Config cf = Config.create ().name("MyConfig.cfg" ).build();
*
* // Parse into a linked-list of strings.
* List l = cf.getObject("MySection/myListOfStrings" , LinkedList.class , String.class );
*
* // Parse into a linked-list of beans.
* List l = cf.getObject("MySection/myListOfBeans" , LinkedList.class , MyBean.class );
*
* // Parse into a linked-list of linked-lists of strings.
* List l = cf.getObject("MySection/my2dListOfStrings" , LinkedList.class ,
* LinkedList.class , String.class );
*
* // Parse into a map of string keys/values.
* Map m = cf.getObject("MySection/myMap" , TreeMap.class , String.class ,
* String.class );
*
* // Parse into a map containing string keys and values of lists containing beans.
* Map m = cf.getObject("MySection/myMapOfListsOfBeans" , TreeMap.class , String.class ,
* List.class , MyBean.class );
*
*
*
* Collection
classes are assumed to be followed by zero or one objects indicating the element type.
*
*
* Map
classes are assumed to be followed by zero or two meta objects indicating the key and value
* types.
*
*
* The array can be arbitrarily long to indicate arbitrarily complex data structures.
*
*
Notes:
*
* -
* Use the {@link #getObject(String, Class)} method instead if you don't need a parameterized map/collection.
*
*
* @param key The key.
* @param type
* The object type to create.
*
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* @param args
* The type arguments of the class if it's a collection or map.
*
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
*
Ignored if the main type is not a map or collection.
* @throws ParseException If parser could not parse the value or if a parser is not registered with this config file.
* @return The value, or null if the section or key does not exist.
*/
public T getObject(String key, Type type, Type...args) throws ParseException {
return getObject(key, (Parser)null, type, args);
}
/**
* Same as {@link #getObject(String, Type, Type...)} but allows you to specify the parser to use to parse the value.
*
* @param key The key.
* @param parser
* The parser to use for parsing the object.
* If null , then uses the predefined parser on the config file.
* @param type
* The object type to create.
*
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* @param args
* The type arguments of the class if it's a collection or map.
*
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
*
Ignored if the main type is not a map or collection.
* @throws ParseException If parser could not parse the value or if a parser is not registered with this config file.
* @return The value, or null if the section or key does not exist.
*/
public T getObject(String key, Parser parser, Type type, Type...args) throws ParseException {
assertFieldNotNull(type, "type");
return parse(getString(key), parser, type, args);
}
/**
* Same as {@link #getObject(String, Type, Type...)} except optimized for a non-parameterized class.
*
*
* This is the preferred parse method for simple types since you don't need to cast the results.
*
*
Examples:
*
* Config cf = Config.create ().name("MyConfig.cfg" ).build();
*
* // Parse into a string.
* String s = cf.getObject("MySection/mySimpleString" , String.class );
*
* // Parse into a bean.
* MyBean b = cf.getObject("MySection/myBean" , MyBean.class );
*
* // Parse into a bean array.
* MyBean[] b = cf.getObject("MySection/myBeanArray" , MyBean[].class );
*
* // Parse into a linked-list of objects.
* List l = cf.getObject("MySection/myList" , LinkedList.class );
*
* // Parse into a map of object keys/values.
* Map m = cf.getObject("MySection/myMap" , TreeMap.class );
*
*
* @param The class type of the object being created.
* @param key The key.
* @param type The object type to create.
* @return The parsed object.
* @throws ParseException
* If the input contains a syntax error or is malformed, or is not valid for the specified type.
* @see BeanSession#getClassMeta(Type,Type...) for argument syntax for maps and collections.
*/
public T getObject(String key, Class type) throws ParseException {
return getObject(key, (Parser)null, type);
}
/**
* Same as {@link #getObject(String, Class)} but allows you to specify the parser to use to parse the value.
*
* @param The class type of the object being created.
* @param key The key.
* @param parser
* The parser to use for parsing the object.
* If null , then uses the predefined parser on the config file.
* @param type The object type to create.
* @return The parsed object.
* @throws ParseException
* If the input contains a syntax error or is malformed, or is not valid for the specified type.
* @see BeanSession#getClassMeta(Type,Type...) for argument syntax for maps and collections.
*/
public T getObject(String key, Parser parser, Class type) throws ParseException {
assertFieldNotNull(type, "c");
return parse(getString(key), parser, type);
}
/**
* Gets the entry with the specified key and converts it to the specified value.
*
*
* Same as {@link #getObject(String, Class)}, but with a default value.
*
* @param key The key.
* @param def The default value if the value does not exist.
* @param type The class to convert the value to.
* @throws ParseException If parser could not parse the value or if a parser is not registered with this config file.
* @return The value, or null if the section or key does not exist.
*/
public T getObjectWithDefault(String key, T def, Class type) throws ParseException {
return getObjectWithDefault(key, null, def, type);
}
/**
* Same as {@link #getObjectWithDefault(String, Object, Class)} but allows you to specify the parser to use to parse
* the value.
*
* @param key The key.
* @param parser
* The parser to use for parsing the object.
* If null , then uses the predefined parser on the config file.
* @param def The default value if the value does not exist.
* @param type The class to convert the value to.
* @throws ParseException If parser could not parse the value or if a parser is not registered with this config file.
* @return The value, or null if the section or key does not exist.
*/
public T getObjectWithDefault(String key, Parser parser, T def, Class type) throws ParseException {
assertFieldNotNull(type, "c");
T t = parse(getString(key), parser, type);
return (t == null ? def : t);
}
/**
* Gets the entry with the specified key and converts it to the specified value.
*
*
* Same as {@link #getObject(String, Type, Type...)}, but with a default value.
*
* @param key The key.
* @param def The default value if the value does not exist.
* @param type
* The object type to create.
*
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* @param args
* The type arguments of the class if it's a collection or map.
*
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
*
Ignored if the main type is not a map or collection.
* @throws ParseException If parser could not parse the value or if a parser is not registered with this config file.
* @return The value, or null if the section or key does not exist.
*/
public T getObjectWithDefault(String key, T def, Type type, Type...args) throws ParseException {
return getObjectWithDefault(key, null, def, type, args);
}
/**
* Same as {@link #getObjectWithDefault(String, Object, Type, Type...)} but allows you to specify the parser to use
* to parse the value.
*
* @param key The key.
* @param parser
* The parser to use for parsing the object.
* If null , then uses the predefined parser on the config file.
* @param def The default value if the value does not exist.
* @param type
* The object type to create.
*
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
* @param args
* The type arguments of the class if it's a collection or map.
*
Can be any of the following: {@link ClassMeta}, {@link Class}, {@link ParameterizedType}, {@link GenericArrayType}
*
Ignored if the main type is not a map or collection.
* @throws ParseException If parser could not parse the value or if a parser is not registered with this config file.
* @return The value, or null if the section or key does not exist.
*/
public T getObjectWithDefault(String key, Parser parser, T def, Type type, Type...args) throws ParseException {
assertFieldNotNull(type, "type");
T t = parse(getString(key), parser, type, args);
return (t == null ? def : t);
}
/**
* Returns the keys of the entries in the specified section.
*
* @param section
* The section name to write from.
*
If empty, refers to the default section.
*
Must not be null .
* @return
* An unmodifiable set of keys, or an empty set if the section doesn't exist.
*/
public Set getKeys(String section) {
return configMap.getKeys(section(section));
}
/**
* Copies the entries in a section to the specified bean by calling the public setters on that bean.
*
* @param section
* The section name to write from.
*
If empty, refers to the default section.
*
Must not be null .
* @param bean The bean to set the properties on.
* @param ignoreUnknownProperties
* If true , don't throw an {@link IllegalArgumentException} if this section contains a key that doesn't
* correspond to a setter method.
* @return An object map of the changes made to the bean.
* @throws ParseException If parser was not set on this config file or invalid properties were found in the section.
* @throws IllegalArgumentException
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config writeProperties(String section, Object bean, boolean ignoreUnknownProperties) throws ParseException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {
checkWrite();
assertFieldNotNull(bean, "bean");
section = section(section);
Set keys = configMap.getKeys(section);
if (keys == null)
throw new IllegalArgumentException("Section '"+section+"' not found in configuration.");
BeanMap> bm = beanSession.toBeanMap(bean);
for (String k : keys) {
BeanPropertyMeta bpm = bm.getPropertyMeta(k);
if (bpm == null) {
if (! ignoreUnknownProperties)
throw new ParseException("Unknown property ''{0}'' encountered in configuration section ''{1}''.", k, section);
} else {
bm.put(k, getObject(section + '/' + k, bpm.getClassMeta().getInnerClass()));
}
}
return this;
}
/**
* Shortcut for calling getSectionAsBean(sectionName, c, false )
.
*
* @param section
* The section name to write from.
*
If empty, refers to the default section.
*
Must not be null .
* @param c The bean class to create.
* @return A new bean instance.
* @throws ParseException
*/
public T getSectionAsBean(String section, Classc) throws ParseException {
return getSectionAsBean(section, c, false);
}
/**
* Converts this config file section to the specified bean instance.
*
*
* Key/value pairs in the config file section get copied as bean property values to the specified bean class.
*
*
Example config file
*
* [MyAddress]
* name = John Smith
* street = 123 Main Street
* city = Anywhere
* state = NY
* zip = 12345
*
*
* Example bean
*
* public class Address {
* public String name, street, city;
* public StateEnum state;
* public int zip;
* }
*
*
* Example usage
*
* Config cf = Config.create ().name("MyConfig.cfg" ).build();
* Address myAddress = cf.getSectionAsBean("MySection" , Address.class );
*
*
* @param section
* The section name to write from.
*
If empty, refers to the default section.
*
Must not be null .
* @param c The bean class to create.
* @param ignoreUnknownProperties
* If false , throws a {@link ParseException} if the section contains an entry that isn't a bean property
* name.
* @return A new bean instance, or null if the section doesn't exist.
* @throws ParseException
*/
public T getSectionAsBean(String section, Class c, boolean ignoreUnknownProperties) throws ParseException {
assertFieldNotNull(c, "c");
section = section(section);
if (! configMap.hasSection(section))
return null;
Set keys = configMap.getKeys(section);
BeanMap bm = beanSession.newBeanMap(c);
for (String k : keys) {
BeanPropertyMeta bpm = bm.getPropertyMeta(k);
if (bpm == null) {
if (! ignoreUnknownProperties)
throw new ParseException("Unknown property ''{0}'' encountered in configuration section ''{1}''.", k, section);
} else {
bm.put(k, getObject(section + '/' + k, bpm.getClassMeta().getInnerClass()));
}
}
return bm.getBean();
}
/**
* Returns a section of this config copied into an {@link ObjectMap}.
*
* @param section
* The section name to write from.
*
If empty, refers to the default section.
*
Must not be null .
* @return A new {@link ObjectMap}, or null if the section doesn't exist.
* @throws ParseException
*/
public ObjectMap getSectionAsMap(String section) throws ParseException {
section = section(section);
if (! configMap.hasSection(section))
return null;
Set keys = configMap.getKeys(section);
ObjectMap om = new ObjectMap();
for (String k : keys)
om.put(k, getObject(section + '/' + k, Object.class));
return om;
}
/**
* Wraps a config file section inside a Java interface so that values in the section can be read and
* write using getters and setters.
*
* Example config file
*
* [MySection]
* string = foo
* int = 123
* enum = ONE
* bean = {foo:'bar',baz:123}
* int3dArray = [[[123,null],null],null]
* bean1d3dListMap = {key:[[[[{foo:'bar',baz:123}]]]]}
*
*
* Example interface
*
* public interface MyConfigInterface {
*
* String getString();
* void setString(String x);
*
* int getInt();
* void setInt(int x);
*
* MyEnum getEnum();
* void setEnum(MyEnum x);
*
* MyBean getBean();
* void setBean(MyBean x);
*
* int [][][] getInt3dArray();
* void setInt3dArray(int [][][] x);
*
* Map<String,List<MyBean[][][]>> getBean1d3dListMap();
* void setBean1d3dListMap(Map<String,List<MyBean[][][]>> x);
* }
*
*
* Example usage
*
* Config cf = Config.create ().name("MyConfig.cfg" ).build();
*
* MyConfigInterface ci = cf.getSectionAsInterface("MySection" , MyConfigInterface.class );
*
* int myInt = ci.getInt();
*
* ci.setBean(new MyBean());
*
* cf.save();
*
*
*
*
* - Calls to setters when the configuration is read-only will cause {@link UnsupportedOperationException} to be thrown.
*
*
* @param section
* The section name to write from.
*
If empty, refers to the default section.
*
Must not be null .
* @param c The proxy interface class.
* @return The proxy interface.
*/
@SuppressWarnings("unchecked")
public T getSectionAsInterface(String section, final Class c) {
assertFieldNotNull(c, "c");
final String section2 = section(section);
if (! c.isInterface())
throw new IllegalArgumentException("Class '"+c.getName()+"' passed to getSectionAsInterface() is not an interface.");
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
BeanInfo bi = Introspector.getBeanInfo(c, null);
for (PropertyDescriptor pd : bi.getPropertyDescriptors()) {
Method rm = pd.getReadMethod(), wm = pd.getWriteMethod();
if (method.equals(rm))
return Config.this.getObject(section2 + '/' + pd.getName(), rm.getGenericReturnType());
if (method.equals(wm))
return Config.this.set(section2 + '/' + pd.getName(), args[0]);
}
throw new UnsupportedOperationException("Unsupported interface method. method='" + method + "'");
}
};
return (T)Proxy.newProxyInstance(c.getClassLoader(), new Class[] { c }, h);
}
/**
* Returns true if this section contains the specified key and the key has a non-blank value.
*
* @param key The key.
* @return true if this section contains the specified key and the key has a non-blank value.
*/
public boolean exists(String key) {
return isNotEmpty(getString(key, null));
}
/**
* Creates the specified section if it doesn't exist.
*
*
* Returns the existing section if it already exists.
*
* @param name
* The section name.
*
Must not be null .
*
Use blank for the default section.
* @param preLines
* Optional comment and blank lines to add immediately before the section.
*
If null , previous pre-lines will not be replaced.
* @return The appended or existing section.
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config setSection(String name, List preLines) {
try {
return setSection(section(name), preLines, null);
} catch (SerializeException e) {
throw new RuntimeException(e); // Impossible.
}
}
/**
* Creates the specified section if it doesn't exist.
*
* @param name
* The section name.
*
Must not be null .
*
Use blank for the default section.
* @param preLines
* Optional comment and blank lines to add immediately before the section.
*
If null , previous pre-lines will not be replaced.
* @param contents
* Values to set in the new section.
*
Can be null .
* @return The appended or existing section.
* @throws SerializeException
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config setSection(String name, List preLines, Map contents) throws SerializeException {
checkWrite();
configMap.setSection(section(name), preLines);
if (contents != null)
for (Map.Entry e : contents.entrySet())
set(section(name) + '/' + e.getKey(), e.getValue());
return this;
}
/**
* Removes the section with the specified name.
*
* @param name The name of the section to remove
* @return This object (for method chaining).
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config removeSection(String name) {
checkWrite();
configMap.removeSection(name);
return this;
}
/**
* Loads the contents of the specified map of maps into this config.
*
* @param m The maps to load.
* @return This object (for method chaining).
* @throws SerializeException
*/
public Config load(Map> m) throws SerializeException {
if (m != null)
for (Map.Entry> e : m.entrySet()) {
setSection(e.getKey(), null, e.getValue());
}
return this;
}
/**
* Commit the changes in this config to the store.
*
* @return This object (for method chaining).
* @throws IOException
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config commit() throws IOException {
checkWrite();
configMap.commit();
return this;
}
/**
* Saves this config file to the specified writer as an INI file.
*
*
* The writer will automatically be closed.
*
* @param w The writer to send the output to.
* @return This object (for method chaining).
* @throws IOException If a problem occurred trying to send contents to the writer.
*/
@Override /* Writable */
public Writer writeTo(Writer w) throws IOException {
return configMap.writeTo(w);
}
/**
* Add a listener to this config to react to modification events.
*
*
* Listeners should be removed using {@link #removeListener(ConfigEventListener)}.
*
* @param listener The new listener to add.
* @return This object (for method chaining).
*/
public Config addListener(ConfigEventListener listener) {
listeners.add(listener);
return this;
}
/**
* Removes a listener from this config.
*
* @param listener The listener to remove.
* @return This object (for method chaining).
*/
public Config removeListener(ConfigEventListener listener) {
listeners.remove(listener);
return this;
}
/**
* Closes this configuration object by unregistering it from the underlying config map.
*
* @throws IOException
*/
public void close() throws IOException {
configMap.unregister(this);
}
/**
* Overwrites the contents of the config file.
*
* @param contents The new contents of the config file.
* @param synchronous Wait until the change has been persisted before returning this map.
* @return This object (for method chaining).
* @throws IOException
* @throws InterruptedException
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config load(Reader contents, boolean synchronous) throws IOException, InterruptedException {
checkWrite();
configMap.load(IOUtils.read(contents), synchronous);
return this;
}
/**
* Overwrites the contents of the config file.
*
* @param contents The new contents of the config file.
* @param synchronous Wait until the change has been persisted before returning this map.
* @return This object (for method chaining).
* @throws IOException
* @throws InterruptedException
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config load(String contents, boolean synchronous) throws IOException, InterruptedException {
checkWrite();
configMap.load(contents, synchronous);
return this;
}
/**
* Does a rollback of any changes on this config currently in memory.
*
* @return This object (for method chaining).
* @throws UnsupportedOperationException If configuration is read only.
*/
public Config rollback() {
checkWrite();
configMap.rollback();
return this;
}
/**
* Returns the values in this config map as a map of maps.
*
*
* This is considered a snapshot copy of the config map.
*
*
* The returned map is modifiable, but modifications to the returned map are not reflected in the config map.
*
* @return A copy of this config as a map of maps.
*/
@Override /* Context */
public ObjectMap asMap() {
return configMap.asMap();
}
//-----------------------------------------------------------------------------------------------------------------
// Interface methods
//-----------------------------------------------------------------------------------------------------------------
/**
* Unused.
*/
@Override /* Context */
public Session createSession(SessionArgs args) {
throw new UnsupportedOperationException();
}
/**
* Unused.
*/
@Override /* Context */
public SessionArgs createDefaultSessionArgs() {
throw new UnsupportedOperationException();
}
@Override /* ConfigEventListener */
public void onConfigChange(List events) {
for (ConfigEventListener l : listeners)
l.onConfigChange(events);
}
@Override /* Writable */
public MediaType getMediaType() {
return MediaType.PLAIN;
}
//-----------------------------------------------------------------------------------------------------------------
// Private methods
//-----------------------------------------------------------------------------------------------------------------
private String serialize(Object value, Serializer serializer) throws SerializeException {
if (value == null)
return "";
if (serializer == null)
serializer = this.serializer;
Class> c = value.getClass();
if (value instanceof CharSequence)
return nlIfMl((CharSequence)value);
if (isSimpleType(c))
return value.toString();
if (value instanceof byte[]) {
String s = null;
byte[] b = (byte[])value;
if (binaryFormat == BinaryFormat.HEX)
s = toHex(b);
else if (binaryFormat == BinaryFormat.SPACED_HEX)
s = toSpacedHex(b);
else
s = base64Encode(b);
int l = binaryLineLength;
if (l <= 0 || s.length() <= l)
return s;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < s.length(); i += l)
sb.append(binaryLineLength > 0 ? "\n" : "").append(s.substring(i, Math.min(s.length(), i + l)));
return sb.toString();
}
String r = null;
if (multiLineValuesOnSeparateLines)
r = "\n" + (String)serializer.serialize(value);
else
r = (String)serializer.serialize(value);
if (r.startsWith("'"))
return r.substring(1, r.length()-1);
return r;
}
private String nlIfMl(CharSequence cs) {
String s = cs.toString();
if (s.indexOf('\n') != -1 && multiLineValuesOnSeparateLines)
return "\n" + s;
return s;
}
@SuppressWarnings({ "unchecked" })
private T parse(String s, Parser parser, Type type, Type...args) throws ParseException {
if (isEmpty(s))
return null;
if (isSimpleType(type))
return (T)beanSession.convertToType(s, (Class>)type);
if (type == byte[].class) {
if (s.indexOf('\n') != -1)
s = s.replaceAll("\n", "");
try {
switch (binaryFormat) {
case HEX: return (T)fromHex(s);
case SPACED_HEX: return (T)fromSpacedHex(s);
default: return (T)base64Decode(s);
}
} catch (Exception e) {
throw new ParseException(e, "Value could not be converted to a byte array.");
}
}
if (parser == null)
parser = this.parser;
if (parser instanceof JsonParser) {
char s1 = firstNonWhitespaceChar(s);
if (isArray(type) && s1 != '[')
s = '[' + s + ']';
else if (s1 != '[' && s1 != '{' && ! "null".equals(s))
s = '\'' + s + '\'';
}
return parser.parse(s, type, args);
}
private 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());
}
private boolean isArray(Type t) {
if (! (t instanceof Class))
return false;
Class> c = (Class>)t;
return (c.isArray());
}
private String sname(String key) {
assertFieldNotNull(key, "key");
int i = key.indexOf('/');
if (i == -1)
return "";
return key.substring(0, i);
}
private String skey(String key) {
int i = key.indexOf('/');
if (i == -1)
return key;
return key.substring(i+1);
}
private String section(String section) {
assertFieldNotNull(section, "section");
if (isEmpty(section))
return "";
return section;
}
private void checkWrite() {
if (readOnly)
throw new UnsupportedOperationException("Cannot call this method on a read-only configuration.");
}
//-----------------------------------------------------------------------------------------------------------------
// Other methods
//-----------------------------------------------------------------------------------------------------------------
@Override /* Object */
public String toString() {
return configMap.toString();
}
@Override /* Object */
protected void finalize() throws Throwable {
close();
}
}