diff --git a/HOCON.md b/HOCON.md
index 472d0ed7..f6f36370 100644
--- a/HOCON.md
+++ b/HOCON.md
@@ -277,8 +277,7 @@ converted to strings as follows (strings shown as quoted strings):
For purposes of value concatenation, it should be rendered
as it was written in the file.
- a substitution is replaced with its value which is then
- converted to a string as above, except that a substitution
- which evaluates to `null` becomes the empty string `""`.
+ converted to a string as above.
- it is invalid for arrays or objects to appear in a value
concatenation.
@@ -398,9 +397,14 @@ implementations may try to resolve them by looking at system
environment variables, Java system properties, or other external
sources of configuration.
-The syntax is `${pathexpression}` where the `pathexpression` is a
-path expression as described above. This path expression has the
-same syntax that you could use for an object key.
+The syntax is `${pathexpression}` or `${?pathexpression}` where
+the `pathexpression` is a path expression as described above. This
+path expression has the same syntax that you could use for an
+object key.
+
+The `?` in `${?pathexpression}` must not have whitespace before
+it; the three characters `${?` must be exactly like that, grouped
+together.
Substitutions are not parsed inside quoted strings. To get a
string containing a substitution, you must use value concatenation
@@ -437,8 +441,21 @@ environment variable. There is no equivalent to JavaScript's
`delete` operation in other words.
If a substitution does not match any value present in the
-configuration and is not resolved by an external source, it is
-evaluated to `null`.
+configuration and is not resolved by an external source, then it
+is undefined. An undefined substitution with the `${foo}` syntax
+is invalid and should generate an error.
+
+If a substitution with the `${?foo}` syntax is undefined:
+
+ - if it is the value of an object field then the field should not
+ be created.
+ - if it is an array element then the element should not be added.
+ - if it is part of a value concatenation then it should become an
+ empty string.
+ - `foo : ${?bar}` would avoid creating field `foo` if `bar` is
+ undefined, but `foo : ${?bar} ${?baz}` would be a value
+ concatenation so if `bar` or `baz` are not defined, the result
+ is an empty string.
Substitutions are only allowed in object field values and array
elements (value concatenations), they are not allowed in keys or
@@ -447,13 +464,7 @@ nested inside other substitutions (path expressions).
A substitution is replaced with any value type (number, object,
string, array, true, false, null). If the substitution is the only
part of a value, then the type is preserved. Otherwise, it is
-value-concatenated to form a string. There is one special rule:
-
- - `null` is converted to an empty string, not the string `null`.
-
-Because missing substitutions are evaluated to `null`, either
-missing or explicitly-set-to-null substitutions become an empty
-string when concatenated.
+value-concatenated to form a string.
Circular substitutions are invalid and should generate an error.
diff --git a/README.md b/README.md
index 0c7d76e5..85a48483 100644
--- a/README.md
+++ b/README.md
@@ -102,6 +102,8 @@ detail.
environment variables if they don't resolve in the
config itself, so `${HOME}` or `${user.home}` would
work as you expect.
+ - substitutions normally cause an error if unresolved, but
+ there is a syntax `${?a.b}` to permit them to be missing.
### Examples of HOCON
@@ -233,6 +235,10 @@ Here are some features that might be nice to add.
in system properties and the environment, for example).
This could be done using the same syntax as `include`,
potentially. It is not a backward-compatible change though.
+ - substitutions with fallbacks; this could be something like
+ `${foo.bar,baz,null}` where it would look up `foo.bar`, then
+ `baz`, then finally fall back to null. One question is whether
+ entire nested objects would be allowed as fallbacks.
## Rationale
diff --git a/config/src/main/java/com/typesafe/config/Config.java b/config/src/main/java/com/typesafe/config/Config.java
index 50294c9a..336c50d5 100644
--- a/config/src/main/java/com/typesafe/config/Config.java
+++ b/config/src/main/java/com/typesafe/config/Config.java
@@ -98,19 +98,19 @@ public interface Config extends ConfigMergeable {
* Config as the root object, that is, a substitution
* ${foo.bar} will be replaced with the result of
* getValue("foo.bar").
- *
+ *
*
* This method uses {@link ConfigResolveOptions#defaults()}, there is
* another variant {@link Config#resolve(ConfigResolveOptions)} which lets
* you specify non-default options.
- *
+ *
*
* A given {@link Config} must be resolved before using it to retrieve
* config values, but ideally should be resolved one time for your entire
* stack of fallbacks (see {@link Config#withFallback}). Otherwise, some
* substitutions that could have resolved with all fallbacks available may
* not resolve, which will be a user-visible oddity.
- *
+ *
*
* resolve() should be invoked on root config objects, rather
* than on a subtree (a subtree is the result of something like
@@ -120,15 +120,19 @@ public interface Config extends ConfigMergeable {
* from the root. For example, if you did
* config.getConfig("foo").resolve() on the below config file,
* it would not work:
- *
+ *
*
- *
+ *
* @return an immutable object with substitutions resolved
+ * @throws ConfigException.UnresolvedSubstitution
+ * if any substitutions refer to nonexistent paths
+ * @throws ConfigException
+ * some other config exception if there are other problems
*/
Config resolve();
diff --git a/config/src/main/java/com/typesafe/config/ConfigException.java b/config/src/main/java/com/typesafe/config/ConfigException.java
index f87b6032..a02916d1 100644
--- a/config/src/main/java/com/typesafe/config/ConfigException.java
+++ b/config/src/main/java/com/typesafe/config/ConfigException.java
@@ -36,7 +36,7 @@ public class ConfigException extends RuntimeException {
* for a given exception, or the kind of exception doesn't meaningfully
* relate to a particular origin file, this returns null. Never assume this
* will return non-null, it can always return null.
- *
+ *
* @return origin of the problem, or null if unknown/inapplicable
*/
public ConfigOrigin origin() {
@@ -229,12 +229,29 @@ public class ConfigException extends RuntimeException {
}
}
+ /**
+ * Exception indicating that a substitution did not resolve to anything.
+ * Thrown by {@link Config#resolve}.
+ */
+ public static class UnresolvedSubstitution extends Parse {
+ private static final long serialVersionUID = 1L;
+
+ public UnresolvedSubstitution(ConfigOrigin origin, String expression, Throwable cause) {
+ super(origin, "Could not resolve substitution to a value: " + expression, cause);
+ }
+
+ public UnresolvedSubstitution(ConfigOrigin origin, String expression) {
+ this(origin, expression, null);
+ }
+ }
+
/**
* Exception indicating that you tried to use a function that requires
- * substitutions to be resolved, but substitutions have not been resolved.
- * This is always a bug in either application code or the library; it's
- * wrong to write a handler for this exception because you should be able to
- * fix the code to avoid it.
+ * substitutions to be resolved, but substitutions have not been resolved
+ * (that is, {@link Config#resolve} was not called). This is always a bug in
+ * either application code or the library; it's wrong to write a handler for
+ * this exception because you should be able to fix the code to avoid it by
+ * adding calls to {@link Config#resolve}.
*/
public static class NotResolved extends BugOrBroken {
private static final long serialVersionUID = 1L;
diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java b/config/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java
index 89118076..e6f2bc6b 100644
--- a/config/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java
+++ b/config/src/main/java/com/typesafe/config/impl/ConfigSubstitution.java
@@ -22,8 +22,8 @@ import com.typesafe.config.ConfigValueType;
final class ConfigSubstitution extends AbstractConfigValue implements
Unmergeable {
- // this is a list of String and Path where the Path
- // have to be resolved to values, then if there's more
+ // this is a list of String and SubstitutionExpression where the
+ // SubstitutionExpression has to be resolved to values, then if there's more
// than one piece everything is stringified and concatenated
final private List