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