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 private 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 ArrayList<ConfigEvent>()); 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 private final ReadWriteLock lock = new ReentrantReadWriteLock(); 052 053 /** 054 * Constructor. 055 * 056 * @param store The config store. 057 * @param name The config name. 058 * @throws IOException 059 */ 060 public ConfigMap(ConfigStore store, String name) throws IOException { 061 this.store = store; 062 this.name = name; 063 load(store.read(name)); 064 } 065 066 ConfigMap(String contents) { 067 this.store = null; 068 this.name = null; 069 load(contents); 070 } 071 072 private ConfigMap load(String contents) { 073 if (contents == null) 074 contents = ""; 075 this.contents = contents; 076 077 entries.clear(); 078 oentries.clear(); 079 080 List<String> lines = new LinkedList<>(); 081 try (Scanner scanner = new Scanner(contents)) { 082 while (scanner.hasNextLine()) { 083 String line = scanner.nextLine(); 084 char c = StringUtils.firstNonWhitespaceChar(line); 085 if (c != 0 || c != '#') { 086 if (c == '[') { 087 int c2 = StringUtils.lastNonWhitespaceChar(line); 088 String l = line.trim(); 089 if (c2 != ']' || ! isValidNewSectionName(l.substring(1, l.length()-1))) 090 throw new ConfigException("Invalid section name found in configuration: {0}", line) ; 091 } 092 } 093 lines.add(line); 094 } 095 } 096 097 // Add [blank] section. 098 boolean inserted = false; 099 boolean foundComment = false; 100 for (ListIterator<String> li = lines.listIterator(); li.hasNext();) { 101 String l = li.next(); 102 char c = StringUtils.firstNonWhitespaceChar(l); 103 if (c != '#') { 104 if (c == 0 && foundComment) { 105 li.set("[]"); 106 inserted = true; 107 } 108 break; 109 } 110 foundComment = true; 111 } 112 if (! inserted) 113 lines.add(0, "[]"); 114 115 // Collapse any multi-lines. 116 ListIterator<String> li = lines.listIterator(lines.size()); 117 String accumulator = null; 118 while (li.hasPrevious()) { 119 String l = li.previous(); 120 char c = l.isEmpty() ? 0 : l.charAt(0); 121 if (c == '\t') { 122 c = firstNonWhitespaceChar(l); 123 if (c != '#') { 124 if (accumulator == null) 125 accumulator = l.substring(1); 126 else 127 accumulator = l.substring(1) + "\n" + accumulator; 128 li.remove(); 129 } 130 } else if (accumulator != null) { 131 li.set(l + "\n" + accumulator); 132 accumulator = null; 133 } 134 } 135 136 lines = new ArrayList<>(lines); 137 int last = lines.size()-1; 138 int S1 = 1; // Looking for section. 139 int S2 = 2; // Found section, looking for start. 140 int state = S1; 141 142 List<ConfigSection> sections = new ArrayList<>(); 143 144 for (int i = last; i >= 0; i--) { 145 String l = lines.get(i); 146 char c = StringUtils.firstNonWhitespaceChar(l); 147 148 if (state == S1) { 149 if (c == '[') { 150 state = S2; 151 } 152 } else { 153 if (c != '#' && (c == '[' || l.indexOf('=') != -1)) { 154 sections.add(new ConfigSection(lines.subList(i+1, last+1))); 155 last = i + 1;// (c == '[' ? i+1 : i); 156 state = (c == '[' ? S2 : S1); 157 } 158 } 159 } 160 161 sections.add(new ConfigSection(lines.subList(0, last+1))); 162 163 for (int i = sections.size() - 1; i >= 0; i--) { 164 ConfigSection cs = sections.get(i); 165 if (entries.containsKey(cs.name)) 166 throw new ConfigException("Duplicate section found in configuration: [{0}]", cs.name); 167 entries.put(cs.name, cs); 168 } 169 170 oentries.putAll(entries); 171 return this; 172 } 173 174 175 //----------------------------------------------------------------------------------------------------------------- 176 // Getters 177 //----------------------------------------------------------------------------------------------------------------- 178 179 /** 180 * Reads an entry from this map. 181 * 182 * @param section 183 * The section name. 184 * <br>Must not be <jk>null</jk>. 185 * <br>Use blank to refer to the default section. 186 * @param key 187 * The entry key. 188 * <br>Must not be <jk>null</jk>. 189 * @return The entry, or <jk>null</jk> if the entry doesn't exist. 190 */ 191 public ConfigEntry getEntry(String section, String key) { 192 checkSectionName(section); 193 checkKeyName(key); 194 readLock(); 195 try { 196 ConfigSection cs = entries.get(section); 197 return cs == null ? null : cs.entries.get(key); 198 } finally { 199 readUnlock(); 200 } 201 } 202 203 /** 204 * Returns the pre-lines on the specified section. 205 * 206 * <p> 207 * The pre-lines are all lines such as blank lines and comments that preceed a section. 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 * @return 214 * An unmodifiable list of lines, or <jk>null</jk> if the section doesn't exist. 215 */ 216 public List<String> getPreLines(String section) { 217 checkSectionName(section); 218 readLock(); 219 try { 220 ConfigSection cs = entries.get(section); 221 return cs == null ? null : cs.preLines; 222 } finally { 223 readUnlock(); 224 } 225 } 226 227 /** 228 * Returns the keys of the entries in the specified section. 229 * 230 * @return 231 * An unmodifiable set of keys. 232 */ 233 public Set<String> getSections() { 234 return Collections.unmodifiableSet(entries.keySet()); 235 } 236 237 /** 238 * Returns the keys of the entries in the specified section. 239 * 240 * @param section 241 * The section name. 242 * <br>Must not be <jk>null</jk>. 243 * <br>Use blank to refer to the default section. 244 * @return 245 * An unmodifiable set of keys, or an empty set if the section doesn't exist. 246 */ 247 public Set<String> getKeys(String section) { 248 checkSectionName(section); 249 ConfigSection cs = entries.get(section); 250 return cs == null ? Collections.<String>emptySet() : Collections.unmodifiableSet(cs.entries.keySet()); 251 } 252 253 /** 254 * Returns <jk>true</jk> if this config has the specified section. 255 * 256 * @param section 257 * The section name. 258 * <br>Must not be <jk>null</jk>. 259 * <br>Use blank to refer to the default section. 260 * @return <jk>true</jk> if this config has the specified section. 261 */ 262 public boolean hasSection(String section) { 263 checkSectionName(section); 264 return entries.get(section) != null; 265 } 266 267 //----------------------------------------------------------------------------------------------------------------- 268 // Setters 269 //----------------------------------------------------------------------------------------------------------------- 270 271 /** 272 * Adds a new section or replaces the pre-lines on an existing section. 273 * 274 * @param section 275 * The section name. 276 * <br>Must not be <jk>null</jk>. 277 * <br>Use blank to refer to the default section. 278 * @param preLines 279 * The pre-lines on the section. 280 * <br>If <jk>null</jk>, the previous value will not be overwritten. 281 * @return This object (for method chaining). 282 */ 283 public ConfigMap setSection(String section, List<String> preLines) { 284 checkSectionName(section); 285 return applyChange(true, ConfigEvent.setSection(section, preLines)); 286 } 287 288 /** 289 * Adds or overwrites an existing entry. 290 * 291 * @param section 292 * The section name. 293 * <br>Must not be <jk>null</jk>. 294 * <br>Use blank to refer to the default section. 295 * @param key 296 * The entry key. 297 * <br>Must not be <jk>null</jk>. 298 * @param value 299 * The entry value. 300 * <br>If <jk>null</jk>, the previous value will not be overwritten. 301 * @param modifiers 302 * Optional modifiers. 303 * <br>If <jk>null</jk>, the previous value will not be overwritten. 304 * @param comment 305 * Optional comment. 306 * <br>If <jk>null</jk>, the previous value will not be overwritten. 307 * @param preLines 308 * Optional pre-lines. 309 * <br>If <jk>null</jk>, the previous value will not be overwritten. 310 * @return This object (for method chaining). 311 */ 312 public ConfigMap setEntry(String section, String key, String value, String modifiers, String comment, List<String> preLines) { 313 checkSectionName(section); 314 checkKeyName(key); 315 if (modifiers != null && ! MOD_CHARS.containsOnly(modifiers)) 316 throw new ConfigException("Invalid modifiers: {0}", modifiers); 317 return applyChange(true, ConfigEvent.setEntry(section, key, value, modifiers, comment, preLines)); 318 } 319 320 /** 321 * Removes a section. 322 * 323 * <p> 324 * This eliminates all entries in the section as well. 325 * 326 * @param section 327 * The section name. 328 * <br>Must not be <jk>null</jk>. 329 * <br>Use blank to refer to the default section. 330 * @return This object (for method chaining). 331 */ 332 public ConfigMap removeSection(String section) { 333 checkSectionName(section); 334 return applyChange(true, ConfigEvent.removeSection(section)); 335 } 336 337 /** 338 * Removes an entry. 339 * 340 * @param section 341 * The section name. 342 * <br>Must not be <jk>null</jk>. 343 * <br>Use blank to refer to the default section. 344 * @param key 345 * The entry key. 346 * <br>Must not be <jk>null</jk>. 347 * @return This object (for method chaining). 348 */ 349 public ConfigMap removeEntry(String section, String key) { 350 checkSectionName(section); 351 checkKeyName(key); 352 return applyChange(true, ConfigEvent.removeEntry(section, key)); 353 } 354 355 private ConfigMap applyChange(boolean addToChangeList, ConfigEvent ce) { 356 if (ce == null) 357 return this; 358 writeLock(); 359 try { 360 String section = ce.getSection(); 361 ConfigSection cs = entries.get(section); 362 if (ce.getType() == SET_ENTRY) { 363 if (cs == null) { 364 cs = new ConfigSection(section); 365 entries.put(section, cs); 366 } 367 ConfigEntry oe = cs.entries.get(ce.getKey()); 368 if (oe == null) 369 oe = ConfigEntry.NULL; 370 cs.addEntry( 371 ce.getKey(), 372 ce.getValue() == null ? oe.value : ce.getValue(), 373 ce.getModifiers() == null ? oe.modifiers : ce.getModifiers(), 374 ce.getComment() == null ? oe.comment : ce.getComment(), 375 ce.getPreLines() == null ? oe.preLines : ce.getPreLines() 376 ); 377 } else if (ce.getType() == SET_SECTION) { 378 if (cs == null) { 379 cs = new ConfigSection(section); 380 entries.put(section, cs); 381 } 382 if (ce.getPreLines() != null) 383 cs.setPreLines(ce.getPreLines()); 384 } else if (ce.getType() == REMOVE_ENTRY) { 385 if (cs != null) 386 cs.entries.remove(ce.getKey()); 387 } else if (ce.getType() == REMOVE_SECTION) { 388 if (cs != null) 389 entries.remove(section); 390 } 391 if (addToChangeList) 392 changes.add(ce); 393 } finally { 394 writeUnlock(); 395 } 396 return this; 397 } 398 399 /** 400 * Overwrites the contents of the config file. 401 * 402 * @param contents The new contents of the config file. 403 * @param synchronous Wait until the change has been persisted before returning this map. 404 * @return This object (for method chaining). 405 * @throws IOException 406 * @throws InterruptedException 407 */ 408 public ConfigMap load(String contents, boolean synchronous) throws IOException, InterruptedException { 409 410 if (synchronous) { 411 final CountDownLatch latch = new CountDownLatch(1); 412 ConfigStoreListener l = new ConfigStoreListener() { 413 @Override 414 public void onChange(String contents) { 415 latch.countDown(); 416 } 417 }; 418 store.register(name, l); 419 store.write(name, null, contents); 420 latch.await(30, TimeUnit.SECONDS); 421 store.unregister(name, l); 422 } else { 423 store.write(name, null, contents); 424 } 425 return this; 426 } 427 428 //----------------------------------------------------------------------------------------------------------------- 429 // Lifecycle events 430 //----------------------------------------------------------------------------------------------------------------- 431 432 /** 433 * Persist any changes made to this map and signal all listeners. 434 * 435 * <p> 436 * If the underlying contents of the file have changed, this will reload it and apply the changes 437 * on top of the modified file. 438 * 439 * <p> 440 * Subsequent changes made to the underlying file will also be signaled to all listeners. 441 * 442 * <p> 443 * We try saving the file up to 10 times. 444 * <br>If the file keeps changing on the file system, we throw an exception. 445 * 446 * @return This object (for method chaining). 447 * @throws IOException 448 */ 449 public ConfigMap commit() throws IOException { 450 writeLock(); 451 try { 452 String newContents = asString(); 453 for (int i = 0; i <= 10; i++) { 454 if (i == 10) 455 throw new ConfigException("Unable to store contents of config to store."); 456 String currentContents = store.write(name, contents, newContents); 457 if (currentContents == null) 458 break; 459 onChange(currentContents); 460 } 461 this.changes.clear(); 462 } finally { 463 writeUnlock(); 464 } 465 return this; 466 } 467 468 469 //----------------------------------------------------------------------------------------------------------------- 470 // Listeners 471 //----------------------------------------------------------------------------------------------------------------- 472 473 /** 474 * Registers an event listener on this map. 475 * 476 * @param listener The new listener. 477 * @return This object (for method chaining). 478 */ 479 public ConfigMap register(ConfigEventListener listener) { 480 listeners.add(listener); 481 return this; 482 } 483 484 /** 485 * Unregisters an event listener from this map. 486 * 487 * @param listener The listener to remove. 488 * @return This object (for method chaining). 489 */ 490 public ConfigMap unregister(ConfigEventListener listener) { 491 listeners.remove(listener); 492 return this; 493 } 494 495 @Override /* ConfigStoreListener */ 496 public void onChange(String newContents) { 497 List<ConfigEvent> changes = null; 498 writeLock(); 499 try { 500 if (! StringUtils.isEquals(contents, newContents)) { 501 changes = findDiffs(newContents); 502 load(newContents); 503 504 // Reapply our changes on top of the modifications. 505 for (ConfigEvent ce : this.changes) 506 applyChange(false, ce); 507 } 508 } finally { 509 writeUnlock(); 510 } 511 if (changes != null && ! changes.isEmpty()) 512 signal(changes); 513 } 514 515 @Override /* Object */ 516 public String toString() { 517 readLock(); 518 try { 519 return asString(); 520 } finally { 521 readUnlock(); 522 } 523 } 524 525 /** 526 * Returns the values in this config map as a map of maps. 527 * 528 * <p> 529 * This is considered a snapshot copy of the config map. 530 * 531 * <p> 532 * The returned map is modifiable, but modifications to the returned map are not reflected in the config map. 533 * 534 * @return A copy of this config as a map of maps. 535 */ 536 public ObjectMap asMap() { 537 ObjectMap m = new ObjectMap(); 538 readLock(); 539 try { 540 for (ConfigSection cs : entries.values()) { 541 Map<String,String> m2 = new LinkedHashMap<>(); 542 for (ConfigEntry ce : cs.entries.values()) 543 m2.put(ce.key, ce.value); 544 m.put(cs.name, m2); 545 } 546 } finally { 547 readUnlock(); 548 } 549 return m; 550 } 551 552 /** 553 * Serializes this map to the specified writer. 554 * 555 * @param w The writer to serialize to. 556 * @return The same writer passed in. 557 * @throws IOException 558 */ 559 public Writer writeTo(Writer w) throws IOException { 560 readLock(); 561 try { 562 for (ConfigSection cs : entries.values()) 563 cs.writeTo(w); 564 } finally { 565 readUnlock(); 566 } 567 return w; 568 } 569 570 /** 571 * Does a rollback of any changes on this map currently in memory. 572 * 573 * @return This object (for method chaining). 574 */ 575 public ConfigMap rollback() { 576 if (changes.size() > 0) { 577 writeLock(); 578 try { 579 changes.clear(); 580 load(contents); 581 } finally { 582 writeUnlock(); 583 } 584 } 585 return this; 586 } 587 588 589 //-------------------------------------------------------------------------------- 590 // Private methods 591 //-------------------------------------------------------------------------------- 592 593 private void readLock() { 594 lock.readLock().lock(); 595 } 596 597 private void readUnlock() { 598 lock.readLock().unlock(); 599 } 600 601 private void writeLock() { 602 lock.writeLock().lock(); 603 } 604 605 private void writeUnlock() { 606 lock.writeLock().unlock(); 607 } 608 609 private void checkSectionName(String s) { 610 if (! ("".equals(s) || isValidNewSectionName(s))) 611 throw new IllegalArgumentException("Invalid section name: '" + s + "'"); 612 } 613 614 private void checkKeyName(String s) { 615 if (! isValidKeyName(s)) 616 throw new IllegalArgumentException("Invalid key name: '" + s + "'"); 617 } 618 619 private boolean isValidKeyName(String s) { 620 if (s == null) 621 return false; 622 s = s.trim(); 623 if (s.isEmpty()) 624 return false; 625 for (int i = 0; i < s.length(); i++) { 626 char c = s.charAt(i); 627 if (c == '/' || c == '\\' || c == '[' || c == ']' || c == '=' || c == '#') 628 return false; 629 } 630 return true; 631 } 632 633 private boolean isValidNewSectionName(String s) { 634 if (s == null) 635 return false; 636 s = s.trim(); 637 if (s.isEmpty()) 638 return false; 639 for (int i = 0; i < s.length(); i++) { 640 char c = s.charAt(i); 641 if (c == '/' || c == '\\' || c == '[' || c == ']') 642 return false; 643 } 644 return true; 645 } 646 647 private void signal(List<ConfigEvent> changes) { 648 for (ConfigEventListener l : listeners) 649 l.onConfigChange(changes); 650 } 651 652 private List<ConfigEvent> findDiffs(String updatedContents) { 653 List<ConfigEvent> changes = new ArrayList<>(); 654 ConfigMap newMap = new ConfigMap(updatedContents); 655 for (ConfigSection ns : newMap.oentries.values()) { 656 ConfigSection s = oentries.get(ns.name); 657 if (s == null) { 658 //changes.add(ConfigEvent.setSection(ns.name, ns.preLines)); 659 for (ConfigEntry ne : ns.entries.values()) { 660 changes.add(ConfigEvent.setEntry(ns.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines)); 661 } 662 } else { 663 for (ConfigEntry ne : ns.oentries.values()) { 664 ConfigEntry e = s.oentries.get(ne.key); 665 if (e == null || ! isEquals(e.value, ne.value)) { 666 changes.add(ConfigEvent.setEntry(s.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines)); 667 } 668 } 669 for (ConfigEntry e : s.oentries.values()) { 670 ConfigEntry ne = ns.oentries.get(e.key); 671 if (ne == null) { 672 changes.add(ConfigEvent.removeEntry(s.name, e.key)); 673 } 674 } 675 } 676 } 677 for (ConfigSection s : oentries.values()) { 678 ConfigSection ns = newMap.oentries.get(s.name); 679 if (ns == null) { 680 //changes.add(ConfigEvent.removeSection(s.name)); 681 for (ConfigEntry e : s.oentries.values()) 682 changes.add(ConfigEvent.removeEntry(s.name, e.key)); 683 } 684 } 685 return changes; 686 } 687 688 // This method should only be called from behind a lock. 689 private String asString() { 690 try { 691 StringWriter sw = new StringWriter(); 692 for (ConfigSection cs : entries.values()) 693 cs.writeTo(sw); 694 return sw.toString(); 695 } catch (IOException e) { 696 throw new RuntimeException(e); // Not possible. 697 } 698 } 699 700 701 //--------------------------------------------------------------------------------------------- 702 // ConfigSection 703 //--------------------------------------------------------------------------------------------- 704 705 class ConfigSection { 706 707 final String name; // The config section name, or blank if the default section. Never null. 708 709 final List<String> preLines = Collections.synchronizedList(new ArrayList<String>()); 710 private final String rawLine; 711 712 final Map<String,ConfigEntry> oentries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigEntry>()); 713 final Map<String,ConfigEntry> entries = Collections.synchronizedMap(new LinkedHashMap<String,ConfigEntry>()); 714 715 /** 716 * Constructor. 717 */ 718 ConfigSection(String name) { 719 this.name = name; 720 this.rawLine = "[" + name + "]"; 721 } 722 723 /** 724 * Constructor. 725 */ 726 ConfigSection(List<String> lines) { 727 728 String name = null, rawLine = null; 729 730 int S1 = 1; // Looking for section. 731 int S2 = 2; // Found section, looking for end. 732 int state = S1; 733 int start = 0; 734 735 for (int i = 0; i < lines.size(); i++) { 736 String l = lines.get(i); 737 char c = StringUtils.firstNonWhitespaceChar(l); 738 if (state == S1) { 739 if (c == '[') { 740 int i1 = l.indexOf('['), i2 = l.indexOf(']'); 741 name = l.substring(i1+1, i2).trim(); 742 rawLine = l; 743 state = S2; 744 start = i+1; 745 } else { 746 preLines.add(l); 747 } 748 } else { 749 if (c != '#' && l.indexOf('=') != -1) { 750 ConfigEntry e = new ConfigEntry(l, lines.subList(start, i)); 751 if (entries.containsKey(e.key)) 752 throw new ConfigException("Duplicate entry found in section [{0}] of configuration: {1}", name, e.key); 753 entries.put(e.key, e); 754 start = i+1; 755 } 756 } 757 } 758 759 this.name = name; 760 this.rawLine = rawLine; 761 this.oentries.putAll(entries); 762 } 763 764 ConfigSection addEntry(String key, String value, String modifiers, String comment, List<String> preLines) { 765 ConfigEntry e = new ConfigEntry(key, value, modifiers, comment, preLines); 766 this.entries.put(e.key, e); 767 return this; 768 } 769 770 ConfigSection setPreLines(List<String> preLines) { 771 this.preLines.clear(); 772 this.preLines.addAll(preLines); 773 return this; 774 } 775 776 Writer writeTo(Writer w) throws IOException { 777 for (String s : preLines) 778 w.append(s).append('\n'); 779 780 if (! name.equals("")) 781 w.append(rawLine).append('\n'); 782 else { 783 // Need separation between default prelines and first-entry prelines. 784 if (! preLines.isEmpty()) 785 w.append('\n'); 786 } 787 788 for (ConfigEntry e : entries.values()) 789 e.writeTo(w); 790 791 return w; 792 } 793 } 794}