Add Config.fromPathMap() which takes map with paths

Share implementation of this with the Properties parser;
it makes the Properties parser somewhat less efficient due
to creating the intermediate Path objects, but should not matter.
This commit is contained in:
Havoc Pennington 2011-11-17 12:09:12 -05:00
parent 88e81d7c67
commit 7d5f72e248
6 changed files with 214 additions and 52 deletions
src
main/java/com/typesafe/config
test/scala/com/typesafe/config/impl

View File

@ -228,6 +228,9 @@ public final class Config {
* wrapped in ConfigValue. To get nested ConfigObject, some of the values in
* the map would have to be more maps.
*
* There is a separate fromPathMap() that interprets the keys in the map as
* path expressions.
*
* @param values
* @param originDescription
* @return
@ -237,6 +240,27 @@ public final class Config {
return (ConfigObject) fromAnyRef(values, originDescription);
}
/**
* Similar to fromMap(), but the keys in the map are path expressions,
* rather than keys. This is more convenient if you are writing literal maps
* in code, and less convenient if you are getting your maps from some data
* source such as a parser.
*
* An exception will be thrown (and it is a bug in the caller of the method)
* if a path is both an object and a value, for example if you had both
* "a=foo" and "a.b=bar", then "a" is both the string "foo" and the parent
* object of "b". The caller of this method should ensure that doesn't
* happen.
*
* @param values
* @param originDescription
* @return
*/
public static ConfigObject fromPathMap(
Map<String, ? extends Object> values, String originDescription) {
return ConfigImpl.fromPathMap(values, originDescription);
}
/**
* See the fromAnyRef() documentation for details. This is a typesafe
* wrapper that only works on Iterable and returns ConfigList rather than
@ -273,6 +297,17 @@ public final class Config {
return fromMap(values, null);
}
/**
* See the other overload of fromPathMap() for details, this one just uses a
* default origin description.
*
* @param values
* @return
*/
public static ConfigObject fromPathMap(Map<String, ? extends Object> values) {
return fromPathMap(values, null);
}
/**
* See the other overload of fromIterable() for details, this one just uses
* a default origin description.

View File

@ -175,10 +175,19 @@ public class ConfigImpl {
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
public static ConfigValue fromAnyRef(Object object, String originDescription) {
ConfigOrigin origin = valueOrigin(originDescription);
return fromAnyRef(object, origin);
return fromAnyRef(object, origin, FromMapMode.KEYS_ARE_KEYS);
}
static AbstractConfigValue fromAnyRef(Object object, ConfigOrigin origin) {
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
public static ConfigObject fromPathMap(
Map<String, ? extends Object> pathMap, String originDescription) {
ConfigOrigin origin = valueOrigin(originDescription);
return (ConfigObject) fromAnyRef(pathMap, origin,
FromMapMode.KEYS_ARE_PATHS);
}
static AbstractConfigValue fromAnyRef(Object object, ConfigOrigin origin,
FromMapMode mapMode) {
if (origin == null)
throw new ConfigException.BugOrBroken(
"origin not supposed to be null");
@ -218,18 +227,23 @@ public class ConfigImpl {
if (((Map<?, ?>) object).isEmpty())
return emptyObject(origin);
Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
Object key = entry.getKey();
if (!(key instanceof String))
throw new ConfigException.BugOrBroken(
"bug in method caller: not valid to create ConfigObject from map with non-String key: "
+ key);
AbstractConfigValue value = fromAnyRef(entry.getValue(), origin);
values.put((String) key, value);
}
if (mapMode == FromMapMode.KEYS_ARE_KEYS) {
Map<String, AbstractConfigValue> values = new HashMap<String, AbstractConfigValue>();
for (Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
Object key = entry.getKey();
if (!(key instanceof String))
throw new ConfigException.BugOrBroken(
"bug in method caller: not valid to create ConfigObject from map with non-String key: "
+ key);
AbstractConfigValue value = fromAnyRef(entry.getValue(),
origin, mapMode);
values.put((String) key, value);
}
return new SimpleConfigObject(origin, values);
return new SimpleConfigObject(origin, values);
} else {
return PropertiesParser.fromPathMap(origin, (Map<?, ?>) object);
}
} else if (object instanceof Iterable) {
Iterator<?> i = ((Iterable<?>) object).iterator();
if (!i.hasNext())
@ -237,7 +251,7 @@ public class ConfigImpl {
List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>();
while (i.hasNext()) {
AbstractConfigValue v = fromAnyRef(i.next(), origin);
AbstractConfigValue v = fromAnyRef(i.next(), origin, mapMode);
values.add(v);
}

View File

@ -0,0 +1,5 @@
package com.typesafe.config.impl;
enum FromMapMode {
KEYS_ARE_PATHS, KEYS_ARE_KEYS
}

View File

@ -5,7 +5,6 @@ import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -13,6 +12,7 @@ import java.util.Map;
import java.util.Properties;
import java.util.Set;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
final class PropertiesParser {
@ -39,69 +39,127 @@ final class PropertiesParser {
return path.substring(0, i);
}
static Path pathFromPropertyKey(String key) {
String last = lastElement(key);
String exceptLast = exceptLastElement(key);
Path path = new Path(last, null);
while (exceptLast != null) {
last = lastElement(exceptLast);
exceptLast = exceptLastElement(exceptLast);
path = new Path(last, path);
}
return path;
}
static AbstractConfigObject fromProperties(ConfigOrigin origin,
Properties props) {
/*
* First, build a list of paths that will have values, either strings or
* objects.
*/
Set<String> scopePaths = new HashSet<String>();
Set<String> valuePaths = new HashSet<String>();
Enumeration<?> i = props.propertyNames();
while (i.hasMoreElements()) {
Object o = i.nextElement();
if (o instanceof String) {
// add value's path
String path = (String) o;
valuePaths.add(path);
Map<Path, Object> pathMap = new HashMap<Path, Object>();
for (Map.Entry<Object, Object> entry : props.entrySet()) {
Object key = entry.getKey();
if (key instanceof String) {
Path path = pathFromPropertyKey((String) key);
pathMap.put(path, entry.getValue());
}
}
return fromPathMap(origin, pathMap, true /* from properties */);
}
// all parent paths are objects
String next = exceptLastElement(path);
while (next != null) {
scopePaths.add(next);
next = exceptLastElement(next);
static AbstractConfigObject fromPathMap(ConfigOrigin origin,
Map<?, ?> pathExpressionMap) {
Map<Path, Object> pathMap = new HashMap<Path, Object>();
for (Map.Entry<?, ?> entry : pathExpressionMap.entrySet()) {
Object keyObj = entry.getKey();
if (!(keyObj instanceof String)) {
throw new ConfigException.BugOrBroken(
"Map has a non-string as a key, expecting a path expression as a String");
}
Path path = Path.newPath((String) keyObj);
pathMap.put(path, entry.getValue());
}
return fromPathMap(origin, pathMap, false /* from properties */);
}
private static AbstractConfigObject fromPathMap(ConfigOrigin origin,
Map<Path, Object> pathMap, boolean convertedFromProperties) {
/*
* First, build a list of paths that will have values, either string or
* object values.
*/
Set<Path> scopePaths = new HashSet<Path>();
Set<Path> valuePaths = new HashSet<Path>();
for (Path path : pathMap.keySet()) {
// add value's path
valuePaths.add(path);
// all parent paths are objects
Path next = path.parent();
while (next != null) {
scopePaths.add(next);
next = next.parent();
}
}
if (convertedFromProperties) {
/*
* If any string values are also objects containing other values,
* drop those string values - objects "win".
*/
valuePaths.removeAll(scopePaths);
} else {
/* If we didn't start out as properties, then this is an error. */
for (Path path : valuePaths) {
if (scopePaths.contains(path)) {
throw new ConfigException.BugOrBroken(
"In the map, path '"
+ path.render()
+ "' occurs as both the parent object of a value and as a value. "
+ "Because Map has no defined ordering, this is a broken situation.");
}
}
}
/*
* If any string values are also objects containing other values, drop
* those string values - objects "win".
*/
valuePaths.removeAll(scopePaths);
/*
* Create maps for the object-valued values.
*/
Map<String, AbstractConfigValue> root = new HashMap<String, AbstractConfigValue>();
Map<String, Map<String, AbstractConfigValue>> scopes = new HashMap<String, Map<String, AbstractConfigValue>>();
Map<Path, Map<String, AbstractConfigValue>> scopes = new HashMap<Path, Map<String, AbstractConfigValue>>();
for (String path : scopePaths) {
for (Path path : scopePaths) {
Map<String, AbstractConfigValue> scope = new HashMap<String, AbstractConfigValue>();
scopes.put(path, scope);
}
/* Store string values in the associated scope maps */
for (String path : valuePaths) {
String parentPath = exceptLastElement(path);
for (Path path : valuePaths) {
Path parentPath = path.parent();
Map<String, AbstractConfigValue> parent = parentPath != null ? scopes
.get(parentPath) : root;
String last = lastElement(path);
String value = props.getProperty(path);
parent.put(last, new ConfigString(origin, value));
String last = path.last();
Object rawValue = pathMap.get(path);
AbstractConfigValue value;
if (convertedFromProperties) {
value = new ConfigString(origin, (String) rawValue);
} else {
value = ConfigImpl.fromAnyRef(pathMap.get(path), origin,
FromMapMode.KEYS_ARE_PATHS);
}
parent.put(last, value);
}
/*
* Make a list of scope paths from longest to shortest, so children go
* before parents.
*/
List<String> sortedScopePaths = new ArrayList<String>();
List<Path> sortedScopePaths = new ArrayList<Path>();
sortedScopePaths.addAll(scopePaths);
// sort descending by length
Collections.sort(sortedScopePaths, new Comparator<String>() {
Collections.sort(sortedScopePaths, new Comparator<Path>() {
@Override
public int compare(String a, String b) {
public int compare(Path a, Path b) {
// Path.length() is O(n) so in theory this sucks
// but in practice we can make Path precompute length
// if it ever matters.
return b.length() - a.length();
}
});
@ -111,16 +169,16 @@ final class PropertiesParser {
* parents to avoid modifying any already-created ConfigObject. This is
* where we need the sorted list.
*/
for (String scopePath : sortedScopePaths) {
for (Path scopePath : sortedScopePaths) {
Map<String, AbstractConfigValue> scope = scopes.get(scopePath);
String parentPath = exceptLastElement(scopePath);
Path parentPath = scopePath.parent();
Map<String, AbstractConfigValue> parent = parentPath != null ? scopes
.get(parentPath) : root;
AbstractConfigObject o = new SimpleConfigObject(origin, scope,
ResolveStatus.RESOLVED);
parent.put(lastElement(scopePath), o);
parent.put(scopePath.last(), o);
}
// return root config object

View File

@ -34,6 +34,15 @@ class PropertiesTest extends TestUtils {
assertEquals("..", exceptLast("..."))
}
@Test
def pathObjectCreating() {
def p(key: String) = PropertiesParser.pathFromPropertyKey(key)
assertEquals(path("a"), p("a"))
assertEquals(path("a", "b"), p("a.b"))
assertEquals(path(""), p(""))
}
@Test
def funkyPathsInProperties() {
def testPath(propsPath: String, confPath: String) {

View File

@ -176,6 +176,47 @@ class PublicApiTest extends TestUtils {
assertEquals(reunwrapped, unwrapped)
}
private def testFromPathMap(expectedValue: ConfigObject, createFrom: java.util.Map[String, Object]) {
assertEquals(expectedValue, Config.fromPathMap(createFrom))
assertEquals(defaultValueDesc, Config.fromPathMap(createFrom).origin().description())
assertEquals(expectedValue, Config.fromPathMap(createFrom, "foo"))
assertEquals("foo", Config.fromPathMap(createFrom, "foo").origin().description())
}
@Test
def fromJavaPathMap() {
// first the same tests as with fromMap
val emptyMapValue = Collections.emptyMap[String, AbstractConfigValue]
val aMapValue = Map("a" -> 1, "b" -> 2, "c" -> 3).mapValues(intValue(_): AbstractConfigValue).asJava
testFromPathMap(new SimpleConfigObject(fakeOrigin(), emptyMapValue),
Collections.emptyMap[String, Object])
testFromPathMap(new SimpleConfigObject(fakeOrigin(), aMapValue),
Map("a" -> 1, "b" -> 2, "c" -> 3).asInstanceOf[Map[String, AnyRef]].asJava)
assertEquals("hardcoded value", Config.fromPathMap(Map("a" -> 1, "b" -> 2, "c" -> 3).asJava).origin().description())
assertEquals("foo", Config.fromPathMap(Map("a" -> 1, "b" -> 2, "c" -> 3).asJava, "foo").origin().description())
// now some tests with paths; be sure to test nested path maps
val simplePathMapValue = Map("x.y" -> 4, "z" -> 5).asInstanceOf[Map[String, AnyRef]].asJava
val pathMapValue = Map("a.c" -> 1, "b" -> simplePathMapValue).asInstanceOf[Map[String, AnyRef]].asJava
val obj = Config.fromPathMap(pathMapValue)
assertEquals(2, obj.size)
assertEquals(4, obj.getInt("b.x.y"))
assertEquals(5, obj.getInt("b.z"))
assertEquals(1, obj.getInt("a.c"))
}
@Test
def brokenPathMap() {
// "a" is both number 1 and an object
val pathMapValue = Map("a" -> 1, "a.b" -> 2).asInstanceOf[Map[String, AnyRef]].asJava
intercept[ConfigException.BugOrBroken] {
Config.fromPathMap(pathMapValue)
}
}
private def resource(filename: String) = {
val resourceDir = new File("src/test/resources")
if (!resourceDir.exists())