Merge pull request #280 from fpringvaldsen/task/create-ConfigNode

Add ConfigDocument API
This commit is contained in:
Havoc Pennington 2015-03-24 13:37:01 -04:00
commit 25a9f91230
26 changed files with 2490 additions and 219 deletions

View File

@ -0,0 +1,58 @@
package com.typesafe.config;
/**
* An object parsed from the original input text, which can be used to
* replace individual values and exactly render the original text of the
* input.
*
* <p>
* Because this object is immutable, it is safe to use from multiple threads and
* there's no need for "defensive copies."
*
* <p>
* <em>Do not implement interface {@code ConfigNode}</em>; it should only be
* implemented by the config library. Arbitrary implementations will not work
* because the library internals assume a specific concrete implementation.
* Also, this interface is likely to grow new methods over time, so third-party
* implementations will break.
*/
public interface ConfigDocument {
/**
* Returns a new ConfigDocument that is a copy of the current ConfigDocument,
* but with the desired value set at the desired path. If the path exists, it will
* remove all duplicates before the final occurrence of the path, and replace the value
* at the final occurrence of the path. If the path does not exist, it will be added.
*
* @param path the path at which to set the desired value
* @param newValue the value to set at the desired path, represented as a string. This
* string will be parsed into a ConfigNode using the same options used to
* parse the entire document, and the text will be inserted
* as-is into the document. Leading and trailing comments, whitespace, or
* newlines are not allowed, and if present an exception will be thrown.
* If a concatenation is passed in for newValue but the document was parsed
* with JSON, the first value in the concatenation will be parsed and inserted
* into the ConfigDocument.
* @return a copy of the ConfigDocument with the desired value at the desired path
*/
ConfigDocument setValue(String path, String newValue);
/**
* Returns a new ConfigDocument that is a copy of the current ConfigDocument,
* but with the desired value set at the desired path as with {@link #setValue(String, String)},
* but takes a ConfigValue instead of a string.
*
* @param path the path at which to set the desired value
* @param newValue the value to set at the desired path, represented as a ConfigValue.
* The rendered text of the ConfigValue will be inserted into the
* ConfigDocument.
* @return a copy of the ConfigDocument with the desired value at the desired path
*/
ConfigDocument setValue(String path, ConfigValue newValue);
/**
* The original text of the input, modified if necessary with
* any replaced or added values.
* @return the modified original text
*/
String render();
}

View File

