View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.juneau.config;
18  
19  import static org.apache.juneau.TestUtils.*;
20  import static org.junit.jupiter.api.Assertions.*;
21  
22  import java.util.*;
23  
24  import org.apache.juneau.*;
25  import org.apache.juneau.common.utils.*;
26  import org.apache.juneau.config.store.*;
27  import org.junit.jupiter.api.*;
28  
29  class ConfigMap_Test extends TestBase {
30  
31  	static final String ENCODED = "*";
32  	static final String BASE64 = "^";
33  
34  	//-----------------------------------------------------------------------------------------------------------------
35  	// Should be able to read non-existent files without errors.
36  	//-----------------------------------------------------------------------------------------------------------------
37  	@Test void a01_nonExistentConfig() throws Exception {
38  		var s = MemoryStore.create().build();
39  		var cm = s.getMap("A.cfg");
40  		assertEquals("", cm.toString());
41  	}
42  
43  	//-----------------------------------------------------------------------------------------------------------------
44  	// Should be able to read blank files without errors.
45  	//-----------------------------------------------------------------------------------------------------------------
46  	@Test void a02_blankConfig() throws Exception {
47  
48  		var s = initStore("A.cfg", "");
49  		var cm = s.getMap("A.cfg");
50  		assertEquals("", cm.toString());
51  
52  		s.update("A.cfg", "   \n   \n   ");
53  		s.getMap("A.cfg");
54  	}
55  
56  	//-----------------------------------------------------------------------------------------------------------------
57  	// Simple one-line file.
58  	//-----------------------------------------------------------------------------------------------------------------
59  	@Test void a03_simpleOneLine() throws Exception {
60  		var s = initStore("A.cfg",
61  			"foo=bar"
62  		);
63  		var cm = s.getMap("A.cfg");
64  
65  		assertEquals("foo=bar|", pipedLines(cm));
66  
67  		assertEquals("", Utils.join(cm.getPreLines(""), '|'));
68  		assertEquals("", Utils.join(cm.getEntry("", "foo").getPreLines(), '|'));
69  
70  		assertEquals("bar", cm.getEntry("", "foo").getValue());
71  
72  		// Round trip.
73  		s.update("A.cfg", cm.toString());
74  		cm = s.getMap("A.cfg");
75  		assertEquals("foo=bar|", pipedLines(cm));
76  	}
77  
78  	//-----------------------------------------------------------------------------------------------------------------
79  	// Simple one-line file with leading comments.
80  	//-----------------------------------------------------------------------------------------------------------------
81  	@Test void a04_simpleOneLineWithComments() throws Exception {
82  		var s = initStore("A.cfg",
83  			"#comment",
84  			"foo=bar"
85  		);
86  		var cm = s.getMap("A.cfg");
87  
88  		assertEquals("#comment|foo=bar|", pipedLines(cm));
89  
90  		assertEquals("", Utils.join(cm.getPreLines(""), '|'));
91  		assertEquals("#comment", Utils.join(cm.getEntry("", "foo").getPreLines(), '|'));
92  
93  		assertEquals("bar", cm.getEntry("", "foo").getValue());
94  
95  		// Round trip.
96  		s.update("A.cfg", cm.toString());
97  		cm = s.getMap("A.cfg");
98  		assertEquals("#comment|foo=bar|", pipedLines(cm));
99  	}
100 
101 	//-----------------------------------------------------------------------------------------------------------------
102 	// Simple section.
103 	//-----------------------------------------------------------------------------------------------------------------
104 	@Test void a05_simpleSection() throws Exception {
105 		var s = initStore("A.cfg",
106 			"[MySection]",
107 			"foo=bar"
108 		);
109 		var cm = s.getMap("A.cfg");
110 
111 		assertEquals("[MySection]|foo=bar|", pipedLines(cm));
112 
113 		assertEquals("", Utils.join(cm.getPreLines(""), '|'));
114 		assertEquals("", Utils.join(cm.getPreLines("MySection"), '|'));
115 		assertEquals("", Utils.join(cm.getEntry("MySection", "foo").getPreLines(), '|'));
116 
117 		assertEquals("bar", cm.getEntry("MySection", "foo").getValue());
118 
119 		// Round trip.
120 		s.update("A.cfg", cm.toString());
121 		cm = s.getMap("A.cfg");
122 		assertEquals("[MySection]|foo=bar|", pipedLines(cm));
123 	}
124 
125 	//-----------------------------------------------------------------------------------------------------------------
126 	// Non-existent values should not throw exceptions.
127 	//-----------------------------------------------------------------------------------------------------------------
128 	@Test void a06_nonExistentValues() throws Exception {
129 		var s = initStore("A.cfg",
130 			"[MySection]",
131 			"foo=bar"
132 		);
133 		var cm = s.getMap("A.cfg");
134 
135 		assertEquals("[MySection]|foo=bar|", pipedLines(cm));
136 
137 		assertEquals("", Utils.join(cm.getPreLines(""), '|'));
138 
139 		assertNull(cm.getPreLines("XXX"));
140 
141 		assertNull(cm.getEntry("XXX", "yyy"));
142 		assertNull(cm.getEntry("MySection", "yyy"));
143 	}
144 
145 	//-----------------------------------------------------------------------------------------------------------------
146 	// testSimpleSectionWithComments
147 	//-----------------------------------------------------------------------------------------------------------------
148 	@Test void a07_simpleSectionWithComments() throws Exception {
149 		var s = initStore("A.cfg",
150 			"#S1",
151 			"[S1]",
152 			"#k1",
153 			"k1=v1",
154 			"#S2",
155 			"[S2]",
156 			"#k2",
157 			"k2=v2"
158 		);
159 		var cm = s.getMap("A.cfg");
160 		assertEquals("#S1|[S1]|#k1|k1=v1|#S2|[S2]|#k2|k2=v2|", pipedLines(cm));
161 
162 		assertEquals("", Utils.join(cm.getPreLines(""), '|'));
163 		assertEquals("#S1", Utils.join(cm.getPreLines("S1"), '|'));
164 		assertEquals("#k1", Utils.join(cm.getEntry("S1", "k1").getPreLines(), '|'));
165 		assertEquals("#S2", Utils.join(cm.getPreLines("S2"), '|'));
166 		assertEquals("#k2", Utils.join(cm.getEntry("S2", "k2").getPreLines(), '|'));
167 
168 		assertEquals("v1", cm.getEntry("S1", "k1").getValue());
169 		assertEquals("v2", cm.getEntry("S2", "k2").getValue());
170 
171 		// Round trip.
172 		s.update("A.cfg", cm.toString());
173 		cm = s.getMap("A.cfg");
174 		assertEquals("#S1|[S1]|#k1|k1=v1|#S2|[S2]|#k2|k2=v2|", pipedLines(cm));
175 	}
176 
177 	//-----------------------------------------------------------------------------------------------------------------
178 	// testSimpleAndDefaultSectionsWithComments
179 	//-----------------------------------------------------------------------------------------------------------------
180 	@Test void a08_simpleAndDefaultSectionsWithComments() throws Exception {
181 		var s = initStore("A.cfg",
182 			"#D",
183 			"",
184 			"#k",
185 			"k=v",
186 			"#S1",
187 			"[S1]",
188 			"#k1",
189 			"k1=v1"
190 		);
191 		var cm = s.getMap("A.cfg");
192 		assertEquals("#D||#k|k=v|#S1|[S1]|#k1|k1=v1|", pipedLines(cm));
193 
194 		assertEquals("#D", Utils.join(cm.getPreLines(""), '|'));
195 		assertEquals("#k", Utils.join(cm.getEntry("", "k").getPreLines(), '|'));
196 		assertEquals("#S1", Utils.join(cm.getPreLines("S1"), '|'));
197 		assertEquals("#k1", Utils.join(cm.getEntry("S1", "k1").getPreLines(), '|'));
198 
199 		assertEquals("v", cm.getEntry("", "k").getValue());
200 		assertEquals("v1", cm.getEntry("S1", "k1").getValue());
201 
202 		// Round trip.
203 		s.update("A.cfg", cm.toString());
204 		cm = s.getMap("A.cfg");
205 		assertEquals("#D||#k|k=v|#S1|[S1]|#k1|k1=v1|", pipedLines(cm));
206 	}
207 
208 	//-----------------------------------------------------------------------------------------------------------------
209 	// testSimpleAndDefaultSectionsWithCommentsAndExtraSpaces
210 	//-----------------------------------------------------------------------------------------------------------------
211 	@Test void a09_simpleAndDefaultSectionsWithCommentsAndExtraSpaces() throws Exception {
212 		var s = initStore("A.cfg",
213 			"#Da",
214 			"#Db",
215 			"",
216 			"#ka",
217 			"",
218 			"#kb",
219 			"",
220 			"k=v",
221 			"",
222 			"#S1a",
223 			"",
224 			"#S1b",
225 			"",
226 			"[S1]",
227 			"",
228 			"#k1a",
229 			"",
230 			"#k1b",
231 			"",
232 			"k1=v1"
233 		);
234 		var cm = s.getMap("A.cfg");
235 		assertEquals("#Da|#Db||#ka||#kb||k=v||#S1a||#S1b||[S1]||#k1a||#k1b||k1=v1|", pipedLines(cm));
236 
237 		assertEquals("#Da|#Db", Utils.join(cm.getPreLines(""), '|'));
238 		assertEquals("#ka||#kb|", Utils.join(cm.getEntry("", "k").getPreLines(), '|'));
239 		assertEquals("|#S1a||#S1b|", Utils.join(cm.getPreLines("S1"), '|'));
240 		assertEquals("|#k1a||#k1b|", Utils.join(cm.getEntry("S1", "k1").getPreLines(), '|'));
241 
242 		assertEquals("v", cm.getEntry("", "k").getValue());
243 		assertEquals("v1", cm.getEntry("S1", "k1").getValue());
244 
245 		// Round trip.
246 		s.update("A.cfg", cm.toString());
247 		cm = s.getMap("A.cfg");
248 		assertEquals("#Da|#Db||#ka||#kb||k=v||#S1a||#S1b||[S1]||#k1a||#k1b||k1=v1|", pipedLines(cm));
249 	}
250 
251 	//-----------------------------------------------------------------------------------------------------------------
252 	// Error conditions.
253 	//-----------------------------------------------------------------------------------------------------------------
254 	@Test void a10_malformedSectionHeaders() {
255 
256 		var test = a(
257 			"[]", "[  ]",
258 			"[/]", "[[]", "[]]", "[\\]",
259 			"[foo/bar]", "[foo[bar]", "[foo]bar]", "[foo\\bar]",
260 			"[]", "[ ]", "[\t]"
261 		);
262 
263 		for (var t : test) {
264 			var s = initStore("A.cfg", t);
265 			assertThrowsWithMessage(Exception.class, "Invalid section name", ()->s.getMap("A.cfg"));
266 		}
267 	}
268 
269 	@Test void a01_duplicateSectionNames() {
270 		var s = initStore("A.cfg", "[S1]", "[S1]");
271 		assertThrowsWithMessage(ConfigException.class, "Duplicate section found in configuration:  [S1]", ()->s.getMap("A.cfg"));
272 	}
273 
274 	@Test void a02_duplicateEntryNames() {
275 		var s = initStore("A.cfg", "[S1]", "foo=v1", "foo=v2");
276 		assertThrowsWithMessage(ConfigException.class, "Duplicate entry found in section [S1] of configuration:  foo", ()->s.getMap("A.cfg"));
277 	}
278 
279 	//-----------------------------------------------------------------------------------------------------------------
280 	// Lines can be split up.
281 	//-----------------------------------------------------------------------------------------------------------------
282 	@Test void a03_multipleLines() throws Exception {
283 		var s = initStore("A.cfg",
284 			"k1 = v1a,",
285 			"\tv1b,",
286 			"\tv1c",
287 			"k2 = v2a,",
288 			"\tv2b,",
289 			"\tv2c"
290 		);
291 		var cm = s.getMap("A.cfg");
292 
293 		assertEquals("", Utils.join(cm.getEntry("", "k1").getPreLines(), '|'));
294 		assertEquals("", Utils.join(cm.getEntry("", "k2").getPreLines(), '|'));
295 
296 		assertEquals("k1 = v1a,|\tv1b,|\tv1c|k2 = v2a,|\tv2b,|\tv2c|", pipedLines(cm));
297 
298 		assertEquals("v1a,\nv1b,\nv1c", cm.getEntry("", "k1").getValue());
299 		assertEquals("v2a,\nv2b,\nv2c", cm.getEntry("", "k2").getValue());
300 
301 		// Round trip.
302 		s.update("A.cfg", cm.toString());
303 		cm = s.getMap("A.cfg");
304 		assertEquals("k1 = v1a,|\tv1b,|\tv1c|k2 = v2a,|\tv2b,|\tv2c|", pipedLines(cm));
305 	}
306 
307 	@Test void a04_multipleLinesWithSpacesAndComments() throws Exception {
308 		var s = initStore("A.cfg",
309 			"",
310 			"#k1",
311 			"",
312 			"k1 = v1a,",
313 			"\tv1b,",
314 			"\tv1c",
315 			"",
316 			"#k2",
317 			"",
318 			"k2 = v2a,",
319 			"\tv2b,",
320 			"\tv2c"
321 		);
322 		var cm = s.getMap("A.cfg");
323 
324 		assertEquals("|#k1|", Utils.join(cm.getEntry("", "k1").getPreLines(), '|'));
325 		assertEquals("|#k2|", Utils.join(cm.getEntry("", "k2").getPreLines(), '|'));
326 
327 		assertEquals("|#k1||k1 = v1a,|	v1b,|	v1c||#k2||k2 = v2a,|	v2b,|	v2c|", pipedLines(cm));
328 
329 		assertEquals("v1a,\nv1b,\nv1c", cm.getEntry("", "k1").getValue());
330 		assertEquals("v2a,\nv2b,\nv2c", cm.getEntry("", "k2").getValue());
331 
332 		// Round trip.
333 		s.update("A.cfg", cm.toString());
334 		cm = s.getMap("A.cfg");
335 		assertEquals("|#k1||k1 = v1a,|	v1b,|	v1c||#k2||k2 = v2a,|	v2b,|	v2c|", pipedLines(cm));
336 	}
337 
338 	@Test void a05_multipleLinesInSection() throws Exception {
339 		var s = initStore("A.cfg",
340 			"[S1]",
341 			"k1 = v1a,",
342 			"\tv1b,",
343 			"\tv1c",
344 			"k2 = v2a,",
345 			"\tv2b,",
346 			"\tv2c"
347 		);
348 		var cm = s.getMap("A.cfg");
349 
350 		assertEquals("", Utils.join(cm.getEntry("S1", "k1").getPreLines(), '|'));
351 		assertEquals("", Utils.join(cm.getEntry("S1", "k2").getPreLines(), '|'));
352 
353 		assertEquals("[S1]|k1 = v1a,|\tv1b,|\tv1c|k2 = v2a,|\tv2b,|\tv2c|", pipedLines(cm));
354 
355 		assertEquals("v1a,\nv1b,\nv1c", cm.getEntry("S1", "k1").getValue());
356 		assertEquals("v2a,\nv2b,\nv2c", cm.getEntry("S1", "k2").getValue());
357 
358 		// Round trip.
359 		s.update("A.cfg", cm.toString());
360 		cm = s.getMap("A.cfg");
361 		assertEquals("[S1]|k1 = v1a,|\tv1b,|\tv1c|k2 = v2a,|\tv2b,|\tv2c|", pipedLines(cm));
362 	}
363 
364 	@Test void a06_multipleLinesInSectionWithSpacesAndPrelines() throws Exception {
365 		var s = initStore("A.cfg",
366 			"",
367 			"#S1",
368 			"",
369 			"[S1]",
370 			"",
371 			"#k1",
372 			"",
373 			"k1 = v1a,",
374 			"\tv1b,",
375 			"\tv1c",
376 			"",
377 			"#k2",
378 			"",
379 			"k2 = v2a,",
380 			"\tv2b,",
381 			"\tv2c"
382 		);
383 		var cm = s.getMap("A.cfg");
384 
385 		assertEquals("|#S1|", Utils.join(cm.getPreLines("S1"), '|'));
386 		assertEquals("|#k1|", Utils.join(cm.getEntry("S1", "k1").getPreLines(), '|'));
387 		assertEquals("|#k2|", Utils.join(cm.getEntry("S1", "k2").getPreLines(), '|'));
388 
389 		assertEquals("|#S1||[S1]||#k1||k1 = v1a,|	v1b,|	v1c||#k2||k2 = v2a,|	v2b,|	v2c|", pipedLines(cm));
390 
391 		assertEquals("v1a,\nv1b,\nv1c", cm.getEntry("S1", "k1").getValue());
392 		assertEquals("v2a,\nv2b,\nv2c", cm.getEntry("S1", "k2").getValue());
393 
394 		// Round trip.
395 		s.update("A.cfg", cm.toString());
396 		cm = s.getMap("A.cfg");
397 		assertEquals("|#S1||[S1]||#k1||k1 = v1a,|	v1b,|	v1c||#k2||k2 = v2a,|	v2b,|	v2c|", pipedLines(cm));
398 	}
399 
400 	//-----------------------------------------------------------------------------------------------------------------
401 	// Entry lines can have trailing comments.
402 	//-----------------------------------------------------------------------------------------------------------------
403 	@Test void a07_entriesWithComments() throws Exception {
404 		var s = initStore("A.cfg",
405 			"[S1]",
406 			"k1 = foo # comment"
407 		);
408 		var cm = s.getMap("A.cfg");
409 
410 		assertEquals("[S1]|k1 = foo # comment|", pipedLines(cm));
411 		assertEquals("foo", cm.getEntry("S1", "k1").getValue());
412 		assertEquals("comment", cm.getEntry("S1", "k1").getComment());
413 
414 		cm.setEntry("S1", "k1", null, null, "newcomment", null);
415 		assertEquals("[S1]|k1 = foo # newcomment|", pipedLines(cm));
416 		assertEquals("foo", cm.getEntry("S1", "k1").getValue());
417 		assertEquals("newcomment", cm.getEntry("S1", "k1").getComment());
418 
419 		cm.setEntry("S1", "k1", null, null, "", null);
420 		assertEquals("[S1]|k1 = foo|", pipedLines(cm));
421 		assertEquals("foo", cm.getEntry("S1", "k1").getValue());
422 		assertEquals("", cm.getEntry("S1", "k1").getComment());
423 
424 		cm.setEntry("S1", "k1", null, null, null, null);
425 		assertEquals("[S1]|k1 = foo|", pipedLines(cm));
426 		assertEquals("foo", cm.getEntry("S1", "k1").getValue());
427 		assertEquals("", cm.getEntry("S1", "k1").getComment());
428 	}
429 
430 	@Test void a08_entriesWithOddComments() throws Exception {
431 		var s = initStore("A.cfg",
432 			"[S1]",
433 			"k1 = foo#",
434 			"k2 = foo # "
435 		);
436 		var cm = s.getMap("A.cfg");
437 		assertEquals("[S1]|k1 = foo#|k2 = foo # |", pipedLines(cm));
438 		assertEquals("", cm.getEntry("S1", "k1").getComment());
439 		assertEquals("", cm.getEntry("S1", "k2").getComment());
440 	}
441 
442 	@Test void a09_entriesWithEscapedComments() throws Exception {
443 		var s = initStore("A.cfg",
444 			"[S1]",
445 			"k1 = foo\\#bar",
446 			"k2 = foo \\# bar",
447 			"k3 = foo \\# bar # real-comment"
448 		);
449 		var cm = s.getMap("A.cfg");
450 		assertEquals("[S1]|k1 = foo\\#bar|k2 = foo \\# bar|k3 = foo \\# bar # real-comment|", pipedLines(cm));
451 
452 		assertEquals(null, cm.getEntry("S1", "k1").getComment());
453 		assertEquals(null, cm.getEntry("S1", "k2").getComment());
454 		assertEquals("real-comment", cm.getEntry("S1", "k3").getComment());
455 	}
456 
457 	//-----------------------------------------------------------------------------------------------------------------
458 	// Test setting entries.
459 	//-----------------------------------------------------------------------------------------------------------------
460 	@Test void a10_settingEntries() throws Exception {
461 		var s = initStore("A.cfg",
462 			"[S1]",
463 			"k1 = v1a",
464 			"k2 = v2a"
465 		);
466 		var cm = s.getMap("A.cfg");
467 
468 		cm.setEntry("S1", "k1", "v1b", null, null, null);
469 		cm.setEntry("S1", "k2", null, null, null, null);
470 		cm.setEntry("S1", "k3", "v3b", null, null, null);
471 
472 		assertEquals("[S1]|k1 = v1b|k2 = v2a|k3 = v3b|", pipedLines(cm));
473 
474 		cm.commit();
475 		assertEquals("[S1]|k1 = v1b|k2 = v2a|k3 = v3b|", pipedLines(cm));
476 
477 		// Round trip.
478 		cm = s.getMap("A.cfg");
479 		assertEquals("[S1]|k1 = v1b|k2 = v2a|k3 = v3b|", pipedLines(cm));
480 	}
481 
482 	@Test void a11_settingEntriesWithPreLines() throws Exception {
483 		var s = initStore("A.cfg",
484 			"",
485 			"#S1",
486 			"",
487 			"[S1]",
488 			"",
489 			"#k1",
490 			"",
491 			"k1 = v1a",
492 			"",
493 			"#k2",
494 			"",
495 			"k2 = v2a"
496 		);
497 		var cm = s.getMap("A.cfg");
498 
499 		cm.setEntry("S1", "k1", "v1b", null, null, null);
500 		cm.setEntry("S1", "k2", null, null, null, null);
501 		cm.setEntry("S1", "k3", "v3b", null, null, null);
502 		cm.setEntry("S1", "k4", "v4b", null, null, Arrays.asList("","#k4",""));
503 
504 		assertEquals("|#S1||[S1]||#k1||k1 = v1b||#k2||k2 = v2a|k3 = v3b||#k4||k4 = v4b|", pipedLines(cm));
505 
506 		cm.commit();
507 		assertEquals("|#S1||[S1]||#k1||k1 = v1b||#k2||k2 = v2a|k3 = v3b||#k4||k4 = v4b|", pipedLines(s.read("A.cfg")));
508 
509 		// Round trip.
510 		cm = s.getMap("A.cfg");
511 		assertEquals("|#S1||[S1]||#k1||k1 = v1b||#k2||k2 = v2a|k3 = v3b||#k4||k4 = v4b|", pipedLines(cm));
512 	}
513 
514 	@Test void a12_settingEntriesWithNewlines() throws Exception {
515 		var s = initStore("A.cfg");
516 		var cm = s.getMap("A.cfg");
517 
518 		cm.setEntry("", "k", "v1\nv2\nv3", null, null, null);
519 		cm.setEntry("S1", "k1", "v1\nv2\nv3", null, null, null);
520 
521 		assertEquals("k = v1|	v2|	v3|[S1]|k1 = v1|	v2|	v3|", pipedLines(cm));
522 
523 		assertEquals("v1\nv2\nv3", cm.getEntry("", "k").getValue());
524 		assertEquals("v1\nv2\nv3", cm.getEntry("S1", "k1").getValue());
525 		cm.commit();
526 		assertEquals("k = v1|	v2|	v3|[S1]|k1 = v1|	v2|	v3|", pipedLines(cm));
527 
528 		// Round trip.
529 		cm = s.getMap("A.cfg");
530 		assertEquals("k = v1|	v2|	v3|[S1]|k1 = v1|	v2|	v3|", pipedLines(cm));
531 	}
532 
533 	@Test void a13_settingEntriesWithNewlinesAndSpaces() throws Exception {
534 		var s = initStore("A.cfg");
535 		var cm = s.getMap("A.cfg");
536 
537 		cm.setEntry("", "k", "v1 \n v2 \n v3", null, null, null);
538 		cm.setEntry("S1", "k1", "v1\t\n\tv2\t\n\tv3", null, null, null);
539 
540 		assertEquals("k = v1 |	 v2 |	 v3|[S1]|k1 = v1	|		v2	|		v3|", pipedLines(cm));
541 
542 		assertEquals("v1 \n v2 \n v3", cm.getEntry("", "k").getValue());
543 		assertEquals("v1\t\n\tv2\t\n\tv3", cm.getEntry("S1", "k1").getValue());
544 		cm.commit();
545 		assertEquals("k = v1 |	 v2 |	 v3|[S1]|k1 = v1	|		v2	|		v3|", pipedLines(cm));
546 
547 		// Round trip.
548 		cm = s.getMap("A.cfg");
549 		assertEquals("k = v1 |	 v2 |	 v3|[S1]|k1 = v1	|		v2	|		v3|", pipedLines(cm));
550 	}
551 
552 	//-----------------------------------------------------------------------------------------------------------------
553 	// setSection()
554 	//-----------------------------------------------------------------------------------------------------------------
555 	@Test void a14_setSectionOnExistingSection() throws Exception {
556 		var s = initStore("A.cfg",
557 			"[S1]",
558 			"k1 = v1"
559 		);
560 		var cm = s.getMap("A.cfg");
561 
562 		cm.setSection("S1", Arrays.asList("#S1"));
563 		assertEquals("#S1|[S1]|k1 = v1|", pipedLines(cm));
564 		cm.setSection("S1", Collections.<String>emptyList());
565 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
566 		cm.setSection("S1", null);
567 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
568 	}
569 
570 	@Test void a15_setSectionOnDefaultSection() throws Exception {
571 		var s = initStore("A.cfg",
572 			"[S1]",
573 			"k1 = v1"
574 		);
575 		var cm = s.getMap("A.cfg");
576 
577 		cm.setSection("", Arrays.asList("#D"));
578 		assertEquals("#D||[S1]|k1 = v1|", pipedLines(cm));
579 		cm.setSection("", Collections.<String>emptyList());
580 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
581 		cm.setSection("", null);
582 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
583 	}
584 
585 	@Test void a16_setSectionOnNewSection() throws Exception {
586 		var s = initStore("A.cfg",
587 			"[S1]",
588 			"k1 = v1"
589 		);
590 		var cm = s.getMap("A.cfg");
591 
592 		cm.setSection("S2", Arrays.asList("#S2"));
593 		assertEquals("[S1]|k1 = v1|#S2|[S2]|", pipedLines(cm));
594 		cm.setSection("S3", Collections.<String>emptyList());
595 		assertEquals("[S1]|k1 = v1|#S2|[S2]|[S3]|", pipedLines(cm));
596 		cm.setSection("S4", null);
597 		assertEquals("[S1]|k1 = v1|#S2|[S2]|[S3]|[S4]|", pipedLines(cm));
598 	}
599 
600 	@Test void a17_setSectionBadNames() throws Exception {
601 		var s = initStore("A.cfg");
602 		var cm = s.getMap("A.cfg");
603 
604 		var test = a(
605 			"/", "[", "]",
606 			"foo/bar", "foo[bar", "foo]bar",
607 			" ",
608 			null
609 		);
610 
611 		for (var t : test) {
612 			assertThrowsWithMessage(Exception.class, "Invalid section name", ()->cm.setSection(t, null));
613 		}
614 	}
615 
616 	@Test void a18_setSectionOkNames() throws Exception {
617 		var s = initStore("A.cfg");
618 		var cm = s.getMap("A.cfg");
619 
620 		// These are all okay characters to use in section names.
621 		String validChars = "~`!@#$%^&*()_-+={}|:;\"\'<,>.?";
622 
623 		for (var c : validChars.toCharArray()) {
624 			var test = ""+c;
625 			cm.setSection(test, Arrays.asList("test"));
626 			cm.commit();
627 			assertEquals("test", cm.getPreLines(test).get(0));
628 
629 			test = "foo"+c+"bar";
630 			cm.setSection(test, Arrays.asList("test"));
631 			cm.commit();
632 			assertEquals("test", cm.getPreLines(test).get(0));
633 		}
634 	}
635 
636 	//-----------------------------------------------------------------------------------------------------------------
637 	// removeSection()
638 	//-----------------------------------------------------------------------------------------------------------------
639 	@Test void a19_removeSectionOnExistingSection() throws Exception {
640 		var s = initStore("A.cfg",
641 			"[S1]",
642 			"k1 = v1",
643 			"[S2]",
644 			"k2 = v2"
645 
646 		);
647 		var cm = s.getMap("A.cfg");
648 
649 		cm.removeSection("S1");
650 		assertEquals("[S2]|k2 = v2|", pipedLines(cm));
651 	}
652 
653 	@Test void a20_removeSectionOnNonExistingSection() throws Exception {
654 		var s = initStore("A.cfg",
655 			"[S1]",
656 			"k1 = v1",
657 			"[S2]",
658 			"k2 = v2"
659 
660 		);
661 		var cm = s.getMap("A.cfg");
662 
663 		cm.removeSection("S3");
664 		assertEquals("[S1]|k1 = v1|[S2]|k2 = v2|", pipedLines(cm));
665 
666 		assertThrowsWithMessage(IllegalArgumentException.class, "Invalid section name: 'null'", ()->cm.removeSection(null));
667 	}
668 
669 	@Test void a21_removeDefaultSection() throws Exception {
670 		var s = initStore("A.cfg",
671 			"k = v",
672 			"[S1]",
673 			"k1 = v1",
674 			"[S2]",
675 			"k2 = v2"
676 
677 		);
678 		var cm = s.getMap("A.cfg");
679 
680 		cm.removeSection("");
681 		assertEquals("[S1]|k1 = v1|[S2]|k2 = v2|", pipedLines(cm));
682 	}
683 
684 	@Test void a22_removeDefaultSectionWithComments() throws Exception {
685 		var s = initStore("A.cfg",
686 			"#D",
687 			"",
688 			"#k",
689 			"k = v",
690 			"[S1]",
691 			"k1 = v1",
692 			"[S2]",
693 			"k2 = v2"
694 
695 		);
696 		var cm = s.getMap("A.cfg");
697 
698 		cm.removeSection("");
699 		assertEquals("[S1]|k1 = v1|[S2]|k2 = v2|", pipedLines(cm));
700 	}
701 
702 	//-----------------------------------------------------------------------------------------------------------------
703 	// setPreLines()
704 	//-----------------------------------------------------------------------------------------------------------------
705 	@Test void a23_setPrelinesOnExistingEntry() throws Exception {
706 		var s = initStore("A.cfg",
707 			"[S1]",
708 			"k1 = v1"
709 		);
710 		var cm = s.getMap("A.cfg");
711 
712 		cm.setEntry("S1", "k1", null, null, null, Arrays.asList("#k1"));
713 		assertEquals("[S1]|#k1|k1 = v1|", pipedLines(cm));
714 		cm.setEntry("S1", "k1", null, null, null, Collections.<String>emptyList());
715 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
716 		cm.setEntry("S1", "k1", null, null, null, null);
717 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
718 	}
719 
720 	@Test void a24_setPrelinesOnExistingEntryWithAtrributes() throws Exception {
721 		var s = initStore("A.cfg",
722 			"[S1]",
723 			"#k1a",
724 			"k1 = v1 # comment"
725 		);
726 		var cm = s.getMap("A.cfg");
727 
728 		cm.setEntry("S1", "k1", null, null, null, Arrays.asList("#k1b"));
729 		assertEquals("[S1]|#k1b|k1 = v1 # comment|", pipedLines(cm));
730 	}
731 
732 	@Test void a25_setPrelinesOnNonExistingEntry() throws Exception {
733 		var s = initStore("A.cfg",
734 			"[S1]",
735 			"k1 = v1"
736 		);
737 		var cm = s.getMap("A.cfg");
738 
739 		cm.setEntry("S1", "k2", null, null, null, Arrays.asList("#k2"));
740 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
741 		cm.setEntry("S1", "k2", null, null, null, Collections.<String>emptyList());
742 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
743 		cm.setEntry("S1", "k2", null, null, null, null);
744 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
745 
746 		cm.setEntry("S2", "k2", null, null, null, Arrays.asList("#k2"));
747 		assertEquals("[S1]|k1 = v1|[S2]|", pipedLines(cm));
748 		cm.setEntry("S2", "k2", null, null, null, Collections.<String>emptyList());
749 		assertEquals("[S1]|k1 = v1|[S2]|", pipedLines(cm));
750 		cm.setEntry("S2", "k2", null, null, null, null);
751 		assertEquals("[S1]|k1 = v1|[S2]|", pipedLines(cm));
752 	}
753 
754 	//-----------------------------------------------------------------------------------------------------------------
755 	// setValue()
756 	//-----------------------------------------------------------------------------------------------------------------
757 	@Test void a26_setValueOnExistingEntry() throws Exception {
758 		var s = initStore("A.cfg",
759 			"[S1]",
760 			"k1 = v1"
761 		);
762 		var cm = s.getMap("A.cfg");
763 
764 		cm.setEntry("S1", "k1", "v2", null, null, null);
765 		assertEquals("[S1]|k1 = v2|", pipedLines(cm));
766 	}
767 
768 	@Test void a27_setValueOnExistingEntryWithAttributes() throws Exception {
769 		var s = initStore("A.cfg",
770 			"[S1]",
771 			"#k1",
772 			"k1 = v1 # comment"
773 		);
774 		var cm = s.getMap("A.cfg");
775 
776 		cm.setEntry("S1", "k1", "v2", null, null, null);
777 		assertEquals("[S1]|#k1|k1 = v2 # comment|", pipedLines(cm));
778 	}
779 
780 	@Test void a28_setValueToNullOnExistingEntry() throws Exception {
781 		var s = initStore("A.cfg",
782 			"[S1]",
783 			"k1 = v1"
784 		);
785 		var cm = s.getMap("A.cfg");
786 
787 		cm.setEntry("S1", "k1", null, null, null, null);
788 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
789 	}
790 
791 	@Test void a29_setValueOnNonExistingEntry() throws Exception {
792 		var s = initStore("A.cfg",
793 			"[S1]",
794 			"k1 = v1"
795 		);
796 		var cm = s.getMap("A.cfg");
797 
798 		cm.setEntry("S1", "k2", "v2", null, null, null);
799 		assertEquals("[S1]|k1 = v1|k2 = v2|", pipedLines(cm));
800 		cm.setEntry("S1", "k2", null, null, null, null);
801 		assertEquals("[S1]|k1 = v1|k2 = v2|", pipedLines(cm));
802 		cm.setEntry("S1", "k2", null, null, null, null);
803 		assertEquals("[S1]|k1 = v1|k2 = v2|", pipedLines(cm));
804 	}
805 
806 	@Test void a30_setValueOnNonExistingEntryOnNonExistentSection() throws Exception {
807 		var s = initStore("A.cfg",
808 			"[S1]",
809 			"k1 = v1"
810 		);
811 		var cm = s.getMap("A.cfg");
812 
813 		cm.setEntry("S2", "k2", "v2", null, null, null);
814 		assertEquals("[S1]|k1 = v1|[S2]|k2 = v2|", pipedLines(cm));
815 	}
816 
817 	@Test void a31_setValueInvalidSectionNames() throws Exception {
818 		var s = initStore("A.cfg");
819 		var cm = s.getMap("A.cfg");
820 
821 		var test = a(
822 			"/", "[", "]",
823 			"foo/bar", "foo[bar", "foo]bar",
824 			" ",
825 			null
826 		);
827 
828 		for (var t : test) {
829 			assertThrowsWithMessage(Exception.class, "Invalid section name:", ()->cm.setEntry(t, "k1", "foo", null, null, null));
830 		}
831 	}
832 
833 	@Test void a32_setValueInvalidKeyNames() throws Exception {
834 		var s = initStore("A.cfg");
835 		var cm = s.getMap("A.cfg");
836 
837 		var test = a(
838 			"", " ", "\t",
839 			"foo=bar", "=",
840 			"foo/bar", "/",
841 			"foo[bar", "]",
842 			"foo]bar", "]",
843 			"foo\\bar", "\\",
844 			"foo#bar", "#",
845 			null
846 		);
847 
848 		for (var t : test) {
849 			assertThrowsWithMessage(Exception.class, "Invalid key name", ()->cm.setEntry("S1", t, "foo", null, null, null));
850 		}
851 	}
852 
853 	@Test void a33_setValueWithCommentChars() throws Exception {
854 		var s = initStore("A.cfg",
855 			"[S1]",
856 			"k1 = v1"
857 		);
858 		var cm = s.getMap("A.cfg");
859 
860 		// If value has # in it, it should get escaped.
861 		cm.setEntry("S1", "k1", "v1 # foo", null, null, null);
862 		assertEquals("[S1]|k1 = v1 \\u0023 foo|", pipedLines(cm));
863 	}
864 
865 	//-----------------------------------------------------------------------------------------------------------------
866 	// setComment()
867 	//-----------------------------------------------------------------------------------------------------------------
868 	@Test void a34_setCommentOnExistingEntry() throws Exception {
869 		var s = initStore("A.cfg",
870 			"[S1]",
871 			"k1 = v1"
872 		);
873 		var cm = s.getMap("A.cfg");
874 
875 		cm.setEntry("S1", "k1", null, null, "c1", null);
876 		assertEquals("[S1]|k1 = v1 # c1|", pipedLines(cm));
877 
878 		cm.setEntry("S1", "k1", null, null, "", null);
879 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
880 		cm.commit();
881 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
882 
883 		cm.setEntry("S1", "k1", null, null, null, null);
884 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
885 	}
886 
887 	@Test void a35_setCommentOnExistingEntryWithAttributes() throws Exception {
888 		var s = initStore("A.cfg",
889 			"[S1]",
890 			"#k1a",
891 			"k1 = v1 # c1"
892 		);
893 		var cm = s.getMap("A.cfg");
894 
895 		cm.setEntry("S1", "k1", null, null, "c2", null);
896 		assertEquals("[S1]|#k1a|k1 = v1 # c2|", pipedLines(cm));
897 	}
898 
899 	@Test void a36_setCommentOnNonExistingEntry() throws Exception {
900 		var s = initStore("A.cfg",
901 			"[S1]",
902 			"k1 = v1"
903 		);
904 		var cm = s.getMap("A.cfg");
905 
906 		cm.setEntry("S1", "k2", null, null, "foo", null);
907 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
908 		cm.setEntry("S1", "k2", null, null, null, null);
909 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
910 
911 		cm.setEntry("S2", "k2", null, null, "foo", null);
912 		assertEquals("[S1]|k1 = v1|[S2]|", pipedLines(cm));
913 		cm.setEntry("S2", "k2", null, null, null, null);
914 		assertEquals("[S1]|k1 = v1|[S2]|", pipedLines(cm));
915 	}
916 
917 	//-----------------------------------------------------------------------------------------------------------------
918 	// setValue()
919 	//-----------------------------------------------------------------------------------------------------------------
920 	@Test void a37_setEntryOnExistingEntry() throws Exception {
921 		var s = initStore("A.cfg",
922 			"[S1]",
923 			"k1 = v1"
924 		);
925 		var cm = s.getMap("A.cfg");
926 
927 		cm.setEntry("S1", "k1", "v2", null, null, null);
928 		assertEquals("[S1]|k1 = v2|", pipedLines(cm));
929 
930 		cm.setEntry("S1", "k1", "v3", ENCODED, "c3", Arrays.asList("#k1a"));
931 		assertEquals("[S1]|#k1a|k1<*> = v3 # c3|", pipedLines(cm));
932 
933 		cm.setEntry("S1", "k1", "v4", BASE64, "c4", Arrays.asList("#k1b"));
934 		assertEquals("[S1]|#k1b|k1<^> = v4 # c4|", pipedLines(cm));
935 	}
936 
937 	@Test void a38_setEntryOnExistingEntryWithAttributes() throws Exception {
938 		var s = initStore("A.cfg",
939 			"[S1]",
940 			"#k1",
941 			"k1 = v1 # comment"
942 		);
943 		var cm = s.getMap("A.cfg");
944 
945 		cm.setEntry("S1", "k1", "v2", null, null, null);
946 		assertEquals("[S1]|#k1|k1 = v2 # comment|", pipedLines(cm));
947 
948 		cm.setEntry("S1", "k1", "v3", ENCODED, "c3", Arrays.asList("#k1a"));
949 		assertEquals("[S1]|#k1a|k1<*> = v3 # c3|", pipedLines(cm));
950 
951 		cm.setEntry("S1", "k1", "v4", BASE64, "c4", Arrays.asList("#k1b"));
952 		assertEquals("[S1]|#k1b|k1<^> = v4 # c4|", pipedLines(cm));
953 	}
954 
955 	@Test void a39_setEntryToNullOnExistingEntry() throws Exception {
956 		var s = initStore("A.cfg",
957 			"[S1]",
958 			"k1 = v1"
959 		);
960 		var cm = s.getMap("A.cfg");
961 
962 		cm.setEntry("S1", "k1", null, null, null, null);
963 		assertEquals("[S1]|k1 = v1|", pipedLines(cm));
964 	}
965 
966 	@Test void a40_setEntryOnNonExistingEntry() throws Exception {
967 		var s = initStore("A.cfg",
968 			"[S1]",
969 			"k1 = v1"
970 		);
971 		var cm = s.getMap("A.cfg");
972 
973 		cm.setEntry("S1", "k2", "v2", null, null, null);
974 		assertEquals("[S1]|k1 = v1|k2 = v2|", pipedLines(cm));
975 		cm.setEntry("S1", "k2", null, null, null, null);
976 		assertEquals("[S1]|k1 = v1|k2 = v2|", pipedLines(cm));
977 		cm.setEntry("S1", "k2", "", null, null, null);
978 		assertEquals("[S1]|k1 = v1|k2 = |", pipedLines(cm));
979 	}
980 
981 	@Test void a41_setEntryOnNonExistingEntryOnNonExistentSection() throws Exception {
982 		var s = initStore("A.cfg",
983 			"[S1]",
984 			"k1 = v1"
985 		);
986 		var cm = s.getMap("A.cfg");
987 
988 		cm.setEntry("S2", "k2", "v2", null, null, null);
989 		assertEquals("[S1]|k1 = v1|[S2]|k2 = v2|", pipedLines(cm));
990 	}
991 
992 	@Test void a42_setEntryInvalidSectionNames() throws Exception {
993 		var s = initStore("A.cfg");
994 		var cm = s.getMap("A.cfg");
995 
996 		var test = a(
997 			"/", "[", "]",
998 			"foo/bar", "foo[bar", "foo]bar",
999 			" ",
1000 			null
1001 		);
1002 
1003 		for (var t : test) {
1004 			assertThrowsWithMessage(Exception.class, "Invalid section name", ()->cm.setEntry(t, "k1", "foo", null, null, null));
1005 		}
1006 	}
1007 
1008 	@Test void a43_setEntryInvalidKeyNames() throws Exception {
1009 		var s = initStore("A.cfg");
1010 		var cm = s.getMap("A.cfg");
1011 
1012 		var test = a(
1013 			"", " ", "\t",
1014 			"foo=bar", "=",
1015 			"foo/bar", "/",
1016 			"foo[bar", "]",
1017 			"foo]bar", "]",
1018 			"foo\\bar", "\\",
1019 			"foo#bar", "#",
1020 			null
1021 		);
1022 
1023 		for (var t : test) {
1024 			assertThrowsWithMessage(Exception.class, "Invalid key name", ()->cm.setEntry("S1", t, "foo", null, null, null));
1025 		}
1026 	}
1027 
1028 	@Test void a44_setEntryWithCommentChars() throws Exception {
1029 		var s = initStore("A.cfg",
1030 			"[S1]",
1031 			"k1 = v1"
1032 		);
1033 		var cm = s.getMap("A.cfg");
1034 
1035 		// If value has # in it, it should get escaped.
1036 		cm.setEntry("S1", "k1", "v1 # foo", null, null, null);
1037 		assertEquals("[S1]|k1 = v1 \\u0023 foo|", pipedLines(cm));
1038 	}
1039 
1040 	//-----------------------------------------------------------------------------------------------------------------
1041 	// Modifiers
1042 	//-----------------------------------------------------------------------------------------------------------------
1043 	@Test void a45_modifiers() throws Exception {
1044 		var s = initStore("A.cfg",
1045 			"[S1]",
1046 			"k1<^> = v1",
1047 			"k2<*> = v2",
1048 			"k3<*^> = v3"
1049 		);
1050 		var cm = s.getMap("A.cfg");
1051 
1052 		assertEquals("[S1]|k1<^> = v1|k2<*> = v2|k3<*^> = v3|", pipedLines(cm));
1053 		assertEquals("^", cm.getEntry("S1", "k1").getModifiers());
1054 		assertEquals("*", cm.getEntry("S1", "k2").getModifiers());
1055 		assertEquals("*^", cm.getEntry("S1", "k3").getModifiers());
1056 
1057 		cm.setEntry("S1", "k1", "v1", "#$%&*+^@~", null, null);
1058 		assertEquals("[S1]|k1<#$%&*+^@~> = v1|k2<*> = v2|k3<*^> = v3|", pipedLines(cm));
1059 	}
1060 
1061 	@Test void a46_invalidModifier() throws Exception {
1062 		var s = initStore("A.cfg",
1063 			"[S1]",
1064 			"k1^ = v1",
1065 			"k2* = v2",
1066 			"k3*^ = v3"
1067 		);
1068 		var cm = s.getMap("A.cfg");
1069 
1070 		// This is okay.
1071 		assertDoesNotThrow(()->cm.setEntry("S1", "k1", "v1", "", null, null));
1072 	}
1073 
1074 	private static ConfigStore initStore(String name, String...contents) {
1075 		return MemoryStore.create().build().update(name, contents);
1076 	}
1077 }