From 6991862deff9f5b0d6844c10a45105cd2b40de63 Mon Sep 17 00:00:00 2001 From: Havoc Pennington Date: Fri, 9 Dec 2011 10:20:02 -0500 Subject: [PATCH] add ConfigUtil.joinPath, .splitPath, .quoteString To help people get their escaping and quoting right. --- .../main/java/com/typesafe/config/Config.java | 6 +- .../com/typesafe/config/ConfigObject.java | 18 +++-- .../java/com/typesafe/config/ConfigUtil.java | 70 +++++++++++++++++++ .../typesafe/config/impl/ConfigImplUtil.java | 38 +++++++++- .../typesafe/config/impl/PublicApiTest.scala | 31 ++++++++ 5 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 config/src/main/java/com/typesafe/config/ConfigUtil.java diff --git a/config/src/main/java/com/typesafe/config/Config.java b/config/src/main/java/com/typesafe/config/Config.java index 1f5dc427..44eebe11 100644 --- a/config/src/main/java/com/typesafe/config/Config.java +++ b/config/src/main/java/com/typesafe/config/Config.java @@ -34,6 +34,10 @@ import java.util.Set; * {@code ConfigObject} is a tree of nested maps from keys to values. * *

+ * Use {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath} to convert + * between path expressions and individual path elements (keys). + * + *

