more work

This commit is contained in:
Havoc Pennington 2011-11-06 15:30:04 -05:00
parent 9ca157d34a
commit 6b54720ddd
7 changed files with 378 additions and 21 deletions

View File

@ -1,13 +1,150 @@
package com.typesafe.config; package com.typesafe.config;
import java.util.concurrent.TimeUnit;
import com.typesafe.config.impl.ConfigFactory; import com.typesafe.config.impl.ConfigFactory;
public class Config { public final class Config {
public static ConfigObject load(ConfigConfig configConfig) { public static ConfigObject load(ConfigConfig configConfig) {
return ConfigFactory.getConfig(configConfig); return ConfigFactory.loadConfig(configConfig);
} }
public static ConfigObject load(String rootPath) { public static ConfigObject load(String rootPath) {
return ConfigFactory.getConfig(new ConfigConfig(rootPath, null)); return ConfigFactory.loadConfig(new ConfigConfig(rootPath, null));
}
private static String getUnits(String s) {
int i = s.length() - 1;
while (i >= 0) {
char c = s.charAt(i);
if (!Character.isLetter(c))
break;
i -= 1;
}
return s.substring(i + 1);
}
/**
* Parses a duration string. If no units are specified in the string, it is
* assumed to be in milliseconds. The returned duration is in nanoseconds.
*
* @param input
* the string to parse
* @param originForException
* origin of the value being parsed
* @param pathForException
* path to include in exceptions
* @return duration in nanoseconds
* @throws ConfigException
* if string is invalid
*/
public static long parseDuration(String input,
ConfigOrigin originForException, String pathForException) {
String s = input.trim();
String unitString = getUnits(s);
String numberString = s.substring(0, s.length() - unitString.length()).trim();
TimeUnit units = null;
// note that this is deliberately case-sensitive
if (unitString == "" || unitString == "ms" || unitString == "milliseconds") {
units = TimeUnit.MILLISECONDS;
} else if (unitString == "us" || unitString == "microseconds") {
units = TimeUnit.MICROSECONDS;
} else if (unitString == "ns" || unitString == "nanoseconds") {
units = TimeUnit.NANOSECONDS;
} else if (unitString == "d" || unitString == "days") {
units = TimeUnit.DAYS;
} else if (unitString == "s" || unitString == "seconds") {
units = TimeUnit.SECONDS;
} else if (unitString == "m" || unitString == "minutes") {
units = TimeUnit.MINUTES;
} else {
throw new ConfigException.BadValue(originForException,
pathForException, "Could not parse time unit '"
+ unitString + "' (try ns, us, ms, s, m, d)");
}
try {
// if the string is purely digits, parse as an integer to avoid possible precision loss;
// otherwise as a double.
if (numberString.matches("[0-9]+")) {
return units.toNanos(Long.parseLong(numberString));
} else {
long nanosInUnit = units.toNanos(1);
return (new Double(Double.parseDouble(numberString) * nanosInUnit)).longValue();
}
} catch (NumberFormatException e) {
throw new ConfigException.BadValue(originForException, pathForException,
"Could not parse duration number '"
+ numberString + "'");
}
}
private static enum MemoryUnit {
BYTES(1), KILOBYTES(1024), MEGABYTES(1024 * 1024), GIGABYTES(
1024 * 1024 * 1024);
int bytes;
MemoryUnit(int bytes) {
this.bytes = bytes;
}
}
/**
* Parses a memory-size string. If no units are specified in the string, it
* is assumed to be in bytes. The returned value is in bytes.
*
* @param input
* the string to parse
* @param originForException
* origin of the value being parsed
* @param pathForException
* path to include in exceptions
* @return size in bytes
* @throws ConfigException
* if string is invalid
*/
public static long parseMemorySize(String input,
ConfigOrigin originForException, String pathForException) {
String s = input.trim();
String unitString = getUnits(s);
String unitStringLower = unitString.toLowerCase();
String numberString = s.substring(0, s.length() - unitString.length())
.trim();
MemoryUnit units = null;
// the short abbreviations are case-insensitive but you can't write the
// long form words in all caps.
if (unitString == "" || unitStringLower == "b"
|| unitString == "bytes") {
units = MemoryUnit.BYTES;
} else if (unitStringLower == "k" || unitString == "kilobytes") {
units = MemoryUnit.KILOBYTES;
} else if (unitStringLower == "m" || unitString == "megabytes") {
units = MemoryUnit.MEGABYTES;
} else if (unitStringLower == "g" || unitString == "gigabytes") {
units = MemoryUnit.GIGABYTES;
} else {
throw new ConfigException.BadValue(originForException,
pathForException, "Could not parse size unit '"
+ unitString + "' (try b, k, m, g)");
}
try {
// if the string is purely digits, parse as an integer to avoid
// possible precision loss;
// otherwise as a double.
if (numberString.matches("[0-9]+")) {
return Long.parseLong(numberString) * units.bytes;
} else {
return (new Double(Double.parseDouble(numberString)
* units.bytes)).longValue();
}
} catch (NumberFormatException e) {
throw new ConfigException.BadValue(originForException,
pathForException, "Could not parse memory size number '"
+ numberString
+ "'");
}
} }
} }

