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