mirror of
https://github.com/lightbend/config.git
synced 2025-01-28 21:20:07 +08:00
Merge pull request #280 from fpringvaldsen/task/create-ConfigNode
Add ConfigDocument API
This commit is contained in:
commit
25a9f91230
58
config/src/main/java/com/typesafe/config/ConfigDocument.java
Normal file
58
config/src/main/java/com/typesafe/config/ConfigDocument.java
Normal 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();
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
27
config/src/main/java/com/typesafe/config/ConfigNode.java
Normal file
27
config/src/main/java/com/typesafe/config/ConfigNode.java
Normal 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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.typesafe.config.impl;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
final class ConfigNodeArray extends ConfigNodeComplexValue {
|
||||
ConfigNodeArray(Collection<AbstractConfigNode> children) {
|
||||
super(children);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.typesafe.config.impl;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
final class ConfigNodeConcatenation extends ConfigNodeComplexValue {
|
||||
ConfigNodeConcatenation(Collection<AbstractConfigNode> children) {
|
||||
super(children);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
281
config/src/main/java/com/typesafe/config/impl/PathParser.java
Normal file
281
config/src/main/java/com/typesafe/config/impl/PathParser.java
Normal 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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 =>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user