From 639a3eae5bbb9ab5da7fc51f2c4615571d7a4b5f Mon Sep 17 00:00:00 2001 From: Preben Ingvaldsen Date: Tue, 17 Mar 2015 15:25:58 -0700 Subject: [PATCH] Add ConfigDocument tests Add ConfigDocument tests that parse a String or a file, ensure that the original text can be rendered, and test value replacement. --- .../com/typesafe/config/ConfigDocument.java | 38 ++++ .../config/ConfigDocumentFactory.java | 63 ++++++ .../config/impl/ConfigDocumentParser.java | 35 +++- .../config/impl/ConfigNodeObject.java | 23 ++- .../config/impl/ConfigNodeSingleToken.java | 2 + .../com/typesafe/config/impl/Parseable.java | 83 ++++++-- .../config/impl/SimpleConfigDocument.java | 36 ++++ .../impl/ConfigDocumentParserTest.scala | 71 +++++++ .../config/impl/ConfigDocumentTest.scala | 186 ++++++++++++++++++ 9 files changed, 521 insertions(+), 16 deletions(-) create mode 100644 config/src/main/java/com/typesafe/config/ConfigDocument.java create mode 100644 config/src/main/java/com/typesafe/config/ConfigDocumentFactory.java create mode 100644 config/src/main/java/com/typesafe/config/impl/SimpleConfigDocument.java create mode 100644 config/src/test/scala/com/typesafe/config/impl/ConfigDocumentTest.scala diff --git a/config/src/main/java/com/typesafe/config/ConfigDocument.java b/config/src/main/java/com/typesafe/config/ConfigDocument.java new file mode 100644 index 00000000..30a807f8 --- /dev/null +++ b/config/src/main/java/com/typesafe/config/ConfigDocument.java @@ -0,0 +1,38 @@ +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. + * + *

+ * Because this object is immutable, it is safe to use from multiple threads and + * there's no need for "defensive copies." + * + *

