attempt to rationalize the loader/includer code

Introduce ConfigIncluder pluggable includer interface.

Move all the parse-something logic from Config, Loader,
ConfigImpl, Parser into the Parseable abstraction.
This commit is contained in:
Havoc Pennington 2011-11-14 15:42:50 -05:00
parent 9b3efbd2d0
commit d257c3bbd9
12 changed files with 598 additions and 314 deletions

View File

@ -14,6 +14,11 @@ import com.typesafe.config.impl.Parseable;
/**
* This class holds some global static methods for the config package.
*
* The methods with "load" in the name do some sort of higher-level operation
* potentially parsing multiple resources and resolving substitutions, while the
* ones with "parse" in the name just create a ConfigValue from a resource and
* nothing else.
*/
public final class Config {
@ -87,33 +92,24 @@ public final class Config {
public static ConfigObject parse(Properties properties,
ConfigParseOptions options) {
return ConfigImpl.parse(properties,
options.withFallbackOriginDescription("properties"));
return Parseable.newProperties(properties, options).parse();
}
public static ConfigObject parse(Reader reader, ConfigParseOptions options) {
Parseable p = Parseable.newReader(reader);
return ConfigImpl.parse(p,
options.withFallbackOriginDescription("Reader"));
return Parseable.newReader(reader, options).parse();
}
public static ConfigObject parse(URL url, ConfigParseOptions options) {
Parseable p = Parseable.newURL(url);
return ConfigImpl.parse(p,
options.withFallbackOriginDescription(url.toExternalForm()));
return Parseable.newURL(url, options).parse();
}
public static ConfigObject parse(File file, ConfigParseOptions options) {
Parseable p = Parseable.newFile(file);
return ConfigImpl.parse(p,
options.withFallbackOriginDescription(file.getPath()));
return Parseable.newFile(file, options).parse();
}
public static ConfigObject parse(Class<?> klass, String resource,
ConfigParseOptions options) {
Parseable p = Parseable.newResource(klass, resource);
return ConfigImpl.parse(p,
options.withFallbackOriginDescription(resource));
return Parseable.newResource(klass, resource, options).parse();
}
/**

View File

@ -0,0 +1,26 @@
package com.typesafe.config;
/**
* A ConfigIncludeContext is passed to a ConfigIncluder. This interface is not
* intended for apps to implement.
*/
public interface ConfigIncludeContext {
/**
* Tries to find a name relative to whatever is doing the including, for
* example in the same directory as the file doing the including. Returns
* null if it can't meaningfully create a relative name. The returned
* parseable may not exist; this function is not required to do any IO, just
* compute what the name would be.
*
* The passed-in filename has to be a complete name (with extension), not
* just a basename. (Include statements in config files are allowed to give
* just a basename.)
*
* @param filename
* the name to make relative to the resource doing the including
* @return parseable item relative to the resource doing the including, or
* null
*/
ConfigParseable relativeTo(String filename);
}

View File

@ -0,0 +1,37 @@
package com.typesafe.config;
/**
* Interface you have to implement to customize "include" statements in config
* files.
*/
public interface ConfigIncluder {
/**
* Returns a new includer that falls back to the given includer. This is how
* you can obtain the default includer; it will be provided as a fallback.
* It's up to your includer to chain to it if you want to. You might want to
* merge any files found by the fallback includer with any objects you load
* yourself.
*
* It's important to handle the case where you already have the fallback
* with a "return this", i.e. this method should not create a new object if
* the fallback is the same one you already have. The same fallback may be
* added repeatedly.
*
* @param fallback
* @return a new includer
*/
ConfigIncluder withFallback(ConfigIncluder fallback);
/**
* Parses another item to be included. The returned object typically would
* not have substitutions resolved. You can throw a ConfigException here to
* abort parsing, or return an empty object, but may not return null.
*
* @param context
* some info about the include context
* @param what
* the include statement's argument
* @return a non-null ConfigObject
*/
ConfigObject include(ConfigIncludeContext context, String what);
}

View File

@ -5,16 +5,18 @@ public final class ConfigParseOptions {
final ConfigSyntax syntax;
final String originDescription;
final boolean allowMissing;
final ConfigIncluder includer;
protected ConfigParseOptions(ConfigSyntax syntax, String originDescription,
boolean allowMissing) {
boolean allowMissing, ConfigIncluder includer) {
this.syntax = syntax;
this.originDescription = originDescription;
this.allowMissing = allowMissing;
this.includer = includer;
}
public static ConfigParseOptions defaults() {
return new ConfigParseOptions(null, null, true);
return new ConfigParseOptions(null, null, true, null);
}
/**
@ -29,7 +31,7 @@ public final class ConfigParseOptions {
return this;
else
return new ConfigParseOptions(syntax, this.originDescription,
this.allowMissing);
this.allowMissing, this.includer);
}
public ConfigSyntax getSyntax() {
@ -53,7 +55,7 @@ public final class ConfigParseOptions {
return this;
else
return new ConfigParseOptions(this.syntax, originDescription,
this.allowMissing);
this.allowMissing, this.includer);
}
public String getOriginDescription() {
@ -81,10 +83,47 @@ public final class ConfigParseOptions {
return this;
else
return new ConfigParseOptions(this.syntax, this.originDescription,
allowMissing);
allowMissing, this.includer);
}
public boolean getAllowMissing() {
return allowMissing;
}
/**
* Set a ConfigIncluder which customizes how includes are handled.
*
* @param includer
* @return new version of the parse options with different includer
*/
public ConfigParseOptions setIncluder(ConfigIncluder includer) {
if (this.includer == includer)
return this;
else
return new ConfigParseOptions(this.syntax, this.originDescription,
this.allowMissing, includer);
}
public ConfigParseOptions prependIncluder(ConfigIncluder includer) {
if (this.includer == includer)
return this;
else if (this.includer != null)
return setIncluder(includer.withFallback(this.includer));
else
return setIncluder(includer);
}
public ConfigParseOptions appendIncluder(ConfigIncluder includer) {
if (this.includer == includer)
return this;
else if (this.includer != null)
return setIncluder(this.includer.withFallback(includer));
else
return setIncluder(includer);
}
public ConfigIncluder getIncluder() {
return includer;
}
}

View File

@ -0,0 +1,20 @@
package com.typesafe.config;
import java.net.URL;
/** An opaque handle to something that can be parsed. */
public interface ConfigParseable {
/**
* Parse whatever it is.
*
* @param options
* parse options, should be based on the ones from options()
*/
ConfigObject parse(ConfigParseOptions options);
/** Possibly return a URL representing the resource; this may return null. */
URL url();
/** Get the initial options, which can be modified then passed to parse(). */
ConfigParseOptions options();
}

View File

@ -2,38 +2,65 @@ package com.typesafe.config.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigIncludeContext;
import com.typesafe.config.ConfigIncluder;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigParseable;
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 {
private static AbstractConfigObject forceParsedToObject(ConfigValue value) {
if (value instanceof AbstractConfigObject) {
return (AbstractConfigObject) value;
private interface NameSource {
ConfigParseable nameToParseable(String name);
}
// this function is a little tricky because there are two places we're
// trying to use it; for 'include "basename"' in a .conf file, and for
// loading app.{conf,json,properties} from classpath.
private static ConfigObject fromBasename(NameSource source, String name,
ConfigParseOptions options) {
ConfigObject obj;
if (name.endsWith(".conf") || name.endsWith(".json")
|| name.endsWith(".properties")) {
ConfigParseable p = source.nameToParseable(name);
if (p != null) {
obj = p.parse(p.options().setAllowMissing(
options.getAllowMissing()));
} else {
obj = SimpleConfigObject.emptyMissing(new SimpleConfigOrigin(
name));
}
} else {
throw new ConfigException.WrongType(value.origin(), "",
"object at file root", value.valueType().name());
ConfigParseable confHandle = source.nameToParseable(name + ".conf");
ConfigParseable jsonHandle = source.nameToParseable(name + ".json");
ConfigParseable propsHandle = source.nameToParseable(name
+ ".properties");
if (!options.getAllowMissing() && confHandle == null
&& jsonHandle == null && propsHandle == null) {
throw new ConfigException.IO(new SimpleConfigOrigin(name),
"No config files {.conf,.json,.properties} found");
}
obj = SimpleConfigObject.empty(new SimpleConfigOrigin(name));
if (confHandle != null)
obj = confHandle.parse(confHandle.options()
.setAllowMissing(true).setSyntax(ConfigSyntax.CONF));
if (jsonHandle != null)
obj = obj.withFallback(jsonHandle.parse(jsonHandle.options()
.setAllowMissing(true).setSyntax(ConfigSyntax.JSON)));
if (propsHandle != null)
obj = obj.withFallback(propsHandle.parse(propsHandle.options()
.setAllowMissing(true)
.setSyntax(ConfigSyntax.PROPERTIES)));
}
}
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));
return obj;
}
private static String makeResourceBasename(Path path) {
@ -54,108 +81,19 @@ public class ConfigImpl {
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) {
final 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");
NameSource source = new NameSource() {
@Override
public ConfigParseable nameToParseable(String name) {
return Parseable.newResource(ConfigImpl.class, name,
baseOptions);
}
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;
}
};
return fromBasename(source, basename, baseOptions);
}
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
@ -179,13 +117,6 @@ public class ConfigImpl {
}
}
/** 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() {
@ -195,17 +126,56 @@ public class ConfigImpl {
return defaultTransformer;
}
private static IncludeHandler defaultIncluder = null;
private static class SimpleIncluder implements ConfigIncluder {
synchronized static IncludeHandler defaultIncluder() {
if (defaultIncluder == null) {
defaultIncluder = new IncludeHandler() {
private ConfigIncluder fallback;
SimpleIncluder(ConfigIncluder fallback) {
this.fallback = fallback;
}
@Override
public ConfigObject include(final ConfigIncludeContext context,
String name) {
NameSource source = new NameSource() {
@Override
public AbstractConfigObject include(String name) {
return Loader.load(name, this);
public ConfigParseable nameToParseable(String name) {
return context.relativeTo(name);
}
};
ConfigObject obj = fromBasename(source, name, ConfigParseOptions
.defaults().setAllowMissing(true));
// now use the fallback includer if any and merge
// its result.
if (fallback != null) {
return obj.withFallback(fallback.include(context, name));
} else {
return obj;
}
}
@Override
public ConfigIncluder withFallback(ConfigIncluder fallback) {
if (this == fallback) {
throw new ConfigException.BugOrBroken(
"trying to create includer cycle");
} else if (this.fallback == fallback) {
return this;
} else if (this.fallback != null) {
return new SimpleIncluder(this.fallback.withFallback(fallback));
} else {
return new SimpleIncluder(fallback);
}
}
}
private static ConfigIncluder defaultIncluder = null;
synchronized static ConfigIncluder defaultIncluder() {
if (defaultIncluder == null) {
defaultIncluder = new SimpleIncluder(null);
}
return defaultIncluder;
}
@ -221,7 +191,10 @@ public class ConfigImpl {
}
private static AbstractConfigObject loadSystemProperties() {
return Loader.fromProperties("system property", System.getProperties());
return (AbstractConfigObject) Parseable.newProperties(
System.getProperties(),
ConfigParseOptions.defaults().setOriginDescription(
"system properties")).parse();
}
// this is a hack to let us set system props in the test suite

View File

@ -1,11 +0,0 @@
package com.typesafe.config.impl;
/**
* This is sort of a placeholder so that something per-config-load is passed in
* to the parser to handle includes. The eventual idea is to let apps customize
* how an included name gets searched for, which would involve some nicer
* interface in the public API.
*/
interface IncludeHandler {
AbstractConfigObject include(String name);
}

View File

@ -10,19 +10,64 @@ import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Iterator;
import java.util.Properties;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigIncludeContext;
import com.typesafe.config.ConfigObject;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigParseable;
import com.typesafe.config.ConfigSyntax;
import com.typesafe.config.ConfigValue;
/**
* 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.
* have just one overload that takes a Parseable. Also it's used as an abstract
* "resource handle" in the ConfigIncluder interface.
*/
public abstract class Parseable {
public abstract class Parseable implements ConfigParseable {
private ConfigIncludeContext includeContext;
private ConfigParseOptions options;
protected Parseable() {
}
private ConfigParseOptions fixupOptions(ConfigParseOptions baseOptions) {
ConfigSyntax syntax = baseOptions.getSyntax();
if (syntax == null) {
syntax = guessSyntax();
}
if (syntax == null) {
syntax = ConfigSyntax.CONF;
}
ConfigParseOptions modified = baseOptions.setSyntax(syntax);
if (modified.getOriginDescription() == null)
modified = modified.setOriginDescription(originDescription());
modified = modified.appendIncluder(ConfigImpl.defaultIncluder());
return modified;
}
protected void postConstruct(ConfigParseOptions baseOptions) {
this.options = fixupOptions(baseOptions);
this.includeContext = new ConfigIncludeContext() {
@Override
public ConfigParseable relativeTo(String filename) {
return Parseable.this.relativeTo(filename);
}
};
}
// the general idea is that any work should be in here, not in the
@ -32,12 +77,90 @@ public abstract class Parseable {
// 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;
protected abstract Reader reader() throws IOException;
ConfigSyntax guessSyntax() {
return null;
}
ConfigParseable relativeTo(String filename) {
return null;
}
ConfigIncludeContext includeContext() {
return includeContext;
}
static AbstractConfigObject forceParsedToObject(ConfigValue value) {
if (value instanceof AbstractConfigObject) {
return (AbstractConfigObject) value;
} else {
throw new ConfigException.WrongType(value.origin(), "",
"object at file root", value.valueType().name());
}
}
@Override
public ConfigObject parse(ConfigParseOptions baseOptions) {
return forceParsedToObject(parseValue(baseOptions));
}
AbstractConfigValue parseValue(ConfigParseOptions baseOptions) {
// note that we are NOT using our "options" and "origin" fields,
// but using the ones from the passed-in options. The idea is that
// callers can get our original options and then parse with different
// ones if they want.
ConfigParseOptions options = fixupOptions(baseOptions);
ConfigOrigin origin = new SimpleConfigOrigin(
options.getOriginDescription());
return parseValue(origin, options);
}
protected AbstractConfigValue parseValue(ConfigOrigin origin,
ConfigParseOptions finalOptions) {
try {
Reader reader = reader();
try {
if (options.getSyntax() == ConfigSyntax.PROPERTIES) {
return PropertiesParser.parse(reader, origin);
} else {
Iterator<Token> tokens = Tokenizer.tokenize(origin, reader,
options.getSyntax());
return Parser.parse(tokens, origin, options,
includeContext());
}
} finally {
reader.close();
}
} catch (IOException e) {
if (options.getAllowMissing()) {
return SimpleConfigObject.emptyMissing(origin);
} else {
throw new ConfigException.IO(origin, e.getMessage(), e);
}
}
}
public ConfigObject parse() {
return forceParsedToObject(parseValue(options()));
}
AbstractConfigValue parseValue() {
return parseValue(options());
}
abstract String originDescription();
@Override
public URL url() {
return null;
}
@Override
public ConfigParseOptions options() {
return options;
}
@Override
public String toString() {
return getClass().getSimpleName();
@ -77,74 +200,130 @@ public abstract class Parseable {
};
}
private static URL urlParent(URL url) {
String path = url.getPath();
if (path == null)
return null;
File f = new File(path);
String parent = f.getParent();
try {
return new URL(url.getProtocol(), url.getHost(), url.getPort(),
parent);
} catch (MalformedURLException e) {
return null;
}
}
static URL relativeTo(URL url, String filename) {
// I'm guessing this completely fails on Windows, help wanted
if (new File(filename).isAbsolute())
return null;
URL parentURL = urlParent(url);
if (parentURL == null)
return null;
try {
URI parent = parentURL.toURI();
URI relative = new URI(null, null, "/" + filename, null);
return parent.relativize(relative).toURL();
} catch (MalformedURLException e) {
return null;
} catch (URISyntaxException e) {
return null;
}
}
private final static class ParseableInputStream extends Parseable {
final private InputStream input;
ParseableInputStream(InputStream input) {
ParseableInputStream(InputStream input, ConfigParseOptions options) {
this.input = input;
postConstruct(options);
}
@Override
Reader reader() {
protected Reader reader() {
return doNotClose(readerFromStream(input));
}
@Override
String originDescription() {
return "InputStream";
}
}
/**
* 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);
public static Parseable newInputStream(InputStream input,
ConfigParseOptions options) {
return new ParseableInputStream(input, options);
}
private final static class ParseableReader extends Parseable {
final private Reader reader;
ParseableReader(Reader reader) {
ParseableReader(Reader reader, ConfigParseOptions options) {
this.reader = reader;
postConstruct(options);
}
@Override
Reader reader() {
protected Reader reader() {
return reader;
}
@Override
String originDescription() {
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));
public static Parseable newReader(Reader reader, ConfigParseOptions options) {
return new ParseableReader(doNotClose(reader), options);
}
private final static class ParseableString extends Parseable {
final private String input;
ParseableString(String input) {
ParseableString(String input, ConfigParseOptions options) {
this.input = input;
postConstruct(options);
}
@Override
Reader reader() {
protected Reader reader() {
return new StringReader(input);
}
@Override
String originDescription() {
return "String";
}
}
public static Parseable newString(String input) {
return new ParseableString(input);
public static Parseable newString(String input, ConfigParseOptions options) {
return new ParseableString(input, options);
}
private final static class ParseableURL extends Parseable {
final private URL input;
ParseableURL(URL input) {
ParseableURL(URL input, ConfigParseOptions options) {
this.input = input;
postConstruct(options);
}
@Override
Reader reader() throws IOException {
protected Reader reader() throws IOException {
InputStream stream = input.openStream();
return readerFromStream(stream);
}
@ -154,6 +333,22 @@ public abstract class Parseable {
return syntaxFromExtension(input.getPath());
}
@Override
ConfigParseable relativeTo(String filename) {
return newURL(relativeTo(input, filename), options()
.setOriginDescription(null));
}
@Override
String originDescription() {
return input.toExternalForm();
}
@Override
public URL url() {
return input;
}
@Override
public String toString() {
return getClass().getSimpleName() + "(" + input.toExternalForm()
@ -161,19 +356,20 @@ public abstract class Parseable {
}
}
public static Parseable newURL(URL input) {
return new ParseableURL(input);
public static Parseable newURL(URL input, ConfigParseOptions options) {
return new ParseableURL(input, options);
}
private final static class ParseableFile extends Parseable {
final private File input;
ParseableFile(File input) {
ParseableFile(File input, ConfigParseOptions options) {
this.input = input;
postConstruct(options);
}
@Override
Reader reader() throws IOException {
protected Reader reader() throws IOException {
InputStream stream = new FileInputStream(input);
return readerFromStream(stream);
}
@ -183,28 +379,58 @@ public abstract class Parseable {
return syntaxFromExtension(input.getName());
}
@Override
ConfigParseable relativeTo(String filename) {
try {
return newURL(relativeTo(input.toURI().toURL(), filename),
options().setOriginDescription(null));
} catch (MalformedURLException e) {
return null;
}
}
@Override
String originDescription() {
return input.getPath();
}
@Override
public URL url() {
try {
return input.toURI().toURL();
} catch (MalformedURLException e) {
return null;
}
}
@Override
public String toString() {
return getClass().getSimpleName() + "(" + input.getPath() + ")";
}
}
public static Parseable newFile(File input) {
return new ParseableFile(input);
public static Parseable newFile(File input, ConfigParseOptions options) {
return new ParseableFile(input, options);
}
private final static class ParseableResource extends Parseable {
final private Class<?> klass;
final private String resource;
ParseableResource(Class<?> klass, String resource) {
ParseableResource(Class<?> klass, String resource,
ConfigParseOptions options) {
this.klass = klass;
this.resource = resource;
postConstruct(options);
}
@Override
Reader reader() throws IOException {
protected Reader reader() throws IOException {
InputStream stream = klass.getResourceAsStream(resource);
if (stream == null) {
throw new IOException("resource not found on classpath: "
+ resource);
}
return readerFromStream(stream);
}
@ -213,6 +439,37 @@ public abstract class Parseable {
return syntaxFromExtension(resource);
}
@Override
ConfigParseable relativeTo(String filename) {
// not using File.isAbsolute because resource paths always use '/'
// (?)
if (filename.startsWith("/"))
return null;
// here we want to build a new resource name and let
// the class loader have it, rather than getting the
// url with getResource() and relativizing to that url.
// This is needed in case the class loader is going to
// search a classpath.
File parent = new File(resource).getParentFile();
if (parent == null)
return newResource(klass, "/" + filename, options()
.setOriginDescription(null));
else
return newResource(klass, new File(parent, filename).getPath(),
options().setOriginDescription(null));
}
@Override
String originDescription() {
return resource + " on classpath";
}
@Override
public URL url() {
return klass.getResource(resource);
}
@Override
public String toString() {
return getClass().getSimpleName() + "(" + resource + ","
@ -221,7 +478,54 @@ public abstract class Parseable {
}
}
public static Parseable newResource(Class<?> klass, String resource) {
return new ParseableResource(klass, resource);
public static Parseable newResource(Class<?> klass, String resource,
ConfigParseOptions options) {
return new ParseableResource(klass, resource, options);
}
private final static class ParseableProperties extends Parseable {
final private Properties props;
ParseableProperties(Properties props, ConfigParseOptions options) {
this.props = props;
postConstruct(options);
}
@Override
protected Reader reader() throws IOException {
throw new ConfigException.BugOrBroken(
"reader() should not be called on props");
}
@Override
protected AbstractConfigObject parseValue(ConfigOrigin origin,
ConfigParseOptions finalOptions) {
return PropertiesParser.fromProperties(origin, props);
}
@Override
ConfigSyntax guessSyntax() {
return ConfigSyntax.PROPERTIES;
}
@Override
String originDescription() {
return "properties";
}
@Override
public URL url() {
return null;
}
@Override
public String toString() {
return getClass().getSimpleName() + "(" + props.size() + " props)";
}
}
public static Parseable newProperties(Properties properties,
ConfigParseOptions options) {
return new ParseableProperties(properties, options);
}
}

View File

@ -1,7 +1,5 @@
package com.typesafe.config.impl;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
@ -14,6 +12,8 @@ import java.util.Map;
import java.util.Stack;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigIncludeContext;
import com.typesafe.config.ConfigIncluder;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigSyntax;
@ -21,45 +21,11 @@ import com.typesafe.config.ConfigValueType;
final class Parser {
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();
}
if (syntax == null) {
syntax = ConfigSyntax.CONF;
}
ConfigParseOptions options = baseOptions.setSyntax(syntax);
try {
Reader reader = input.reader();
try {
Iterator<Token> tokens = Tokenizer.tokenize(origin, reader,
syntax);
return parse(tokens, origin, options, includer);
} finally {
reader.close();
}
} catch (IOException e) {
if (options.getAllowMissing()) {
return SimpleConfigObject.emptyMissing(origin);
} else {
throw new ConfigException.IO(origin, e.getMessage(), e);
}
}
}
private static AbstractConfigValue parse(Iterator<Token> tokens,
static AbstractConfigValue parse(Iterator<Token> tokens,
ConfigOrigin origin, ConfigParseOptions options,
IncludeHandler includer) {
ConfigIncludeContext includeContext) {
ParseContext context = new ParseContext(options.getSyntax(), origin,
tokens, includer);
tokens, options.getIncluder(), includeContext);
return context.parse();
}
@ -67,19 +33,22 @@ final class Parser {
private int lineNumber;
final private Stack<Token> buffer;
final private Iterator<Token> tokens;
final private IncludeHandler includer;
final private ConfigIncluder includer;
final private ConfigIncludeContext includeContext;
final private ConfigSyntax flavor;
final private ConfigOrigin baseOrigin;
final private LinkedList<Path> pathStack;
ParseContext(ConfigSyntax flavor, ConfigOrigin origin,
Iterator<Token> tokens, IncludeHandler includer) {
Iterator<Token> tokens, ConfigIncluder includer,
ConfigIncludeContext includeContext) {
lineNumber = 0;
buffer = new Stack<Token>();
this.tokens = tokens;
this.flavor = flavor;
this.baseOrigin = origin;
this.includer = includer;
this.includeContext = includeContext;
this.pathStack = new LinkedList<Path>();
}
@ -349,7 +318,8 @@ final class Parser {
if (Tokens.isValueWithType(t, ConfigValueType.STRING)) {
String name = (String) Tokens.getValue(t).unwrapped();
AbstractConfigObject obj = includer.include(name);
AbstractConfigObject obj = (AbstractConfigObject) includer
.include(includeContext, name);
if (!pathStack.isEmpty()) {
Path prefix = new Path(pathStack);

View File

@ -1,9 +1,7 @@
package com.typesafe.config.impl;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
@ -14,78 +12,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>();
// if name has an extension, only use that; otherwise merge all three
if (name.endsWith(".conf") || name.endsWith(".json")
|| name.endsWith(".properties")) {
addResource(name, includer, stack);
} else {
// .conf wins over .json wins over .properties;
// arbitrary, but deterministic
addResource(name + ".conf", includer, stack);
addResource(name + ".json", includer, stack);
addResource(name + ".properties", includer, stack);
}
AbstractConfigObject merged = AbstractConfigObject.merge(stack);
return merged;
}
private static void addResource(String name, IncludeHandler includer,
List<AbstractConfigObject> stack) {
URL url = ConfigImpl.class.getResource("/" + name);
if (url != null) {
stack.add(loadURL(url, includer));
}
}
private static AbstractConfigObject loadURL(URL url, IncludeHandler includer) {
if (url.getPath().endsWith(".properties")) {
ConfigOrigin origin = new SimpleConfigOrigin(url.toExternalForm());
Properties props = new Properties();
InputStream stream = null;
try {
stream = url.openStream();
stream = new BufferedInputStream(stream);
props.load(stream);
} catch (IOException e) {
throw new ConfigException.IO(origin, "failed to open url", e);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
}
}
}
return fromProperties(url.toExternalForm(), props);
} else {
return forceParsedToObject(Parser.parse(Parseable.newURL(url),
new SimpleConfigOrigin(url.toExternalForm()),
ConfigParseOptions.defaults(),
includer));
}
}
private static AbstractConfigObject forceParsedToObject(
AbstractConfigValue value) {
if (value instanceof AbstractConfigObject) {
return (AbstractConfigObject) value;
} else {
throw new ConfigException.WrongType(value.origin(), "",
"object at file root", value.valueType().name());
}
final class PropertiesParser {
static AbstractConfigObject parse(Reader reader,
ConfigOrigin origin) throws IOException {
Properties props = new Properties();
props.load(reader);
return fromProperties(origin, props);
}
static void verifyPath(String path) {
@ -116,7 +49,7 @@ final class Loader {
return path.substring(0, i);
}
static AbstractConfigObject fromProperties(String originPrefix,
static AbstractConfigObject fromProperties(ConfigOrigin origin,
Properties props) {
Map<String, Map<String, AbstractConfigValue>> scopes = new HashMap<String, Map<String, AbstractConfigValue>>();
Enumeration<?> i = props.propertyNames();
@ -136,8 +69,7 @@ final class Loader {
scopes.put(exceptLast, scope);
}
String value = props.getProperty(path);
scope.put(last, new ConfigString(new SimpleConfigOrigin(
originPrefix + " " + path), value));
scope.put(last, new ConfigString(origin, value));
} catch (ConfigException.BadPath e) {
// just skip this one (log it?)
}
@ -169,8 +101,7 @@ final class Loader {
// Also we assume here that any info based on the map that
// SimpleConfigObject computes and caches in its constructor
// will not change. Basically this is a bad hack.
AbstractConfigObject o = new SimpleConfigObject(
new SimpleConfigOrigin(originPrefix + " " + path),
AbstractConfigObject o = new SimpleConfigObject(origin,
scopes.get(path), ResolveStatus.RESOLVED);
String basename = lastElement(path);
parent.put(basename, o);
@ -184,7 +115,6 @@ final class Loader {
}
// return root config object
return new SimpleConfigObject(new SimpleConfigOrigin(originPrefix),
root, ResolveStatus.RESOLVED);
return new SimpleConfigObject(origin, root, ResolveStatus.RESOLVED);
}
}

View File

@ -14,7 +14,7 @@ class ConfParserTest extends TestUtils {
val options = ConfigParseOptions.defaults().
setOriginDescription("test conf string").
setSyntax(ConfigSyntax.CONF);
ConfigImpl.parseValue(Parseable.newString(s), options);
Parseable.newString(s, options).parseValue().asInstanceOf[AbstractConfigValue]
}
def parse(s: String) = {

View File

@ -15,14 +15,14 @@ class JsonTest extends TestUtils {
val options = ConfigParseOptions.defaults().
setOriginDescription("test json string").
setSyntax(ConfigSyntax.JSON);
ConfigImpl.parseValue(Parseable.newString(s), options);
Parseable.newString(s, options).parseValue();
}
def parseAsConf(s: String): ConfigValue = {
val options = ConfigParseOptions.defaults().
setOriginDescription("test conf string").
setSyntax(ConfigSyntax.CONF);
ConfigImpl.parseValue(Parseable.newString(s), options);
Parseable.newString(s, options).parseValue();
}
private[this] def toLift(value: ConfigValue): lift.JValue = {