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(x -> x.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 l = new ConfigStoreListener() { 481 @Override 482 public void onChange(String contents) { 483 latch.countDown(); 484 } 485 }; 486 store.register(name, l); 487 store.write(name, null, contents); 488 latch.await(30, TimeUnit.SECONDS); 489 store.unregister(name, l); 490 } else { 491 store.write(name, null, contents); 492 } 493 return this; 494 } 495 496 //----------------------------------------------------------------------------------------------------------------- 497 // Lifecycle events 498 //----------------------------------------------------------------------------------------------------------------- 499 500 /** 501 * Persist any changes made to this map and signal all listeners. 502 * 503 * <p> 504 * If the underlying contents of the file have changed, this will reload it and apply the changes 505 * on top of the modified file. 506 * 507 * <p> 508 * Subsequent changes made to the underlying file will also be signaled to all listeners. 509 * 510 * <p> 511 * We try saving the file up to 10 times. 512 * <br>If the file keeps changing on the file system, we throw an exception. 513 * 514 * @return This object. 515 * @throws IOException Thrown by underlying stream. 516 */ 517 public ConfigMap commit() throws IOException { 518 try (SimpleLock x = lock.write()) { 519 String newContents = asString(); 520 for (int i = 0; i <= 10; i++) { 521 if (i == 10) 522 throw new ConfigException("Unable to store contents of config to store."); 523 String currentContents = store.write(name, contents, newContents); 524 if (currentContents == null) 525 break; 526 onChange(currentContents); 527 } 528 this.changes.clear(); 529 } 530 return this; 531 } 532 533 534 //----------------------------------------------------------------------------------------------------------------- 535 // Listeners 536 //----------------------------------------------------------------------------------------------------------------- 537 538 /** 539 * Registers an event listener on this map. 540 * 541 * @param listener The new listener. 542 * @return This object. 543 */ 544 public ConfigMap register(ConfigEventListener listener) { 545 listeners.add(listener); 546 imports.forEach(x -> x.register(listener)); 547 return this; 548 } 549 550 boolean hasEntry(String section, String key) { 551 ConfigSection cs = entries.get(section); 552 ConfigMapEntry ce = cs == null ? null : cs.entries.get(key); 553 return ce != null; 554 } 555 556 /** 557 * Unregisters an event listener from this map. 558 * 559 * @param listener The listener to remove. 560 * @return This object. 561 */ 562 public ConfigMap unregister(ConfigEventListener listener) { 563 listeners.remove(listener); 564 imports.forEach(x -> x.register(listener)); 565 return this; 566 } 567 568 /** 569 * Returns the listeners currently associated with this config map. 570 * 571 * @return The listeners currently associated with this config map. 572 */ 573 public Set<ConfigEventListener> getListeners() { 574 return unmodifiable(listeners); 575 } 576 577 @Override /* ConfigStoreListener */ 578 public void onChange(String newContents) { 579 ConfigEvents changes = null; 580 try (SimpleLock x = lock.write()) { 581 if (ne(contents, newContents)) { 582 changes = findDiffs(newContents); 583 load(newContents); 584 585 // Reapply our changes on top of the modifications. 586 this.changes.forEach(y -> applyChange(false, y)); 587 } 588 } catch (IOException e) { 589 throw asRuntimeException(e); 590 } 591 if (changes != null && ! changes.isEmpty()) 592 signal(changes); 593 } 594 595 @Override /* Object */ 596 public String toString() { 597 try (SimpleLock x = lock.read()) { 598 return asString(); 599 } 600 } 601 602 /** 603 * Returns the values in this config map as a map of maps. 604 * 605 * <p> 606 * This is considered a snapshot copy of the config map. 607 * 608 * <p> 609 * The returned map is modifiable, but modifications to the returned map are not reflected in the config map. 610 * 611 * @return A copy of this config as a map of maps. 612 */ 613 public JsonMap asMap() { 614 JsonMap m = new JsonMap(); 615 try (SimpleLock x = lock.read()) { 616 imports.forEach(y -> m.putAll(y.getConfigMap().asMap())); 617 entries.values().forEach(z -> { 618 Map<String,String> m2 = map(); 619 z.entries.values().forEach(y -> m2.put(y.key, y.value)); 620 m.put(z.name, m2); 621 }); 622 } 623 return m; 624 } 625 626 /** 627 * Serializes this map to the specified writer. 628 * 629 * @param w The writer to serialize to. 630 * @return The same writer passed in. 631 * @throws IOException Thrown by underlying stream. 632 */ 633 public Writer writeTo(Writer w) throws IOException { 634 try (SimpleLock x = lock.read()) { 635 for (ConfigSection cs : entries.values()) 636 cs.writeTo(w); 637 } 638 return w; 639 } 640 641 /** 642 * Does a rollback of any changes on this map currently in memory. 643 * 644 * @return This object. 645 */ 646 public ConfigMap rollback() { 647 if (changes.size() > 0) { 648 try (SimpleLock x = lock.write()) { 649 changes.clear(); 650 load(contents); 651 } catch (IOException e) { 652 throw asRuntimeException(e); 653 } 654 } 655 return this; 656 } 657 658 659 //----------------------------------------------------------------------------------------------------------------- 660 // Private methods 661 //----------------------------------------------------------------------------------------------------------------- 662 663 private void checkSectionName(String s) { 664 if (! ("".equals(s) || isValidNewSectionName(s))) 665 throw new IllegalArgumentException("Invalid section name: '"+s+"'"); 666 } 667 668 private void checkKeyName(String s) { 669 if (! isValidKeyName(s)) 670 throw new IllegalArgumentException("Invalid key name: '"+s+"'"); 671 } 672 673 private boolean isValidKeyName(String s) { 674 if (s == null) 675 return false; 676 s = s.trim(); 677 if (s.isEmpty()) 678 return false; 679 for (int i = 0; i < s.length(); i++) { 680 char c = s.charAt(i); 681 if (c == '/' || c == '\\' || c == '[' || c == ']' || c == '=' || c == '#') 682 return false; 683 } 684 return true; 685 } 686 687 private boolean isValidNewSectionName(String s) { 688 if (s == null) 689 return false; 690 s = s.trim(); 691 if (s.isEmpty()) 692 return false; 693 for (int i = 0; i < s.length(); i++) { 694 char c = s.charAt(i); 695 if (c == '/' || c == '\\' || c == '[' || c == ']') 696 return false; 697 } 698 return true; 699 } 700 701 private boolean isValidConfigName(String s) { 702 if (s == null) 703 return false; 704 s = s.trim(); 705 if (s.isEmpty()) 706 return false; 707 for (int i = 0; i < s.length(); i++) { 708 char c = s.charAt(i); 709 if (i == 0) { 710 if (! Character.isJavaIdentifierStart(c)) 711 return false; 712 } else { 713 if (! Character.isJavaIdentifierPart(c)) 714 return false; 715 } 716 } 717 return true; 718 } 719 720 private void signal(ConfigEvents changes) { 721 if (changes.size() > 0) 722 listeners.forEach(x -> x.onConfigChange(changes)); 723 } 724 725 private ConfigEvents findDiffs(String updatedContents) throws IOException { 726 ConfigEvents changes = new ConfigEvents(); 727 ConfigMap newMap = new ConfigMap(store, name, updatedContents); 728 729 // Imports added. 730 for (Import i : newMap.imports) { 731 if (! imports.contains(i)) { 732 for (ConfigSection s : i.getConfigMap().entries.values()) { 733 for (ConfigMapEntry e : s.oentries.values()) { 734 if (! newMap.hasEntry(s.name, e.key)) { 735 changes.add(ConfigEvent.setEntry(name, s.name, e.key, e.value, e.modifiers, e.comment, e.preLines)); 736 } 737 } 738 } 739 } 740 } 741 742 // Imports removed. 743 for (Import i : imports) { 744 if (! newMap.imports.contains(i)) { 745 for (ConfigSection s : i.getConfigMap().entries.values()) { 746 for (ConfigMapEntry e : s.oentries.values()) { 747 if (! newMap.hasEntry(s.name, e.key)) { 748 changes.add(ConfigEvent.removeEntry(name, s.name, e.key)); 749 } 750 } 751 } 752 } 753 } 754 755 for (ConfigSection ns : newMap.oentries.values()) { 756 ConfigSection s = oentries.get(ns.name); 757 if (s == null) { 758 //changes.add(ConfigEvent.setSection(ns.name, ns.preLines)); 759 for (ConfigMapEntry ne : ns.entries.values()) { 760 changes.add(ConfigEvent.setEntry(name, ns.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines)); 761 } 762 } else { 763 for (ConfigMapEntry ne : ns.oentries.values()) { 764 ConfigMapEntry e = s.oentries.get(ne.key); 765 if (e == null || ne(e.value, ne.value)) { 766 changes.add(ConfigEvent.setEntry(name, s.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines)); 767 } 768 } 769 for (ConfigMapEntry e : s.oentries.values()) { 770 ConfigMapEntry ne = ns.oentries.get(e.key); 771 if (ne == null) { 772 changes.add(ConfigEvent.removeEntry(name, s.name, e.key)); 773 } 774 } 775 } 776 } 777 778 for (ConfigSection s : oentries.values()) { 779 ConfigSection ns = newMap.oentries.get(s.name); 780 if (ns == null) { 781 //changes.add(ConfigEvent.removeSection(s.name)); 782 for (ConfigMapEntry e : s.oentries.values()) 783 changes.add(ConfigEvent.removeEntry(name, s.name, e.key)); 784 } 785 } 786 787 return changes; 788 } 789 790 // This method should only be called from behind a lock. 791 private String asString() { 792 try { 793 StringWriter sw = new StringWriter(); 794 for (ConfigSection cs : entries.values()) 795 cs.writeTo(sw); 796 return sw.toString(); 797 } catch (IOException e) { 798 throw asRuntimeException(e); // Not possible. 799 } 800 } 801 802 803 //--------------------------------------------------------------------------------------------- 804 // ConfigSection 805 //--------------------------------------------------------------------------------------------- 806 807 class ConfigSection { 808 809 final String name; // The config section name, or blank if the default section. Never null. 810 811 final List<String> preLines = synced(list()); 812 private final String rawLine; 813 814 final Map<String,ConfigMapEntry> oentries = synced(map()); 815 final Map<String,ConfigMapEntry> entries = synced(map()); 816 817 /** 818 * Constructor. 819 */ 820 ConfigSection(String name) { 821 this.name = name; 822 this.rawLine = "[" + name + "]"; 823 } 824 825 /** 826 * Constructor. 827 */ 828 ConfigSection(List<String> lines) { 829 830 String name = null, rawLine = null; 831 832 int S1 = 1; // Looking for section. 833 int S2 = 2; // Found section, looking for end. 834 int state = S1; 835 int start = 0; 836 837 for (int i = 0; i < lines.size(); i++) { 838 String l = lines.get(i); 839 char c = StringUtils.firstNonWhitespaceChar(l); 840 if (state == S1) { 841 if (c == '[') { 842 int i1 = l.indexOf('['), i2 = l.indexOf(']'); 843 name = l.substring(i1+1, i2).trim(); 844 rawLine = l; 845 state = S2; 846 start = i+1; 847 } else { 848 preLines.add(l); 849 } 850 } else { 851 if (c != '#' && l.indexOf('=') != -1) { 852 ConfigMapEntry e = new ConfigMapEntry(l, lines.subList(start, i)); 853 if (entries.containsKey(e.key)) 854 throw new ConfigException("Duplicate entry found in section [{0}] of configuration: {1}", name, e.key); 855 entries.put(e.key, e); 856 start = i+1; 857 } 858 } 859 } 860 861 this.name = name; 862 this.rawLine = rawLine; 863 this.oentries.putAll(entries); 864 } 865 866 ConfigSection addEntry(String key, String value, String modifiers, String comment, List<String> preLines) { 867 ConfigMapEntry e = new ConfigMapEntry(key, value, modifiers, comment, preLines); 868 this.entries.put(e.key, e); 869 return this; 870 } 871 872 ConfigSection setPreLines(List<String> preLines) { 873 this.preLines.clear(); 874 this.preLines.addAll(preLines); 875 return this; 876 } 877 878 Writer writeTo(Writer w) throws IOException { 879 for (String s : preLines) 880 w.append(s).append('\n'); 881 882 if (! name.equals("")) 883 w.append(rawLine).append('\n'); 884 else { 885 // Need separation between default prelines and first-entry prelines. 886 if (! preLines.isEmpty()) 887 w.append('\n'); 888 } 889 890 for (ConfigMapEntry e : entries.values()) 891 e.writeTo(w); 892 893 return w; 894 } 895 } 896 897 898 //--------------------------------------------------------------------------------------------- 899 // Import 900 //--------------------------------------------------------------------------------------------- 901 902 class Import { 903 904 private final ConfigMap configMap; 905 private final Map<ConfigEventListener,ConfigEventListener> listenerMap = synced(map()); 906 907 Import(ConfigMap configMap) { 908 this.configMap = configMap; 909 } 910 911 synchronized Import register(Collection<ConfigEventListener> listeners) { 912 listeners.forEach(x -> register(x)); 913 return this; 914 } 915 916 synchronized Import register(final ConfigEventListener listener) { 917 ConfigEventListener l2 = new ConfigEventListener() { 918 @Override 919 public void onConfigChange(ConfigEvents events) { 920 ConfigEvents events2 = new ConfigEvents(); 921 events.stream().filter(x -> ! hasEntry(x.getSection(), x.getKey())).forEach(x -> events2.add(x)); 922 if (events2.size() > 0) 923 listener.onConfigChange(events2); 924 } 925 }; 926 listenerMap.put(listener, l2); 927 configMap.register(l2); 928 return this; 929 } 930 931 synchronized Import unregister(final ConfigEventListener listener) { 932 configMap.unregister(listenerMap.remove(listener)); 933 return this; 934 } 935 936 synchronized Import unregisterAll() { 937 listenerMap.values().forEach(x -> configMap.unregister(x)); 938 listenerMap.clear(); 939 return this; 940 } 941 942 String getConfigName() { 943 return configMap.name; 944 } 945 946 ConfigMap getConfigMap() { 947 return configMap; 948 } 949 950 @Override 951 public boolean equals(Object o) { 952 if (o instanceof Import) { 953 Import ir = (Import)o; 954 if (ir.getConfigName().equals(getConfigName())) 955 return true; 956 } 957 return false; 958 } 959 960 @Override 961 public int hashCode() { 962 return getConfigName().hashCode(); 963 } 964 } 965}