add methods to ConfigOrigin for filename, resource, url, and lineNumber

Play wanted to use these to jump to the problematic spot in a
config file when an exception occurs.
This commit is contained in:
Havoc Pennington 2011-11-25 14:04:05 -05:00
parent 2e283367de
commit 21d9b8b358
14 changed files with 303 additions and 70 deletions

View File

@ -9,9 +9,12 @@ package com.typesafe.config;
public class ConfigException extends RuntimeException {
private static final long serialVersionUID = 1L;
final private ConfigOrigin origin;
protected ConfigException(ConfigOrigin origin, String message,
Throwable cause) {
super(origin.description() + ": " + message, cause);
this.origin = origin;
}
protected ConfigException(ConfigOrigin origin, String message) {
@ -20,12 +23,26 @@ public class ConfigException extends RuntimeException {
protected ConfigException(String message, Throwable cause) {
super(message, cause);
this.origin = null;
}
protected ConfigException(String message) {
this(message, null);
}
/**
* Returns an "origin" (such as a filename and line number) for the
* exception, or null if none is available. If there's no sensible origin
* for a given exception, or the kind of exception doesn't meaningfully
* relate to a particular origin file, this returns null. Never assume this
* will return non-null, it can always return null.
*
* @return origin of the problem, or null if unknown/inapplicable
*/
public ConfigOrigin origin() {
return origin;
}
/**
* Exception indicating that the type of a value does not match the type you
* requested.

View File

@ -3,10 +3,21 @@
*/
package com.typesafe.config;
import java.net.URL;
/**
* Represents the origin (such as filename and line number) of a
* {@link ConfigValue} for use in error messages. Obtain the origin of a value
* with {@link ConfigValue#origin}.
* with {@link ConfigValue#origin}. Exceptions may have an origin, see
* {@link ConfigException#origin}, but be careful because
* <code>ConfigException.origin()</code> may return null.
*
* <p>
* It's best to use this interface only for debugging; its accuracy is
* "best effort" rather than guaranteed, and a potentially-noticeable amount of
* memory could probably be saved if origins were not kept around, so in the
* future there might be some option to discard origins.
*
* <p>
* <em>Do not implement this interface</em>; it should only be implemented by
@ -16,5 +27,43 @@ package com.typesafe.config;
* implementations will break.
*/
public interface ConfigOrigin {
/**
* Returns a string describing the origin of a value or exception. This will
* never return null.
*
* @return string describing the origin
*/
public String description();
/**
* Returns a filename describing the origin. This will return null if the
* origin was not a file.
*
* @return filename of the origin or null
*/
public String filename();
/**
* Returns a URL describing the origin. This will return null if the origin
* has no meaningful URL.
*
* @return url of the origin or null
*/
public URL url();
/**
* Returns a classpath resource name describing the origin. This will return
* null if the origin was not a classpath resource.
*
* @return resource name of the origin or null
*/
public String resource();
/**
* Returns a line number where the value or exception originated. This will
* return -1 if there's no meaningful line number.
*
* @return line number or -1 if none is available
*/
public int lineNumber();
}

View File

@ -164,18 +164,13 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
if (stack.isEmpty())
throw new ConfigException.BugOrBroken(
"can't merge origins on empty list");
final String prefix = "merge of ";
StringBuilder sb = new StringBuilder();
List<ConfigOrigin> origins = new ArrayList<ConfigOrigin>();
ConfigOrigin firstOrigin = null;
int numMerged = 0;
for (AbstractConfigValue v : stack) {
if (firstOrigin == null)
firstOrigin = v.origin();
String desc = v.origin().description();
if (desc.startsWith(prefix))
desc = desc.substring(prefix.length());
if (v instanceof AbstractConfigObject
&& ((AbstractConfigObject) v).resolveStatus() == ResolveStatus.RESOLVED
&& ((ConfigObject) v).isEmpty()) {
@ -183,22 +178,17 @@ abstract class AbstractConfigObject extends AbstractConfigValue implements
// config in the description, since they are
// likely to be "implementation details"
} else {
sb.append(desc);
sb.append(",");
origins.add(v.origin());
numMerged += 1;
}
}
if (numMerged > 0) {
sb.setLength(sb.length() - 1); // chop comma
if (numMerged > 1) {
return new SimpleConfigOrigin(prefix + sb.toString());
} else {
return new SimpleConfigOrigin(sb.toString());
}
} else {
// the configs were all empty.
return firstOrigin;
if (numMerged == 0) {
// the configs were all empty, so just use the first one
origins.add(firstOrigin);
}
return SimpleConfigOrigin.mergeOrigins(origins);
}
static ConfigOrigin mergeOrigins(AbstractConfigObject... stack) {

View File

@ -44,8 +44,7 @@ public class ConfigImpl {
obj = p.parse(p.options().setAllowMissing(
options.getAllowMissing()));
} else {
obj = SimpleConfigObject.emptyMissing(new SimpleConfigOrigin(
name));
obj = SimpleConfigObject.emptyMissing(SimpleConfigOrigin.newSimple(name));
}
} else {
ConfigParseable confHandle = source.nameToParseable(name + ".conf");
@ -55,13 +54,13 @@ public class ConfigImpl {
if (!options.getAllowMissing() && confHandle == null
&& jsonHandle == null && propsHandle == null) {
throw new ConfigException.IO(new SimpleConfigOrigin(name),
throw new ConfigException.IO(SimpleConfigOrigin.newSimple(name),
"No config files {.conf,.json,.properties} found");
}
ConfigSyntax syntax = options.getSyntax();
obj = SimpleConfigObject.empty(new SimpleConfigOrigin(name));
obj = SimpleConfigObject.empty(SimpleConfigOrigin.newSimple(name));
if (confHandle != null
&& (syntax == null || syntax == ConfigSyntax.CONF)) {
obj = confHandle.parse(confHandle.options()
@ -139,8 +138,8 @@ public class ConfigImpl {
}
static AbstractConfigObject emptyObject(String originDescription) {
ConfigOrigin origin = originDescription != null ? new SimpleConfigOrigin(
originDescription) : null;
ConfigOrigin origin = originDescription != null ? SimpleConfigOrigin
.newSimple(originDescription) : null;
return emptyObject(origin);
}
@ -154,8 +153,8 @@ public class ConfigImpl {
}
// default origin for values created with fromAnyRef and no origin specified
final private static ConfigOrigin defaultValueOrigin = new SimpleConfigOrigin(
"hardcoded value");
final private static ConfigOrigin defaultValueOrigin = SimpleConfigOrigin
.newSimple("hardcoded value");
final private static ConfigBoolean defaultTrueValue = new ConfigBoolean(
defaultValueOrigin, true);
final private static ConfigBoolean defaultFalseValue = new ConfigBoolean(
@ -188,7 +187,7 @@ public class ConfigImpl {
if (originDescription == null)
return defaultValueOrigin;
else
return new SimpleConfigOrigin(originDescription);
return SimpleConfigOrigin.newSimple(originDescription);
}
/** For use ONLY by library internals, DO NOT TOUCH not guaranteed ABI */
@ -386,10 +385,11 @@ public class ConfigImpl {
Map<String, AbstractConfigValue> m = new HashMap<String, AbstractConfigValue>();
for (Map.Entry<String, String> entry : env.entrySet()) {
String key = entry.getKey();
m.put(key, new ConfigString(
new SimpleConfigOrigin("env var " + key), entry.getValue()));
m.put(key,
new ConfigString(SimpleConfigOrigin.newSimple("env var " + key), entry
.getValue()));
}
return new SimpleConfigObject(new SimpleConfigOrigin("env variables"),
return new SimpleConfigObject(SimpleConfigOrigin.newSimple("env variables"),
m, ResolveStatus.RESOLVED, false /* ignoresFallbacks */);
}

View File

@ -0,0 +1,8 @@
package com.typesafe.config.impl;
enum OriginType {
GENERIC,
FILE,
URL,
RESOURCE
}

View File

@ -54,9 +54,6 @@ public abstract class Parseable implements ConfigParseable {
}
ConfigParseOptions modified = baseOptions.setSyntax(syntax);
if (modified.getOriginDescription() == null)
modified = modified.setOriginDescription(originDescription());
modified = modified.appendIncluder(ConfigImpl.defaultIncluder());
return modified;
@ -109,13 +106,18 @@ public abstract class Parseable implements ConfigParseable {
}
AbstractConfigValue parseValue(ConfigParseOptions baseOptions) {
// note that we are NOT using our "options" and "origin" fields,
// note that we are NOT using our "initialOptions",
// 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());
// passed-in options can override origin
ConfigOrigin origin;
if (options.getOriginDescription() != null)
origin = SimpleConfigOrigin.newSimple(options.getOriginDescription());
else
origin = origin();
return parseValue(origin, options);
}
@ -152,7 +154,7 @@ public abstract class Parseable implements ConfigParseable {
return parseValue(options());
}
abstract String originDescription();
abstract ConfigOrigin origin();
@Override
public URL url() {
@ -242,8 +244,8 @@ public abstract class Parseable implements ConfigParseable {
}
@Override
String originDescription() {
return "Reader";
ConfigOrigin origin() {
return SimpleConfigOrigin.newSimple("Reader");
}
}
@ -269,8 +271,8 @@ public abstract class Parseable implements ConfigParseable {
}
@Override
String originDescription() {
return "String";
ConfigOrigin origin() {
return SimpleConfigOrigin.newSimple("String");
}
}
@ -307,8 +309,8 @@ public abstract class Parseable implements ConfigParseable {
}
@Override
String originDescription() {
return input.toExternalForm();
ConfigOrigin origin() {
return SimpleConfigOrigin.newURL(input);
}
@Override
@ -359,8 +361,8 @@ public abstract class Parseable implements ConfigParseable {
}
@Override
String originDescription() {
return input.getPath();
ConfigOrigin origin() {
return SimpleConfigOrigin.newFile(input.getPath());
}
@Override
@ -430,8 +432,8 @@ public abstract class Parseable implements ConfigParseable {
}
@Override
String originDescription() {
return resource + " on classpath";
ConfigOrigin origin() {
return SimpleConfigOrigin.newResource(resource);
}
@Override
@ -478,8 +480,8 @@ public abstract class Parseable implements ConfigParseable {
}
@Override
String originDescription() {
return "properties";
ConfigOrigin origin() {
return SimpleConfigOrigin.newSimple("properties");
}
@Override

View File

@ -219,8 +219,7 @@ final class Parser {
}
private ConfigOrigin lineOrigin() {
return new SimpleConfigOrigin(baseOrigin.description() + ": line "
+ lineNumber);
return ((SimpleConfigOrigin) baseOrigin).addLineNumber(lineNumber);
}
private ConfigException parseError(String message) {
@ -681,7 +680,7 @@ final class Parser {
return pb.result();
}
static ConfigOrigin apiOrigin = new SimpleConfigOrigin("path parameter");
static ConfigOrigin apiOrigin = SimpleConfigOrigin.newSimple("path parameter");
static Path parsePath(String path) {
Path speculated = speculativeFastParsePath(path);

View File

@ -112,8 +112,8 @@ final class SimpleConfigObject extends AbstractConfigObject {
}
final private static String EMPTY_NAME = "empty config";
final private static SimpleConfigObject emptyInstance = empty(new SimpleConfigOrigin(
EMPTY_NAME));
final private static SimpleConfigObject emptyInstance = empty(SimpleConfigOrigin
.newSimple(EMPTY_NAME));
final static SimpleConfigObject empty() {
return emptyInstance;
@ -128,7 +128,7 @@ final class SimpleConfigObject extends AbstractConfigObject {
}
final static SimpleConfigObject emptyMissing(ConfigOrigin baseOrigin) {
return new SimpleConfigObject(new SimpleConfigOrigin(
return new SimpleConfigObject(SimpleConfigOrigin.newSimple(
baseOrigin.description() + " (not found)"),
Collections.<String, AbstractConfigValue> emptyMap());
}

View File

@ -3,26 +3,62 @@
*/
package com.typesafe.config.impl;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
final class SimpleConfigOrigin implements ConfigOrigin {
final private String description;
final private int lineNumber;
final private OriginType originType;
SimpleConfigOrigin(String description) {
private SimpleConfigOrigin(String description, int lineNumber, OriginType originType) {
this.lineNumber = lineNumber;
this.originType = originType;
this.description = description;
}
static SimpleConfigOrigin newSimple(String description) {
return new SimpleConfigOrigin(description, -1, OriginType.GENERIC);
}
static SimpleConfigOrigin newFile(String filename) {
return new SimpleConfigOrigin(filename, -1, OriginType.FILE);
}
static SimpleConfigOrigin newURL(URL url) {
return new SimpleConfigOrigin(url.toExternalForm(), -1, OriginType.URL);
}
static SimpleConfigOrigin newResource(String resource) {
return new SimpleConfigOrigin(resource, -1, OriginType.RESOURCE);
}
// important, this should also be able to _change_ an existing line
// number
SimpleConfigOrigin addLineNumber(int lineNumber) {
return new SimpleConfigOrigin(this.description, lineNumber, this.originType);
}
@Override
public String description() {
return description;
if (lineNumber < 0) {
return description;
} else {
return description + ": " + lineNumber;
}
}
@Override
public boolean equals(Object other) {
if (other instanceof SimpleConfigOrigin) {
return this.description
.equals(((SimpleConfigOrigin) other).description);
// two origins are equal if they are described to the user in the
// same way, for now at least this seems fine
return this.description.equals(((SimpleConfigOrigin) other).description);
} else {
return false;
}
@ -37,4 +73,81 @@ final class SimpleConfigOrigin implements ConfigOrigin {
public String toString() {
return "ConfigOrigin(" + description + ")";
}
@Override
public String filename() {
if (originType == OriginType.FILE) {
return description;
} else if (originType == OriginType.URL) {
URL url;
try {
url = new URL(description);
} catch (MalformedURLException e) {
return null;
}
if (url.getProtocol().equals("file")) {
return url.getFile();
} else {
return null;
}
} else {
return null;
}
}
@Override
public URL url() {
if (originType == OriginType.URL) {
try {
return new URL(description);
} catch (MalformedURLException e) {
return null;
}
} else if (originType == OriginType.FILE) {
try {
return (new File(description)).toURI().toURL();
} catch (MalformedURLException e) {
return null;
}
} else {
return null;
}
}
@Override
public String resource() {
if (originType == OriginType.RESOURCE) {
return description;
} else {
return null;
}
}
@Override
public int lineNumber() {
return lineNumber;
}
static final String MERGE_OF_PREFIX = "merge of ";
static ConfigOrigin mergeOrigins(Collection<? extends ConfigOrigin> stack) {
if (stack.isEmpty()) {
throw new ConfigException.BugOrBroken("can't merge empty list of origins");
} else if (stack.size() == 1) {
return stack.iterator().next();
} else {
StringBuilder sb = new StringBuilder();
for (ConfigOrigin o : stack) {
String desc = o.description();
if (desc.startsWith(MERGE_OF_PREFIX))
desc = desc.substring(MERGE_OF_PREFIX.length());
sb.append(desc);
sb.append(",");
}
sb.setLength(sb.length() - 1); // chop comma
return newSimple(MERGE_OF_PREFIX + sb.toString());
}
}
}

View File

@ -219,8 +219,7 @@ final class Tokenizer {
private static ConfigOrigin lineOrigin(ConfigOrigin baseOrigin,
int lineNumber) {
return new SimpleConfigOrigin(baseOrigin.description() + ": line "
+ lineNumber);
return ((SimpleConfigOrigin) baseOrigin).addLineNumber(lineNumber);
}
// chars JSON allows a number to start with

View File

@ -761,6 +761,27 @@ class ConfigTest extends TestUtils {
}
}
@Test
def test01Origins() {
val conf = ConfigFactory.load("test01")
val o1 = conf.getValue("ints.fortyTwo").origin()
assertEquals("/test01.conf: 3", o1.description)
assertEquals("/test01.conf", o1.resource)
assertEquals(3, o1.lineNumber)
val o2 = conf.getValue("fromJson1").origin()
assertEquals("/test01.json: 2", o2.description)
assertEquals("/test01.json", o2.resource)
assertEquals(2, o2.lineNumber)
val o3 = conf.getValue("fromProps.bool").origin()
assertEquals("/test01.properties", o3.description)
assertEquals("/test01.properties", o3.resource)
// we don't have line numbers for properties files
assertEquals(-1, o3.lineNumber)
}
@Test
def test02SubstitutionsWithWeirdPaths() {
val conf = ConfigFactory.load("test02")

View File

@ -7,6 +7,7 @@ import org.junit.Assert._
import org.junit._
import com.typesafe.config.ConfigValue
import java.util.Collections
import java.net.URL
import scala.collection.JavaConverters._
import com.typesafe.config.ConfigObject
import com.typesafe.config.ConfigList
@ -18,9 +19,9 @@ class ConfigValueTest extends TestUtils {
@Test
def configOriginEquality() {
val a = new SimpleConfigOrigin("foo")
val sameAsA = new SimpleConfigOrigin("foo")
val b = new SimpleConfigOrigin("bar")
val a = SimpleConfigOrigin.newSimple("foo")
val sameAsA = SimpleConfigOrigin.newSimple("foo")
val b = SimpleConfigOrigin.newSimple("bar")
checkEqualObjects(a, a)
checkEqualObjects(a, sameAsA)
@ -362,7 +363,7 @@ class ConfigValueTest extends TestUtils {
val values = new java.util.HashMap[String, AbstractConfigValue]()
if (!empty)
values.put("hello", intValue(37))
new SimpleConfigObject(new SimpleConfigOrigin(desc), values);
new SimpleConfigObject(SimpleConfigOrigin.newSimple(desc), values);
}
def m(values: AbstractConfigObject*) = {
AbstractConfigObject.mergeOrigins(values: _*).description()
@ -446,4 +447,38 @@ class ConfigValueTest extends TestUtils {
assertEquals(false, falses.getBoolean("b"))
assertEquals(false, falses.getBoolean("c"))
}
@Test
def configOriginFileAndLine() {
val hasFilename = SimpleConfigOrigin.newFile("foo")
val noFilename = SimpleConfigOrigin.newSimple("bar")
val filenameWithLine = hasFilename.addLineNumber(3)
val noFilenameWithLine = noFilename.addLineNumber(4)
assertEquals("foo", hasFilename.filename())
assertEquals("foo", filenameWithLine.filename())
assertNull(noFilename.filename())
assertNull(noFilenameWithLine.filename())
assertEquals("foo", hasFilename.description())
assertEquals("bar", noFilename.description())
assertEquals(-1, hasFilename.lineNumber())
assertEquals(-1, noFilename.lineNumber())
assertEquals("foo: 3", filenameWithLine.description())
assertEquals("bar: 4", noFilenameWithLine.description());
assertEquals(3, filenameWithLine.lineNumber())
assertEquals(4, noFilenameWithLine.lineNumber())
// the filename is made absolute when converting to url
assertTrue(hasFilename.url.toExternalForm.contains("foo"))
assertNull(noFilename.url)
assertEquals("file:/baz", SimpleConfigOrigin.newFile("/baz").url.toExternalForm)
val urlOrigin = SimpleConfigOrigin.newURL(new URL("file:/foo"))
assertEquals("/foo", urlOrigin.filename)
assertEquals("file:/foo", urlOrigin.url.toExternalForm)
}
}

View File

@ -83,7 +83,7 @@ class JsonTest extends TestUtils {
block
} catch {
case e: lift.JsonParser.ParseException =>
throw new ConfigException.Parse(new SimpleConfigOrigin("lift parser"), e.getMessage(), e)
throw new ConfigException.Parse(SimpleConfigOrigin.newSimple("lift parser"), e.getMessage(), e)
}
}

View File

@ -82,7 +82,7 @@ abstract trait TestUtils {
}
def fakeOrigin() = {
new SimpleConfigOrigin("fake origin")
SimpleConfigOrigin.newSimple("fake origin")
}
def includer() = {
@ -391,7 +391,7 @@ abstract trait TestUtils {
}
def tokenize(input: Reader): java.util.Iterator[Token] = {
tokenize(new SimpleConfigOrigin("anonymous Reader"), input)
tokenize(SimpleConfigOrigin.newSimple("anonymous Reader"), input)
}
def tokenize(s: String): java.util.Iterator[Token] = {