Overhaul the public API.

This patch is a little half-baked (it needs tests, and
there are a few bugs I know about). Fixes will be along.
This commit is contained in:
Havoc Pennington 2011-11-14 02:13:25 -05:00
parent f1bedacdfc
commit 54cd4ec3dd
31 changed files with 979 additions and 292 deletions

View File

@ -1,9 +1,16 @@
package com.typesafe.config;
import java.io.File;
import java.io.Reader;
import java.net.URL;
import java.util.Collection;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import com.typesafe.config.impl.ConfigImpl;
import com.typesafe.config.impl.ConfigUtil;
import com.typesafe.config.impl.Parseable;
/**
* This class holds some global static methods for the config package.
@ -11,23 +18,125 @@ import com.typesafe.config.impl.ConfigUtil;
public final class Config {
/**
* Loads a configuration for the given root path. The root path should be a
* short word that scopes the package being configured; typically it's the
* Loads a configuration for the given root path in a "standard" way.
* Oversimplified, if your root path is foo.bar then this will load files
* from the classpath: foo-bar.conf, foo-bar.json, foo-bar.properties,
* foo-bar-reference.conf, foo-bar-reference.json,
* foo-bar-reference.properties. It will override all those files with any
* system properties that begin with "foo.bar.", as well.
*
* The root path should be a path expression, usually just a single short
* word, that scopes the package being configured; typically it's the
* package name or something similar. System properties overriding values in
* the configuration will have to be prefixed with the root path. The root
* path may have periods in it if you like but other punctuation or
* whitespace will probably cause you headaches. Example root paths: "akka",
* "sbt", "jsoup", "heroku", "mongo", etc.
*
* This object will already be resolved (substitutions have already been
* processed).
* The loaded object will already be resolved (substitutions have already
* been processed). As a result, if you add more fallbacks then they won't
* be seen by substitutions. Substitutions are the "${foo.bar}" syntax. If
* you want to parse additional files or something then you need to use
* loadWithoutResolving().
*
* @param rootPath
* the configuration "domain"
* @return configuration object for the requested root path
*/
public static ConfigRoot load(String rootPath) {
return ConfigImpl.loadConfig(rootPath);
return loadWithoutResolving(rootPath).resolve();
}
/**
* Like load() but does not resolve the object, so you can go ahead and add
* more fallbacks and stuff and have them seen by substitutions when you do
* call {@link ConfigRoot.resolve()}.
*
* @param rootPath
* @return
*/
public static ConfigRoot loadWithoutResolving(String rootPath) {
ConfigRoot system = systemPropertiesRoot(rootPath);
ConfigValue mainFiles = parse(rootPath, ConfigParseOptions.defaults());
ConfigValue referenceFiles = parse(rootPath + ".reference",
ConfigParseOptions.defaults());
return system.withFallbacks(mainFiles, referenceFiles);
}
public static ConfigRoot emptyRoot(String rootPath) {
return ConfigImpl.emptyRoot(rootPath);
}
public static ConfigRoot systemPropertiesRoot(String rootPath) {
return ConfigImpl.systemPropertiesRoot(rootPath);
}
public static ConfigObject systemProperties() {
return ConfigImpl.systemPropertiesAsConfig();
}
public static ConfigObject systemEnvironment() {
return ConfigImpl.envVariablesAsConfig();
}
public static ConfigObject parse(Properties properties,
ConfigParseOptions options) {
return ConfigImpl.parse(properties,
options.withFallbackOriginDescription("properties"));
}
public static ConfigObject parse(Reader reader, ConfigParseOptions options) {
Parseable p = Parseable.newReader(reader);
return ConfigImpl.parse(p,
options.withFallbackOriginDescription("Reader"));
}
public static ConfigObject parse(URL url, ConfigParseOptions options) {
Parseable p = Parseable.newURL(url);
return ConfigImpl.parse(p,
options.withFallbackOriginDescription(url.toExternalForm()));
}
public static ConfigObject parse(File file, ConfigParseOptions options) {
Parseable p = Parseable.newFile(file);
return ConfigImpl.parse(p,
options.withFallbackOriginDescription(file.getPath()));
}
public static ConfigObject parse(Class<?> klass, String resource,
ConfigParseOptions options) {
Parseable p = Parseable.newResource(klass, resource);
return ConfigImpl.parse(p,
options.withFallbackOriginDescription(resource));
}
/**
* Parses classpath resources corresponding to this path expression.
* Essentially if the path is "foo.bar" then the resources are
* "/foo-bar.conf", "/foo-bar.json", and "/foo-bar.properties". If more than
* one of those exists, they are merged.
*
* @param path
* @param options
* @return
*/
public static ConfigObject parse(String path, ConfigParseOptions options) {
// null originDescription is allowed in parseResourcesForPath
return ConfigImpl.parseResourcesForPath(path, options);
}
public static ConfigValue fromAnyRef(Object object) {
throw new UnsupportedOperationException("not implemented yet");
}
public static ConfigObject fromMap(Map<String, ? extends Object> values) {
throw new UnsupportedOperationException("not implemented yet");
}
public static ConfigList fromCollection(Collection<? extends Object> values) {
throw new UnsupportedOperationException("not implemented yet");
}
private static String getUnits(String s) {

View File

@ -35,6 +35,9 @@ public interface ConfigObject extends ConfigValue, Map<String, ConfigValue> {
@Override
ConfigObject withFallback(ConfigValue other);
@Override
ConfigObject withFallbacks(ConfigValue... others);
boolean getBoolean(String path);
Number getNumber(String path);

View File

@ -0,0 +1,90 @@
package com.typesafe.config;
public final class ConfigParseOptions {
final ConfigSyntax syntax;
final String originDescription;
final boolean allowMissing;
protected ConfigParseOptions(ConfigSyntax syntax, String originDescription,
boolean allowMissing) {
this.syntax = syntax;
this.originDescription = originDescription;
this.allowMissing = allowMissing;
}
public static ConfigParseOptions defaults() {
return new ConfigParseOptions(null, null, true);
}
/**
* Set the file format. If set to null, try to guess from any available
* filename extension; if guessing fails, assume ConfigSyntax.CONF.
*
* @param syntax
* @return
*/
public ConfigParseOptions setSyntax(ConfigSyntax syntax) {
if (this.syntax == syntax)
return this;
else
return new ConfigParseOptions(syntax, this.originDescription,
this.allowMissing);
}
public ConfigSyntax getSyntax() {
return syntax;
}
/**
* Set a description for the thing being parsed. In most cases this will be
* set up for you to something like the filename, but if you provide just an
* input stream you might want to improve on it. Set to null to allow the
* library to come up with something automatically.
*
* @param originDescription
* @return
*/
public ConfigParseOptions setOriginDescription(String originDescription) {
if (this.originDescription == originDescription)
return this;
else if (this.originDescription != null && originDescription != null
&& this.originDescription.equals(originDescription))
return this;
else
return new ConfigParseOptions(this.syntax, originDescription,
this.allowMissing);
}
public String getOriginDescription() {
return originDescription;
}
/** this is package-private, not public API */
ConfigParseOptions withFallbackOriginDescription(String originDescription) {
if (this.originDescription == null)
return setOriginDescription(originDescription);
else
return this;
}
/**
* Set to false to throw an exception if the item being parsed (for example
* a file) is missing. Set to true to just return an empty document in that
* case.
*
* @param allowMissing
* @return
*/
public ConfigParseOptions setAllowMissing(boolean allowMissing) {
if (this.allowMissing == allowMissing)
return this;
else
return new ConfigParseOptions(this.syntax, this.originDescription,
allowMissing);
}
public boolean getAllowMissing() {
return allowMissing;
}
}

View File

@ -0,0 +1,36 @@
package com.typesafe.config;
public final class ConfigResolveOptions {
private final boolean useSystemProperties;
private final boolean useSystemEnvironment;
private ConfigResolveOptions(boolean useSystemProperties,
boolean useSystemEnvironment) {
this.useSystemProperties = useSystemProperties;
this.useSystemEnvironment = useSystemEnvironment;
}
public static ConfigResolveOptions defaults() {
return new ConfigResolveOptions(true, true);
}
public static ConfigResolveOptions noSystem() {
return new ConfigResolveOptions(false, false);
}
public ConfigResolveOptions setUseSystemProperties(boolean value) {
return new ConfigResolveOptions(value, useSystemEnvironment);
}
public ConfigResolveOptions setUseSystemEnvironment(boolean value) {
return new ConfigResolveOptions(useSystemProperties, value);
}
public boolean getUseSystemProperties() {
return useSystemProperties;
}
public boolean getUseSystemEnvironment() {
return useSystemEnvironment;
}
}

View File

@ -10,12 +10,24 @@ public interface ConfigRoot extends ConfigObject {
* Returns a replacement root object with all substitutions (the
* "${foo.bar}" syntax) resolved. Substitutions are looked up in this root
* object. A configuration value tree must be resolved before you can use
* it.
*
* it. This method uses ConfigResolveOptions.defaults().
*
* @return an immutable object with substitutions resolved
*/
ConfigRoot resolve();
ConfigRoot resolve(ConfigResolveOptions options);
@Override
ConfigRoot withFallback(ConfigValue fallback);
@Override
ConfigRoot withFallbacks(ConfigValue... fallbacks);
/**
* Gets the global app name that this root represents.
*
* @return the app's root config path
*/
String rootPath();
}

View File

@ -0,0 +1,5 @@
package com.typesafe.config;
public enum ConfigSyntax {
JSON, CONF, PROPERTIES;
}

View File

@ -1,6 +1,5 @@
package com.typesafe.config;
/**
* Interface implemented by any configuration value. From the perspective of
* users of this interface, the object is immutable. It is therefore safe to use
@ -42,4 +41,13 @@ public interface ConfigValue {
* used)
*/
ConfigValue withFallback(ConfigValue other);
/**
* Convenience method just calls withFallback() on each of the values;
* earlier values in the list win over later ones.
*
* @param fallbacks
* @return a version of the object with the requested fallbacks merged in
*/
ConfigValue withFallbacks(ConfigValue... fallbacks);
}

View File

@ -16,7 +16,7 @@ import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRoot;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
@ -38,14 +38,20 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
*
* @return a config root
*/
protected ConfigRoot asRoot() {
return new RootConfigObject(this);
protected ConfigRootImpl asRoot(Path rootPath) {
return new RootConfigObject(this, rootPath);
}
protected static ConfigRoot resolve(ConfigRoot root) {
protected static ConfigRootImpl resolve(ConfigRootImpl root) {
return resolve(root, ConfigResolveOptions.defaults());
}
protected static ConfigRootImpl resolve(ConfigRootImpl root,
ConfigResolveOptions options) {
AbstractConfigValue resolved = SubstitutionResolver.resolve(
(AbstractConfigValue) root, (AbstractConfigObject) root);
return ((AbstractConfigObject) resolved).asRoot();
(AbstractConfigValue) root, (AbstractConfigObject) root,
options);
return ((AbstractConfigObject) resolved).asRoot(root.rootPathObject());
}
/**
@ -58,12 +64,12 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
protected abstract AbstractConfigValue peek(String key);
protected AbstractConfigValue peek(String key,
SubstitutionResolver resolver,
int depth, boolean withFallbacks) {
SubstitutionResolver resolver, int depth,
ConfigResolveOptions options) {
AbstractConfigValue v = peek(key);
if (v != null && resolver != null) {
v = resolver.resolve(v, depth, withFallbacks);
v = resolver.resolve(v, depth, options);
}
return v;
@ -75,18 +81,18 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
* resolver != null.
*/
protected ConfigValue peekPath(Path path, SubstitutionResolver resolver,
int depth,
boolean withFallbacks) {
return peekPath(this, path, resolver, depth, withFallbacks);
int depth, ConfigResolveOptions options) {
return peekPath(this, path, resolver, depth, options);
}
private static ConfigValue peekPath(AbstractConfigObject self, Path path,
SubstitutionResolver resolver, int depth, boolean withFallbacks) {
SubstitutionResolver resolver, int depth,
ConfigResolveOptions options) {
String key = path.first();
Path next = path.remainder();
if (next == null) {
ConfigValue v = self.peek(key, resolver, depth, withFallbacks);
ConfigValue v = self.peek(key, resolver, depth, options);
return v;
} else {
// it's important to ONLY resolve substitutions here, not
@ -98,13 +104,12 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
ConfigValue v = self.peek(key);
if (v instanceof ConfigSubstitution && resolver != null) {
v = resolver.resolve((AbstractConfigValue) v, depth,
withFallbacks);
v = resolver.resolve((AbstractConfigValue) v, depth, options);
}
if (v instanceof AbstractConfigObject) {
return peekPath((AbstractConfigObject) v, next, resolver,
depth, withFallbacks);
depth, options);
} else {
return null;
}
@ -177,6 +182,11 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
originalPath);
}
@Override
public AbstractConfigObject withFallbacks(ConfigValue... others) {
return (AbstractConfigObject) super.withFallbacks(others);
}
@Override
public AbstractConfigObject withFallback(ConfigValue other) {
if (other instanceof Unmergeable) {
@ -291,7 +301,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
@Override
AbstractConfigObject resolveSubstitutions(final SubstitutionResolver resolver,
final int depth,
final boolean withFallbacks) {
final ConfigResolveOptions options) {
if (resolveStatus() == ResolveStatus.RESOLVED)
return this;
@ -299,7 +309,7 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
@Override
public AbstractConfigValue modifyChild(AbstractConfigValue v) {
return resolver.resolve(v, depth, withFallbacks);
return resolver.resolve(v, depth, options);
}
}, ResolveStatus.RESOLVED);

View File

@ -1,6 +1,7 @@
package com.typesafe.config.impl;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
abstract class AbstractConfigValue implements ConfigValue {
@ -24,13 +25,13 @@ abstract class AbstractConfigValue implements ConfigValue {
* @param depth
* the number of substitutions followed in resolving the current
* one
* @param withFallbacks
* @param options
* whether to look at system props and env vars
* @return a new value if there were changes, or this if no changes
*/
AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver,
int depth,
boolean withFallbacks) {
ConfigResolveOptions options) {
return this;
}
@ -63,6 +64,15 @@ abstract class AbstractConfigValue implements ConfigValue {
return this;
}
@Override
public AbstractConfigValue withFallbacks(ConfigValue... fallbacks) {
AbstractConfigValue merged = this;
for (ConfigValue f : fallbacks) {
merged = merged.withFallback(f);
}
return merged;
}
AbstractConfigValue transformed(ConfigTransformer transformer) {
return this;
}

View File

@ -6,6 +6,7 @@ import java.util.List;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
@ -46,21 +47,20 @@ final class ConfigDelayedMerge extends AbstractConfigValue implements
@Override
AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver,
int depth, boolean withFallbacks) {
return resolveSubstitutions(stack, resolver, depth, withFallbacks);
int depth, ConfigResolveOptions options) {
return resolveSubstitutions(stack, resolver, depth, options);
}
// static method also used by ConfigDelayedMergeObject
static AbstractConfigValue resolveSubstitutions(
List<AbstractConfigValue> stack, SubstitutionResolver resolver,
int depth, boolean withFallbacks) {
int depth, ConfigResolveOptions options) {
// to resolve substitutions, we need to recursively resolve
// the stack of stuff to merge, and then merge the stack.
List<AbstractConfigObject> toMerge = new ArrayList<AbstractConfigObject>();
for (AbstractConfigValue v : stack) {
AbstractConfigValue resolved = resolver.resolve(v, depth,
withFallbacks);
AbstractConfigValue resolved = resolver.resolve(v, depth, options);
if (resolved instanceof AbstractConfigObject) {
toMerge.add((AbstractConfigObject) resolved);

View File

@ -8,7 +8,7 @@ import java.util.Set;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigRoot;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
// This is just like ConfigDelayedMerge except we know statically
@ -37,30 +37,56 @@ class ConfigDelayedMergeObject extends AbstractConfigObject implements
}
final private static class Root extends ConfigDelayedMergeObject implements
ConfigRoot {
Root(ConfigDelayedMergeObject original) {
ConfigRootImpl {
final private Path rootPath;
Root(ConfigDelayedMergeObject original, Path rootPath) {
super(original.origin(), original.stack);
this.rootPath = rootPath;
}
@Override
protected Root asRoot() {
return this;
protected Root asRoot(Path newRootPath) {
if (newRootPath.equals(this.rootPath))
return this;
else
return new Root(this, newRootPath);
}
@Override
public ConfigRoot resolve() {
public ConfigRootImpl resolve() {
return resolve(this);
}
@Override
public ConfigRootImpl resolve(ConfigResolveOptions options) {
return resolve(this, options);
}
@Override
public Root withFallback(ConfigValue value) {
return super.withFallback(value).asRoot();
return super.withFallback(value).asRoot(rootPath);
}
@Override
public Root withFallbacks(ConfigValue... values) {
return super.withFallbacks(values).asRoot(rootPath);
}
@Override
public String rootPath() {
return rootPath.render();
}
@Override
public Path rootPathObject() {
return rootPath;
}
}
@Override
protected Root asRoot() {
return new Root(this);
protected Root asRoot(Path rootPath) {
return new Root(this, rootPath);
}
@Override
@ -74,10 +100,10 @@ class ConfigDelayedMergeObject extends AbstractConfigObject implements
@Override
AbstractConfigObject resolveSubstitutions(SubstitutionResolver resolver,
int depth, boolean withFallbacks) {
int depth, ConfigResolveOptions options) {
AbstractConfigValue merged = ConfigDelayedMerge.resolveSubstitutions(
stack, resolver, depth,
withFallbacks);
options);
if (merged instanceof AbstractConfigObject) {
return (AbstractConfigObject) merged;
} else {
@ -122,6 +148,11 @@ class ConfigDelayedMergeObject extends AbstractConfigObject implements
}
}
@Override
public ConfigDelayedMergeObject withFallbacks(ConfigValue... others) {
return (ConfigDelayedMergeObject) super.withFallbacks(others);
}
@Override
public Collection<AbstractConfigValue> unmergedValues() {
return stack;

View File

@ -1,71 +1,185 @@
package com.typesafe.config.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigRoot;
import com.typesafe.config.ConfigSyntax;
import com.typesafe.config.ConfigValue;
/** This is public but is only supposed to be used by the "config" package */
public class ConfigImpl {
public static ConfigRoot loadConfig(String rootPath) {
ConfigTransformer transformer = withExtraTransformer(null);
AbstractConfigObject system = null;
try {
system = systemPropertiesConfig().getObject(rootPath);
} catch (ConfigException e) {
// no system props in the requested root path
}
List<AbstractConfigObject> stack = new ArrayList<AbstractConfigObject>();
// higher-priority configs are first
if (system != null)
stack.add(system.transformed(transformer));
// this is a conceptual placeholder for a customizable
// object that the app might be able to pass in.
IncludeHandler includer = defaultIncluder();
stack.add(includer.include(rootPath).transformed(
transformer));
AbstractConfigObject merged = AbstractConfigObject.merge(stack);
ConfigRoot resolved = merged.asRoot().resolve();
return resolved;
}
static ConfigObject getEnvironmentAsConfig() {
// This should not need to create a new config object
// as long as the transformer is just the default transformer.
return envVariablesConfig().transformed(withExtraTransformer(null));
}
static ConfigObject getSystemPropertiesAsConfig() {
// This should not need to create a new config object
// as long as the transformer is just the default transformer.
return systemPropertiesConfig().transformed(withExtraTransformer(null));
}
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);
private static AbstractConfigObject forceParsedToObject(ConfigValue value) {
if (value instanceof AbstractConfigObject) {
return (AbstractConfigObject) value;
} else {
return defaultConfigTransformer();
throw new ConfigException.WrongType(value.origin(), "",
"object at file root", value.valueType().name());
}
}
static AbstractConfigValue parseValue(Parseable parseable,
ConfigParseOptions options) {
ConfigOrigin origin = new SimpleConfigOrigin(
options.getOriginDescription());
return Parser.parse(parseable, origin, options);
}
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
public static ConfigObject parse(Parseable parseable,
ConfigParseOptions options) {
return forceParsedToObject(parseValue(parseable, options));
}
private static String makeResourceBasename(Path path) {
StringBuilder sb = new StringBuilder("/");
String next = path.first();
Path remaining = path.remainder();
while (next != null) {
sb.append(next);
sb.append('-');
if (remaining == null)
break;
next = remaining.first();
remaining = remaining.remainder();
}
sb.setLength(sb.length() - 1); // chop extra hyphen
return sb.toString();
}
// ConfigParseOptions has a package-private method for this but I don't want
// to make it public
static ConfigOrigin originWithSuffix(ConfigParseOptions options,
String suffix) {
return new SimpleConfigOrigin(options.getOriginDescription() + suffix);
}
static String syntaxSuffix(ConfigSyntax syntax) {
switch (syntax) {
case PROPERTIES:
return ".properties";
case CONF:
return ".conf";
case JSON:
return ".json";
}
throw new ConfigException.BugOrBroken("not a valid ConfigSyntax: "
+ syntax);
}
static AbstractConfigObject loadForResource(Class<?> loadClass,
String basename, ConfigSyntax syntax, ConfigParseOptions options) {
String suffix = syntaxSuffix(syntax);
String resource = basename + suffix;
// we want null rather than empty object if missingness is allowed,
// so we can handle it.
if (options.getAllowMissing()
&& loadClass.getResource(resource) == null) {
return null;
} else {
return forceParsedToObject(Parser.parse(
Parseable.newResource(loadClass, resource),
originWithSuffix(options, suffix),
options.setSyntax(syntax)));
}
}
static AbstractConfigObject checkAllowMissing(AbstractConfigObject obj,
ConfigOrigin origin, ConfigParseOptions options) {
if (obj == null) {
if (options.getAllowMissing()) {
return SimpleConfigObject.emptyMissing(origin);
} else {
throw new ConfigException.IO(origin,
"Resource not found on classpath");
}
} else {
return obj;
}
}
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
public static ConfigObject parseResourcesForPath(String expression,
ConfigParseOptions baseOptions) {
Path path = Parser.parsePath(expression);
String basename = makeResourceBasename(path);
Class<?> loadClass = ConfigImpl.class;
ConfigParseOptions options;
if (baseOptions.getOriginDescription() != null)
options = baseOptions;
else
options = baseOptions.setOriginDescription(basename);
if (options.getSyntax() != null) {
ConfigSyntax syntax = options.getSyntax();
AbstractConfigObject obj = loadForResource(loadClass, basename,
syntax, options);
return checkAllowMissing(obj, originWithSuffix(options, syntaxSuffix(syntax)), options);
} else {
// we want to try all three then
ConfigParseOptions allowMissing = options.setAllowMissing(true);
AbstractConfigObject conf = loadForResource(loadClass, basename,
ConfigSyntax.CONF, allowMissing);
AbstractConfigObject json = loadForResource(loadClass, basename,
ConfigSyntax.JSON, allowMissing);
AbstractConfigObject props = loadForResource(loadClass, basename,
ConfigSyntax.PROPERTIES, allowMissing);
ConfigOrigin baseOrigin = new SimpleConfigOrigin(options
.getOriginDescription());
if (!options.getAllowMissing() && conf == null && json == null && props == null) {
throw new ConfigException.IO(baseOrigin,
"No config files {.conf,.json,.properties} found on classpath");
}
AbstractConfigObject merged = SimpleConfigObject
.empty(baseOrigin);
if (conf != null)
merged = merged.withFallback(conf);
if (json != null)
merged = merged.withFallback(json);
if (props != null)
merged = merged.withFallback(props);
return merged;
}
}
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
public static ConfigRoot emptyRoot(String rootPath) {
return SimpleConfigObject.empty().asRoot(Parser.parsePath(rootPath));
}
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
public static ConfigRoot systemPropertiesRoot(String rootPath) {
Path path = Parser.parsePath(rootPath);
try {
return systemPropertiesAsConfig().getObject(rootPath).asRoot(path);
} catch (ConfigException.Missing e) {
return SimpleConfigObject.empty().asRoot(path);
}
}
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
public static ConfigObject parse(Properties properties,
ConfigParseOptions options) {
return Loader
.fromProperties(options.getOriginDescription(), properties);
}
private static ConfigTransformer defaultTransformer = null;
synchronized static ConfigTransformer defaultConfigTransformer() {
@ -92,7 +206,8 @@ public class ConfigImpl {
private static AbstractConfigObject systemProperties = null;
synchronized static AbstractConfigObject systemPropertiesConfig() {
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
public synchronized static AbstractConfigObject systemPropertiesAsConfig() {
if (systemProperties == null) {
systemProperties = loadSystemProperties();
}
@ -110,7 +225,8 @@ public class ConfigImpl {
private static AbstractConfigObject envVariables = null;
synchronized static AbstractConfigObject envVariablesConfig() {
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
public synchronized static AbstractConfigObject envVariablesAsConfig() {
if (envVariables == null) {
envVariables = loadEnvVariables();
}

View File

@ -0,0 +1,11 @@
package com.typesafe.config.impl;
import com.typesafe.config.ConfigRoot;
import com.typesafe.config.ConfigValue;
interface ConfigRootImpl extends ConfigRoot {
Path rootPathObject();
@Override
ConfigRootImpl withFallbacks(ConfigValue... fallbacks);
}

View File

@ -7,6 +7,7 @@ import java.util.List;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
@ -85,7 +86,7 @@ final class ConfigSubstitution extends AbstractConfigValue implements
private ConfigValue findInObject(AbstractConfigObject root,
SubstitutionResolver resolver, /* null if we should not have refs */
Path subst, int depth,
boolean withFallbacks) {
ConfigResolveOptions options) {
if (depth > MAX_DEPTH) {
throw new ConfigException.BadValue(origin(), subst.render(),
"Substitution ${" + subst.render()
@ -93,7 +94,7 @@ final class ConfigSubstitution extends AbstractConfigValue implements
}
ConfigValue result = root.peekPath(subst, resolver, depth,
withFallbacks);
options);
if (result instanceof ConfigSubstitution) {
throw new ConfigException.BugOrBroken(
@ -108,31 +109,34 @@ final class ConfigSubstitution extends AbstractConfigValue implements
}
private ConfigValue resolve(SubstitutionResolver resolver, Path subst,
int depth, boolean withFallbacks) {
int depth, ConfigResolveOptions options) {
ConfigValue result = findInObject(resolver.root(), resolver, subst,
depth, withFallbacks);
if (withFallbacks) {
// when looking up system props and env variables,
// we don't want the prefix that was added when
// we were included in another file.
Path unprefixed = subst.subPath(prefixLength);
if (result == null) {
result = findInObject(ConfigImpl.systemPropertiesConfig(),
null, unprefixed, depth, withFallbacks);
}
if (result == null) {
result = findInObject(ConfigImpl.envVariablesConfig(), null,
unprefixed, depth, withFallbacks);
}
depth, options);
// when looking up system props and env variables,
// we don't want the prefix that was added when
// we were included in another file.
Path unprefixed = subst.subPath(prefixLength);
if (result == null && options.getUseSystemProperties()) {
result = findInObject(ConfigImpl.systemPropertiesAsConfig(), null,
unprefixed, depth, options);
}
if (result == null && options.getUseSystemEnvironment()) {
result = findInObject(ConfigImpl.envVariablesAsConfig(), null,
unprefixed, depth, options);
}
if (result == null) {
result = new ConfigNull(origin());
}
return result;
}
private ConfigValue resolve(SubstitutionResolver resolver, int depth,
boolean withFallbacks) {
ConfigResolveOptions options) {
if (pieces.size() > 1) {
// need to concat everything into a string
StringBuilder sb = new StringBuilder();
@ -140,8 +144,7 @@ final class ConfigSubstitution extends AbstractConfigValue implements
if (p instanceof String) {
sb.append((String) p);
} else {
ConfigValue v = resolve(resolver, (Path) p,
depth, withFallbacks);
ConfigValue v = resolve(resolver, (Path) p, depth, options);
switch (v.valueType()) {
case NULL:
// nothing; becomes empty string
@ -162,19 +165,18 @@ final class ConfigSubstitution extends AbstractConfigValue implements
if (!(pieces.get(0) instanceof Path))
throw new ConfigException.BugOrBroken(
"ConfigSubstitution should never contain a single String piece");
return resolve(resolver, (Path) pieces.get(0), depth,
withFallbacks);
return resolve(resolver, (Path) pieces.get(0), depth, options);
}
}
@Override
AbstractConfigValue resolveSubstitutions(SubstitutionResolver resolver,
int depth,
boolean withFallbacks) {
ConfigResolveOptions options) {
// only ConfigSubstitution adds to depth here, because the depth
// is the substitution depth not the recursion depth
AbstractConfigValue resolved = (AbstractConfigValue) resolve(resolver,
depth + 1, withFallbacks);
depth + 1, options);
return resolved;
}

View File

@ -29,6 +29,14 @@ abstract class DelegatingConfigObject extends AbstractConfigObject {
return underlying.resolveStatus();
}
@Override
final protected ConfigRootImpl asRoot(Path rootPath) {
return asRoot(underlying, rootPath);
}
protected abstract ConfigRootImpl asRoot(AbstractConfigObject underlying,
Path rootPath);
@Override
public boolean containsKey(Object key) {
return underlying.containsKey(key);
@ -54,6 +62,11 @@ abstract class DelegatingConfigObject extends AbstractConfigObject {
return underlying.withFallback(value);
}
@Override
public AbstractConfigObject withFallbacks(ConfigValue... values) {
return underlying.withFallbacks(values);
}
@Override
public boolean containsValue(Object value) {
return underlying.containsValue(value);

View File

@ -14,7 +14,13 @@ import java.util.Properties;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigParseOptions;
/**
* FIXME this file needs to die; the load() part should use the code in
* ConfigImpl instead and the properties stuff should be in its own file.
*
*/
final class Loader {
static AbstractConfigObject load(String name, IncludeHandler includer) {
List<AbstractConfigObject> stack = new ArrayList<AbstractConfigObject>();
@ -65,7 +71,10 @@ final class Loader {
}
return fromProperties(url.toExternalForm(), props);
} else {
return forceParsedToObject(Parser.parse(url, includer));
return forceParsedToObject(Parser.parse(Parseable.newURL(url),
new SimpleConfigOrigin(url.toExternalForm()),
ConfigParseOptions.defaults(),
includer));
}
}

View File

@ -0,0 +1,227 @@
package com.typesafe.config.impl;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilterReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigSyntax;
/**
* This is public but it's only for use by the config package; DO NOT TOUCH. The
* point of this class is to avoid "propagating" each overload on
* "thing which can be parsed" through multiple interfaces. Most interfaces can
* have just one overload that takes a Parseable.
*/
public abstract class Parseable {
protected Parseable() {
}
// the general idea is that any work should be in here, not in the
// constructor,
// so that exceptions are thrown from the public parse() function and not
// from the creation of the Parseable. Essentially this is a lazy field.
// The parser should close the reader when it's done with it.
// ALSO, IMPORTANT: if the file or URL is not found, this must throw.
// to support the "allow missing" feature.
abstract Reader reader() throws IOException;
ConfigSyntax guessSyntax() {
return null;
}
@Override
public String toString() {
return getClass().getSimpleName();
}
private static ConfigSyntax syntaxFromExtension(String name) {
if (name.endsWith(".json"))
return ConfigSyntax.JSON;
else if (name.endsWith(".conf"))
return ConfigSyntax.CONF;
else if (name.endsWith(".properties"))
return ConfigSyntax.PROPERTIES;
else
return null;
}
private static Reader readerFromStream(InputStream input) {
try {
// well, this is messed up. If we aren't going to close
// the passed-in InputStream then we have no way to
// close these readers. So maybe we should not have an
// InputStream version, only a Reader version.
Reader reader = new InputStreamReader(input, "UTF-8");
return new BufferedReader(reader);
} catch (UnsupportedEncodingException e) {
throw new ConfigException.BugOrBroken(
"Java runtime does not support UTF-8", e);
}
}
private static Reader doNotClose(Reader input) {
return new FilterReader(input) {
@Override
public void close() {
// NOTHING.
}
};
}
private final static class ParseableInputStream extends Parseable {
final private InputStream input;
ParseableInputStream(InputStream input) {
this.input = input;
}
@Override
Reader reader() {
return doNotClose(readerFromStream(input));
}
}
/**
* note that we will never close this stream; you have to do it when parsing
* is complete.
*/
public static Parseable newInputStream(InputStream input) {
return new ParseableInputStream(input);
}
private final static class ParseableReader extends Parseable {
final private Reader reader;
ParseableReader(Reader reader) {
this.reader = reader;
}
@Override
Reader reader() {
return reader;
}
}
/**
* note that we will never close this reader; you have to do it when parsing
* is complete.
*/
public static Parseable newReader(Reader reader) {
return new ParseableReader(doNotClose(reader));
}
private final static class ParseableString extends Parseable {
final private String input;
ParseableString(String input) {
this.input = input;
}
@Override
Reader reader() {
return new StringReader(input);
}
}
public static Parseable newString(String input) {
return new ParseableString(input);
}
private final static class ParseableURL extends Parseable {
final private URL input;
ParseableURL(URL input) {
this.input = input;
}
@Override
Reader reader() throws IOException {
InputStream stream = input.openStream();
return readerFromStream(stream);
}
@Override
ConfigSyntax guessSyntax() {
return syntaxFromExtension(input.getPath());
}
@Override
public String toString() {
return getClass().getSimpleName() + "(" + input.toExternalForm()
+ ")";
}
}
public static Parseable newURL(URL input) {
return new ParseableURL(input);
}
private final static class ParseableFile extends Parseable {
final private File input;
ParseableFile(File input) {
this.input = input;
}
@Override
Reader reader() throws IOException {
InputStream stream = new FileInputStream(input);
return readerFromStream(stream);
}
@Override
ConfigSyntax guessSyntax() {
return syntaxFromExtension(input.getName());
}
@Override
public String toString() {
return getClass().getSimpleName() + "(" + input.getPath() + ")";
}
}
public static Parseable newFile(File input) {
return new ParseableFile(input);
}
private final static class ParseableResource extends Parseable {
final private Class<?> klass;
final private String resource;
ParseableResource(Class<?> klass, String resource) {
this.klass = klass;
this.resource = resource;
}
@Override
Reader reader() throws IOException {
InputStream stream = klass.getResourceAsStream(resource);
return readerFromStream(stream);
}
@Override
ConfigSyntax guessSyntax() {
return syntaxFromExtension(resource);
}
@Override
public String toString() {
return getClass().getSimpleName() + "(" + resource + ","
+ klass.getName()
+ ")";
}
}
public static Parseable newResource(Class<?> klass, String resource) {
return new ParseableResource(klass, resource);
}
}

View File

@ -1,15 +1,8 @@
package com.typesafe.config.impl;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -22,94 +15,52 @@ import java.util.Stack;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigSyntax;
import com.typesafe.config.ConfigValueType;
final class Parser {
/**
* Parses an input stream, which must be in UTF-8 encoding and should not be
* buffered because we'll use a BufferedReader instead. Does not close the
* stream; you have to arrange to do that yourself.
*/
static AbstractConfigValue parse(SyntaxFlavor flavor, ConfigOrigin origin,
InputStream input, IncludeHandler includer) {
try {
// well, this is messed up. If we aren't going to close
// the passed-in InputStream then we have no way to
// close these readers. So maybe we should not have an
// InputStream version, only a Reader version.
Reader reader = new InputStreamReader(input, "UTF-8");
reader = new BufferedReader(reader);
return parse(flavor, origin, reader,
includer);
} catch (UnsupportedEncodingException e) {
throw new ConfigException.BugOrBroken(
"Java runtime does not support UTF-8", e);
static AbstractConfigValue parse(Parseable input, ConfigOrigin origin,
ConfigParseOptions options) {
return parse(input, origin, options, ConfigImpl.defaultIncluder());
}
static AbstractConfigValue parse(Parseable input, ConfigOrigin origin,
ConfigParseOptions baseOptions, IncludeHandler includer) {
ConfigSyntax syntax = baseOptions.getSyntax();
if (syntax == null) {
syntax = input.guessSyntax();
}
}
static AbstractConfigValue parse(SyntaxFlavor flavor, ConfigOrigin origin,
Reader input, IncludeHandler includer) {
Iterator<Token> tokens = Tokenizer.tokenize(origin, input, flavor);
return parse(flavor, origin, tokens, includer);
}
static AbstractConfigValue parse(SyntaxFlavor flavor, ConfigOrigin origin,
String input, IncludeHandler includer) {
return parse(flavor, origin, new StringReader(input), includer);
}
private static SyntaxFlavor flavorFromExtension(String name,
ConfigOrigin origin) {
if (name.endsWith(".json"))
return SyntaxFlavor.JSON;
else if (name.endsWith(".conf"))
return SyntaxFlavor.CONF;
else
throw new ConfigException.IO(origin, "Unknown filename extension");
}
static AbstractConfigValue parse(File f, IncludeHandler includer) {
return parse(null, f, includer);
}
static AbstractConfigValue parse(SyntaxFlavor flavor, File f,
IncludeHandler includer) {
ConfigOrigin origin = new SimpleConfigOrigin(f.getPath());
try {
return parse(flavor, origin, f.toURI().toURL(), includer);
} catch (MalformedURLException e) {
throw new ConfigException.IO(origin,
"failed to create url from file path", e);
if (syntax == null) {
syntax = ConfigSyntax.CONF;
}
}
ConfigParseOptions options = baseOptions.setSyntax(syntax);
static AbstractConfigValue parse(URL url, IncludeHandler includer) {
return parse(null, url, includer);
}
static AbstractConfigValue parse(SyntaxFlavor flavor, URL url,
IncludeHandler includer) {
ConfigOrigin origin = new SimpleConfigOrigin(url.toExternalForm());
return parse(flavor, origin, url, includer);
}
static AbstractConfigValue parse(SyntaxFlavor flavor, ConfigOrigin origin,
URL url, IncludeHandler includer) {
AbstractConfigValue result = null;
try {
InputStream stream = url.openStream();
Reader reader = input.reader();
try {
result = parse(
flavor != null ? flavor : flavorFromExtension(
url.getPath(), origin), origin, stream,
includer);
Iterator<Token> tokens = Tokenizer.tokenize(origin, reader,
syntax);
return parse(tokens, origin, options, includer);
} finally {
stream.close();
reader.close();
}
} catch (IOException e) {
throw new ConfigException.IO(origin, "failed to read url", e);
if (options.getAllowMissing()) {
return SimpleConfigObject.emptyMissing(origin);
} else {
throw new ConfigException.IO(origin, e.getMessage(), e);
}
}
return result;
}
private static AbstractConfigValue parse(Iterator<Token> tokens,
ConfigOrigin origin, ConfigParseOptions options,
IncludeHandler includer) {
ParseContext context = new ParseContext(options.getSyntax(), origin,
tokens, includer);
return context.parse();
}
static private final class ParseContext {
@ -117,11 +68,11 @@ final class Parser {
final private Stack<Token> buffer;
final private Iterator<Token> tokens;
final private IncludeHandler includer;
final private SyntaxFlavor flavor;
final private ConfigSyntax flavor;
final private ConfigOrigin baseOrigin;
final private LinkedList<Path> pathStack;
ParseContext(SyntaxFlavor flavor, ConfigOrigin origin,
ParseContext(ConfigSyntax flavor, ConfigOrigin origin,
Iterator<Token> tokens, IncludeHandler includer) {
lineNumber = 0;
buffer = new Stack<Token>();
@ -140,7 +91,7 @@ final class Parser {
t = buffer.pop();
}
if (flavor == SyntaxFlavor.JSON) {
if (flavor == ConfigSyntax.JSON) {
if (Tokens.isUnquotedText(t)) {
throw parseError("Token not allowed in valid JSON: '"
+ Tokens.getUnquotedText(t) + "'");
@ -172,7 +123,7 @@ final class Parser {
// either a newline or a comma. The iterator
// is left just after the comma or the newline.
private boolean checkElementSeparator() {
if (flavor == SyntaxFlavor.JSON) {
if (flavor == ConfigSyntax.JSON) {
Token t = nextTokenIgnoringNewline();
if (t == Tokens.COMMA) {
return true;
@ -206,7 +157,7 @@ final class Parser {
// value.
private void consolidateValueTokens() {
// this trick is not done in JSON
if (flavor == SyntaxFlavor.JSON)
if (flavor == ConfigSyntax.JSON)
return;
List<Token> values = null; // create only if we have value tokens
@ -351,7 +302,7 @@ final class Parser {
}
private Path parseKey(Token token) {
if (flavor == SyntaxFlavor.JSON) {
if (flavor == ConfigSyntax.JSON) {
if (Tokens.isValueWithType(token, ConfigValueType.STRING)) {
String key = (String) Tokens.getValue(token).unwrapped();
return Path.newKey(key);
@ -422,7 +373,7 @@ final class Parser {
}
private boolean isKeyValueSeparatorToken(Token t) {
if (flavor == SyntaxFlavor.JSON) {
if (flavor == ConfigSyntax.JSON) {
return t == Tokens.COLON;
} else {
return t == Tokens.COLON || t == Tokens.EQUALS;
@ -437,7 +388,7 @@ final class Parser {
while (true) {
Token t = nextTokenIgnoringNewline();
if (t == Tokens.CLOSE_CURLY) {
if (flavor == SyntaxFlavor.JSON && afterComma) {
if (flavor == ConfigSyntax.JSON && afterComma) {
throw parseError("expecting a field name after comma, got a close brace }");
} else if (!hadOpenCurly) {
throw parseError("unbalanced close brace '}' with no open brace");
@ -446,7 +397,7 @@ final class Parser {
} else if (t == Tokens.END && !hadOpenCurly) {
putBack(t);
break;
} else if (flavor != SyntaxFlavor.JSON && isIncludeKeyword(t)) {
} else if (flavor != ConfigSyntax.JSON && isIncludeKeyword(t)) {
parseInclude(values);
afterComma = false;
@ -459,7 +410,7 @@ final class Parser {
Token valueToken;
AbstractConfigValue newValue;
if (flavor == SyntaxFlavor.CONF
if (flavor == ConfigSyntax.CONF
&& afterKey == Tokens.OPEN_CURLY) {
// can omit the ':' or '=' before an object value
valueToken = afterKey;
@ -488,7 +439,7 @@ final class Parser {
// if the value is an object (or substitution that
// could become an object).
if (flavor == SyntaxFlavor.JSON) {
if (flavor == ConfigSyntax.JSON) {
throw parseError("JSON does not allow duplicate fields: '"
+ key
+ "' was already seen at "
@ -499,7 +450,7 @@ final class Parser {
}
values.put(key, newValue);
} else {
if (flavor == SyntaxFlavor.JSON) {
if (flavor == ConfigSyntax.JSON) {
throw new ConfigException.BugOrBroken(
"somehow got multi-element path in JSON mode");
}
@ -593,7 +544,7 @@ final class Parser {
values.add(parseObject(true));
} else if (t == Tokens.OPEN_SQUARE) {
values.add(parseArray());
} else if (flavor != SyntaxFlavor.JSON
} else if (flavor != ConfigSyntax.JSON
&& t == Tokens.CLOSE_SQUARE) {
// we allow one trailing comma
putBack(t);
@ -620,7 +571,7 @@ final class Parser {
} else if (t == Tokens.OPEN_SQUARE) {
result = parseArray();
} else {
if (flavor == SyntaxFlavor.JSON) {
if (flavor == ConfigSyntax.JSON) {
if (t == Tokens.END) {
throw parseError("Empty document");
} else {
@ -646,13 +597,6 @@ final class Parser {
}
}
private static AbstractConfigValue parse(SyntaxFlavor flavor,
ConfigOrigin origin, Iterator<Token> tokens, IncludeHandler includer) {
ParseContext context = new ParseContext(flavor, origin, tokens,
includer);
return context.parse();
}
static class Element {
StringBuilder sb;
// an element can be empty if it has a quoted empty string "" in it
@ -762,7 +706,7 @@ final class Parser {
try {
Iterator<Token> tokens = Tokenizer.tokenize(apiOrigin, reader,
SyntaxFlavor.CONF);
ConfigSyntax.CONF);
tokens.next(); // drop START
return parsePathExpression(tokens, apiOrigin);
} finally {

View File

@ -1,35 +1,61 @@
package com.typesafe.config.impl;
import com.typesafe.config.ConfigRoot;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
final class RootConfigObject extends DelegatingConfigObject implements
ConfigRoot {
ConfigRootImpl {
RootConfigObject(AbstractConfigObject underlying) {
final private Path rootPath;
RootConfigObject(AbstractConfigObject underlying, Path rootPath) {
super(underlying.transformer, underlying);
this.rootPath = rootPath;
}
@Override
protected ConfigRoot asRoot() {
return this;
protected ConfigRootImpl asRoot(AbstractConfigObject underlying,
Path newRootPath) {
if (newRootPath.equals(this.rootPath))
return this;
else
return new RootConfigObject(underlying, newRootPath);
}
@Override
public RootConfigObject newCopy(AbstractConfigObject underlying,
ConfigTransformer newTransformer, ResolveStatus newStatus) {
return new RootConfigObject(underlying.newCopy(newTransformer,
newStatus));
newStatus), rootPath);
}
@Override
public ConfigRoot resolve() {
return ((AbstractConfigObject) SubstitutionResolver.resolve(this, this))
.asRoot();
public ConfigRootImpl resolve() {
return resolve(this);
}
@Override
public ConfigRootImpl resolve(ConfigResolveOptions options) {
return resolve(this, options);
}
@Override
public RootConfigObject withFallback(ConfigValue value) {
return new RootConfigObject(super.withFallback(value));
return new RootConfigObject(super.withFallback(value), rootPath);
}
@Override
public RootConfigObject withFallbacks(ConfigValue... values) {
return new RootConfigObject(super.withFallbacks(values), rootPath);
}
@Override
public Path rootPathObject() {
return rootPath;
}
@Override
public String rootPath() {
return rootPath.render();
}
}

View File

@ -9,6 +9,7 @@ import java.util.ListIterator;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigList;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigResolveOptions;
import com.typesafe.config.ConfigValue;
import com.typesafe.config.ConfigValueType;
@ -84,14 +85,14 @@ final class SimpleConfigList extends AbstractConfigValue implements ConfigList {
@Override
SimpleConfigList resolveSubstitutions(final SubstitutionResolver resolver,
final int depth, final boolean withFallbacks) {
final int depth, final ConfigResolveOptions options) {
if (resolved)
return this;
return modify(new Modifier() {
@Override
public AbstractConfigValue modifyChild(AbstractConfigValue v) {
return resolver.resolve(v, depth, withFallbacks);
return resolver.resolve(v, depth, options);
}
}, ResolveStatus.RESOLVED);

View File

@ -117,4 +117,15 @@ final class SimpleConfigObject extends AbstractConfigObject {
return new SimpleConfigObject(new SimpleConfigOrigin("empty config"),
Collections.<String, AbstractConfigValue> emptyMap());
}
final static SimpleConfigObject empty(ConfigOrigin origin) {
return new SimpleConfigObject(origin,
Collections.<String, AbstractConfigValue> emptyMap());
}
final static SimpleConfigObject emptyMissing(ConfigOrigin baseOrigin) {
return new SimpleConfigObject(new SimpleConfigOrigin(
baseOrigin.description() + " (not found)"),
Collections.<String, AbstractConfigValue> emptyMap());
}
}

View File

@ -4,6 +4,7 @@ import java.util.IdentityHashMap;
import java.util.Map;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigResolveOptions;
/**
* This exists because we have to memoize resolved substitutions as we go
@ -21,13 +22,12 @@ final class SubstitutionResolver {
}
AbstractConfigValue resolve(AbstractConfigValue original, int depth,
boolean withFallbacks) {
ConfigResolveOptions options) {
if (memos.containsKey(original)) {
return memos.get(original);
} else {
AbstractConfigValue resolved = original.resolveSubstitutions(this,
depth,
withFallbacks);
depth, options);
if (resolved.resolveStatus() != ResolveStatus.RESOLVED)
throw new ConfigException.BugOrBroken(
"resolveSubstitutions() did not give us a resolved object");
@ -40,19 +40,9 @@ final class SubstitutionResolver {
return this.root;
}
private static AbstractConfigValue resolve(AbstractConfigValue value,
AbstractConfigObject root, boolean withFallbacks) {
SubstitutionResolver resolver = new SubstitutionResolver(root);
return resolver.resolve(value, 0, withFallbacks);
}
static AbstractConfigValue resolve(AbstractConfigValue value,
AbstractConfigObject root) {
return resolve(value, root, true /* withFallbacks */);
}
static AbstractConfigValue resolveWithoutFallbacks(
AbstractConfigValue value, AbstractConfigObject root) {
return resolve(value, root, false /* withFallbacks */);
AbstractConfigObject root, ConfigResolveOptions options) {
SubstitutionResolver resolver = new SubstitutionResolver(root);
return resolver.resolve(value, 0, options);
}
}

View File

@ -1,5 +0,0 @@
package com.typesafe.config.impl;
enum SyntaxFlavor {
JSON, CONF
}

View File

@ -10,14 +10,15 @@ import java.util.Queue;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigSyntax;
final class Tokenizer {
/**
* Tokenizes a Reader. Does not close the reader; you have to arrange to do
* that after you're done with the returned iterator.
*/
static Iterator<Token> tokenize(ConfigOrigin origin, Reader input, SyntaxFlavor flavor) {
return new TokenIterator(origin, input, flavor != SyntaxFlavor.JSON);
static Iterator<Token> tokenize(ConfigOrigin origin, Reader input, ConfigSyntax flavor) {
return new TokenIterator(origin, input, flavor != ConfigSyntax.JSON);
}
private static class TokenIterator implements Iterator<Token> {

View File

@ -11,7 +11,10 @@ import scala.collection.JavaConverters._
class ConfParserTest extends TestUtils {
def parseWithoutResolving(s: String) = {
Parser.parse(SyntaxFlavor.CONF, new SimpleConfigOrigin("test conf string"), s, includer())
val options = ConfigParseOptions.defaults().
setOriginDescription("test conf string").
setSyntax(ConfigSyntax.CONF);
ConfigImpl.parseValue(Parseable.newString(s), options);
}
def parse(s: String) = {
@ -21,7 +24,7 @@ class ConfParserTest extends TestUtils {
// interpolating arrays into strings
tree match {
case obj: AbstractConfigObject =>
SubstitutionResolver.resolveWithoutFallbacks(tree, obj)
SubstitutionResolver.resolve(tree, obj, ConfigResolveOptions.noSystem())
case _ =>
tree
}

View File

@ -4,21 +4,26 @@ import org.junit.Assert._
import org.junit._
import com.typesafe.config.ConfigValue
import com.typesafe.config.ConfigException
import com.typesafe.config.ConfigResolveOptions
class ConfigSubstitutionTest extends TestUtils {
private def resolveWithoutFallbacks(v: AbstractConfigObject) = {
SubstitutionResolver.resolveWithoutFallbacks(v, v).asInstanceOf[AbstractConfigObject]
val options = ConfigResolveOptions.noSystem()
SubstitutionResolver.resolve(v, v, options).asInstanceOf[AbstractConfigObject]
}
private def resolveWithoutFallbacks(s: ConfigSubstitution, root: AbstractConfigObject) = {
SubstitutionResolver.resolveWithoutFallbacks(s, root)
val options = ConfigResolveOptions.noSystem()
SubstitutionResolver.resolve(s, root, options)
}
private def resolve(v: AbstractConfigObject) = {
SubstitutionResolver.resolve(v, v).asInstanceOf[AbstractConfigObject]
val options = ConfigResolveOptions.defaults()
SubstitutionResolver.resolve(v, v, options).asInstanceOf[AbstractConfigObject]
}
private def resolve(s: ConfigSubstitution, root: AbstractConfigObject) = {
SubstitutionResolver.resolve(s, root)
val options = ConfigResolveOptions.defaults()
SubstitutionResolver.resolve(s, root, options)
}
private val simpleObject = {

View File

@ -8,6 +8,7 @@ import com.typesafe.config.ConfigObject
import com.typesafe.config.ConfigException
import java.util.concurrent.TimeUnit
import scala.collection.JavaConverters._
import com.typesafe.config.ConfigResolveOptions
class ConfigTest extends TestUtils {
@ -212,13 +213,17 @@ class ConfigTest extends TestUtils {
}
}
private def resolveNoSystem(v: AbstractConfigValue, root: AbstractConfigObject) = {
SubstitutionResolver.resolve(v, root, ConfigResolveOptions.noSystem())
}
@Test
def mergeSubstitutedValues() {
val obj1 = parseObject("""{ "a" : { "x" : 1, "z" : 4 }, "c" : ${a} }""")
val obj2 = parseObject("""{ "b" : { "y" : 2, "z" : 5 }, "c" : ${b} }""")
val merged = merge(obj1, obj2)
val resolved = SubstitutionResolver.resolveWithoutFallbacks(merged, merged) match {
val resolved = resolveNoSystem(merged, merged) match {
case x: ConfigObject => x
}
@ -234,7 +239,7 @@ class ConfigTest extends TestUtils {
val obj2 = parseObject("""{ "b" : { "y" : 2, "z" : 5 }, "c" : ${b} }""")
val merged = merge(obj1, obj2)
val resolved = SubstitutionResolver.resolveWithoutFallbacks(merged, merged) match {
val resolved = resolveNoSystem(merged, merged) match {
case x: ConfigObject => x
}
@ -243,7 +248,7 @@ class ConfigTest extends TestUtils {
assertEquals(42, resolved.getInt("c.z"))
val merged2 = merge(obj2, obj1)
val resolved2 = SubstitutionResolver.resolveWithoutFallbacks(merged2, merged2) match {
val resolved2 = resolveNoSystem(merged2, merged2) match {
case x: ConfigObject => x
}
@ -268,13 +273,13 @@ class ConfigTest extends TestUtils {
// that's been overridden, and thus not end up with a cycle as long
// as we override the problematic link in the cycle.
val e = intercept[ConfigException.BadValue] {
val v = SubstitutionResolver.resolveWithoutFallbacks(subst("foo"), cycleObject)
val v = resolveNoSystem(subst("foo"), cycleObject)
}
assertTrue(e.getMessage().contains("cycle"))
val fixUpCycle = parseObject(""" { "a" : { "b" : { "c" : 57 } } } """)
val merged = merge(fixUpCycle, cycleObject)
val v = SubstitutionResolver.resolveWithoutFallbacks(subst("foo"), merged)
val v = resolveNoSystem(subst("foo"), merged)
assertEquals(intValue(57), v);
}
@ -284,14 +289,14 @@ class ConfigTest extends TestUtils {
// we have to evaluate the substitution to see if it's an object to merge,
// so we don't avoid the cycle.
val e = intercept[ConfigException.BadValue] {
val v = SubstitutionResolver.resolveWithoutFallbacks(subst("foo"), cycleObject)
val v = resolveNoSystem(subst("foo"), cycleObject)
}
assertTrue(e.getMessage().contains("cycle"))
val fixUpCycle = parseObject(""" { "a" : { "b" : { "c" : { "q" : "u" } } } } """)
val merged = merge(fixUpCycle, cycleObject)
val e2 = intercept[ConfigException.BadValue] {
val v = SubstitutionResolver.resolveWithoutFallbacks(subst("foo"), merged)
val v = resolveNoSystem(subst("foo"), merged)
}
assertTrue(e2.getMessage().contains("cycle"))
}
@ -303,7 +308,7 @@ class ConfigTest extends TestUtils {
val obj3 = parseObject("""{ "c" : { "z" : 3, "q" : 6 }, "j" : ${c} }""")
associativeMerge(Seq(obj1, obj2, obj3)) { merged =>
val resolved = SubstitutionResolver.resolveWithoutFallbacks(merged, merged) match {
val resolved = resolveNoSystem(merged, merged) match {
case x: ConfigObject => x
}
@ -322,7 +327,7 @@ class ConfigTest extends TestUtils {
val obj3 = parseObject("""{ "c" : { "z" : 3, "q" : 6 }, "j" : ${c} }""")
associativeMerge(Seq(obj1, obj2, obj3)) { merged =>
val resolved = SubstitutionResolver.resolveWithoutFallbacks(merged, merged) match {
val resolved = resolveNoSystem(merged, merged) match {
case x: ConfigObject => x
}
@ -340,7 +345,7 @@ class ConfigTest extends TestUtils {
val obj3 = parseObject("""{ "c" : { "z" : 3, "q" : 6 }, "j" : ${c} }""")
associativeMerge(Seq(obj1, obj2, obj3)) { merged =>
val resolved = SubstitutionResolver.resolveWithoutFallbacks(merged, merged) match {
val resolved = resolveNoSystem(merged, merged) match {
case x: ConfigObject => x
}
@ -360,7 +365,7 @@ class ConfigTest extends TestUtils {
val obj4 = parseObject("""{ "c" : { "z" : 4, "q" : 8 }, "j" : ${c} }""")
associativeMerge(Seq(obj1, obj2, obj3, obj4)) { merged =>
val resolved = SubstitutionResolver.resolveWithoutFallbacks(merged, merged) match {
val resolved = resolveNoSystem(merged, merged) match {
case x: ConfigObject => x
}

View File

@ -75,7 +75,7 @@ class ConfigValueTest extends TestUtils {
checkNotEqualObjects(a, c)
checkNotEqualObjects(b, c)
val root = a.asRoot()
val root = a.asRoot(path("foo"))
checkEqualObjects(a, root)
checkNotEqualObjects(root, b)
checkNotEqualObjects(root, c)

View File

@ -34,18 +34,20 @@ class EquivalentsTest extends TestUtils {
// for purposes of these tests, substitutions are only
// against the same file's root, and without looking at
// system prop or env variable fallbacks.
SubstitutionResolver.resolveWithoutFallbacks(v, v)
SubstitutionResolver.resolve(v, v, ConfigResolveOptions.noSystem())
case v =>
v
}
}
private def parse(flavor: SyntaxFlavor, f: File) = {
postParse(Parser.parse(flavor, f, includer()))
private def parse(flavor: ConfigSyntax, f: File) = {
val options = ConfigParseOptions.defaults().setSyntax(flavor)
postParse(Config.parse(f, options))
}
private def parse(f: File) = {
postParse(Parser.parse(f, includer()))
val options = ConfigParseOptions.defaults()
postParse(Config.parse(f, options))
}
// would like each "equivNN" directory to be a suite and each file in the dir
@ -75,7 +77,7 @@ class EquivalentsTest extends TestUtils {
// check that all .json files can be parsed as .conf,
// i.e. .conf must be a superset of JSON
if (testFile.getName().endsWith(".json")) {
val parsedAsConf = parse(SyntaxFlavor.CONF, testFile)
val parsedAsConf = parse(ConfigSyntax.CONF, testFile)
describeFailure(testFile.getPath() + " parsed as .conf") {
assertEquals(original, parsedAsConf)
}

View File

@ -12,11 +12,17 @@ import java.util.Collections
class JsonTest extends TestUtils {
def parse(s: String): ConfigValue = {
Parser.parse(SyntaxFlavor.JSON, new SimpleConfigOrigin("test json string"), s, includer())
val options = ConfigParseOptions.defaults().
setOriginDescription("test json string").
setSyntax(ConfigSyntax.JSON);
ConfigImpl.parseValue(Parseable.newString(s), options);
}
def parseAsConf(s: String): ConfigValue = {
Parser.parse(SyntaxFlavor.CONF, new SimpleConfigOrigin("test conf string"), s, includer())
val options = ConfigParseOptions.defaults().
setOriginDescription("test conf string").
setSyntax(ConfigSyntax.CONF);
ConfigImpl.parseValue(Parseable.newString(s), options);
}
private[this] def toLift(value: ConfigValue): lift.JValue = {

View File

@ -5,6 +5,9 @@ import org.junit._
import com.typesafe.config.ConfigOrigin
import java.io.Reader
import java.io.StringReader
import com.typesafe.config.ConfigParseOptions
import com.typesafe.config.Config
import com.typesafe.config.ConfigSyntax
abstract trait TestUtils {
protected def intercept[E <: Throwable: Manifest](block: => Unit): E = {
@ -319,7 +322,10 @@ abstract trait TestUtils {
protected def doubleValue(d: Double) = new ConfigDouble(fakeOrigin(), d, null)
protected def parseObject(s: String) = {
Parser.parse(SyntaxFlavor.CONF, new SimpleConfigOrigin("test string"), s, includer()).asInstanceOf[AbstractConfigObject]
val options = ConfigParseOptions.defaults().
setOriginDescription("test string").
setSyntax(ConfigSyntax.CONF);
Config.parse(new StringReader(s), options).asInstanceOf[AbstractConfigObject]
}
protected def subst(ref: String) = {
@ -354,7 +360,7 @@ abstract trait TestUtils {
def tokenKeySubstitution(s: String) = tokenSubstitution(tokenString(s))
def tokenize(origin: ConfigOrigin, input: Reader): java.util.Iterator[Token] = {
Tokenizer.tokenize(origin, input, SyntaxFlavor.CONF)
Tokenizer.tokenize(origin, input, ConfigSyntax.CONF)
}
def tokenize(input: Reader): java.util.Iterator[Token] = {