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