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