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