View File

@ -3,7 +3,7 @@ package com.typesafe.config;
/** /**
* Configuration for a configuration! * Configuration for a configuration!
*/ */
public class ConfigConfig { public final class ConfigConfig {
private String rootPath; private String rootPath;
private ConfigTransformer extraTransformer; private ConfigTransformer extraTransformer;

View File

@ -81,6 +81,27 @@ public class ConfigException extends RuntimeException {
} }
} }
public static class BadValue extends ConfigException {
private static final long serialVersionUID = 1L;
public BadValue(ConfigOrigin origin, String path, String message,
Throwable cause) {
super(origin, "Invalid value at '" + path + "': " + message, cause);
}
public BadValue(ConfigOrigin origin, String path, String message) {
this(origin, path, message, null);
}
public BadValue(String path, String message, Throwable cause) {
super("Invalid value at '" + path + "': " + message, cause);
}
public BadValue(String path, String message) {
this(path, message, null);
}
}
public static class BadPath extends ConfigException { public static class BadPath extends ConfigException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;

View File

@ -30,6 +30,23 @@ public interface ConfigObject extends ConfigValue {
ConfigValue get(String path); ConfigValue get(String path);
/** Get value as a size in bytes (parses special strings like "128M") */
Long getMemorySize(String path);
/**
* Get value as a duration in milliseconds. If the value is already a
* number, then it's left alone; if it's a string, it's parsed understanding
* units suffixes like "10m" or "5ns"
*/
Long getMilliseconds(String path);
/**
* Get value as a duration in nanoseconds. If the value is already a number
* it's taken as milliseconds. If it's a string, it's parsed understanding
* unit suffixes.
*/
Long getNanoseconds(String path);
List<ConfigValue> getList(String path); List<ConfigValue> getList(String path);
List<Boolean> getBooleanList(String path); List<Boolean> getBooleanList(String path);
@ -46,6 +63,12 @@ public interface ConfigObject extends ConfigValue {
List<Object> getAnyList(String path); List<Object> getAnyList(String path);
List<Long> getMemorySizeList(String path);
List<Long> getMillisecondsList(String path);
List<Long> getNanosecondsList(String path);
boolean containsKey(String key); boolean containsKey(String key);
Set<String> keySet(); Set<String> keySet();

View File

@ -1,7 +1,13 @@
package com.typesafe.config.impl; package com.typesafe.config.impl;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigException; import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigObject; import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigOrigin;
@ -19,6 +25,13 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
this.transformer = transformer; this.transformer = transformer;
} }
/**
* This looks up the key with no transformation or type conversion of any
* kind, and returns null if the key is not present.
*
* @param key
* @return the unmodified raw value or null
*/
protected abstract ConfigValue peek(String key); protected abstract ConfigValue peek(String key);
@Override @Override
@ -26,6 +39,18 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
return ConfigValueType.OBJECT; return ConfigValueType.OBJECT;
} }
static AbstractConfigObject transformed(AbstractConfigObject obj,
ConfigTransformer transformer) {
if (obj.transformer != transformer)
return new TransformedConfigObject(transformer, obj);
else
return obj;
}
private AbstractConfigObject transformed(AbstractConfigObject obj) {
return transformed(obj, transformer);
}
static private ConfigValue resolve(AbstractConfigObject self, String path, static private ConfigValue resolve(AbstractConfigObject self, String path,
ConfigValueType expected, ConfigTransformer transformer, ConfigValueType expected, ConfigTransformer transformer,
String originalPath) { String originalPath) {
@ -59,6 +84,58 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
return resolve(this, path, expected, transformer, originalPath); return resolve(this, path, expected, transformer, originalPath);
} }
/**
* Stack should be from overrides to fallbacks (earlier items win). Test
* suite should check: merging of objects with a non-object in the middle.
* Override of object with non-object, override of non-object with object.
* Merging 0, 1, N objects.
*/
static AbstractConfigObject merge(ConfigOrigin origin,
List<AbstractConfigObject> stack,
ConfigTransformer transformer) {
if (stack.isEmpty()) {
return new SimpleConfigObject(origin, transformer,
Collections.<String, ConfigValue> emptyMap());
} else if (stack.size() == 1) {
return transformed(stack.get(0), transformer);
} else {
// for non-objects, we just take the first value; but for objects we
// have to do work to combine them.
Map<String, ConfigValue> merged = new HashMap<String, ConfigValue>();
Map<String, List<AbstractConfigObject>> objects = new HashMap<String, List<AbstractConfigObject>>();
for (AbstractConfigObject obj : stack) {
for (String key : obj.keySet()) {
ConfigValue v = obj.peek(key);
if (!merged.containsKey(key)) {
if (v.valueType() == ConfigValueType.OBJECT) {
// requires recursive merge and transformer fixup
List<AbstractConfigObject> stackForKey = null;
if (objects.containsKey(key)) {
stackForKey = objects.get(key);
} else {
stackForKey = new ArrayList<AbstractConfigObject>();
}
stackForKey.add(transformed(
(AbstractConfigObject) v,
transformer));
} else {
if (!objects.containsKey(key)) {
merged.put(key, v);
}
}
}
}
}
for (String key : objects.keySet()) {
List<AbstractConfigObject> stackForKey = objects.get(key);
AbstractConfigObject obj = merge(origin, stackForKey, transformer);
merged.put(key, obj);
}
return new SimpleConfigObject(origin, transformer, merged);
}
}
@Override @Override
public ConfigValue get(String path) { public ConfigValue get(String path) {
return resolve(path, null, path); return resolve(path, null, path);
@ -105,8 +182,9 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
@Override @Override
public AbstractConfigObject getObject(String path) { public AbstractConfigObject getObject(String path) {
ConfigValue v = resolve(path, ConfigValueType.OBJECT, path); AbstractConfigObject obj = (AbstractConfigObject) resolve(path,
return (AbstractConfigObject) v; ConfigValueType.OBJECT, path);
return transformed(obj);
} }
@Override @Override
@ -115,6 +193,36 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
return v.unwrapped(); return v.unwrapped();
} }
@Override
public Long getMemorySize(String path) {
Long size = null;
try {
size = getLong(path);
} catch (ConfigException.WrongType e) {
ConfigValue v = resolve(path, ConfigValueType.STRING, path);
size = Config.parseMemorySize((String) v.unwrapped(), v.origin(),
path);
}
return size;
}
@Override
public Long getMilliseconds(String path) {
return TimeUnit.NANOSECONDS.toMillis(getNanoseconds(path));
}
@Override
public Long getNanoseconds(String path) {
Long ns = null;
try {
ns = getLong(path);
} catch (ConfigException.WrongType e) {
ConfigValue v = resolve(path, ConfigValueType.STRING, path);
ns = Config.parseDuration((String) v.unwrapped(), v.origin(), path);
}
return ns;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> List<T> getHomogeneousUnwrappedList(String path, private <T> List<T> getHomogeneousUnwrappedList(String path,
ConfigValueType expected) { ConfigValueType expected) {
@ -177,7 +285,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
if (v.valueType() != ConfigValueType.OBJECT) if (v.valueType() != ConfigValueType.OBJECT)
throw new ConfigException.WrongType(v.origin(), path, throw new ConfigException.WrongType(v.origin(), path,
ConfigValueType.OBJECT.name(), v.valueType().name()); ConfigValueType.OBJECT.name(), v.valueType().name());
l.add((ConfigObject) v); l.add(transformed((AbstractConfigObject) v));
} }
return l; return l;
} }
@ -191,4 +299,55 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
} }
return l; return l;
} }
@Override
public List<Long> getMemorySizeList(String path) {
List<Long> l = new ArrayList<Long>();
List<ConfigValue> list = getList(path);
for (ConfigValue v : list) {
if (v.valueType() == ConfigValueType.NUMBER) {
l.add(((Number) v.unwrapped()).longValue());
} else if (v.valueType() == ConfigValueType.STRING) {
String s = (String) v.unwrapped();
Long n = Config.parseMemorySize(s, v.origin(), path);
l.add(n);
} else {
throw new ConfigException.WrongType(v.origin(), path,
"memory size string or number of bytes", v.valueType()
.name());
}
}
return l;
}
@Override
public List<Long> getMillisecondsList(String path) {
List<Long> nanos = getNanosecondsList(path);
List<Long> l = new ArrayList<Long>();
for (Long n : nanos) {
l.add(TimeUnit.NANOSECONDS.toMillis(n));
}
return l;
}
@Override
public List<Long> getNanosecondsList(String path) {
List<Long> l = new ArrayList<Long>();
List<ConfigValue> list = getList(path);
for (ConfigValue v : list) {
if (v.valueType() == ConfigValueType.NUMBER) {
l.add(((Number) v.unwrapped()).longValue());
} else if (v.valueType() == ConfigValueType.STRING) {
String s = (String) v.unwrapped();
Long n = Config.parseDuration(s, v.origin(), path);
l.add(n);
} else {
throw new ConfigException.WrongType(v.origin(), path,
"duration string or number of nanoseconds", v
.valueType().name());
}
}
return l;
}
} }

