001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.config.internal; 018 019import static org.apache.juneau.common.utils.StringUtils.*; 020import static org.apache.juneau.common.utils.ThrowableUtils.*; 021import static org.apache.juneau.common.utils.Utils.*; 022import static org.apache.juneau.config.event.ConfigEventType.*; 023import static org.apache.juneau.internal.CollectionUtils.*; 024import static org.apache.juneau.internal.CollectionUtils.map; 025 026import java.io.*; 027import java.util.*; 028import java.util.concurrent.*; 029import java.util.stream.*; 030 031import org.apache.juneau.*; 032import org.apache.juneau.collections.*; 033import org.apache.juneau.common.utils.*; 034import org.apache.juneau.config.event.*; 035import org.apache.juneau.config.store.*; 036import org.apache.juneau.internal.*; 037 038/** 039 * Represents the parsed contents of a configuration. 040 */ 041public class ConfigMap implements ConfigStoreListener { 042 043 private final ConfigStore store; // The store that created this object. 044 private volatile String contents; // The original contents of this object. 045 final String name; // The name of this object. 046 047 // Changes that have been applied since the last load. 048 private final List<ConfigEvent> changes = synced(new ConfigEvents()); 049 050 // Registered listeners listening for changes during saves or reloads. 051 private final Set<ConfigEventListener> listeners = synced(Utils.set()); 052 053 // The parsed entries of this map with all changes applied. 054 final Map<String,ConfigSection> entries = synced(map()); 055 056 // The original entries of this map before any changes were applied. 057 final Map<String,ConfigSection> oentries = synced(map()); 058 059 // Import statements in this config. 060 final List<Import> imports = new CopyOnWriteArrayList<>(); 061 062 private final SimpleReadWriteLock lock = new SimpleReadWriteLock(); 063 064 /** 065 * Constructor. 066 * 067 * @param store The config store. 068 * @param name The config name. 069 * @throws IOException Thrown by underlying stream. 070 */ 071 public ConfigMap(ConfigStore store, String name) throws IOException { 072 this.store = store; 073 this.name = name; 074 load(store.read(name)); 075 } 076 077 ConfigMap(ConfigStore store, String name, String contents) throws IOException { 078 this.store = store; 079 this.name = name; 080 load(contents); 081 } 082 083 private ConfigMap load(String contents) throws IOException { 084 if (contents == null) 085 contents = ""; 086 this.contents = contents; 087 088 entries.clear(); 089 oentries.clear(); 090 imports.forEach(Import::unregisterAll); 091 imports.clear(); 092 093 var imports2 = mapOf(String.class, ConfigMap.class); 094 095 List<String> lines = new LinkedList<>(); 096 try (var scanner = new Scanner(contents)) { 097 while (scanner.hasNextLine()) { 098 var line = scanner.nextLine(); 099 var c = firstChar(line); 100 var c2 = StringUtils.lastNonWhitespaceChar(line); 101 if (c == '[') { 102 var l = line.trim(); 103 if (c2 != ']' || ! isValidNewSectionName(l.substring(1, l.length()-1))) 104 throw new ConfigException("Invalid section name found in configuration: {0}", line); 105 } else if (c == '<') { 106 var l = line.trim(); 107 var i = l.indexOf('>'); 108 if (i != -1) { 109 var l2 = l.substring(1, i); 110 if (! isValidConfigName(l2)) 111 throw new ConfigException("Invalid import config name found in configuration: {0}", line); 112 var l3 = l.substring(i+1); 113 if (! (Utils.isEmpty(l3) || firstChar(l3) == '#')) 114 throw new ConfigException("Invalid import config name found in configuration: {0}", line); 115 var importName = l2.trim(); 116 try { 117 if (! imports2.containsKey(importName)) 118 imports2.put(importName, store.getMap(importName)); 119 } catch (StackOverflowError e) { 120 throw new IOException("Import loop detected in configuration '"+name+"'->'"+importName+"'"); 121 } 122 } 123 } 124 lines.add(line); 125 } 126 } 127 128 List<Import> irl = Utils.listOfSize(imports2.size()); 129 forEachReverse(listFrom(imports2.values()), x -> irl.add(new Import(x).register(listeners))); 130 this.imports.addAll(irl); 131 132 // Add [blank] section. 133 var inserted = false; 134 var foundComment = false; 135 for (var li = lines.listIterator(); li.hasNext();) { 136 var l = li.next(); 137 var c = firstNonWhitespaceChar(l); 138 if (c != '#') { 139 if (c == 0 && foundComment) { 140 li.set("[]"); 141 inserted = true; 142 } 143 break; 144 } 145 foundComment = true; 146 } 147 if (! inserted) 148 lines.add(0, "[]"); 149 150 // Collapse any multi-lines. 151 var li = lines.listIterator(lines.size()); 152 String accumulator = null; 153 while (li.hasPrevious()) { 154 var l = li.previous(); 155 var c = firstChar(l); 156 if (c == '\t') { 157 c = firstNonWhitespaceChar(l); 158 if (c != '#') { 159 if (accumulator == null) 160 accumulator = l.substring(1); 161 else 162 accumulator = l.substring(1) + "\n" + accumulator; // NOSONAR - Intentionally not using StringBuilder. 163 li.remove(); 164 } 165 } else if (accumulator != null) { 166 li.set(l + "\n" + accumulator); 167 accumulator = null; 168 } 169 } 170 171 lines = copyOf(lines); 172 var last = lines.size()-1; 173 174 final int 175 S1 = 1, // Looking for section. 176 S2 = 2; // Found section, looking for start. 177 178 var state = S1; 179 180 var sections = listOf(ConfigSection.class); 181 182 for (var i = last; i >= 0; i--) { 183 var l = lines.get(i); 184 var c = firstChar(l); 185 186 if (state == S1) { 187 if (c == '[') { 188 state = S2; 189 } 190 } else { 191 if (c != '#' && (c == '[' || l.indexOf('=') != -1)) { 192 sections.add(new ConfigSection(lines.subList(i+1, last+1))); 193 last = i + 1; 194 state = (c == '[' ? S2 : S1); 195 } 196 } 197 } 198 199 sections.add(new ConfigSection(lines.subList(0, last+1))); 200 201 for (var i = sections.size() - 1; i >= 0; i--) { 202 var cs = sections.get(i); 203 if (entries.containsKey(cs.name)) 204 throw new ConfigException("Duplicate section found in configuration: [{0}]", cs.name); 205 entries.put(cs.name, cs); 206 } 207 208 oentries.putAll(entries); 209 return this; 210 } 211 212 //----------------------------------------------------------------------------------------------------------------- 213 // Getters 214 //----------------------------------------------------------------------------------------------------------------- 215 216 /** 217 * Reads an entry from this map. 218 * 219 * @param section 220 * The section name. 221 * <br>Must not be <jk>null</jk>. 222 * <br>Use blank to refer to the default section. 223 * @param key 224 * The entry key. 225 * <br>Must not be <jk>null</jk>. 226 * @return The entry, or <jk>null</jk> if the entry doesn't exist. 227 */ 228 public ConfigMapEntry getEntry(String section, String key) { 229 checkSectionName(section); 230 checkKeyName(key); 231 try (var x = lock.read()) { 232 var cs = entries.get(section); 233 var ce = cs == null ? null : cs.entries.get(key); 234 235 if (ce == null) 236 ce = imports.stream().map(y -> y.getConfigMap().getEntry(section, key)).filter(y -> y != null).findFirst().orElse(null); 237 238 return ce; 239 } 240 } 241 242 /** 243 * Returns the pre-lines on the specified section. 244 * 245 * <p> 246 * The pre-lines are all lines such as blank lines and comments that preceed a section. 247 * 248 * @param section 249 * The section name. 250 * <br>Must not be <jk>null</jk>. 251 * <br>Use blank to refer to the default section. 252 * @return 253 * An unmodifiable list of lines, or <jk>null</jk> if the section doesn't exist. 254 */ 255 public List<String> getPreLines(String section) { 256 checkSectionName(section); 257 try (var x = lock.read()) { 258 var cs = entries.get(section); 259 return cs == null ? null : cs.preLines; 260 } 261 } 262 263 /** 264 * Returns the keys of the entries in the specified section. 265 * 266 * @return 267 * An unmodifiable set of keys. 268 */ 269 public Set<String> getSections() { 270 var s = imports.isEmpty() ? entries.keySet() : setOf(String.class); 271 if (! imports.isEmpty()) { 272 imports.forEach(x -> s.addAll(x.getConfigMap().getSections())); 273 s.addAll(entries.keySet()); 274 } 275 return u(s); 276 } 277 278 /** 279 * Returns the keys of the entries in the specified section. 280 * 281 * @param section 282 * The section name. 283 * <br>Must not be <jk>null</jk>. 284 * <br>Use blank to refer to the default section. 285 * @return 286 * An unmodifiable set of keys, or an empty set if the section doesn't exist. 287 */ 288 public Set<String> getKeys(String section) { 289 checkSectionName(section); 290 var cs = entries.get(section); 291 var s = imports.isEmpty() && cs != null ? cs.entries.keySet() : Utils.<String>set(); 292 if (! imports.isEmpty()) { 293 imports.forEach(x -> s.addAll(x.getConfigMap().getKeys(section))); 294 if (cs != null) 295 s.addAll(cs.entries.keySet()); 296 } 297 return u(s); 298 } 299 300 /** 301 * Returns <jk>true</jk> if this config has the specified section. 302 * 303 * @param section 304 * The section name. 305 * <br>Must not be <jk>null</jk>. 306 * <br>Use blank to refer to the default section. 307 * @return <jk>true</jk> if this config has the specified section. 308 */ 309 public boolean hasSection(String section) { 310 checkSectionName(section); 311 return entries.get(section) != null || imports.stream().anyMatch(x -> x.getConfigMap().hasSection(section)); 312 } 313 314 //----------------------------------------------------------------------------------------------------------------- 315 // Setters 316 //----------------------------------------------------------------------------------------------------------------- 317 318 /** 319 * Adds a new section or replaces the pre-lines on an existing section. 320 * 321 * @param section 322 * The section name. 323 * <br>Must not be <jk>null</jk>. 324 * <br>Use blank to refer to the default section. 325 * @param preLines 326 * The pre-lines on the section. 327 * <br>If <jk>null</jk>, the previous value will not be overwritten. 328 * @return This object. 329 */ 330 public ConfigMap setSection(String section, List<String> preLines) { 331 checkSectionName(section); 332 return applyChange(true, ConfigEvent.setSection(name, section, preLines)); 333 } 334 335 /** 336 * Adds or overwrites an existing entry. 337 * 338 * @param section 339 * The section name. 340 * <br>Must not be <jk>null</jk>. 341 * <br>Use blank to refer to the default section. 342 * @param key 343 * The entry key. 344 * <br>Must not be <jk>null</jk>. 345 * @param value 346 * The entry value. 347 * <br>If <jk>null</jk>, the previous value will not be overwritten. 348 * @param modifiers 349 * Optional modifiers. 350 * <br>If <jk>null</jk>, the previous value will not be overwritten. 351 * @param comment 352 * Optional comment. 353 * <br>If <jk>null</jk>, the previous value will not be overwritten. 354 * @param preLines 355 * Optional pre-lines. 356 * <br>If <jk>null</jk>, the previous value will not be overwritten. 357 * @return This object. 358 */ 359 public ConfigMap setEntry(String section, String key, String value, String modifiers, String comment, List<String> preLines) { 360 checkSectionName(section); 361 checkKeyName(key); 362 return applyChange(true, ConfigEvent.setEntry(name, section, key, value, modifiers, comment, preLines)); 363 } 364 365 366 /** 367 * Not implemented. 368 * 369 * @param section 370 * The section name where to place the import statement. 371 * <br>Must not be <jk>null</jk>. 372 * <br>Use blank for the default section. 373 * @param importName 374 * The import name. 375 * <br>Must not be <jk>null</jk>. 376 * @param preLines 377 * Optional comment and blank lines to add immediately before the import statement. 378 * <br>If <jk>null</jk>, previous pre-lines will not be replaced. 379 * @return This object. 380 */ 381 public ConfigMap setImport(String section, String importName, List<String> preLines) { 382 throw new UnsupportedOperationException("Not implemented."); 383 } 384 385 /** 386 * Removes a section. 387 * 388 * <p> 389 * This eliminates all entries in the section as well. 390 * 391 * @param section 392 * The section name. 393 * <br>Must not be <jk>null</jk>. 394 * <br>Use blank to refer to the default section. 395 * @return This object. 396 */ 397 public ConfigMap removeSection(String section) { 398 checkSectionName(section); 399 return applyChange(true, ConfigEvent.removeSection(name, section)); 400 } 401 402 /** 403 * Removes an entry. 404 * 405 * @param section 406 * The section name. 407 * <br>Must not be <jk>null</jk>. 408 * <br>Use blank to refer to the default section. 409 * @param key 410 * The entry key. 411 * <br>Must not be <jk>null</jk>. 412 * @return This object. 413 */ 414 public ConfigMap removeEntry(String section, String key) { 415 checkSectionName(section); 416 checkKeyName(key); 417 return applyChange(true, ConfigEvent.removeEntry(name, section, key)); 418 } 419 420 /** 421 * Not implemented. 422 * 423 * @param section 424 * The section name where to place the import statement. 425 * <br>Must not be <jk>null</jk>. 426 * <br>Use blank for the default section. 427 * @param importName 428 * The import name. 429 * <br>Must not be <jk>null</jk>. 430 * @return This object. 431 */ 432 public ConfigMap removeImport(String section, String importName) { 433 throw new UnsupportedOperationException("Not implemented."); 434 } 435 436 private ConfigMap applyChange(boolean addToChangeList, ConfigEvent ce) { 437 if (ce == null) 438 return this; 439 try (var x = lock.write()) { 440 var section = ce.getSection(); 441 var cs = entries.get(section); 442 if (ce.getType() == SET_ENTRY) { 443 if (cs == null) { 444 cs = new ConfigSection(section); 445 entries.put(section, cs); 446 } 447 var oe = cs.entries.get(ce.getKey()); 448 if (oe == null) 449 oe = ConfigMapEntry.NULL; 450 cs.addEntry( 451 ce.getKey(), 452 ce.getValue() == null ? oe.value : ce.getValue(), 453 ce.getModifiers() == null ? oe.modifiers : ce.getModifiers(), 454 ce.getComment() == null ? oe.comment : ce.getComment(), 455 ce.getPreLines() == null ? oe.preLines : ce.getPreLines() 456 ); 457 } else if (ce.getType() == SET_SECTION) { 458 if (cs == null) { 459 cs = new ConfigSection(section); 460 entries.put(section, cs); 461 } 462 if (ce.getPreLines() != null) 463 cs.setPreLines(ce.getPreLines()); 464 } else if (ce.getType() == REMOVE_ENTRY) { 465 if (cs != null) 466 cs.entries.remove(ce.getKey()); 467 } else if (ce.getType() == REMOVE_SECTION) { 468 if (cs != null) // NOSONAR - Intentional. 469 entries.remove(section); 470 } 471 if (addToChangeList) 472 changes.add(ce); 473 } 474 return this; 475 } 476 477 /** 478 * Overwrites the contents of the config file. 479 * 480 * @param contents The new contents of the config file. 481 * @param synchronous Wait until the change has been persisted before returning this map. 482 * @return This object. 483 * @throws IOException Thrown by underlying stream. 484 * @throws InterruptedException Thread was interrupted. 485 */ 486 public ConfigMap load(String contents, boolean synchronous) throws IOException, InterruptedException { 487 488 if (synchronous) { 489 final var latch = new CountDownLatch(1); 490 var listener = (ConfigStoreListener) x -> latch.countDown(); 491 store.register(name, listener); 492 store.write(name, null, contents); 493 if (latch.await(30, TimeUnit.SECONDS)) { 494 store.unregister(name, listener); 495 } else { 496 throw new ConfigException("Unable to store contents of config to store."); 497 } 498 } else { 499 store.write(name, null, contents); 500 } 501 return this; 502 } 503 504 //----------------------------------------------------------------------------------------------------------------- 505 // Lifecycle events 506 //----------------------------------------------------------------------------------------------------------------- 507 508 /** 509 * Persist any changes made to this map and signal all listeners. 510 * 511 * <p> 512 * If the underlying contents of the file have changed, this will reload it and apply the changes 513 * on top of the modified file. 514 * 515 * <p> 516 * Subsequent changes made to the underlying file will also be signaled to all listeners. 517 * 518 * <p> 519 * We try saving the file up to 10 times. 520 * <br>If the file keeps changing on the file system, we throw an exception. 521 * 522 * @return This object. 523 * @throws IOException Thrown by underlying stream. 524 */ 525 public ConfigMap commit() throws IOException { 526 try (var x = lock.write()) { 527 var newContents = asString(); 528 for (var i = 0; i <= 10; i++) { 529 if (i == 10) 530 throw new ConfigException("Unable to store contents of config to store."); 531 var currentContents = store.write(name, contents, newContents); 532 if (currentContents == null) 533 break; 534 onChange(currentContents); 535 } 536 this.changes.clear(); 537 } 538 return this; 539 } 540 541 //----------------------------------------------------------------------------------------------------------------- 542 // Listeners 543 //----------------------------------------------------------------------------------------------------------------- 544 545 /** 546 * Registers an event listener on this map. 547 * 548 * @param listener The new listener. 549 * @return This object. 550 */ 551 public ConfigMap register(ConfigEventListener listener) { 552 listeners.add(listener); 553 imports.forEach(x -> x.register(listener)); 554 return this; 555 } 556 557 boolean hasEntry(String section, String key) { 558 var cs = entries.get(section); 559 var ce = cs == null ? null : cs.entries.get(key); 560 return ce != null; 561 } 562 563 /** 564 * Unregisters an event listener from this map. 565 * 566 * @param listener The listener to remove. 567 * @return This object. 568 */ 569 public ConfigMap unregister(ConfigEventListener listener) { 570 listeners.remove(listener); 571 imports.forEach(x -> x.register(listener)); 572 return this; 573 } 574 575 /** 576 * Returns the listeners currently associated with this config map. 577 * 578 * @return The listeners currently associated with this config map. 579 */ 580 public Set<ConfigEventListener> getListeners() { 581 return u(listeners); 582 } 583 584 @Override /* ConfigStoreListener */ 585 public void onChange(String newContents) { 586 ConfigEvents changes2 = null; 587 try (var x = lock.write()) { 588 if (Utils.ne(contents, newContents)) { 589 changes2 = findDiffs(newContents); 590 load(newContents); 591 592 // Reapply our changes on top of the modifications. 593 this.changes.forEach(y -> applyChange(false, y)); 594 } 595 } catch (IOException e) { 596 throw asRuntimeException(e); 597 } 598 if (changes2 != null && ! changes2.isEmpty()) 599 signal(changes2); 600 } 601 602 @Override /* Object */ 603 public String toString() { 604 try (var x = lock.read()) { 605 return asString(); 606 } 607 } 608 609 /** 610 * Returns the values in this config map as a map of maps. 611 * 612 * <p> 613 * This is considered a snapshot copy of the config map. 614 * 615 * <p> 616 * The returned map is modifiable, but modifications to the returned map are not reflected in the config map. 617 * 618 * @return A copy of this config as a map of maps. 619 */ 620 public JsonMap asMap() { 621 var m = new JsonMap(); 622 try (var x = lock.read()) { 623 imports.forEach(y -> m.putAll(y.getConfigMap().asMap())); 624 entries.values().forEach(z -> { 625 var m2 = mapOf(String.class, String.class); 626 z.entries.values().forEach(y -> m2.put(y.key, y.value)); 627 m.put(z.name, m2); 628 }); 629 } 630 return m; 631 } 632 633 /** 634 * Serializes this map to the specified writer. 635 * 636 * @param w The writer to serialize to. 637 * @return The same writer passed in. 638 * @throws IOException Thrown by underlying stream. 639 */ 640 public Writer writeTo(Writer w) throws IOException { 641 try (var x = lock.read()) { 642 for (var cs : entries.values()) 643 cs.writeTo(w); 644 } 645 return w; 646 } 647 648 /** 649 * Does a rollback of any changes on this map currently in memory. 650 * 651 * @return This object. 652 */ 653 public ConfigMap rollback() { 654 if (!changes.isEmpty()) { 655 try (var x = lock.write()) { 656 changes.clear(); 657 load(contents); 658 } catch (IOException e) { 659 throw asRuntimeException(e); 660 } 661 } 662 return this; 663 } 664 665 //----------------------------------------------------------------------------------------------------------------- 666 // Private methods 667 //----------------------------------------------------------------------------------------------------------------- 668 669 private void checkSectionName(String s) { 670 if (! ("".equals(s) || isValidNewSectionName(s))) 671 throw new IllegalArgumentException("Invalid section name: '"+s+"'"); 672 } 673 674 private void checkKeyName(String s) { 675 if (! isValidKeyName(s)) 676 throw new IllegalArgumentException("Invalid key name: '"+s+"'"); 677 } 678 679 private boolean isValidKeyName(String s) { 680 if (s == null) 681 return false; 682 s = s.trim(); 683 if (s.isEmpty()) 684 return false; 685 for (var i = 0; i < s.length(); i++) { 686 var c = s.charAt(i); 687 if (c == '/' || c == '\\' || c == '[' || c == ']' || c == '=' || c == '#') 688 return false; 689 } 690 return true; 691 } 692 693 private boolean isValidNewSectionName(String s) { 694 if (s == null) 695 return false; 696 s = s.trim(); 697 if (s.isEmpty()) 698 return false; 699 for (var i = 0; i < s.length(); i++) { 700 var c = s.charAt(i); 701 if (c == '/' || c == '\\' || c == '[' || c == ']') 702 return false; 703 } 704 return true; 705 } 706 707 private boolean isValidConfigName(String s) { 708 if (s == null) 709 return false; 710 s = s.trim(); 711 if (s.isEmpty()) 712 return false; 713 for (var i = 0; i < s.length(); i++) { 714 var c = s.charAt(i); 715 if (i == 0) { 716 if (! Character.isJavaIdentifierStart(c)) 717 return false; 718 } else { 719 if (! Character.isJavaIdentifierPart(c)) 720 return false; 721 } 722 } 723 return true; 724 } 725 726 private void signal(ConfigEvents changes) { 727 if (Utils.isNotEmpty(changes)) 728 listeners.forEach(x -> x.onConfigChange(changes)); 729 } 730 731 private ConfigEvents findDiffs(String updatedContents) throws IOException { 732 var changes2 = new ConfigEvents(); 733 var newMap = new ConfigMap(store, name, updatedContents); 734 735 // Imports added. 736 for (var i : newMap.imports) { 737 if (! imports.contains(i)) { 738 for (var s : i.getConfigMap().entries.values()) { 739 for (var e : s.oentries.values()) { 740 if (! newMap.hasEntry(s.name, e.key)) { 741 changes2.add(ConfigEvent.setEntry(name, s.name, e.key, e.value, e.modifiers, e.comment, e.preLines)); 742 } 743 } 744 } 745 } 746 } 747 748 // Imports removed. 749 for (var i : imports) { 750 if (! newMap.imports.contains(i)) { 751 for (var s : i.getConfigMap().entries.values()) { 752 for (var e : s.oentries.values()) { 753 if (! newMap.hasEntry(s.name, e.key)) { 754 changes2.add(ConfigEvent.removeEntry(name, s.name, e.key)); 755 } 756 } 757 } 758 } 759 } 760 761 for (var ns : newMap.oentries.values()) { 762 var s = oentries.get(ns.name); 763 if (s == null) { 764 for (var ne : ns.entries.values()) { 765 changes2.add(ConfigEvent.setEntry(name, ns.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines)); 766 } 767 } else { 768 for (var ne : ns.oentries.values()) { 769 var e = s.oentries.get(ne.key); 770 if (e == null || Utils.ne(e.value, ne.value)) { 771 changes2.add(ConfigEvent.setEntry(name, s.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines)); 772 } 773 } 774 for (var e : s.oentries.values()) { 775 var ne = ns.oentries.get(e.key); 776 if (ne == null) { 777 changes2.add(ConfigEvent.removeEntry(name, s.name, e.key)); 778 } 779 } 780 } 781 } 782 783 for (var s : oentries.values()) { 784 var ns = newMap.oentries.get(s.name); 785 if (ns == null) { 786 for (var e : s.oentries.values()) 787 changes2.add(ConfigEvent.removeEntry(name, s.name, e.key)); 788 } 789 } 790 791 return changes2; 792 } 793 794 // This method should only be called from behind a lock. 795 private String asString() { 796 try { 797 var sw = new StringWriter(); 798 for (var cs : entries.values()) 799 cs.writeTo(sw); 800 return sw.toString(); 801 } catch (IOException e) { 802 throw asRuntimeException(e); // Not possible. 803 } 804 } 805 806 //--------------------------------------------------------------------------------------------- 807 // ConfigSection 808 //--------------------------------------------------------------------------------------------- 809 810 class ConfigSection { 811 812 final String name; // The config section name, or blank if the default section. Never null. 813 814 final List<String> preLines = synced(Utils.list()); 815 private final String rawLine; 816 817 final Map<String,ConfigMapEntry> oentries = synced(map()); 818 final Map<String,ConfigMapEntry> entries = synced(map()); 819 820 /** 821 * Constructor. 822 */ 823 ConfigSection(String name) { 824 this.name = name; 825 this.rawLine = "[" + name + "]"; 826 } 827 828 /** 829 * Constructor. 830 */ 831 ConfigSection(List<String> lines) { 832 833 String name2 = null, rawLine2 = null; 834 835 final int 836 S1 = 1, // Looking for section. 837 S2 = 2; // Found section, looking for end. 838 839 var state = S1; 840 var start = 0; 841 842 for (var i = 0; i < lines.size(); i++) { 843 var l = lines.get(i); 844 var c = StringUtils.firstNonWhitespaceChar(l); 845 if (state == S1) { 846 if (c == '[') { 847 var i1 = l.indexOf('['); 848 var i2 = l.indexOf(']'); 849 name2 = l.substring(i1+1, i2).trim(); 850 rawLine2 = l; 851 state = S2; 852 start = i+1; 853 } else { 854 preLines.add(l); 855 } 856 } else { 857 if (c != '#' && l.indexOf('=') != -1) { 858 var e = new ConfigMapEntry(l, lines.subList(start, i)); 859 if (entries.containsKey(e.key)) 860 throw new ConfigException("Duplicate entry found in section [{0}] of configuration: {1}", name2, e.key); 861 entries.put(e.key, e); 862 start = i+1; 863 } 864 } 865 } 866 867 this.name = name2; 868 this.rawLine = rawLine2; 869 this.oentries.putAll(entries); 870 } 871 872 ConfigSection addEntry(String key, String value, String modifiers, String comment, List<String> preLines) { 873 var e = new ConfigMapEntry(key, value, modifiers, comment, preLines); 874 this.entries.put(e.key, e); 875 return this; 876 } 877 878 ConfigSection setPreLines(List<String> preLines) { 879 this.preLines.clear(); 880 this.preLines.addAll(preLines); 881 return this; 882 } 883 884 Writer writeTo(Writer w) throws IOException { 885 for (var s : preLines) 886 w.append(s).append('\n'); 887 888 if (! name.isEmpty()) 889 w.append(rawLine).append('\n'); 890 else { 891 // Need separation between default prelines and first-entry prelines. 892 if (! preLines.isEmpty()) 893 w.append('\n'); 894 } 895 896 for (var e : entries.values()) 897 e.writeTo(w); 898 899 return w; 900 } 901 } 902 903 //--------------------------------------------------------------------------------------------- 904 // Import 905 //--------------------------------------------------------------------------------------------- 906 907 class Import { 908 909 private final ConfigMap configMap; 910 private final Map<ConfigEventListener,ConfigEventListener> listenerMap = synced(map()); 911 912 Import(ConfigMap configMap) { 913 this.configMap = configMap; 914 } 915 916 synchronized Import register(Collection<ConfigEventListener> listeners) { 917 listeners.forEach(this::register); 918 return this; 919 } 920 921 synchronized Import register(final ConfigEventListener listener) { 922 var l2 = (ConfigEventListener) events -> { 923 var events2 = events.stream().filter(x -> ! hasEntry(x.getSection(), x.getKey())).collect(Collectors.toCollection(ConfigEvents::new)); 924 if (!events2.isEmpty()) 925 listener.onConfigChange(events2); 926 }; 927 listenerMap.put(listener, l2); 928 configMap.register(l2); 929 return this; 930 } 931 932 synchronized Import unregister(final ConfigEventListener listener) { 933 configMap.unregister(listenerMap.remove(listener)); 934 return this; 935 } 936 937 synchronized Import unregisterAll() { 938 listenerMap.values().forEach(configMap::unregister); 939 listenerMap.clear(); 940 return this; 941 } 942 943 String getConfigName() { 944 return configMap.name; 945 } 946 947 ConfigMap getConfigMap() { 948 return configMap; 949 } 950 951 @Override 952 public boolean equals(Object o) { 953 return o instanceof Import ir && ir.getConfigName().equals(getConfigName()); 954 } 955 956 @Override 957 public int hashCode() { 958 return getConfigName().hashCode(); 959 } 960 } 961}