From 6b54720ddd19f8b34bfde86f0accbff591ff8b3e Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Sun, 6 Nov 2011 15:30:04 -0500 Subject: [PATCH] more work --- src/com/typesafe/config/Config.java | 143 ++++++++++++++- src/com/typesafe/config/ConfigConfig.java | 2 +- src/com/typesafe/config/ConfigException.java | 21 +++ src/com/typesafe/config/ConfigObject.java | 23 +++ .../config/impl/AbstractConfigObject.java | 165 +++++++++++++++++- .../typesafe/config/impl/ConfigFactory.java | 41 +++-- .../config/impl/StackConfigObject.java | 4 + 7 files changed, 378 insertions(+), 21 deletions(-) diff --git a/src/com/typesafe/config/Config.java b/src/com/typesafe/config/Config.java index 35c8f5a0..5161785c 100644 --- a/src/com/typesafe/config/Config.java +++ b/src/com/typesafe/config/Config.java @@ -1,13 +1,150 @@ package com.typesafe.config; +import java.util.concurrent.TimeUnit; + import com.typesafe.config.impl.ConfigFactory; -public class Config { +public final class Config { public static ConfigObject load(ConfigConfig configConfig) { - return ConfigFactory.getConfig(configConfig); + return ConfigFactory.loadConfig(configConfig); } 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 + + "'"); + } } } diff --git a/src/com/typesafe/config/ConfigConfig.java b/src/com/typesafe/config/ConfigConfig.java index 9633c8bb..a09483b2 100644 --- a/src/com/typesafe/config/ConfigConfig.java +++ b/src/com/typesafe/config/ConfigConfig.java @@ -3,7 +3,7 @@ package com.typesafe.config; /** * Configuration for a configuration! */ -public class ConfigConfig { +public final class ConfigConfig { private String rootPath; private ConfigTransformer extraTransformer; diff --git a/src/com/typesafe/config/ConfigException.java b/src/com/typesafe/config/ConfigException.java index 3402fa39..d9cf3823 100644 --- a/src/com/typesafe/config/ConfigException.java +++ b/src/com/typesafe/config/ConfigException.java @@ -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 { private static final long serialVersionUID = 1L; diff --git a/src/com/typesafe/config/ConfigObject.java b/src/com/typesafe/config/ConfigObject.java index 55a031cf..dd2fc047 100644 --- a/src/com/typesafe/config/ConfigObject.java +++ b/src/com/typesafe/config/ConfigObject.java @@ -30,6 +30,23 @@ public interface ConfigObject extends ConfigValue { 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 getList(String path); List getBooleanList(String path); @@ -46,6 +63,12 @@ public interface ConfigObject extends ConfigValue { List getAnyList(String path); + List getMemorySizeList(String path); + + List getMillisecondsList(String path); + + List getNanosecondsList(String path); + boolean containsKey(String key); Set keySet(); diff --git a/src/com/typesafe/config/impl/AbstractConfigObject.java b/src/com/typesafe/config/impl/AbstractConfigObject.java index 14225387..623bb799 100644 --- a/src/com/typesafe/config/impl/AbstractConfigObject.java +++ b/src/com/typesafe/config/impl/AbstractConfigObject.java @@ -1,7 +1,13 @@ package com.typesafe.config.impl; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; 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.ConfigObject; import com.typesafe.config.ConfigOrigin; @@ -19,6 +25,13 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements 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); @Override @@ -26,6 +39,18 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements 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, ConfigValueType expected, ConfigTransformer transformer, String originalPath) { @@ -59,6 +84,58 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements 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 stack, + ConfigTransformer transformer) { + if (stack.isEmpty()) { + return new SimpleConfigObject(origin, transformer, + Collections. 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 merged = new HashMap(); + Map> objects = new HashMap>(); + 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 stackForKey = null; + if (objects.containsKey(key)) { + stackForKey = objects.get(key); + } else { + stackForKey = new ArrayList(); + } + stackForKey.add(transformed( + (AbstractConfigObject) v, + transformer)); + } else { + if (!objects.containsKey(key)) { + merged.put(key, v); + } + } + } + } + } + for (String key : objects.keySet()) { + List stackForKey = objects.get(key); + AbstractConfigObject obj = merge(origin, stackForKey, transformer); + merged.put(key, obj); + } + + return new SimpleConfigObject(origin, transformer, merged); + } + } + @Override public ConfigValue get(String path) { return resolve(path, null, path); @@ -105,8 +182,9 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements @Override public AbstractConfigObject getObject(String path) { - ConfigValue v = resolve(path, ConfigValueType.OBJECT, path); - return (AbstractConfigObject) v; + AbstractConfigObject obj = (AbstractConfigObject) resolve(path, + ConfigValueType.OBJECT, path); + return transformed(obj); } @Override @@ -115,6 +193,36 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements 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") private List getHomogeneousUnwrappedList(String path, ConfigValueType expected) { @@ -177,7 +285,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements if (v.valueType() != ConfigValueType.OBJECT) throw new ConfigException.WrongType(v.origin(), path, ConfigValueType.OBJECT.name(), v.valueType().name()); - l.add((ConfigObject) v); + l.add(transformed((AbstractConfigObject) v)); } return l; } @@ -191,4 +299,55 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements } return l; } + + @Override + public List getMemorySizeList(String path) { + List l = new ArrayList(); + List 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 getMillisecondsList(String path) { + List nanos = getNanosecondsList(path); + List l = new ArrayList(); + for (Long n : nanos) { + l.add(TimeUnit.NANOSECONDS.toMillis(n)); + } + return l; + } + + @Override + public List getNanosecondsList(String path) { + List l = new ArrayList(); + List 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; + } + } \ No newline at end of file diff --git a/src/com/typesafe/config/impl/ConfigFactory.java b/src/com/typesafe/config/impl/ConfigFactory.java index a804137c..0a33c1ef 100644 --- a/src/com/typesafe/config/impl/ConfigFactory.java +++ b/src/com/typesafe/config/impl/ConfigFactory.java @@ -15,7 +15,7 @@ import com.typesafe.config.ConfigValue; /** This is public but is only supposed to be used by the "config" package */ public class ConfigFactory { - public static ConfigObject getConfig(ConfigConfig configConfig) { + public static ConfigObject loadConfig(ConfigConfig configConfig) { AbstractConfigObject system = null; try { system = systemPropertiesConfig() @@ -29,23 +29,36 @@ public class ConfigFactory { if (system != null) stack.add(system); - List transformerStack = new ArrayList(); - transformerStack.add(defaultConfigTransformer()); - ConfigTransformer extraTransformer = configConfig.extraTransformer(); - if (extraTransformer != null) - transformerStack.add(extraTransformer); - ConfigTransformer transformer = new StackTransformer(transformerStack); + ConfigTransformer transformer = withExtraTransformer(configConfig + .extraTransformer()); - StackConfigObject stackConfig = new StackConfigObject( - new SimpleConfigOrigin("config for " + configConfig.rootPath()), - transformer, - stack); + AbstractConfigObject merged = AbstractConfigObject + .merge(new SimpleConfigOrigin("config for " + + configConfig.rootPath()), stack, transformer); - return stackConfig; + return merged; } - public static ConfigObject getEnvironmentAsConfig() { - return envVariablesConfig(); + public static ConfigObject getEnvironmentAsConfig( + 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 transformerStack = new ArrayList(); + transformerStack.add(defaultConfigTransformer()); + transformerStack.add(extraTransformer); + return new StackTransformer(transformerStack); + } else { + return defaultConfigTransformer(); + } } private static ConfigTransformer defaultTransformer = null; diff --git a/src/com/typesafe/config/impl/StackConfigObject.java b/src/com/typesafe/config/impl/StackConfigObject.java index 7b0d1a91..63ffd243 100644 --- a/src/com/typesafe/config/impl/StackConfigObject.java +++ b/src/com/typesafe/config/impl/StackConfigObject.java @@ -11,6 +11,10 @@ import com.typesafe.config.ConfigOrigin; import com.typesafe.config.ConfigTransformer; 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 { private List stack;