* Another difference between {@code Config} and {@code ConfigObject} is that * conceptually, {@code ConfigValue}s with a {@link ConfigValue#valueType() * valueType()} of {@link ConfigValueType#NULL NULL} exist in a @@ -264,7 +268,7 @@ public interface Config extends ConfigMergeable { * recursing {@link #root() the root object}. Note that this is very * different from root().entrySet() which returns the set of * immediate-child keys in the root object and includes null values. - * + * * @return set of paths with non-null values, built up by recursing the * entire tree of {@link ConfigObject} */ diff --git a/config/src/main/java/com/typesafe/config/ConfigObject.java b/config/src/main/java/com/typesafe/config/ConfigObject.java index 86138402..54cce1c3 100644 --- a/config/src/main/java/com/typesafe/config/ConfigObject.java +++ b/config/src/main/java/com/typesafe/config/ConfigObject.java @@ -8,34 +8,38 @@ import java.util.Map; /** * Subtype of {@link ConfigValue} representing an object (dictionary, map) * value, as in JSON's { "a" : 42 } syntax. - * + * *

* {@code ConfigObject} implements {@code java.util.Map} so * you can use it like a regular Java map. Or call {@link #unwrapped()} to * unwrap the map to a map with plain Java values rather than * {@code ConfigValue}. - * + * *

* Like all {@link ConfigValue} subtypes, {@code ConfigObject} is immutable. * This makes it threadsafe and you never have to create "defensive copies." The * mutator methods from {@link java.util.Map} all throw * {@link java.lang.UnsupportedOperationException}. - * + * *

* The {@link ConfigValue#valueType} method on an object returns * {@link ConfigValueType#OBJECT}. - * + * *

* In most cases you want to use the {@link Config} interface rather than this * one. Call {@link #toConfig()} to convert a {@code ConfigObject} to a * {@code Config}. - * + * *

* The API for a {@code ConfigObject} is in terms of keys, while the API for a * {@link Config} is in terms of path expressions. Conceptually, * {@code ConfigObject} is a tree of maps from keys to values, while a * {@code ConfigObject} is a one-level map from paths to values. - * + * + *

+ * Use {@link ConfigUtil#joinPath} and {@link ConfigUtil#splitPath} to convert + * between path expressions and individual path elements (keys). + * *

* A {@code ConfigObject} may contain null values, which will have * {@link ConfigValue#valueType()} equal to {@link ConfigValueType#NULL}. If @@ -43,7 +47,7 @@ import java.util.Map; * file (or wherever this value tree came from). If {@code get()} returns a * {@link ConfigValue} with type {@code ConfigValueType#NULL} then the key was * set to null explicitly in the config file. - * + * *

* Do not implement {@code ConfigObject}; it should only be implemented * by the config library. Arbitrary implementations will not work because the diff --git a/config/src/main/java/com/typesafe/config/ConfigUtil.java b/config/src/main/java/com/typesafe/config/ConfigUtil.java new file mode 100644 index 00000000..1aa463f4 --- /dev/null +++ b/config/src/main/java/com/typesafe/config/ConfigUtil.java @@ -0,0 +1,70 @@ +package com.typesafe.config; + +import java.util.List; + +import com.typesafe.config.impl.ConfigImplUtil; + +public final class ConfigUtil { + private ConfigUtil() { + + } + + /** + * Quotes and escapes a string, as in the JSON specification. + * + * @param s + * a string + * @return the string quoted and escaped + */ + public static String quoteString(String s) { + return ConfigImplUtil.renderJsonString(s); + } + + /** + * Converts a list of keys to a path expression, by quoting the path + * elements as needed and then joining them separated by a period. A path + * expression is usable with a {@link Config}, while individual path + * elements are usable with a {@link ConfigObject}. + * + * @param elements + * the keys in the path + * @return a path expression + * @throws ConfigException + * if there are no elements + */ + public static String joinPath(String... elements) { + return ConfigImplUtil.joinPath(elements); + } + + /** + * Converts a list of strings to a path expression, by quoting the path + * elements as needed and then joining them separated by a period. A path + * expression is usable with a {@link Config}, while individual path + * elements are usable with a {@link ConfigObject}. + * + * @param elements + * the keys in the path + * @return a path expression + * @throws ConfigException + * if the list is empty + */ + public static String joinPath(List elements) { + return ConfigImplUtil.joinPath(elements); + } + + /** + * Converts a path expression into a list of keys, by splitting on period + * and unquoting the individual path elements. A path expression is usable + * with a {@link Config}, while individual path elements are usable with a + * {@link ConfigObject}. + * + * @param path + * a path expression + * @return the individual keys in the path + * @throws ConfigException + * if the path expression is invalid + */ + public static List splitPath(String path) { + return ConfigImplUtil.splitPath(path); + } +} diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java b/config/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java index 11b17d02..cbc0ecca 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigImplUtil.java @@ -6,6 +6,8 @@ package com.typesafe.config.impl; import java.io.File; import java.net.URISyntaxException; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import com.typesafe.config.ConfigException; @@ -23,7 +25,11 @@ final public class ConfigImplUtil { return a.equals(b); } - static String renderJsonString(String s) { + /** + * This is public ONLY for use by the "config" package, DO NOT USE this ABI + * may change. + */ + public static String renderJsonString(String s) { StringBuilder sb = new StringBuilder(); sb.append('"'); for (int i = 0; i < s.length(); ++i) { @@ -146,4 +152,34 @@ final public class ConfigImplUtil { return new File(url.getPath()); } } + + /** + * This is public ONLY for use by the "config" package, DO NOT USE this ABI + * may change. You can use the version in ConfigUtil instead. + */ + public static String joinPath(String... elements) { + return (new Path(elements)).render(); + } + + /** + * This is public ONLY for use by the "config" package, DO NOT USE this ABI + * may change. You can use the version in ConfigUtil instead. + */ + public static String joinPath(List elements) { + return joinPath(elements.toArray(new String[0])); + } + + /** + * This is public ONLY for use by the "config" package, DO NOT USE this ABI + * may change. You can use the version in ConfigUtil instead. + */ + public static List splitPath(String path) { + Path p = Path.newPath(path); + List elements = new ArrayList(); + while (p != null) { + elements.add(p.first()); + p = p.remainder(); + } + return elements; + } } diff --git a/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala b/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala index 11727724..274fb264 100644 --- a/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/PublicApiTest.scala @@ -433,4 +433,35 @@ class PublicApiTest extends TestUtils { assertFalse("same urls in " + v1.origin + " " + v2.origin, v1.origin.url == v2.origin.url) assertFalse(v1.origin.filename == v2.origin.filename) } + + @Test + def splitAndJoinPath() { + // the actual join-path logic should be tested OK in the non-public-API tests, + // this is just to test the public wrappers. + + assertEquals("\"\".a.b.\"$\"", ConfigUtil.joinPath("", "a", "b", "$")) + assertEquals("\"\".a.b.\"$\"", ConfigUtil.joinPath(Seq("", "a", "b", "$").asJava)) + assertEquals(Seq("", "a", "b", "$"), ConfigUtil.splitPath("\"\".a.b.\"$\"").asScala) + + // invalid stuff throws + intercept[ConfigException] { + ConfigUtil.splitPath("$") + } + intercept[ConfigException] { + ConfigUtil.joinPath() + } + intercept[ConfigException] { + ConfigUtil.joinPath(Collections.emptyList[String]()) + } + } + + @Test + def quoteString() { + // the actual quote logic shoudl be tested OK in the non-public-API tests, + // this is just to test the public wrapper. + + assertEquals("\"\"", ConfigUtil.quoteString("")) + assertEquals("\"a\"", ConfigUtil.quoteString("a")) + assertEquals("\"\\n\"", ConfigUtil.quoteString("\n")) + } }