@ -0,0 +1,92 @@
package com.typesafe.config;
import com.typesafe.config.impl.ConfigImpl;
import com.typesafe.config.impl.Parseable;
import java.io.File;
import java.io.Reader;
/**
* Factory for automatically creating a ConfigDocument from a given input. Currently
* only supports files and strings.
*/
public final class ConfigDocumentFactory {
/**
* Parses a Reader into a ConfigDocument instance.
*
* @param reader
* the reader to parse
* @param options
* parse options to control how the reader is interpreted
* @return the parsed configuration
* @throws ConfigException on IO or parse errors
*/
public static ConfigDocument parseReader(Reader reader, ConfigParseOptions options) {
return Parseable.newReader(reader, options).parseConfigDocument();
}
/**
* Parses a reader into a Config instance as with
* {@link #parseReader(Reader,ConfigParseOptions)} but always uses the
* default parse options.
*
* @param reader
* the reader to parse
* @return the parsed configuration
* @throws ConfigException on IO or parse errors
*/
public static ConfigDocument parseReader(Reader reader) {
return parseReader(reader, ConfigParseOptions.defaults());
}
/**
* Parses a file into a ConfigDocument instance.
*
* @param file
* the file to parse
* @param options
* parse options to control how the file is interpreted
* @return the parsed configuration
* @throws ConfigException on IO or parse errors
*/
public static ConfigDocument parseFile(File file, ConfigParseOptions options) {
return Parseable.newFile(file, options).parseConfigDocument();
}
/**
* Parses a file into a ConfigDocument instance as with
* {@link #parseFile(File,ConfigParseOptions)} but always uses the
* default parse options.
*
* @param file
* the file to parse
* @return the parsed configuration
* @throws ConfigException on IO or parse errors
*/
public static ConfigDocument parseFile(File file) {
return parseFile(file, ConfigParseOptions.defaults());
}
/**
* Parses a string which should be valid HOCON or JSON.
*
* @param s string to parse
* @param options parse options
* @return the parsed configuration
*/
public static ConfigDocument parseString(String s, ConfigParseOptions options) {
return Parseable.newString(s, options).parseConfigDocument();
}
/**
* Parses a string (which should be valid HOCON or JSON). Uses the
* default parse options.
*
* @param s string to parse
* @return the parsed configuration
*/
public static ConfigDocument parseString(String s) {
return parseString(s, ConfigParseOptions.defaults());
}
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config;
/**
* An immutable node that makes up the ConfigDocument AST, and which can be
* used to reproduce part or all of the original text of an input.
*
* <p>
* Because this object is immutable, it is safe to use from multiple threads and
* there's no need for "defensive copies."
*
* <p>
* <em>Do not implement interface {@code ConfigNode}</em>; it should only be
* implemented by the config library. Arbitrary implementations will not work
* because the library internals assume a specific concrete implementation.
* Also, this interface is likely to grow new methods over time, so third-party
* implementations will break.
*/
public interface ConfigNode {
/**
* The original text of the input which was used to form this particular node.
* @return the original text used to form this node as a String
*/
public String render();
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config.impl;
import com.typesafe.config.ConfigNode;
import java.util.Collection;
abstract class AbstractConfigNode implements ConfigNode {
abstract Collection<Token> tokens();
final public String render() {
StringBuilder origText = new StringBuilder();
Iterable<Token> tokens = tokens();
for (Token t : tokens) {
origText.append(t.tokenText());
}
return origText.toString();
}
@Override
final public boolean equals(Object other) {
return other instanceof AbstractConfigNode && render().equals(((AbstractConfigNode)other).render());
}
@Override
final public int hashCode() {
return render().hashCode();
}
}

View File

@ -0,0 +1,11 @@
/**
* Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config.impl;
// This is required if we want
// to be referencing the AbstractConfigNode class in implementation rather than the
// ConfigNode interface, as we can't cast an AbstractConfigNode to an interface
abstract class AbstractConfigNodeValue extends AbstractConfigNode {
}

View File

@ -0,0 +1,678 @@
/**
* Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config.impl;
import java.util.*;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigSyntax;
import com.typesafe.config.ConfigValueType;
final class ConfigDocumentParser {
static ConfigNodeComplexValue parse(Iterator<Token> tokens, ConfigParseOptions options) {
ParseContext context = new ParseContext(options.getSyntax(), tokens);
return context.parse();
}
static ConfigNodeComplexValue parse(Iterator<Token> tokens) {
ParseContext context = new ParseContext(ConfigSyntax.CONF, tokens);
return context.parse();
}
static AbstractConfigNodeValue parseValue(Iterator<Token> tokens, ConfigParseOptions options) {
ParseContext context = new ParseContext(options.getSyntax(), tokens);
return context.parseSingleValue();
}
static private final class ParseContext {
final private Stack<Token> buffer;
final private Iterator<Token> tokens;
final private ConfigSyntax flavor;
final private LinkedList<Path> pathStack;
// this is the number of "equals" we are inside,
// used to modify the error message to reflect that
// someone may think this is .properties format.
int equalsCount;
// the number of lists we are inside; this is used to detect the "cannot
// generate a reference to a list element" problem, and once we fix that
// problem we should be able to get rid of this variable.
int arrayCount;
ParseContext(ConfigSyntax flavor, Iterator<Token> tokens) {
buffer = new Stack<Token>();
this.tokens = tokens;
this.flavor = flavor;
this.pathStack = new LinkedList<Path>();
this.equalsCount = 0;
this.arrayCount = 0;
}
private Token popToken() {
if (buffer.isEmpty()) {
return tokens.next();
}
return buffer.pop();
}
private Token nextToken() {
Token t = popToken();
if (flavor == ConfigSyntax.JSON) {
if (Tokens.isUnquotedText(t) && !isUnquotedWhitespace(t)) {
throw parseError(addKeyName("Token not allowed in valid JSON: '"
+ Tokens.getUnquotedText(t) + "'"));
} else if (Tokens.isSubstitution(t)) {
throw parseError(addKeyName("Substitutions (${} syntax) not allowed in JSON"));
}
}
return t;
}
private Token nextTokenIgnoringWhitespace(Collection<AbstractConfigNode> nodes) {
while (true) {
Token t = nextToken();
if (Tokens.isIgnoredWhitespace(t) || Tokens.isComment(t) || Tokens.isNewline(t) || isUnquotedWhitespace(t)) {
nodes.add(new ConfigNodeSingleToken(t));
} else {
return t;
}
}
}
private void putBack(Token token) {
buffer.push(token);
}
// In arrays and objects, comma can be omitted
// as long as there's at least one newline instead.
// this skips any newlines in front of a comma,
// skips the comma, and returns true if it found
// either a newline or a comma. The iterator
// is left just after the comma or the newline.
private boolean checkElementSeparator(Collection<AbstractConfigNode> nodes) {
if (flavor == ConfigSyntax.JSON) {
Token t = nextTokenIgnoringWhitespace(nodes);
if (t == Tokens.COMMA) {
nodes.add(new ConfigNodeSingleToken(t));
return true;
} else {
putBack(t);
return false;
}
} else {
boolean sawSeparatorOrNewline = false;
Token t = nextToken();
while (true) {
if (Tokens.isIgnoredWhitespace(t) || isUnquotedWhitespace(t)) {
//do nothing
} else if (Tokens.isNewline(t)) {
sawSeparatorOrNewline = true;
// we want to continue to also eat
// a comma if there is one.
} else if (t == Tokens.COMMA) {
nodes.add(new ConfigNodeSingleToken(t));
return true;
} else {
// non-newline-or-comma
putBack(t);
return sawSeparatorOrNewline;
}
nodes.add(new ConfigNodeSingleToken(t));
t = nextToken();
}
}
}
// parse a concatenation. If there is no concatenation, return the next value
private AbstractConfigNodeValue consolidateValues(Collection<AbstractConfigNode> nodes) {
// this trick is not done in JSON
if (flavor == ConfigSyntax.JSON)
return null;
// create only if we have value tokens
ArrayList<AbstractConfigNode> values = new ArrayList<AbstractConfigNode>();
int valueCount = 0;
// ignore a newline up front
Token t = nextTokenIgnoringWhitespace(nodes);
while (true) {
AbstractConfigNodeValue v = null;
if (Tokens.isIgnoredWhitespace(t)) {
values.add(new ConfigNodeSingleToken(t));
t = nextToken();
continue;
}
else if (Tokens.isValue(t) || Tokens.isUnquotedText(t)
|| Tokens.isSubstitution(t) || t == Tokens.OPEN_CURLY
|| t == Tokens.OPEN_SQUARE) {
// there may be newlines _within_ the objects and arrays
v = parseValue(t);
valueCount++;
} else {
break;
}
if (v == null)
throw new ConfigException.BugOrBroken("no value");
values.add(v);
t = nextToken(); // but don't consolidate across a newline
}
putBack(t);
// No concatenation was seen, but a single value may have been parsed, so return it, and put back
// all succeeding tokens
if (valueCount < 2) {
AbstractConfigNodeValue value = null;
for (AbstractConfigNode node : values) {
if (node instanceof AbstractConfigNodeValue)
value = (AbstractConfigNodeValue)node;
else if (node == null)
nodes.add(node);
else
putBack((new ArrayList<Token>(node.tokens())).get(0));
}
return value;
}
// Put back any trailing whitespace, as the parent object is responsible for tracking
// any leading/trailing whitespace
for (int i = values.size() - 1; i >= 0; i--) {
if (values.get(i) instanceof ConfigNodeSingleToken) {
putBack((new ArrayList<Token>(values.get(i).tokens())).get(0));
values.remove(i);
} else {
break;
}
}
return new ConfigNodeConcatenation(values);
}
private ConfigException parseError(String message) {
return parseError(message, null);
}
private ConfigException parseError(String message, Throwable cause) {
return new ConfigException.Parse(SimpleConfigOrigin.newSimple(""), message, cause);
}
private String previousFieldName(Path lastPath) {
if (lastPath != null) {
return lastPath.render();
} else if (pathStack.isEmpty())
return null;
else
return pathStack.peek().render();
}
private String previousFieldName() {
return previousFieldName(null);
}
private String addKeyName(String message) {
String previousFieldName = previousFieldName();
if (previousFieldName != null) {
return "in value for key '" + previousFieldName + "': " + message;
} else {
return message;
}
}
private String addQuoteSuggestion(String badToken, String message) {
return addQuoteSuggestion(null, equalsCount > 0, badToken, message);
}
private String addQuoteSuggestion(Path lastPath, boolean insideEquals, String badToken,
String message) {
String previousFieldName = previousFieldName(lastPath);
String part;
if (badToken.equals(Tokens.END.toString())) {
// EOF requires special handling for the error to make sense.
if (previousFieldName != null)
part = message + " (if you intended '" + previousFieldName
+ "' to be part of a value, instead of a key, "
+ "try adding double quotes around the whole value";
else
return message;
} else {
if (previousFieldName != null) {
part = message + " (if you intended " + badToken
+ " to be part of the value for '" + previousFieldName + "', "
+ "try enclosing the value in double quotes";
} else {
part = message + " (if you intended " + badToken
+ " to be part of a key or string value, "
+ "try enclosing the key or value in double quotes";
}
}
if (insideEquals)
return part
+ ", or you may be able to rename the file .properties rather than .conf)";
else
return part + ")";
}
private AbstractConfigNodeValue parseValue(Token t) {
AbstractConfigNodeValue v = null;
int startingArrayCount = arrayCount;
int startingEqualsCount = equalsCount;
if (Tokens.isValue(t) || Tokens.isUnquotedText(t) || Tokens.isSubstitution(t)) {
v = new ConfigNodeSimpleValue(t);
} else if (t == Tokens.OPEN_CURLY) {
v = parseObject(true);
} else if (t== Tokens.OPEN_SQUARE) {
v = parseArray();
} else {
throw parseError(addQuoteSuggestion(t.toString(),
"Expecting a value but got wrong token: " + t));
}
if (arrayCount != startingArrayCount)
throw new ConfigException.BugOrBroken("Bug in config parser: unbalanced array count");
if (equalsCount != startingEqualsCount)
throw new ConfigException.BugOrBroken("Bug in config parser: unbalanced equals count");
return v;
}
private ConfigNodePath parseKey(Token token) {
if (flavor == ConfigSyntax.JSON) {
if (Tokens.isValueWithType(token, ConfigValueType.STRING)) {
return PathParser.parsePathNodeExpression(Collections.singletonList(token).iterator(), null);
} else {
throw parseError(addKeyName("Expecting close brace } or a field name here, got "
+ token));
}
} else {
List<Token> expression = new ArrayList<Token>();
Token t = token;
while (Tokens.isValue(t) || Tokens.isUnquotedText(t)) {
expression.add(t);
t = nextToken(); // note: don't cross a newline
}
if (expression.isEmpty()) {
throw parseError(addKeyName("expecting a close brace or a field name here, got "
+ t));
}
putBack(t); // put back the token we ended with
return PathParser.parsePathNodeExpression(expression.iterator(), null);
}
}
private static boolean isIncludeKeyword(Token t) {
return Tokens.isUnquotedText(t)
&& Tokens.getUnquotedText(t).equals("include");
}
private static boolean isUnquotedWhitespace(Token t) {
if (!Tokens.isUnquotedText(t))
return false;
String s = Tokens.getUnquotedText(t);
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
if (!ConfigImplUtil.isWhitespace(c))
return false;
}
return true;
}
private boolean isKeyValueSeparatorToken(Token t) {
if (flavor == ConfigSyntax.JSON) {
return t == Tokens.COLON;
} else {
return t == Tokens.COLON || t == Tokens.EQUALS || t == Tokens.PLUS_EQUALS;
}
}
private void parseInclude(ArrayList<AbstractConfigNode> children) {
Token t = nextTokenIgnoringWhitespace(children);
// we either have a quoted string or the "file()" syntax
if (Tokens.isUnquotedText(t)) {
// get foo(
String kind = Tokens.getUnquotedText(t);
if (kind.equals("url(")) {
} else if (kind.equals("file(")) {
} else if (kind.equals("classpath(")) {
} else {
throw parseError("expecting include parameter to be quoted filename, file(), classpath(), or url(). No spaces are allowed before the open paren. Not expecting: "
+ t);
}
children.add(new ConfigNodeSingleToken(t));
// skip space inside parens
t = nextTokenIgnoringWhitespace(children);
// quoted string
String name;
if (!Tokens.isValueWithType(t, ConfigValueType.STRING)) {
throw parseError("expecting a quoted string inside file(), classpath(), or url(), rather than: "
+ t);
}
children.add(new ConfigNodeSingleToken(t));
// skip space after string, inside parens
t = nextTokenIgnoringWhitespace(children);
if (Tokens.isUnquotedText(t) && Tokens.getUnquotedText(t).equals(")")) {
// OK, close paren
} else {
throw parseError("expecting a close parentheses ')' here, not: " + t);
}
} else if (Tokens.isValueWithType(t, ConfigValueType.STRING)) {
children.add(new ConfigNodeSimpleValue(t));
} else {
throw parseError("include keyword is not followed by a quoted string, but by: " + t);
}
}
private ConfigNodeComplexValue parseObject(boolean hadOpenCurly) {
// invoked just after the OPEN_CURLY (or START, if !hadOpenCurly)
boolean afterComma = false;
Path lastPath = null;
boolean lastInsideEquals = false;
ArrayList<AbstractConfigNode> objectNodes = new ArrayList<AbstractConfigNode>();
ArrayList<AbstractConfigNode> keyValueNodes;
HashMap<String, Boolean> keys = new HashMap();
if (hadOpenCurly)
objectNodes.add(new ConfigNodeSingleToken(Tokens.OPEN_CURLY));
while (true) {
Token t = nextTokenIgnoringWhitespace(objectNodes);
if (t == Tokens.CLOSE_CURLY) {
if (flavor == ConfigSyntax.JSON && afterComma) {
throw parseError(addQuoteSuggestion(t.toString(),
"expecting a field name after a comma, got a close brace } instead"));
} else if (!hadOpenCurly) {
throw parseError(addQuoteSuggestion(t.toString(),
"unbalanced close brace '}' with no open brace"));
}
objectNodes.add(new ConfigNodeSingleToken(Tokens.CLOSE_CURLY));
break;
} else if (t == Tokens.END && !hadOpenCurly) {
putBack(t);
break;
} else if (flavor != ConfigSyntax.JSON && isIncludeKeyword(t)) {
objectNodes.add(new ConfigNodeSingleToken(t));
parseInclude(objectNodes);
afterComma = false;
} else {
keyValueNodes = new ArrayList<AbstractConfigNode>();
Token keyToken = t;
ConfigNodePath path = parseKey(keyToken);
keyValueNodes.add(path);
Token afterKey = nextTokenIgnoringWhitespace(keyValueNodes);
boolean insideEquals = false;
Token valueToken;
AbstractConfigNodeValue nextValue;
if (flavor == ConfigSyntax.CONF && afterKey == Tokens.OPEN_CURLY) {
// can omit the ':' or '=' before an object value
nextValue = parseValue(afterKey);
} else {
if (!isKeyValueSeparatorToken(afterKey)) {
throw parseError(addQuoteSuggestion(afterKey.toString(),
"Key '" + path.render() + "' may not be followed by token: "
+ afterKey));
}
keyValueNodes.add(new ConfigNodeSingleToken(afterKey));
if (afterKey == Tokens.EQUALS) {
insideEquals = true;
equalsCount += 1;
}
nextValue = consolidateValues(keyValueNodes);
if (nextValue == null) {
nextValue = parseValue(nextTokenIgnoringWhitespace(keyValueNodes));
}
}
keyValueNodes.add(nextValue);
if (insideEquals) {
equalsCount -= 1;
}
lastInsideEquals = insideEquals;
String key = path.value().first();
Path remaining = path.value().remainder();
if (remaining == null) {
Boolean existing = keys.get(key);
if (existing != null) {
// In strict JSON, dups should be an error; while in
// our custom config language, they should be merged
// if the value is an object (or substitution that
// could become an object).
if (flavor == ConfigSyntax.JSON) {
throw parseError("JSON does not allow duplicate fields: '"
+ key
+ "' was already seen");
}
}
keys.put(key, true);
} else {
if (flavor == ConfigSyntax.JSON) {
throw new ConfigException.BugOrBroken(
"somehow got multi-element path in JSON mode");
}
keys.put(key, true);
}
afterComma = false;
objectNodes.add(new ConfigNodeField(keyValueNodes));
}
if (checkElementSeparator(objectNodes)) {
// continue looping
afterComma = true;
} else {
t = nextTokenIgnoringWhitespace(objectNodes);
if (t == Tokens.CLOSE_CURLY) {
if (!hadOpenCurly) {
throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
t.toString(), "unbalanced close brace '}' with no open brace"));
}
objectNodes.add(new ConfigNodeSingleToken(t));
break;
} else if (hadOpenCurly) {
throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
t.toString(), "Expecting close brace } or a comma, got " + t));
} else {
if (t == Tokens.END) {
putBack(t);
break;
} else {
throw parseError(addQuoteSuggestion(lastPath, lastInsideEquals,
t.toString(), "Expecting end of input or a comma, got " + t));
}
}
}
}
return new ConfigNodeObject(objectNodes);
}
private ConfigNodeComplexValue parseArray() {
ArrayList<AbstractConfigNode> children = new ArrayList<AbstractConfigNode>();
children.add(new ConfigNodeSingleToken(Tokens.OPEN_SQUARE));
// invoked just after the OPEN_SQUARE
arrayCount += 1;
Token t;
AbstractConfigNodeValue nextValue = consolidateValues(children);
if (nextValue != null) {
children.add(nextValue);
nextValue = null;
} else {
t = nextTokenIgnoringWhitespace(children);
// special-case the first element
if (t == Tokens.CLOSE_SQUARE) {
arrayCount -= 1;
children.add(new ConfigNodeSingleToken(t));
return new ConfigNodeArray(children);
} else if (Tokens.isValue(t) || t == Tokens.OPEN_CURLY
|| t == Tokens.OPEN_SQUARE || Tokens.isUnquotedText(t)
|| Tokens.isSubstitution(t)) {
nextValue = parseValue(t);
children.add(nextValue);
nextValue = null;
} else {
throw parseError(addKeyName("List should have ] or a first element after the open [, instead had token: "
+ t
+ " (if you want "
+ t
+ " to be part of a string value, then double-quote it)"));
}
}
// now remaining elements
while (true) {
// just after a value
if (checkElementSeparator(children)) {
// comma (or newline equivalent) consumed
} else {
t = nextTokenIgnoringWhitespace(children);
if (t == Tokens.CLOSE_SQUARE) {
arrayCount -= 1;
children.add(new ConfigNodeSingleToken(t));
return new ConfigNodeArray(children);
} else {
throw parseError(addKeyName("List should have ended with ] or had a comma, instead had token: "
+ t
+ " (if you want "
+ t
+ " to be part of a string value, then double-quote it)"));
}
}
// now just after a comma
nextValue = consolidateValues(children);
if (nextValue != null) {
children.add(nextValue);
nextValue = null;
} else {
t = nextTokenIgnoringWhitespace(children);
if (Tokens.isValue(t) || t == Tokens.OPEN_CURLY
|| t == Tokens.OPEN_SQUARE || Tokens.isUnquotedText(t)
|| Tokens.isSubstitution(t)) {
nextValue = parseValue(t);
children.add(nextValue);
nextValue = null;
} else if (flavor != ConfigSyntax.JSON && t == Tokens.CLOSE_SQUARE) {
// we allow one trailing comma
putBack(t);
} else {
throw parseError(addKeyName("List should have had new element after a comma, instead had token: "
+ t
+ " (if you want the comma or "
+ t
+ " to be part of a string value, then double-quote it)"));
}
}
}
}
ConfigNodeComplexValue parse() {
Token t = nextToken();
if (t == Tokens.START) {
// OK
} else {
throw new ConfigException.BugOrBroken(
"token stream did not begin with START, had " + t);
}
t = nextToken();
AbstractConfigNode result = null;
if (t == Tokens.OPEN_CURLY || t == Tokens.OPEN_SQUARE) {
result = parseValue(t);
} else {
if (flavor == ConfigSyntax.JSON) {
if (t == Tokens.END) {
throw parseError("Empty document");
} else {
throw parseError("Document must have an object or array at root, unexpected token: "
+ t);
}
} else {
// the root object can omit the surrounding braces.
// this token should be the first field's key, or part
// of it, so put it back.
putBack(t);
result = parseObject(false);
}
}
ArrayList<AbstractConfigNode> children = new ArrayList<AbstractConfigNode>(((ConfigNodeComplexValue)result).children());
t = nextTokenIgnoringWhitespace(children);
if (t == Tokens.END) {
if (result instanceof ConfigNodeArray) {
return new ConfigNodeArray(children);
}
return new ConfigNodeObject(children);
} else {
throw parseError("Document has trailing tokens after first object or array: "
+ t);
}
}
// Parse a given input stream into a single value node. Used when doing a replace inside a ConfigDocument.
AbstractConfigNodeValue parseSingleValue() {
Token t = nextToken();
if (t == Tokens.START) {
// OK
} else {
throw new ConfigException.BugOrBroken(
"token stream did not begin with START, had " + t);
}
t = nextToken();
if (Tokens.isIgnoredWhitespace(t) || Tokens.isNewline(t) || isUnquotedWhitespace(t) || Tokens.isComment(t)) {
throw parseError("The value from setValue cannot have leading or trailing newlines, whitespace, or comments");
}
if (t == Tokens.END) {
throw parseError("Empty value");
}
if (flavor == ConfigSyntax.JSON) {
AbstractConfigNodeValue node = parseValue(t);
t = nextToken();
if (t == Tokens.END) {
return node;
} else {
throw parseError("Parsing JSON and the value set in setValue was either a concatenation or " +
"had trailing whitespace, newlines, or comments");
}
} else {
putBack(t);
ArrayList<AbstractConfigNode> nodes = new ArrayList();
AbstractConfigNodeValue node = consolidateValues(nodes);
t = nextToken();
if (t == Tokens.END) {
return node;
} else {
throw parseError("The value from setValue cannot have leading or trailing newlines, whitespace, or comments");
}
}
}
}
}

View File

@ -0,0 +1,9 @@
package com.typesafe.config.impl;
import java.util.Collection;
final class ConfigNodeArray extends ConfigNodeComplexValue {
ConfigNodeArray(Collection<AbstractConfigNode> children) {
super(children);
}
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config.impl;
import java.util.*;
abstract class ConfigNodeComplexValue extends AbstractConfigNodeValue {
final protected ArrayList<AbstractConfigNode> children;
ConfigNodeComplexValue(Collection<AbstractConfigNode> children) {
this.children = new ArrayList(children);
}
final public Collection<AbstractConfigNode> children() {
return children;
}
@Override
protected Collection<Token> tokens() {
ArrayList<Token> tokens = new ArrayList();
for (AbstractConfigNode child : children) {
tokens.addAll(child.tokens());
}
return tokens;
}
}

View File

@ -0,0 +1,9 @@
package com.typesafe.config.impl;
import java.util.Collection;
final class ConfigNodeConcatenation extends ConfigNodeComplexValue {
ConfigNodeConcatenation(Collection<AbstractConfigNode> children) {
super(children);
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config.impl;
import com.typesafe.config.ConfigException;
import java.util.ArrayList;
import java.util.Collection;
final class ConfigNodeField extends AbstractConfigNode {
final private ArrayList<AbstractConfigNode> children;
public ConfigNodeField(Collection<AbstractConfigNode> children) {
this.children = new ArrayList(children);
}
@Override
protected Collection<Token> tokens() {
ArrayList<Token> tokens = new ArrayList();
for (AbstractConfigNode child : children) {
tokens.addAll(child.tokens());
}
return tokens;
}
public ConfigNodeField replaceValue(AbstractConfigNodeValue newValue) {
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList(children);
for (int i = 0; i < childrenCopy.size(); i++) {
if (childrenCopy.get(i) instanceof AbstractConfigNodeValue) {
childrenCopy.set(i, newValue);
return new ConfigNodeField(childrenCopy);
}
}
throw new ConfigException.BugOrBroken("Field node doesn't have a value");
}
public AbstractConfigNodeValue value() {
for (int i = 0; i < children.size(); i++) {
if (children.get(i) instanceof AbstractConfigNodeValue) {
return (AbstractConfigNodeValue)children.get(i);
}
}
throw new ConfigException.BugOrBroken("Field node doesn't have a value");
}
public ConfigNodePath path() {
for (int i = 0; i < children.size(); i++) {
if (children.get(i) instanceof ConfigNodePath) {
return (ConfigNodePath)children.get(i);
}
}
throw new ConfigException.BugOrBroken("Field node doesn't have a path");
}
}

View File

@ -0,0 +1,127 @@
package com.typesafe.config.impl;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigSyntax;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
final class ConfigNodeObject extends ConfigNodeComplexValue {
ConfigNodeObject(Collection<AbstractConfigNode> children) {
super(children);
}
protected ConfigNodeObject changeValueOnPath(Path desiredPath, AbstractConfigNodeValue value) {
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList(super.children);
// Copy the value so we can change it to null but not modify the original parameter
AbstractConfigNodeValue valueCopy = value;
for (int i = super.children.size() - 1; i >= 0; i--) {
if (!(super.children.get(i) instanceof ConfigNodeField)) {
continue;
}
ConfigNodeField node = (ConfigNodeField)super.children.get(i);
Path key = node.path().value();
if (key.equals(desiredPath)) {
if (valueCopy == null)
childrenCopy.remove(i);
else {
childrenCopy.set(i, node.replaceValue(value));
valueCopy = null;
}
} else if (desiredPath.startsWith(key)) {
if (node.value() instanceof ConfigNodeObject) {
Path remainingPath = desiredPath.subPath(key.length());
childrenCopy.set(i, node.replaceValue(((ConfigNodeObject)node.value()).changeValueOnPath(remainingPath, valueCopy)));
if (valueCopy != null && !node.equals(super.children.get(i)))
valueCopy = null;
}
}
}
return new ConfigNodeObject(childrenCopy);
}
public ConfigNodeObject setValueOnPath(String desiredPath, AbstractConfigNodeValue value) {
return setValueOnPath(desiredPath, value, ConfigSyntax.CONF);
}
public ConfigNodeObject setValueOnPath(String desiredPath, AbstractConfigNodeValue value, ConfigSyntax flavor) {
ConfigNodePath path = PathParser.parsePathNode(desiredPath, flavor);
return setValueOnPath(path, value, flavor);
}
private ConfigNodeObject setValueOnPath(ConfigNodePath desiredPath, AbstractConfigNodeValue value, ConfigSyntax flavor) {
ConfigNodeObject node = changeValueOnPath(desiredPath.value(), value);
// If the desired Path did not exist, add it
if (node.render().equals(render())) {
return addValueOnPath(desiredPath, value, flavor);
}
return node;
}
protected ConfigNodeObject addValueOnPath(ConfigNodePath desiredPath, AbstractConfigNodeValue value, ConfigSyntax flavor) {
Path path = desiredPath.value();
ArrayList<AbstractConfigNode> childrenCopy = new ArrayList(super.children);
if (path.length() > 1) {
for (int i = super.children.size() - 1; i >= 0; i--) {
if (!(super.children.get(i) instanceof ConfigNodeField)) {
continue;
}
ConfigNodeField node = (ConfigNodeField) super.children.get(i);
Path key = node.path().value();
if (path.startsWith(key) && node.value() instanceof ConfigNodeObject) {
ConfigNodePath remainingPath = desiredPath.subPath(key.length());
ConfigNodeObject newValue = (ConfigNodeObject) node.value();
childrenCopy.set(i, node.replaceValue(newValue.addValueOnPath(remainingPath, value, flavor)));
return new ConfigNodeObject(childrenCopy);
}
}
}
boolean startsWithBrace = super.children.get(0) instanceof ConfigNodeSingleToken &&
((ConfigNodeSingleToken) super.children.get(0)).token() == Tokens.OPEN_CURLY;
ArrayList<AbstractConfigNode> newNodes = new ArrayList();
newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null)));
newNodes.add(desiredPath.first());
newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
newNodes.add(new ConfigNodeSingleToken(Tokens.COLON));
newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " ")));
if (path.length() == 1) {
newNodes.add(value);
} else {
ArrayList<AbstractConfigNode> newObjectNodes = new ArrayList();
newObjectNodes.add(new ConfigNodeSingleToken(Tokens.OPEN_CURLY));
newObjectNodes.add(new ConfigNodeSingleToken(Tokens.CLOSE_CURLY));
ConfigNodeObject newObject = new ConfigNodeObject(newObjectNodes);
newNodes.add(newObject.addValueOnPath(desiredPath.subPath(1), value, flavor));
}
newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null)));
// Combine these two cases so that we only have to iterate once
if (flavor == ConfigSyntax.JSON || startsWithBrace) {
for (int i = childrenCopy.size() - 1; i >= 0; i--) {
// Valid JSON requires all key-value pairs except the last one to be succeeded by a comma,
// so we'll need to add a comma when adding a value
if (flavor == ConfigSyntax.JSON && childrenCopy.get(i) instanceof ConfigNodeField) {
childrenCopy.add(i+1, new ConfigNodeSingleToken(Tokens.COMMA));
break;
}
if (startsWithBrace && childrenCopy.get(i) instanceof ConfigNodeSingleToken &&
((ConfigNodeSingleToken) childrenCopy.get(i)).token == Tokens.CLOSE_CURLY) {
childrenCopy.add(i, new ConfigNodeField(newNodes));
}
}
}
if (!startsWithBrace) {
childrenCopy.add(new ConfigNodeField(newNodes));
}
return new ConfigNodeObject(childrenCopy);
}
public ConfigNodeComplexValue removeValueOnPath(String desiredPath) {
Path path = PathParser.parsePath(desiredPath);
return changeValueOnPath(path, null);
}
}

View File

@ -0,0 +1,52 @@
/**
* Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config.impl;
import com.typesafe.config.ConfigException;
import java.util.ArrayList;
import java.util.Collection;
final class ConfigNodePath extends AbstractConfigNode {
final private Path path;
final ArrayList<Token> tokens;
ConfigNodePath(Path path, Collection<Token> tokens) {
this.path = path;
this.tokens = new ArrayList<Token>(tokens);
}
@Override
protected Collection<Token> tokens() {
return tokens;
}
protected Path value() {
return path;
}
protected ConfigNodePath subPath(int toRemove) {
int periodCount = 0;
ArrayList<Token> tokensCopy = new ArrayList<Token>(tokens);
for (int i = 0; i < tokensCopy.size(); i++) {
if (Tokens.isUnquotedText(tokensCopy.get(i)) &&
tokensCopy.get(i).tokenText().equals("."))
periodCount++;
if (periodCount == toRemove) {
return new ConfigNodePath(path.subPath(toRemove), tokensCopy.subList(i + 1, tokensCopy.size()));
}
}
throw new ConfigException.BugOrBroken("Tried to remove too many elements from a Path node");
}
protected ConfigNodePath first() {
ArrayList<Token> tokensCopy = new ArrayList<Token>(tokens);
for (int i = 0; i < tokensCopy.size(); i++) {
if (Tokens.isUnquotedText(tokensCopy.get(i)) &&
tokensCopy.get(i).tokenText().equals("."))
return new ConfigNodePath(path.subPath(0, 1), tokensCopy.subList(0, i));
}
return this;
}
}

View File

@ -0,0 +1,19 @@
/**
* Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config.impl;
import java.util.Collection;
import java.util.Collections;
class ConfigNodeSimpleValue extends AbstractConfigNodeValue {
final Token token;
ConfigNodeSimpleValue(Token value) {
token = value;
}
@Override
protected Collection<Token> tokens() {
return Collections.singletonList(token);
}
}

View File

@ -0,0 +1,21 @@
/**
* Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config.impl;
import java.util.Collection;
import java.util.Collections;
final class ConfigNodeSingleToken extends AbstractConfigNode{
final Token token;
ConfigNodeSingleToken(Token t) {
token = t;
}
@Override
protected Collection<Token> tokens() {
return Collections.singletonList(token);
}
protected Token token() { return token; }
}

View File

@ -19,19 +19,9 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Properties;
import java.util.*;
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;
import com.typesafe.config.*;
/**
* Internal implementation detail, not ABI stable, do not touch.
@ -199,6 +189,38 @@ public abstract class Parseable implements ConfigParseable {
}
}
final ConfigDocument parseDocument(ConfigParseOptions baseOptions) {
// 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);
// passed-in options can override origin
ConfigOrigin origin;
if (options.getOriginDescription() != null)
origin = SimpleConfigOrigin.newSimple(options.getOriginDescription());
else
origin = initialOrigin;
return parseDocument(origin, options);
}
final private ConfigDocument parseDocument(ConfigOrigin origin,
ConfigParseOptions finalOptions) {
try {
return rawParseDocument(origin, finalOptions);
} catch (IOException e) {
if (finalOptions.getAllowMissing()) {
return new SimpleConfigDocument(new ConfigNodeObject(new ArrayList<AbstractConfigNode>()), finalOptions);
} else {
trace("exception loading " + origin.description() + ": " + e.getClass().getName()
+ ": " + e.getMessage());
throw new ConfigException.IO(origin,
e.getClass().getName() + ": " + e.getMessage(), e);
}
}
}
// this is parseValue without post-processing the IOException or handling
// options.getAllowMissing()
protected AbstractConfigValue rawParseValue(ConfigOrigin origin, ConfigParseOptions finalOptions)
@ -236,10 +258,47 @@ public abstract class Parseable implements ConfigParseable {
}
}
// this is parseValue without post-processing the IOException or handling
// options.getAllowMissing()
protected ConfigDocument rawParseDocument(ConfigOrigin origin, ConfigParseOptions finalOptions)
throws IOException {
Reader reader = reader(finalOptions);
// after reader() we will have loaded the Content-Type.
ConfigSyntax contentType = contentType();
ConfigParseOptions optionsWithContentType;
if (contentType != null) {
if (ConfigImpl.traceLoadsEnabled() && finalOptions.getSyntax() != null)
trace("Overriding syntax " + finalOptions.getSyntax()
+ " with Content-Type which specified " + contentType);
optionsWithContentType = finalOptions.setSyntax(contentType);
} else {
optionsWithContentType = finalOptions;
}
try {
return rawParseDocument(reader, origin, optionsWithContentType);
} finally {
reader.close();
}
}
private ConfigDocument rawParseDocument(Reader reader, ConfigOrigin origin,
ConfigParseOptions finalOptions) throws IOException {
Iterator<Token> tokens = Tokenizer.tokenize(origin, reader, finalOptions.getSyntax());
return new SimpleConfigDocument(ConfigDocumentParser.parse(tokens, finalOptions), finalOptions);
}
public ConfigObject parse() {
return forceParsedToObject(parseValue(options()));
}
public ConfigDocument parseConfigDocument() {
return parseDocument(options());
}
AbstractConfigValue parseValue() {
return parseValue(options());
}

View File

@ -382,7 +382,7 @@ final class Parser {
private static SubstitutionExpression tokenToSubstitutionExpression(Token valueToken) {
List<Token> expression = Tokens.getSubstitutionPathExpression(valueToken);
Path path = parsePathExpression(expression.iterator(), valueToken.origin());
Path path = PathParser.parsePathExpression(expression.iterator(), valueToken.origin());
boolean optional = Tokens.getSubstitutionOptional(valueToken);
return new SubstitutionExpression(path, optional);
@ -604,7 +604,7 @@ final class Parser {
}
putBack(t); // put back the token we ended with
return parsePathExpression(expression.iterator(), lineOrigin());
return PathParser.parsePathExpression(expression.iterator(), lineOrigin());
}
}
@ -1016,198 +1016,4 @@ final class Parser {
}
}
}
static class Element {
StringBuilder sb;
// an element can be empty if it has a quoted empty string "" in it
boolean canBeEmpty;
Element(String initial, boolean canBeEmpty) {
this.canBeEmpty = canBeEmpty;
this.sb = new StringBuilder(initial);
}
@Override
public String toString() {
return "Element(" + sb.toString() + "," + canBeEmpty + ")";
}
}
private static void addPathText(List<Element> buf, boolean wasQuoted,
String newText) {
int i = wasQuoted ? -1 : newText.indexOf('.');
Element current = buf.get(buf.size() - 1);
if (i < 0) {
// add to current path element
current.sb.append(newText);
// any empty quoted string means this element can
// now be empty.
if (wasQuoted && current.sb.length() == 0)
current.canBeEmpty = true;
} else {
// "buf" plus up to the period is an element
current.sb.append(newText.substring(0, i));
// then start a new element
buf.add(new Element("", false));
// recurse to consume remainder of newText
addPathText(buf, false, newText.substring(i + 1));
}
}
private static Path parsePathExpression(Iterator<Token> expression,
ConfigOrigin origin) {
return parsePathExpression(expression, origin, null);
}
// originalText may be null if not available
private static Path parsePathExpression(Iterator<Token> expression,
ConfigOrigin origin, String originalText) {
// each builder in "buf" is an element in the path.
List<Element> buf = new ArrayList<Element>();
buf.add(new Element("", false));
if (!expression.hasNext()) {
throw new ConfigException.BadPath(origin, originalText,
"Expecting a field name or path here, but got nothing");
}
while (expression.hasNext()) {
Token t = expression.next();
// Ignore all IgnoredWhitespace tokens
if (Tokens.isIgnoredWhitespace(t))
continue;
if (Tokens.isValueWithType(t, ConfigValueType.STRING)) {
AbstractConfigValue v = Tokens.getValue(t);
// this is a quoted string; so any periods
// in here don't count as path separators
String s = v.transformToString();
addPathText(buf, true, s);
} else if (t == Tokens.END) {
// ignore this; when parsing a file, it should not happen
// since we're parsing a token list rather than the main
// token iterator, and when parsing a path expression from the
// API, it's expected to have an END.
} else {
// any periods outside of a quoted string count as
// separators
String text;
if (Tokens.isValue(t)) {
// appending a number here may add
// a period, but we _do_ count those as path
// separators, because we basically want
// "foo 3.0bar" to parse as a string even
// though there's a number in it. The fact that
// we tokenize non-string values is largely an
// implementation detail.
AbstractConfigValue v = Tokens.getValue(t);
text = v.transformToString();
} else if (Tokens.isUnquotedText(t)) {
text = Tokens.getUnquotedText(t);
} else {
throw new ConfigException.BadPath(
origin,
originalText,
"Token not allowed in path expression: "
+ t
+ " (you can double-quote this token if you really want it here)");
}
addPathText(buf, false, text);
}
}
PathBuilder pb = new PathBuilder();
for (Element e : buf) {
if (e.sb.length() == 0 && !e.canBeEmpty) {
throw new ConfigException.BadPath(
origin,
originalText,
"path has a leading, trailing, or two adjacent period '.' (use quoted \"\" empty string if you want an empty element)");
} else {
pb.appendKey(e.sb.toString());
}
}
return pb.result();
}
static ConfigOrigin apiOrigin = SimpleConfigOrigin.newSimple("path parameter");
static Path parsePath(String path) {
Path speculated = speculativeFastParsePath(path);
if (speculated != null)
return speculated;
StringReader reader = new StringReader(path);
try {
Iterator<Token> tokens = Tokenizer.tokenize(apiOrigin, reader,
ConfigSyntax.CONF);
tokens.next(); // drop START
return parsePathExpression(tokens, apiOrigin, path);
} finally {
reader.close();
}
}
// the idea is to see if the string has any chars or features
// that might require the full parser to deal with.
private static boolean looksUnsafeForFastParser(String s) {
boolean lastWasDot = true; // start of path is also a "dot"
int len = s.length();
if (s.isEmpty())
return true;
if (s.charAt(0) == '.')
return true;
if (s.charAt(len - 1) == '.')
return true;
for (int i = 0; i < len; ++i) {
char c = s.charAt(i);
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_') {
lastWasDot = false;
continue;
} else if (c == '.') {
if (lastWasDot)
return true; // ".." means we need to throw an error
lastWasDot = true;
} else if (c == '-') {
if (lastWasDot)
return true;
continue;
} else {
return true;
}
}
if (lastWasDot)
return true;
return false;
}
private static Path fastPathBuild(Path tail, String s, int end) {
// lastIndexOf takes last index it should look at, end - 1 not end
int splitAt = s.lastIndexOf('.', end - 1);
// this works even if splitAt is -1; then we start the substring at 0
Path withOneMoreElement = new Path(s.substring(splitAt + 1, end), tail);
if (splitAt < 0) {
return withOneMoreElement;
} else {
return fastPathBuild(withOneMoreElement, s, splitAt);
}
}
// do something much faster than the full parser if
// we just have something like "foo" or "foo.bar"
private static Path speculativeFastParsePath(String path) {
String s = ConfigImplUtil.unicodeTrim(path);
if (looksUnsafeForFastParser(s))
return null;
return fastPathBuild(null, s, s.length());
}
}

View File

@ -3,8 +3,7 @@
*/
package com.typesafe.config.impl;
import java.util.Iterator;
import java.util.List;
import java.util.*;
import com.typesafe.config.ConfigException;
@ -141,6 +140,21 @@ final class Path {
return pb.result();
}
boolean startsWith(Path other) {
Path myRemainder = this;
Path otherRemainder = other;
if (otherRemainder.length() <= myRemainder.length()) {
while(otherRemainder != null) {
if (!otherRemainder.first().equals(myRemainder.first()))
return false;
myRemainder = myRemainder.remainder();
otherRemainder = otherRemainder.remainder();
}
return true;
}
return false;
}
@Override
public boolean equals(Object other) {
if (other instanceof Path) {
@ -213,6 +227,6 @@ final class Path {
}
static Path newPath(String path) {
return Parser.parsePath(path);
return PathParser.parsePath(path);
}
}

View File

@ -3,6 +3,8 @@
*/
package com.typesafe.config.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Stack;
import com.typesafe.config.ConfigException;

View File

@ -0,0 +1,281 @@
/**
* Copyright (C) 2015 Typesafe Inc. <http://typesafe.com>
*/
package com.typesafe.config.impl;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigOrigin;
import com.typesafe.config.ConfigSyntax;
import com.typesafe.config.ConfigValueType;
import java.io.StringReader;
import java.util.*;
final class PathParser {
static class Element {
StringBuilder sb;
// an element can be empty if it has a quoted empty string "" in it
boolean canBeEmpty;
Element(String initial, boolean canBeEmpty) {
this.canBeEmpty = canBeEmpty;
this.sb = new StringBuilder(initial);
}
@Override
public String toString() {
return "Element(" + sb.toString() + "," + canBeEmpty + ")";
}
}
static ConfigOrigin apiOrigin = SimpleConfigOrigin.newSimple("path parameter");
static ConfigNodePath parsePathNode(String path) {
return parsePathNode(path, ConfigSyntax.CONF);
}
static ConfigNodePath parsePathNode(String path, ConfigSyntax flavor) {
StringReader reader = new StringReader(path);
try {
Iterator<Token> tokens = Tokenizer.tokenize(apiOrigin, reader,
flavor);
tokens.next(); // drop START
return parsePathNodeExpression(tokens, apiOrigin, path, flavor);
} finally {
reader.close();
}
}
static Path parsePath(String path) {
Path speculated = speculativeFastParsePath(path);
if (speculated != null)
return speculated;
StringReader reader = new StringReader(path);
try {
Iterator<Token> tokens = Tokenizer.tokenize(apiOrigin, reader,
ConfigSyntax.CONF);
tokens.next(); // drop START
return parsePathExpression(tokens, apiOrigin, path);
} finally {
reader.close();
}
}
protected static Path parsePathExpression(Iterator<Token> expression,
ConfigOrigin origin) {
return parsePathExpression(expression, origin, null, null, ConfigSyntax.CONF);
}
protected static Path parsePathExpression(Iterator<Token> expression,
ConfigOrigin origin, String originalText) {
return parsePathExpression(expression, origin, originalText, null, ConfigSyntax.CONF);
}
protected static ConfigNodePath parsePathNodeExpression(Iterator<Token> expression,
ConfigOrigin origin) {
return parsePathNodeExpression(expression, origin, null, ConfigSyntax.CONF);
}
protected static ConfigNodePath parsePathNodeExpression(Iterator<Token> expression,
ConfigOrigin origin, String originalText, ConfigSyntax flavor) {
ArrayList<Token> pathTokens = new ArrayList<Token>();
Path path = parsePathExpression(expression, origin, originalText, pathTokens, flavor);
return new ConfigNodePath(path, pathTokens);
}
// originalText may be null if not available
protected static Path parsePathExpression(Iterator<Token> expression,
ConfigOrigin origin, String originalText,
ArrayList<Token> pathTokens,
ConfigSyntax flavor) {
// each builder in "buf" is an element in the path.
List<Element> buf = new ArrayList<Element>();
buf.add(new Element("", false));
if (!expression.hasNext()) {
throw new ConfigException.BadPath(origin, originalText,
"Expecting a field name or path here, but got nothing");
}
while (expression.hasNext()) {
Token t = expression.next();
if (pathTokens != null)
pathTokens.add(t);
// Ignore all IgnoredWhitespace tokens
if (Tokens.isIgnoredWhitespace(t))
continue;
if (Tokens.isValueWithType(t, ConfigValueType.STRING)) {
AbstractConfigValue v = Tokens.getValue(t);
// this is a quoted string; so any periods
// in here don't count as path separators
String s = v.transformToString();
addPathText(buf, true, s);
} else if (t == Tokens.END) {
// ignore this; when parsing a file, it should not happen
// since we're parsing a token list rather than the main
// token iterator, and when parsing a path expression from the
// API, it's expected to have an END.
} else {
// any periods outside of a quoted string count as
// separators
String text;
if (Tokens.isValue(t)) {
// appending a number here may add
// a period, but we _do_ count those as path
// separators, because we basically want
// "foo 3.0bar" to parse as a string even
// though there's a number in it. The fact that
// we tokenize non-string values is largely an
// implementation detail.
AbstractConfigValue v = Tokens.getValue(t);
// We need to split the tokens on a . so that we can get sub-paths but still preserve
// the original path text when doing an insertion
if (pathTokens != null) {
pathTokens.remove(pathTokens.size() - 1);
pathTokens.addAll(splitTokenOnPeriod(t, flavor));
}
text = v.transformToString();
} else if (Tokens.isUnquotedText(t)) {
// We need to split the tokens on a . so that we can get sub-paths but still preserve
// the original path text when doing an insertion on ConfigNodeObjects
if (pathTokens != null) {
pathTokens.remove(pathTokens.size() - 1);
pathTokens.addAll(splitTokenOnPeriod(t, flavor));
}
text = Tokens.getUnquotedText(t);
} else {
throw new ConfigException.BadPath(
origin,
originalText,
"Token not allowed in path expression: "
+ t
+ " (you can double-quote this token if you really want it here)");
}
addPathText(buf, false, text);
}
}
PathBuilder pb = new PathBuilder();
for (Element e : buf) {
if (e.sb.length() == 0 && !e.canBeEmpty) {
throw new ConfigException.BadPath(
origin,
originalText,
"path has a leading, trailing, or two adjacent period '.' (use quoted \"\" empty string if you want an empty element)");
} else {
pb.appendKey(e.sb.toString());
}
}
return pb.result();
}
private static Collection<Token> splitTokenOnPeriod(Token t, ConfigSyntax flavor) {
String tokenText = t.tokenText();
if (tokenText.equals(".")) {
return Collections.singletonList(t);
}
String[] splitToken = tokenText.split("\\.");
ArrayList<Token> splitTokens = new ArrayList<Token>();
for (String s : splitToken) {
if (flavor == ConfigSyntax.CONF)
splitTokens.add(Tokens.newUnquotedText(t.origin(), s));
else
splitTokens.add(Tokens.newString(t.origin(), s, "\"" + s + "\""));
splitTokens.add(Tokens.newUnquotedText(t.origin(), "."));
}
if (tokenText.charAt(tokenText.length() - 1) != '.')
splitTokens.remove(splitTokens.size() - 1);
return splitTokens;
}
private static void addPathText(List<Element> buf, boolean wasQuoted,
String newText) {
int i = wasQuoted ? -1 : newText.indexOf('.');
Element current = buf.get(buf.size() - 1);
if (i < 0) {
// add to current path element
current.sb.append(newText);
// any empty quoted string means this element can
// now be empty.
if (wasQuoted && current.sb.length() == 0)
current.canBeEmpty = true;
} else {
// "buf" plus up to the period is an element
current.sb.append(newText.substring(0, i));
// then start a new element
buf.add(new Element("", false));
// recurse to consume remainder of newText
addPathText(buf, false, newText.substring(i + 1));
}
}
// the idea is to see if the string has any chars or features
// that might require the full parser to deal with.
private static boolean looksUnsafeForFastParser(String s) {
boolean lastWasDot = true; // start of path is also a "dot"
int len = s.length();
if (s.isEmpty())
return true;
if (s.charAt(0) == '.')
return true;
if (s.charAt(len - 1) == '.')
return true;
for (int i = 0; i < len; ++i) {
char c = s.charAt(i);
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_') {
lastWasDot = false;
continue;
} else if (c == '.') {
if (lastWasDot)
return true; // ".." means we need to throw an error
lastWasDot = true;
} else if (c == '-') {
if (lastWasDot)
return true;
continue;
} else {
return true;
}
}
if (lastWasDot)
return true;
return false;
}
private static Path fastPathBuild(Path tail, String s, int end) {
// lastIndexOf takes last index it should look at, end - 1 not end
int splitAt = s.lastIndexOf('.', end - 1);
ArrayList<Token> tokens = new ArrayList<Token>();
tokens.add(Tokens.newUnquotedText(null, s));
// this works even if splitAt is -1; then we start the substring at 0
Path withOneMoreElement = new Path(s.substring(splitAt + 1, end), tail);
if (splitAt < 0) {
return withOneMoreElement;
} else {
return fastPathBuild(withOneMoreElement, s, splitAt);
}
}
// do something much faster than the full parser if
// we just have something like "foo" or "foo.bar"
private static Path speculativeFastParsePath(String path) {
String s = ConfigImplUtil.unicodeTrim(path);
if (looksUnsafeForFastParser(s))
return null;
return fastPathBuild(null, s, s.length());
}
}

View File

@ -0,0 +1,40 @@
package com.typesafe.config.impl;
import com.typesafe.config.ConfigDocument;
import com.typesafe.config.ConfigException;
import com.typesafe.config.ConfigParseOptions;
import com.typesafe.config.ConfigValue;
import java.io.StringReader;
import java.util.Iterator;
final class SimpleConfigDocument implements ConfigDocument {
private ConfigNodeComplexValue configNodeTree;
private ConfigParseOptions parseOptions;
SimpleConfigDocument(ConfigNodeComplexValue parsedNode, ConfigParseOptions parseOptions) {
configNodeTree = parsedNode;
this.parseOptions = parseOptions;
}
public ConfigDocument setValue(String path, String newValue) {
if (configNodeTree instanceof ConfigNodeArray) {
throw new ConfigException.Generic("The ConfigDocument had an array at the root level, and values cannot be replaced inside an array.");
}
SimpleConfigOrigin origin = SimpleConfigOrigin.newSimple("single value parsing");
StringReader reader = new StringReader(newValue);
Iterator<Token> tokens = Tokenizer.tokenize(origin, reader, parseOptions.getSyntax());
AbstractConfigNodeValue parsedValue = ConfigDocumentParser.parseValue(tokens, parseOptions);
reader.close();
return new SimpleConfigDocument(((ConfigNodeObject)configNodeTree).setValueOnPath(path, parsedValue, parseOptions.getSyntax()), parseOptions);
}
public ConfigDocument setValue(String path, ConfigValue newValue) {
return setValue(path, newValue.render());
}
public String render() {
return configNodeTree.render();
}
}

View File

@ -90,7 +90,7 @@ class ConfParserTest extends TestUtils {
// also parse with the standalone path parser and be sure the
// outcome is the same.
try {
val shouldBeSame = Parser.parsePath(s)
val shouldBeSame = PathParser.parsePath(s)
assertEquals(result, shouldBeSame)
} catch {
case e: ConfigException =>

View File

@ -0,0 +1,301 @@
package com.typesafe.config.impl
import com.typesafe.config.{ ConfigException, ConfigSyntax, ConfigParseOptions }
import org.junit.Assert._
import org.junit.Test
class ConfigDocumentParserTest extends TestUtils {
private def parseTest(origText: String) {
val node = ConfigDocumentParser.parse(tokenize(origText))
assertEquals(origText, node.render())
}
private def parseJSONFailuresTest(origText: String, containsMessage: String) {
var exceptionThrown = false
try {
ConfigDocumentParser.parse(tokenize(origText), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains(containsMessage))
}
assertTrue(exceptionThrown)
}
private def parseSimpleValueTest(origText: String, finalText: String = null) {
val expectedRenderedText = if (finalText == null) origText else finalText
val node = ConfigDocumentParser.parseValue(tokenize(origText), ConfigParseOptions.defaults())
assertEquals(expectedRenderedText, node.render())
assertTrue(node.isInstanceOf[AbstractConfigNodeValue])
val nodeJSON = ConfigDocumentParser.parseValue(tokenize(origText), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
assertEquals(expectedRenderedText, nodeJSON.render())
assertTrue(nodeJSON.isInstanceOf[AbstractConfigNodeValue])
}
private def parseComplexValueTest(origText: String) {
val node = ConfigDocumentParser.parseValue(tokenize(origText), ConfigParseOptions.defaults())
assertEquals(origText, node.render())
assertTrue(node.isInstanceOf[AbstractConfigNodeValue])
val nodeJSON = ConfigDocumentParser.parseValue(tokenize(origText), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
assertEquals(origText, nodeJSON.render())
assertTrue(nodeJSON.isInstanceOf[AbstractConfigNodeValue])
}
private def parseSingleValueInvalidJSONTest(origText: String, containsMessage: String) {
val node = ConfigDocumentParser.parseValue(tokenize(origText), ConfigParseOptions.defaults())
assertEquals(origText, node.render())
var exceptionThrown = false
try {
ConfigDocumentParser.parse(tokenize(origText), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains(containsMessage))
}
assertTrue(exceptionThrown)
}
private def parseLeadingTrailingFailure(toReplace: String) {
var exceptionThrown = false
try {
ConfigDocumentParser.parseValue(tokenize(toReplace), ConfigParseOptions.defaults())
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains("The value from setValue cannot have leading or trailing newlines, whitespace, or comments"))
}
assertTrue(exceptionThrown)
}
@Test
def parseSuccess {
parseTest("foo:bar")
parseTest(" foo : bar ")
parseTest("""include "foo.conf" """)
// Can parse a map with all simple types
parseTest(
"""
aUnquoted : bar
aString = "qux"
aNum:123
aDouble=123.456
aTrue=true
aFalse=false
aNull=null
aSub = ${a.b}
include "foo.conf"
""")
parseTest("{}")
parseTest("{foo:bar}")
parseTest("{ foo : bar }")
parseTest("{foo:bar} ")
parseTest("""{include "foo.conf"}""")
//Can parse a map with all simple types
parseTest(
"""{
aUnquoted : bar
aString = "qux"
aNum:123
aDouble=123.456
aTrue=true
aFalse=false
aNull=null
aSub = ${a.b}
include "foo.conf"
}""")
// Test that maps can be nested within other maps
parseTest(
"""
foo.bar.baz : {
qux : "abcdefg"
"abc".def."ghi" : 123
abc = { foo:bar }
}
qux = 123.456
""")
// Test that comments can be parsed in maps
parseTest(
"""{
foo: bar
// This is a comment
baz:qux // This is another comment
}""")
// Basic array tests
parseTest("[]")
parseTest("[foo]")
// Test trailing comment and whitespace
parseTest("[foo,]")
parseTest("[foo,] ")
// Can parse arrays with all simple types
parseTest("""[foo, bar,"qux", 123,123.456, true,false, null, ${a.b}]""")
parseTest("""[foo, bar,"qux" , 123 , 123.456, true,false, null, ${a.b} ]""")
// Basic concatenation tests
parseTest("[foo bar baz qux]")
parseTest("{foo: foo bar baz qux}")
parseTest("[abc 123 123.456 null true false [1, 2, 3] {a:b}, 2]")
// Complex node with all types test
parseTest(
"""{
foo: bar baz qux ernie
// The above was a concatenation
baz = [ abc 123, {a:12
b: {
c: 13
d: {
a: 22
b: "abcdefg"
c: [1, 2, 3]
}
}
},
//The above value is a map containing a map containing a map, all in an array
22,
// The below value is an array contained in another array
[1,2,3]]
// This is a map with some nested maps and arrays within it, as well as some concatenations
qux {
baz: abc 123
bar: {
baz: abcdefg
bar: {
a: null
b: true
c: [true false 123, null, [1, 2, 3]]
}
}
}
// Did I cover everything?
}""")
// Can correctly parse a JSON string
val origText =
"""{
"foo": "bar",
"baz": 123,
"qux": true,
"array": [
{"a": true,
"c": false},
12
]
}
"""
val node = ConfigDocumentParser.parse(tokenize(origText), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
assertEquals(origText, node.render())
}
@Test
def parseJSONFailures() {
// JSON does not support concatenations
parseJSONFailuresTest("""{ "foo": 123 456 789 } """, "Expecting close brace } or a comma")
// JSON must begin with { or [
parseJSONFailuresTest(""""a": 123, "b": 456"""", "Document must have an object or array at root")
// JSON does not support unquoted text
parseJSONFailuresTest("""{"foo": unquotedtext}""", "Token not allowed in valid JSON")
// JSON does not support substitutions
parseJSONFailuresTest("""{"foo": ${"a.b"}}""", "Substitutions (${} syntax) not allowed in JSON")
// JSON does not support multi-element paths
parseJSONFailuresTest("""{"foo"."bar": 123}""", "Token not allowed in valid JSON")
// JSON does not support =
parseJSONFailuresTest("""{"foo"=123}""", """Key '"foo"' may not be followed by token: '='""")
// JSON does not support +=
parseJSONFailuresTest("""{"foo" += "bar"}""", """Key '"foo"' may not be followed by token: '+='""")
// JSON does not support duplicate keys
parseJSONFailuresTest("""{"foo" : 123, "foo": 456}""", "JSON does not allow duplicate fields")
// JSON does not support trailing commas
parseJSONFailuresTest("""{"foo" : 123,}""", "expecting a field name after a comma, got a close brace } instead")
// JSON does not support empty documents
parseJSONFailuresTest("", "Empty document")
}
@Test
def parseSingleValues() {
// Parse simple values
parseSimpleValueTest("123")
parseSimpleValueTest("123.456")
parseSimpleValueTest(""""a string"""")
parseSimpleValueTest("true")
parseSimpleValueTest("false")
parseSimpleValueTest("null")
// Can parse complex values
parseComplexValueTest("""{"a": "b"}""")
parseComplexValueTest("""["a","b","c"]""")
// Check that concatenations are handled by CONF parsing
var origText = "123 456 unquotedtext abc"
var node = ConfigDocumentParser.parseValue(tokenize(origText), ConfigParseOptions.defaults())
assertEquals(origText, node.render())
// Check that concatenations in JSON will throw an error
origText = "123 456 789"
var exceptionThrown = false
try {
node = ConfigDocumentParser.parseValue(tokenize(origText), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains("Parsing JSON and the value set in setValue was either a concatenation or had trailing whitespace, newlines, or comments"))
}
assertTrue(exceptionThrown)
}
@Test
def parseSingleValuesFailures {
// Parse Simple Value throws on leading and trailing whitespace, comments, or newlines
parseLeadingTrailingFailure(" 123")
parseLeadingTrailingFailure("123 ")
parseLeadingTrailingFailure(" 123 ")
parseLeadingTrailingFailure("\n123")
parseLeadingTrailingFailure("123\n")
parseLeadingTrailingFailure("\n123\n")
parseLeadingTrailingFailure("#thisisacomment\n123#comment")
// Parse Simple Value correctly throws on whitespace after a concatenation
parseLeadingTrailingFailure("123 456 789 ")
parseSingleValueInvalidJSONTest("unquotedtext", "Token not allowed in valid JSON")
parseSingleValueInvalidJSONTest("${a.b}", "Substitutions (${} syntax) not allowed in JSON")
// Check that concatenations in JSON will throw an error
val origText = "123 456 789"
var exceptionThrown = false
try {
val node = ConfigDocumentParser.parseValue(tokenize(origText), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains("Parsing JSON and the value set in setValue was either a concatenation or had trailing whitespace, newlines, or comments"))
}
assertTrue(exceptionThrown)
}
}

View File

@ -0,0 +1,257 @@
package com.typesafe.config.impl
import java.io.{ BufferedReader, FileReader }
import java.nio.charset.StandardCharsets
import java.nio.file.{ Paths, Files }
import com.typesafe.config._
import org.junit.Assert._
import org.junit.Test
class ConfigDocumentTest extends TestUtils {
private def configDocumentReplaceJsonTest(origText: String, finalText: String, newValue: String, replacePath: String) {
val configDocument = ConfigDocumentFactory.parseString(origText, ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
assertEquals(origText, configDocument.render())
val newDocument = configDocument.setValue(replacePath, newValue)
assertTrue(newDocument.isInstanceOf[SimpleConfigDocument])
assertEquals(finalText, newDocument.render())
}
private def configDocumentReplaceConfTest(origText: String, finalText: String, newValue: String, replacePath: String) {
val configDocument = ConfigDocumentFactory.parseString(origText)
assertEquals(origText, configDocument.render())
val newDocument = configDocument.setValue(replacePath, newValue)
assertTrue(newDocument.isInstanceOf[SimpleConfigDocument])
assertEquals(finalText, newDocument.render())
}
@Test
def configDocumentReplace() {
// Can handle parsing/replacement with a very simple map
configDocumentReplaceConfTest("""{"a":1}""", """{"a":2}""", "2", "a")
configDocumentReplaceJsonTest("""{"a":1}""", """{"a":2}""", "2", "a")
// Can handle parsing/replacement with a map without surrounding braces
configDocumentReplaceConfTest("a: b\nc = d", "a: b\nc = 12", "12", "c")
// Can handle parsing/replacement with a complicated map
var origText =
"""{
"a":123,
"b": 123.456,
"c": true,
"d": false,
"e": null,
"f": "a string",
"g": [1,2,3,4,5],
"h": {
"a": 123,
"b": {
"a": 12
},
"c": [1, 2, 3, {"a": "b"}, [1,2,3]]
}
}"""
var finalText =
"""{
"a":123,
"b": 123.456,
"c": true,
"d": false,
"e": null,
"f": "a string",
"g": [1,2,3,4,5],
"h": {
"a": 123,
"b": {
"a": "i am now a string"
},
"c": [1, 2, 3, {"a": "b"}, [1,2,3]]
}
}"""
configDocumentReplaceConfTest(origText, finalText, """"i am now a string"""", "h.b.a")
configDocumentReplaceJsonTest(origText, finalText, """"i am now a string"""", "h.b.a")
// Can handle replacing values with maps
finalText =
"""{
"a":123,
"b": 123.456,
"c": true,
"d": false,
"e": null,
"f": "a string",
"g": [1,2,3,4,5],
"h": {
"a": 123,
"b": {
"a": {"a":"b", "c":"d"}
},
"c": [1, 2, 3, {"a": "b"}, [1,2,3]]
}
}"""
configDocumentReplaceConfTest(origText, finalText, """{"a":"b", "c":"d"}""", "h.b.a")
configDocumentReplaceJsonTest(origText, finalText, """{"a":"b", "c":"d"}""", "h.b.a")
// Can handle replacing values with arrays
finalText =
"""{
"a":123,
"b": 123.456,
"c": true,
"d": false,
"e": null,
"f": "a string",
"g": [1,2,3,4,5],
"h": {
"a": 123,
"b": {
"a": [1,2,3,4,5]
},
"c": [1, 2, 3, {"a": "b"}, [1,2,3]]
}
}"""
configDocumentReplaceConfTest(origText, finalText, "[1,2,3,4,5]", "h.b.a")
configDocumentReplaceJsonTest(origText, finalText, "[1,2,3,4,5]", "h.b.a")
finalText =
"""{
"a":123,
"b": 123.456,
"c": true,
"d": false,
"e": null,
"f": "a string",
"g": [1,2,3,4,5],
"h": {
"a": 123,
"b": {
"a": this is a concatenation 123 456 {a:b} [1,2,3] {a: this is another 123 concatenation null true}
},
"c": [1, 2, 3, {"a": "b"}, [1,2,3]]
}
}"""
configDocumentReplaceConfTest(origText, finalText,
"this is a concatenation 123 456 {a:b} [1,2,3] {a: this is another 123 concatenation null true}", "h.b.a")
}
@Test
def configDocumentSetNewValueBraceRoot {
val origText = "{\n\t\"a\":\"b\",\n\t\"c\":\"d\"\n}"
val finalTextConf = "{\n\t\"a\":\"b\",\n\t\"c\":\"d\"\n\n\"e\" : \"f\"\n}"
val finalTextJson = "{\n\t\"a\":\"b\",\n\t\"c\":\"d\",\n\n\"e\" : \"f\"\n}"
configDocumentReplaceConfTest(origText, finalTextConf, "\"f\"", "\"e\"")
configDocumentReplaceJsonTest(origText, finalTextJson, "\"f\"", "\"e\"")
}
@Test
def configDocumentSetNewValueNoBraces {
val origText = "\"a\":\"b\",\n\"c\":\"d\"\n"
val finalText = "\"a\":\"b\",\n\"c\":\"d\"\n\n\"e\" : \"f\"\n"
configDocumentReplaceConfTest(origText, finalText, "\"f\"", "\"e\"")
}
@Test
def configDocumentSetNewValueMultiLevelConf {
val origText = "a:b\nc:d"
val finalText = "a:b\nc:d\ne : {\nf : {\ng : 12\n}\n}\n"
configDocumentReplaceConfTest(origText, finalText, "12", "e.f.g")
}
@Test
def configDocumentSetNewValueMultiLevelJson {
val origText = "{\"a\":\"b\",\n\"c\":\"d\"}"
val finalText = "{\"a\":\"b\",\n\"c\":\"d\",\n\"e\" : {\n\"f\" : {\n\"g\" : 12\n}\n}\n}"
configDocumentReplaceJsonTest(origText, finalText, "12", "e.f.g")
}
@Test
def configDocumentSetNewConfigValue {
val origText = "{\"a\": \"b\"}"
val finalText = "{\"a\": 12}"
val configDocHOCON = ConfigDocumentFactory.parseString(origText)
val configDocJSON = ConfigDocumentFactory.parseString(origText, ConfigParseOptions.defaults.setSyntax(ConfigSyntax.JSON))
val newValue = ConfigValueFactory.fromAnyRef(12)
assertEquals(origText, configDocHOCON.render())
assertEquals(origText, configDocJSON.render())
assertEquals(finalText, configDocHOCON.setValue("a", newValue).render())
assertEquals(finalText, configDocJSON.setValue("a", newValue).render())
}
@Test
def configDocumentArrayReplaceFailure {
// Attempting a replace on a ConfigDocument parsed from an array throws an error
val origText = "[1, 2, 3, 4, 5]"
val document = ConfigDocumentFactory.parseString(origText)
var exceptionThrown = false
try {
document.setValue("a", "1")
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains("ConfigDocument had an array at the root level"))
}
assertTrue(exceptionThrown)
}
@Test
def configDocumentJSONReplaceFailure {
// Attempting a replace on a ConfigDocument parsed from JSON with a value using HOCON syntax
// will fail
val origText = "{\"foo\": \"bar\", \"baz\": \"qux\"}"
val document = ConfigDocumentFactory.parseString(origText, ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
var exceptionThrown = false
try {
document.setValue("foo", "unquoted")
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains("Token not allowed in valid JSON"))
}
assertTrue(exceptionThrown)
}
@Test
def configDocumentJSONReplaceWithConcatenationFailure {
// Attempting a replace on a ConfigDocument parsed from JSON with a concatenation will
// fail
val origText = "{\"foo\": \"bar\", \"baz\": \"qux\"}"
val document = ConfigDocumentFactory.parseString(origText, ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON))
var exceptionThrown = false
try {
document.setValue("foo", "1 2 3 concatenation")
} catch {
case e: Exception =>
exceptionThrown = true
assertTrue(e.isInstanceOf[ConfigException])
assertTrue(e.getMessage.contains("Parsing JSON and the value set in setValue was either a concatenation or had trailing whitespace, newlines, or comments"))
}
assertTrue(exceptionThrown)
}
@Test
def configDocumentFileParse {
val configDocument = ConfigDocumentFactory.parseFile(resourceFile("/test03.conf"))
val fileReader = new BufferedReader(new FileReader("config/src/test/resources/test03.conf"))
var line = fileReader.readLine()
var sb = new StringBuilder()
while (line != null) {
sb.append(line)
sb.append("\n")
line = fileReader.readLine()
}
fileReader.close()
val fileText = sb.toString()
assertEquals(fileText, configDocument.render())
}
@Test
def configDocumentReaderParse {
val configDocument = ConfigDocumentFactory.parseReader(new FileReader(resourceFile("/test03.conf")))
val configDocumentFile = ConfigDocumentFactory.parseFile(resourceFile("/test03.conf"))
assertEquals(configDocumentFile.render(), configDocument.render())
}
}

View File

@ -0,0 +1,232 @@
package com.typesafe.config.impl
import org.junit.Assert._
import org.junit.Test
class ConfigNodeTest extends TestUtils {
private def basicNodeTest(token: Token) {
val node = configNodeBasic(token);
assertEquals(node.render(), token.tokenText())
}
private def keyNodeTest(path: String) {
val node = configNodeKey(path)
assertEquals(path, node.render())
}
private def simpleValueNodeTest(token: Token) {
val node = configNodeSimpleValue(token)
assertEquals(node.render(), token.tokenText())
}
private def fieldNodeTest(key: ConfigNodePath, value: AbstractConfigNodeValue, trailingWhitespace: ConfigNodeSingleToken, newValue: AbstractConfigNodeValue) {
val keyValNode = nodeKeyValuePair(key, value, trailingWhitespace)
assertEquals(key.render() + " : " + value.render() + trailingWhitespace.render(), keyValNode.render())
assertEquals(key.render, keyValNode.path().render())
assertEquals(value.render, keyValNode.value().render())
val newKeyValNode = keyValNode.replaceValue(newValue)
assertEquals(key.render() + " : " + newValue.render() + trailingWhitespace.render(), newKeyValNode.render())
assertEquals(newValue.render(), newKeyValNode.value().render())
}
private def topLevelValueReplaceTest(value: AbstractConfigNodeValue, newValue: AbstractConfigNodeValue, key: String = "foo") {
val complexNodeChildren = List(nodeOpenBrace,
nodeKeyValuePair(nodeWhitespace(" "), configNodeKey(key), value, nodeWhitespace(" ")),
nodeCloseBrace)
val complexNode = configNodeObject(complexNodeChildren)
val newNode = complexNode.setValueOnPath(key, newValue)
val origText = "{ " + key + " : " + value.render() + " }"
val finalText = "{ " + key + " : " + newValue.render() + " }"
assertEquals(origText, complexNode.render())
assertEquals(finalText, newNode.render())
}
private def replaceDuplicatesTest(value1: AbstractConfigNodeValue, value2: AbstractConfigNodeValue, value3: AbstractConfigNodeValue) {
val key = configNodeKey("foo")
val keyValPair1 = nodeKeyValuePair(key, value1)
val keyValPair2 = nodeKeyValuePair(key, value2)
val keyValPair3 = nodeKeyValuePair(key, value3)
val complexNode = configNodeObject(List(keyValPair1, keyValPair2, keyValPair3))
val origText = keyValPair1.render() + keyValPair2.render() + keyValPair3.render()
val finalText = key.render() + " : 15"
assertEquals(origText, complexNode.render())
assertEquals(finalText, complexNode.setValueOnPath("foo", nodeInt(15)).render())
}
private def nonExistentPathTest(value: AbstractConfigNodeValue) {
val node = configNodeObject(List(nodeKeyValuePair(configNodeKey("bar"), nodeInt(15))))
assertEquals("bar : 15", node.render())
val newNode = node.setValueOnPath("foo", value)
val finalText = "bar : 15\nfoo : " + value.render() + "\n"
assertEquals(finalText, newNode.render())
}
@Test
def createBasicConfigNode() {
//Ensure a BasicConfigNode can handle all its required token types
basicNodeTest(Tokens.START)
basicNodeTest(Tokens.END)
basicNodeTest(Tokens.OPEN_CURLY)
basicNodeTest(Tokens.CLOSE_CURLY)
basicNodeTest(Tokens.OPEN_SQUARE)
basicNodeTest(Tokens.CLOSE_SQUARE)
basicNodeTest(Tokens.COMMA)
basicNodeTest(Tokens.EQUALS)
basicNodeTest(Tokens.COLON)
basicNodeTest(Tokens.PLUS_EQUALS)
basicNodeTest(tokenUnquoted(" "))
basicNodeTest(tokenWhitespace(" "))
basicNodeTest(tokenLine(1))
basicNodeTest(tokenCommentDoubleSlash(" this is a double slash comment "))
basicNodeTest(tokenCommentHash(" this is a hash comment "))
}
@Test
def createConfigNodeSetting() {
//Ensure a ConfigNodeSetting can handle the normal key types
keyNodeTest("foo")
keyNodeTest("\"Hello I am a key how are you today\"")
}
@Test
def pathNodeSubpath() {
val origPath = "a.b.c.\"@$%#@!@#$\".\"\".1234.5678"
val pathNode = configNodeKey(origPath)
assertEquals(origPath, pathNode.render())
assertEquals("c.\"@$%#@!@#$\".\"\".1234.5678", pathNode.subPath(2).render())
assertEquals("5678", pathNode.subPath(6).render())
}
@Test
def createConfigNodeSimpleValue() {
//Ensure a ConfigNodeSimpleValue can handle the normal value types
simpleValueNodeTest(tokenInt(10))
simpleValueNodeTest(tokenLong(10000))
simpleValueNodeTest(tokenDouble(3.14159))
simpleValueNodeTest(tokenFalse)
simpleValueNodeTest(tokenTrue)
simpleValueNodeTest(tokenNull)
simpleValueNodeTest(tokenString("Hello my name is string"))
simpleValueNodeTest(tokenUnquoted("mynameisunquotedstring"))
simpleValueNodeTest(tokenKeySubstitution("c.d"))
simpleValueNodeTest(tokenOptionalSubstitution(tokenUnquoted("x.y")))
simpleValueNodeTest(tokenSubstitution(tokenUnquoted("a.b")))
}
@Test
def createConfigNodeField() {
// Supports Quoted and Unquoted keys
fieldNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), nodeInt(245))
fieldNodeTest(configNodeKey("abc"), nodeInt(123), nodeLine(1), nodeInt(245))
// Can replace value with values of different types
fieldNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), nodeString("I am a string"))
fieldNodeTest(configNodeKey("\"abc\""), nodeInt(123), nodeLine(1), configNodeObject(List(nodeOpenBrace, nodeCloseBrace)))
}
@Test
def replaceNodes() {
//Ensure simple values can be replaced by other simple values
topLevelValueReplaceTest(nodeInt(10), nodeInt(15))
topLevelValueReplaceTest(nodeLong(10000), nodeInt(20))
topLevelValueReplaceTest(nodeDouble(3.14159), nodeLong(10000))
topLevelValueReplaceTest(nodeFalse, nodeTrue)
topLevelValueReplaceTest(nodeTrue, nodeNull)
topLevelValueReplaceTest(nodeNull, nodeString("Hello my name is string"))
topLevelValueReplaceTest(nodeString("Hello my name is string"), nodeUnquotedText("mynameisunquotedstring"))
topLevelValueReplaceTest(nodeUnquotedText("mynameisunquotedstring"), nodeKeySubstitution("c.d"))
topLevelValueReplaceTest(nodeInt(10), nodeOptionalSubstitution(tokenUnquoted("x.y")))
topLevelValueReplaceTest(nodeInt(10), nodeSubstitution(tokenUnquoted("a.b")))
topLevelValueReplaceTest(nodeSubstitution(tokenUnquoted("a.b")), nodeInt(10))
// Ensure arrays can be replaced
val array = configNodeArray(List(nodeOpenBracket, nodeInt(10), nodeSpace, nodeComma, nodeSpace, nodeInt(15), nodeCloseBracket))
topLevelValueReplaceTest(nodeInt(10), array)
topLevelValueReplaceTest(array, nodeInt(10))
topLevelValueReplaceTest(array, configNodeObject(List(nodeOpenBrace, nodeCloseBrace)))
// Ensure maps can be replaced
val nestedMap = configNodeObject(List(nodeOpenBrace, configNodeKey("abc"),
nodeColon, configNodeSimpleValue(tokenString("a string")),
nodeCloseBrace))
topLevelValueReplaceTest(nestedMap, nodeInt(10))
topLevelValueReplaceTest(nodeInt(10), nestedMap)
topLevelValueReplaceTest(array, nestedMap)
topLevelValueReplaceTest(nestedMap, array)
topLevelValueReplaceTest(nestedMap, configNodeObject(List(nodeOpenBrace, nodeCloseBrace)))
// Ensure concatenations can be replaced
val concatenation = configNodeConcatenation(List(nodeInt(10), nodeSpace, nodeString("Hello")))
topLevelValueReplaceTest(concatenation, nodeInt(12))
topLevelValueReplaceTest(nodeInt(12), concatenation)
topLevelValueReplaceTest(nestedMap, concatenation)
topLevelValueReplaceTest(concatenation, nestedMap)
topLevelValueReplaceTest(array, concatenation)
topLevelValueReplaceTest(concatenation, array)
//Ensure a key with format "a.b" will be properly replaced
topLevelValueReplaceTest(nodeInt(10), nestedMap, "foo.bar")
}
@Test
def removeDuplicates() {
val emptyMapNode = configNodeObject(List(nodeOpenBrace, nodeCloseBrace))
val emptyArrayNode = configNodeArray(List(nodeOpenBracket, nodeCloseBracket))
//Ensure duplicates of a key are removed from a map
replaceDuplicatesTest(nodeInt(10), nodeTrue, nodeNull)
replaceDuplicatesTest(emptyMapNode, emptyMapNode, emptyMapNode)
replaceDuplicatesTest(emptyArrayNode, emptyArrayNode, emptyArrayNode)
replaceDuplicatesTest(nodeInt(10), emptyMapNode, emptyArrayNode)
}
@Test
def addNonExistentPaths() {
nonExistentPathTest(nodeInt(10))
nonExistentPathTest(configNodeArray(List(nodeOpenBracket, nodeInt(15), nodeCloseBracket)))
nonExistentPathTest(configNodeObject(List(nodeOpenBrace, nodeKeyValuePair(configNodeKey("foo"), nodeDouble(3.14), nodeSpace))))
}
@Test
def replaceNestedNodes() {
// Test that all features of node replacement in a map work in a complex map containing nested maps
val origText = "foo : bar\nbaz : {\n\t\"abc.def\" : 123\n\t//This is a comment about the below setting\n\n\tabc : {\n\t\t" +
"def : \"this is a string\"\n\t\tghi : ${\"a.b\"}\n\t}\n}\nbaz.abc.ghi : 52\nbaz.abc.ghi : 53\n}"
val lowestLevelMap = configNodeObject(List(nodeOpenBrace, nodeLine(6),
nodeKeyValuePair(nodeWhitespace("\t\t"), configNodeKey("def"), configNodeSimpleValue(tokenString("this is a string")), nodeLine(7)),
nodeKeyValuePair(nodeWhitespace("\t\t"), configNodeKey("ghi"), configNodeSimpleValue(tokenKeySubstitution("a.b")), nodeLine(8)),
nodeWhitespace("\t"), nodeCloseBrace))
val higherLevelMap = configNodeObject(List(nodeOpenBrace, nodeLine(2),
nodeKeyValuePair(nodeWhitespace("\t"), configNodeKey("\"abc.def\""), configNodeSimpleValue(tokenInt(123)), nodeLine(3)),
nodeWhitespace("\t"), configNodeBasic(tokenCommentDoubleSlash("This is a comment about the below setting")),
nodeLine(4), nodeLine(5),
nodeKeyValuePair(nodeWhitespace("\t"), configNodeKey("abc"), lowestLevelMap, nodeLine(9)), nodeWhitespace(""),
nodeCloseBrace))
val origNode = configNodeObject(List(nodeKeyValuePair(configNodeKey("foo"), configNodeSimpleValue(tokenUnquoted("bar")), nodeLine(1)),
nodeKeyValuePair(configNodeKey("baz"), higherLevelMap, nodeLine(10)),
nodeKeyValuePair(configNodeKey("baz.abc.ghi"), configNodeSimpleValue(tokenInt(52)), nodeLine(11)),
nodeKeyValuePair(configNodeKey("baz.abc.ghi"), configNodeSimpleValue(tokenInt(53)), nodeLine(12)),
nodeCloseBrace))
assertEquals(origText, origNode.render())
val finalText = "foo : bar\nbaz : {\n\t\"abc.def\" : true\n\t//This is a comment about the below setting\n\n\tabc : {\n\t\t" +
"def : false\n\t\n\"this.does.not.exist@@@+$#\" : {\nend : doesnotexist\n}\n}\n}\nbaz.abc.ghi : randomunquotedString\n}"
//Can replace settings in nested maps
// Paths with quotes in the name are treated as a single Path, rather than multiple sub-paths
var newNode = origNode.setValueOnPath("baz.\"abc.def\"", configNodeSimpleValue(tokenTrue))
newNode = newNode.setValueOnPath("baz.abc.def", configNodeSimpleValue(tokenFalse))
// Repeats are removed from nested maps
newNode = newNode.setValueOnPath("baz.abc.ghi", configNodeSimpleValue(tokenUnquoted("randomunquotedString")))
// Missing paths are added to the top level if they don't appear anywhere, including in nested maps
newNode = newNode.setValueOnPath("baz.abc.\"this.does.not.exist@@@+$#\".end", configNodeSimpleValue(tokenUnquoted("doesnotexist")))
// The above operations cause the resultant map to be rendered properly
assertEquals(finalText, newNode.render())
}
}

View File

@ -73,8 +73,8 @@ class PathTest extends TestUtils {
for (t <- tests) {
assertEquals(t.expected, t.path.render())
assertEquals(t.path, Parser.parsePath(t.expected))
assertEquals(t.path, Parser.parsePath(t.path.render()))
assertEquals(t.path, PathParser.parsePath(t.expected))
assertEquals(t.path, PathParser.parsePath(t.path.render()))
}
}
@ -110,6 +110,14 @@ class PathTest extends TestUtils {
assertEquals("b", path("a", "b").last())
}
@Test
def pathStartsWith() {
assertTrue(path("a", "b", "c", "d").startsWith(path("a", "b")))
assertTrue(path("a", "b", "c", "d").startsWith(path("a", "b", "c", "d")))
assertFalse(path("a", "b", "c", "d").startsWith(path("b", "c", "d")))
assertFalse(path("a", "b", "c", "d").startsWith(path("invalidpath")))
}
@Test
def pathsAreInvalid() {
// this test is just of the Path.newPath() wrapper, the extensive

View File

@ -334,7 +334,7 @@ abstract trait TestUtils {
ParseTest(true, " \"foo\" : "), // no value in object with no braces; lift-json thinks this is acceptable
ParseTest(true, " : 10 "), // no key in object with no braces; lift-json is cool with this too
" \"foo\" : 10 } ", // close brace but no open
" \"foo\" : 10 [ ", // no-braces object with trailing gunk
" \"foo\" : 10 [ ", // no-braces object with trailing gunk
"{ \"foo\" }", // no value or colon
"{ \"a\" : [ }", // [ is not a valid value
"{ \"foo\" : 10, true }", // non-key after comma
@ -611,10 +611,10 @@ abstract trait TestUtils {
def tokenFalse = Tokens.newBoolean(fakeOrigin(), false)
def tokenNull = Tokens.newNull(fakeOrigin())
def tokenUnquoted(s: String) = Tokens.newUnquotedText(fakeOrigin(), s)
def tokenString(s: String) = Tokens.newString(fakeOrigin(), s, s)
def tokenDouble(d: Double) = Tokens.newDouble(fakeOrigin(), d, null)
def tokenInt(i: Int) = Tokens.newInt(fakeOrigin(), i, null)
def tokenLong(l: Long) = Tokens.newLong(fakeOrigin(), l, null)
def tokenString(s: String) = Tokens.newString(fakeOrigin(), s, "\"" + s + "\"")
def tokenDouble(d: Double) = Tokens.newDouble(fakeOrigin(), d, "" + d)
def tokenInt(i: Int) = Tokens.newInt(fakeOrigin(), i, "" + i)
def tokenLong(l: Long) = Tokens.newLong(fakeOrigin(), l, l.toString())
def tokenLine(line: Int) = Tokens.newLine(fakeOrigin.withLineNumber(line))
def tokenCommentDoubleSlash(text: String) = Tokens.newCommentDoubleSlash(fakeOrigin(), text)
def tokenCommentHash(text: String) = Tokens.newCommentHash(fakeOrigin(), text)
@ -663,6 +663,63 @@ abstract trait TestUtils {
Tokenizer.render(tokenize(s))
}
def configNodeSimpleValue(value: Token) = {
new ConfigNodeSimpleValue(value)
}
def configNodeKey(path: String) = PathParser.parsePathNode(path)
def configNodeBasic(value: Token) = {
new ConfigNodeSingleToken(value: Token)
}
def configNodeObject(nodes: List[AbstractConfigNode]) = {
new ConfigNodeObject(nodes.asJavaCollection)
}
def configNodeArray(nodes: List[AbstractConfigNode]) = {
new ConfigNodeArray(nodes.asJavaCollection)
}
def configNodeConcatenation(nodes: List[AbstractConfigNode]) = {
new ConfigNodeConcatenation(nodes.asJavaCollection)
}
def nodeColon = new ConfigNodeSingleToken(Tokens.COLON)
def nodeSpace = new ConfigNodeSingleToken(tokenUnquoted(" "))
def nodeOpenBrace = new ConfigNodeSingleToken(Tokens.OPEN_CURLY)
def nodeCloseBrace = new ConfigNodeSingleToken(Tokens.CLOSE_CURLY)
def nodeOpenBracket = new ConfigNodeSingleToken(Tokens.OPEN_SQUARE)
def nodeCloseBracket = new ConfigNodeSingleToken(Tokens.CLOSE_SQUARE)
def nodeComma = new ConfigNodeSingleToken(Tokens.COMMA)
def nodeLine(line: Integer) = new ConfigNodeSingleToken(tokenLine(line))
def nodeWhitespace(whitespace: String) = new ConfigNodeSingleToken(tokenWhitespace(whitespace))
def nodeKeyValuePair(key: ConfigNodePath, value: AbstractConfigNodeValue) = {
val nodes = List(key, nodeSpace, nodeColon, nodeSpace, value)
new ConfigNodeField(nodes.asJavaCollection)
}
def nodeKeyValuePair(key: ConfigNodePath, value: AbstractConfigNodeValue, trailingWhitespace: ConfigNodeSingleToken) = {
val nodes = List(key, nodeSpace, nodeColon, nodeSpace, value, trailingWhitespace)
new ConfigNodeField(nodes.asJavaCollection)
}
def nodeKeyValuePair(leadingWhitespace: ConfigNodeSingleToken, key: ConfigNodePath, value: AbstractConfigNodeValue, trailingWhitespace: ConfigNodeSingleToken) = {
val nodes = List(leadingWhitespace, key, nodeSpace, nodeColon, nodeSpace, value, trailingWhitespace)
new ConfigNodeField(nodes.asJavaCollection)
}
def nodeInt(value: Integer) = new ConfigNodeSimpleValue(tokenInt(value))
def nodeString(value: String) = new ConfigNodeSimpleValue(tokenString(value))
def nodeLong(value: Long) = new ConfigNodeSimpleValue(tokenLong(value))
def nodeDouble(value: Double) = new ConfigNodeSimpleValue(tokenDouble(value))
def nodeTrue = new ConfigNodeSimpleValue(tokenTrue)
def nodeFalse = new ConfigNodeSimpleValue(tokenFalse)
def nodeCommentHash(text: String) = new ConfigNodeSingleToken(tokenCommentHash(text))
def nodeCommentDoubleSlash(text: String) = new ConfigNodeSingleToken(tokenCommentDoubleSlash(text))
def nodeUnquotedText(text: String) = new ConfigNodeSimpleValue(tokenUnquoted(text))
def nodeNull = new ConfigNodeSimpleValue(tokenNull)
def nodeKeySubstitution(s: String) = new ConfigNodeSimpleValue(tokenKeySubstitution(s))
def nodeOptionalSubstitution(expression: Token*) = new ConfigNodeSimpleValue(tokenOptionalSubstitution(expression: _*))
def nodeSubstitution(expression: Token*) = new ConfigNodeSimpleValue(tokenSubstitution(expression: _*))
// this is importantly NOT using Path.newPath, which relies on
// the parser; in the test suite we are often testing the parser,
// so we don't want to use the parser to build the expected result.