+ * Do not implement interface {@code ConfigNode}; 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 + * @return a copy of the ConfigDocument with the desired value at the desired path + */ + ConfigDocument setValue(String path, String newValue); + + /** + * The original text of the input, modified if necessary with + * any replaced or added values. + * @return the modified original text + */ + String render(); +} diff --git a/config/src/main/java/com/typesafe/config/ConfigDocumentFactory.java b/config/src/main/java/com/typesafe/config/ConfigDocumentFactory.java new file mode 100644 index 00000000..79f81220 --- /dev/null +++ b/config/src/main/java/com/typesafe/config/ConfigDocumentFactory.java @@ -0,0 +1,63 @@ +package com.typesafe.config; + +import com.typesafe.config.impl.ConfigImpl; +import com.typesafe.config.impl.Parseable; + +import java.io.File; + +/** + * Factory for automatically creating a ConfigDocument from a given input. Currently + * only supports files and strings. + */ +public final class ConfigDocumentFactory { + + /** + * 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()); + } +} diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java b/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java index 0c84d0e3..3f498843 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigDocumentParser.java @@ -11,16 +11,21 @@ import com.typesafe.config.ConfigSyntax; import com.typesafe.config.ConfigValueType; final class ConfigDocumentParser { - static AbstractConfigNodeValue parse(Iterator tokens, ConfigParseOptions options) { + static ConfigNodeComplexValue parse(Iterator tokens, ConfigParseOptions options) { ParseContext context = new ParseContext(options.getSyntax(), tokens); return context.parse(); } - static AbstractConfigNodeValue parse(Iterator tokens) { + static ConfigNodeComplexValue parse(Iterator tokens) { ParseContext context = new ParseContext(ConfigSyntax.CONF, tokens); return context.parse(); } + static AbstractConfigNodeValue parseValue(Iterator tokens, ConfigParseOptions options) { + ParseContext context = new ParseContext(options.getSyntax(), tokens); + return context.parseSingleValue(); + } + static private final class ParseContext { private int lineNumber; final private Stack buffer; @@ -625,5 +630,31 @@ final class ConfigDocumentParser { + 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(); + while (Tokens.isIgnoredWhitespace(t) || Tokens.isNewline(t) || isUnquotedWhitespace(t)) { + t = nextToken(); + } + if (t == Tokens.END) { + throw parseError("Empty value"); + } + if (flavor == ConfigSyntax.JSON) { + return parseValue(t); + } else { + putBack(t); + ArrayList nodes = new ArrayList(); + return consolidateValues(nodes); + } + } } } diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigNodeObject.java b/config/src/main/java/com/typesafe/config/impl/ConfigNodeObject.java index 9858cc2c..ccd1f8ca 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigNodeObject.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigNodeObject.java @@ -1,7 +1,10 @@ package com.typesafe.config.impl; +import com.typesafe.config.ConfigException; + import java.util.ArrayList; import java.util.Collection; +import java.util.List; final class ConfigNodeObject extends ConfigNodeComplexValue { ConfigNodeObject(Collection children) { @@ -47,17 +50,33 @@ final class ConfigNodeObject extends ConfigNodeComplexValue { // If the desired Path did not exist, add it if (node.render().equals(render())) { + boolean startsWithBrace = super.children.get(0) instanceof ConfigNodeSingleToken && + ((ConfigNodeSingleToken) super.children.get(0)).token() == Tokens.OPEN_CURLY; ArrayList childrenCopy = new ArrayList(super.children); ArrayList newNodes = new ArrayList(); newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null))); + if (startsWithBrace) + newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, "\t"))); newNodes.add(desiredPath); newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " "))); newNodes.add(new ConfigNodeSingleToken(Tokens.COLON)); newNodes.add(new ConfigNodeSingleToken(Tokens.newIgnoredWhitespace(null, " "))); newNodes.add(value); newNodes.add(new ConfigNodeSingleToken(Tokens.newLine(null))); - childrenCopy.add(new ConfigNodeField(newNodes)); - node = new ConfigNodeObject(childrenCopy); + + if (startsWithBrace) { + for (int i = childrenCopy.size() - 1; i >= 0; i--) { + if (childrenCopy.get(i) instanceof ConfigNodeSingleToken && + ((ConfigNodeSingleToken) childrenCopy.get(i)).token == Tokens.CLOSE_CURLY) { + childrenCopy.add(i, new ConfigNodeField(newNodes)); + return new ConfigNodeObject(childrenCopy); + } + } + throw new ConfigException.BugOrBroken("Object had an opening brace, but no closing brace"); + } else { + childrenCopy.add(new ConfigNodeField(newNodes)); + node = new ConfigNodeObject(childrenCopy); + } } return node; } diff --git a/config/src/main/java/com/typesafe/config/impl/ConfigNodeSingleToken.java b/config/src/main/java/com/typesafe/config/impl/ConfigNodeSingleToken.java index 9863b8e2..6f4b5833 100644 --- a/config/src/main/java/com/typesafe/config/impl/ConfigNodeSingleToken.java +++ b/config/src/main/java/com/typesafe/config/impl/ConfigNodeSingleToken.java @@ -16,4 +16,6 @@ final class ConfigNodeSingleToken extends AbstractConfigNode{ protected Collection tokens() { return Collections.singletonList(token); } + + protected Token token() { return token; } } \ No newline at end of file diff --git a/config/src/main/java/com/typesafe/config/impl/Parseable.java b/config/src/main/java/com/typesafe/config/impl/Parseable.java index 643fcfad..daa41f3a 100644 --- a/config/src/main/java/com/typesafe/config/impl/Parseable.java +++ b/config/src/main/java/com/typesafe/config/impl/Parseable.java @@ -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()), 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 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()); } diff --git a/config/src/main/java/com/typesafe/config/impl/SimpleConfigDocument.java b/config/src/main/java/com/typesafe/config/impl/SimpleConfigDocument.java new file mode 100644 index 00000000..73b8901a --- /dev/null +++ b/config/src/main/java/com/typesafe/config/impl/SimpleConfigDocument.java @@ -0,0 +1,36 @@ +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 tokens = Tokenizer.tokenize(origin, reader, parseOptions.getSyntax()); + AbstractConfigNodeValue parsedValue = ConfigDocumentParser.parseValue(tokens, parseOptions); + reader.close(); + + return new SimpleConfigDocument(((ConfigNodeObject)configNodeTree).setValueOnPath(path, parsedValue), parseOptions); + } + + public String render() { + return configNodeTree.render(); + } +} diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala index dba76412..4ee8393a 100644 --- a/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentParserTest.scala @@ -24,6 +24,44 @@ class ConfigDocumentParserTest extends TestUtils { 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) + } + + @Test def parseSuccess { parseTest("foo:bar") @@ -184,4 +222,37 @@ class ConfigDocumentParserTest extends TestUtils { parseJSONFailuresTest("", "Empty document") } + + @Test + def parseSingleValues() { + // Parse simple values + parseSimpleValueTest("123") + parseSimpleValueTest("123.456") + parseSimpleValueTest(""""a string"""") + parseSimpleValueTest("true") + parseSimpleValueTest("false") + parseSimpleValueTest("null") + + // Parse Simple Value throws out trailing and leading whitespace + parseSimpleValueTest(" 123", "123") + parseSimpleValueTest("123 ", "123") + parseSimpleValueTest(" 123 ", "123") + + // Can parse complex values + parseComplexValueTest("""{"a": "b"}""") + parseComplexValueTest("""["a","b","c"]""") + + parseSingleValueInvalidJSONTest("unquotedtext", "Token not allowed in valid JSON") + parseSingleValueInvalidJSONTest("${a.b}", "Substitutions (${} syntax) not allowed in JSON") + + // 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 only return the first value passed in + origText = "123 456 789" + node = ConfigDocumentParser.parseValue(tokenize(origText), ConfigParseOptions.defaults().setSyntax(ConfigSyntax.JSON)) + assertEquals("123", node.render()) + } } diff --git a/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentTest.scala b/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentTest.scala new file mode 100644 index 00000000..c79b8bef --- /dev/null +++ b/config/src/test/scala/com/typesafe/config/impl/ConfigDocumentTest.scala @@ -0,0 +1,186 @@ +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.{ConfigException, ConfigSyntax, ConfigParseOptions, ConfigDocumentFactory} +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 finalText = "{\n\t\"a\":\"b\",\n\t\"c\":\"d\"\n\n\t\"e\" : \"f\"\n}" + configDocumentReplaceConfTest(origText, finalText, "\"f\"", "\"e\"") + configDocumentReplaceJsonTest(origText, finalText, "\"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 configDocumentReplaceFailure { + // 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 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()) + } +}