View File

@ -15,7 +15,7 @@ import com.typesafe.config.ConfigValue;
/** This is public but is only supposed to be used by the "config" package */ /** This is public but is only supposed to be used by the "config" package */
public class ConfigFactory { public class ConfigFactory {
public static ConfigObject getConfig(ConfigConfig configConfig) { public static ConfigObject loadConfig(ConfigConfig configConfig) {
AbstractConfigObject system = null; AbstractConfigObject system = null;
try { try {
system = systemPropertiesConfig() system = systemPropertiesConfig()
@ -29,23 +29,36 @@ public class ConfigFactory {
if (system != null) if (system != null)
stack.add(system); stack.add(system);
List<ConfigTransformer> transformerStack = new ArrayList<ConfigTransformer>(); ConfigTransformer transformer = withExtraTransformer(configConfig
transformerStack.add(defaultConfigTransformer()); .extraTransformer());
ConfigTransformer extraTransformer = configConfig.extraTransformer();
if (extraTransformer != null)
transformerStack.add(extraTransformer);
ConfigTransformer transformer = new StackTransformer(transformerStack);
StackConfigObject stackConfig = new StackConfigObject( AbstractConfigObject merged = AbstractConfigObject
new SimpleConfigOrigin("config for " + configConfig.rootPath()), .merge(new SimpleConfigOrigin("config for "
transformer, + configConfig.rootPath()), stack, transformer);
stack);
return stackConfig; return merged;
} }
public static ConfigObject getEnvironmentAsConfig() { public static ConfigObject getEnvironmentAsConfig(
return envVariablesConfig(); ConfigTransformer extraTransformer) {
// This should not need to create a new config object
// as long as the transformer is just the default transformer.
return AbstractConfigObject.transformed(envVariablesConfig(),
withExtraTransformer(extraTransformer));
}
private static ConfigTransformer withExtraTransformer(
ConfigTransformer extraTransformer) {
// idea is to avoid creating a new, unique transformer if there's no
// extraTransformer
if (extraTransformer != null) {
List<ConfigTransformer> transformerStack = new ArrayList<ConfigTransformer>();
transformerStack.add(defaultConfigTransformer());
transformerStack.add(extraTransformer);
return new StackTransformer(transformerStack);
} else {
return defaultConfigTransformer();
}
} }
private static ConfigTransformer defaultTransformer = null; private static ConfigTransformer defaultTransformer = null;

View File

@ -11,6 +11,10 @@ import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigTransformer; import com.typesafe.config.ConfigTransformer;
import com.typesafe.config.ConfigValue; import com.typesafe.config.ConfigValue;
/**
* This is unused for now, decided that it was too annoying to "lazy merge" and
* better to do the full merge up-front.
*/
final class StackConfigObject extends AbstractConfigObject { final class StackConfigObject extends AbstractConfigObject {
private List<AbstractConfigObject> stack; private List<AbstractConfigObject> stack;