001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.config.internal; 018 019import static org.apache.juneau.commons.lang.StateEnum.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022import static org.apache.juneau.commons.utils.ThrowableUtils.*; 023import static org.apache.juneau.commons.utils.Utils.*; 024import static org.apache.juneau.config.event.ConfigEventType.*; 025 026import java.io.*; 027import java.util.*; 028import java.util.concurrent.*; 029import java.util.stream.*; 030 031import org.apache.juneau.*; 032import org.apache.juneau.collections.*; 033import org.apache.juneau.commons.concurrent.*; 034import org.apache.juneau.commons.utils.*; 035import org.apache.juneau.config.event.*; 036import org.apache.juneau.config.store.*; 037 038/** 039 * Represents the parsed contents of a configuration. 040 */ 041@SuppressWarnings("resource") 042public class ConfigMap implements ConfigStoreListener { 043 044 class ConfigSection { 045 046 final String name; // The config section name, or blank if the default section. Never null. 047 048 final List<String> preLines = synced(list()); 049 private final String rawLine; 050 051 final Map<String,ConfigMapEntry> oentries = synced(map()); 052 final Map<String,ConfigMapEntry> entries = synced(map()); 053 054 /** 055 * Constructor. 056 */ 057 ConfigSection(List<String> lines) { 058 059 var name2 = (String)null; 060 var rawLine2 = (String)null; 061 062 // S1: Looking for section. 063 // S2: Found section, looking for end. 064 065 var state = S1; 066 var start = 0; 067 068 for (var i = 0; i < lines.size(); i++) { 069 var l = lines.get(i); 070 var c = StringUtils.firstNonWhitespaceChar(l); 071 if (state == S1) { 072 if (c == '[') { 073 var i1 = l.indexOf('['); 074 var i2 = l.indexOf(']'); 075 name2 = l.substring(i1 + 1, i2).trim(); 076 rawLine2 = l; 077 state = S2; 078 start = i + 1; 079 } else { 080 preLines.add(l); 081 } 082 } else { 083 if (c != '#' && l.indexOf('=') != -1) { 084 var e = new ConfigMapEntry(l, lines.subList(start, i)); 085 if (entries.containsKey(e.key)) 086 throw new ConfigException("Duplicate entry found in section [{0}] of configuration: {1}", name2, e.key); 087 entries.put(e.key, e); 088 start = i + 1; 089 } 090 } 091 } 092 093 this.name = name2; 094 this.rawLine = rawLine2; 095 this.oentries.putAll(entries); 096 } 097 098 /** 099 * Constructor. 100 */ 101 ConfigSection(String name) { 102 this.name = name; 103 this.rawLine = "[" + name + "]"; 104 } 105 106 ConfigSection addEntry(String key, String value, String modifiers, String comment, List<String> preLines) { 107 var e = new ConfigMapEntry(key, value, modifiers, comment, preLines); 108 this.entries.put(e.key, e); 109 return this; 110 } 111 112 ConfigSection setPreLines(List<String> preLines) { 113 this.preLines.clear(); 114 this.preLines.addAll(preLines); 115 return this; 116 } 117 118 Writer writeTo(Writer w) throws IOException { 119 for (var s : preLines) 120 w.append(s).append('\n'); 121 122 if (! name.isEmpty()) 123 w.append(rawLine).append('\n'); 124 else { 125 // Need separation between default prelines and first-entry prelines. 126 if (! preLines.isEmpty()) 127 w.append('\n'); 128 } 129 130 for (var e : entries.values()) 131 e.writeTo(w); 132 133 return w; 134 } 135 } 136 137 class Import { 138 139 private final ConfigMap configMap; 140 private final Map<ConfigEventListener,ConfigEventListener> listenerMap = synced(map()); 141 142 Import(ConfigMap configMap) { 143 this.configMap = configMap; 144 } 145 146 @Override 147 public boolean equals(Object o) { 148 return o instanceof Import ir && ir.getConfigName().equals(getConfigName()); 149 } 150 151 @Override 152 public int hashCode() { 153 return getConfigName().hashCode(); 154 } 155 156 ConfigMap getConfigMap() { return configMap; } 157 158 String getConfigName() { return configMap.name; } 159 160 synchronized Import register(Collection<ConfigEventListener> listeners) { 161 listeners.forEach(this::register); 162 return this; 163 } 164 165 synchronized Import register(ConfigEventListener listener) { 166 var l2 = (ConfigEventListener)events -> { 167 var events2 = events.stream().filter(x -> ! hasEntry(x.getSection(), x.getKey())).collect(Collectors.toCollection(ConfigEvents::new)); 168 if (! events2.isEmpty()) 169 listener.onConfigChange(events2); 170 }; 171 listenerMap.put(listener, l2); 172 configMap.register(l2); 173 return this; 174 } 175 176 synchronized Import unregister(ConfigEventListener listener) { 177 configMap.unregister(listenerMap.remove(listener)); 178 return this; 179 } 180 181 synchronized Import unregisterAll() { 182 listenerMap.values().forEach(configMap::unregister); 183 listenerMap.clear(); 184 return this; 185 } 186 } 187 188 private static void checkKeyName(String s) { 189 if (! isValidKeyName(s)) 190 throw illegalArg("Invalid key name: ''{0}''", s); 191 } 192 193 private static void checkSectionName(String s) { 194 if (! ("".equals(s) || isValidNewSectionName(s))) 195 throw illegalArg("Invalid section name: ''{0}''", s); 196 } 197 198 private static boolean isValidConfigName(String s) { 199 if (s == null) 200 return false; 201 s = s.trim(); 202 if (s.isEmpty()) 203 return false; 204 for (var i = 0; i < s.length(); i++) { 205 var c = s.charAt(i); 206 if (i == 0) { 207 if (! Character.isJavaIdentifierStart(c)) 208 return false; 209 } else { 210 if (! Character.isJavaIdentifierPart(c)) 211 return false; 212 } 213 } 214 return true; 215 } 216 217 private static boolean isValidKeyName(String s) { 218 if (s == null) 219 return false; 220 s = s.trim(); 221 if (s.isEmpty()) 222 return false; 223 for (var i = 0; i < s.length(); i++) { 224 var c = s.charAt(i); 225 if (c == '/' || c == '\\' || c == '[' || c == ']' || c == '=' || c == '#') 226 return false; 227 } 228 return true; 229 } 230 231 private static boolean isValidNewSectionName(String s) { 232 if (s == null) 233 return false; 234 s = s.trim(); 235 if (s.isEmpty()) 236 return false; 237 for (var i = 0; i < s.length(); i++) { 238 var c = s.charAt(i); 239 if (c == '/' || c == '\\' || c == '[' || c == ']') 240 return false; 241 } 242 return true; 243 } 244 245 private final ConfigStore store; // The store that created this object. 246 247 private volatile String contents; // The original contents of this object. 248 249 final String name; // The name of this object. 250 251 // Changes that have been applied since the last load. 252 private final List<ConfigEvent> changes = synced(new ConfigEvents()); 253 254 // Registered listeners listening for changes during saves or reloads. 255 private final Set<ConfigEventListener> listeners = synced(set()); 256 257 // The parsed entries of this map with all changes applied. 258 final Map<String,ConfigSection> entries = synced(map()); 259 260 // The original entries of this map before any changes were applied. 261 final Map<String,ConfigSection> oentries = synced(map()); 262 263 // Import statements in this config. 264 final List<Import> imports = new CopyOnWriteArrayList<>(); 265 266 private final SimpleReadWriteLock lock = new SimpleReadWriteLock(); 267 268 /** 269 * Constructor. 270 * 271 * @param store The config store. 272 * @param name The config name. 273 * @throws IOException Thrown by underlying stream. 274 */ 275 public ConfigMap(ConfigStore store, String name) throws IOException { 276 this.store = store; 277 this.name = name; 278 load(store.read(name)); 279 } 280 281 ConfigMap(ConfigStore store, String name, String contents) throws IOException { 282 this.store = store; 283 this.name = name; 284 load(contents); 285 } 286 287 /** 288 * Returns the values in this config map as a map of maps. 289 * 290 * <p> 291 * This is considered a snapshot copy of the config map. 292 * 293 * <p> 294 * The returned map is modifiable, but modifications to the returned map are not reflected in the config map. 295 * 296 * @return A copy of this config as a map of maps. 297 */ 298 public JsonMap asMap() { 299 var m = new JsonMap(); 300 try (var x = lock.read()) { 301 imports.forEach(y -> m.putAll(y.getConfigMap().asMap())); 302 entries.values().forEach(z -> { 303 var m2 = mapOf(String.class, String.class); 304 z.entries.values().forEach(y -> m2.put(y.key, y.value)); 305 m.put(z.name, m2); 306 }); 307 } 308 return m; 309 } 310 311 /** 312 * Persist any changes made to this map and signal all listeners. 313 * 314 * <p> 315 * If the underlying contents of the file have changed, this will reload it and apply the changes 316 * on top of the modified file. 317 * 318 * <p> 319 * Subsequent changes made to the underlying file will also be signaled to all listeners. 320 * 321 * <p> 322 * We try saving the file up to 10 times. 323 * <br>If the file keeps changing on the file system, we throw an exception. 324 * 325 * @return This object. 326 * @throws IOException Thrown by underlying stream. 327 */ 328 public ConfigMap commit() throws IOException { 329 try (var x = lock.write()) { 330 var newContents = asString(); 331 for (var i = 0; i <= 10; i++) { 332 if (i == 10) 333 throw new ConfigException("Unable to store contents of config to store."); 334 var currentContents = store.write(name, contents, newContents); 335 if (currentContents == null) 336 break; 337 onChange(currentContents); 338 } 339 this.changes.clear(); 340 } 341 return this; 342 } 343 344 /** 345 * Reads an entry from this map. 346 * 347 * @param section 348 * The section name. 349 * <br>Must not be <jk>null</jk>. 350 * <br>Use blank to refer to the default section. 351 * @param key 352 * The entry key. 353 * <br>Must not be <jk>null</jk>. 354 * @return The entry, or <jk>null</jk> if the entry doesn't exist. 355 */ 356 public ConfigMapEntry getEntry(String section, String key) { 357 checkSectionName(section); 358 checkKeyName(key); 359 try (var x = lock.read()) { 360 var cs = entries.get(section); 361 var ce = cs == null ? null : cs.entries.get(key); 362 363 if (ce == null) 364 ce = imports.stream().map(y -> y.getConfigMap().getEntry(section, key)).filter(y -> nn(y)).findFirst().orElse(null); 365 366 return ce; 367 } 368 } 369 370 /** 371 * Returns the keys of the entries in the specified section. 372 * 373 * @param section 374 * The section name. 375 * <br>Must not be <jk>null</jk>. 376 * <br>Use blank to refer to the default section. 377 * @return 378 * An unmodifiable set of keys, or an empty set if the section doesn't exist. 379 */ 380 public Set<String> getKeys(String section) { 381 checkSectionName(section); 382 var cs = entries.get(section); 383 Set<String> s = imports.isEmpty() && nn(cs) ? cs.entries.keySet() : set(); 384 if (! imports.isEmpty()) { 385 imports.forEach(x -> s.addAll(x.getConfigMap().getKeys(section))); 386 if (nn(cs)) 387 s.addAll(cs.entries.keySet()); 388 } 389 return u(s); 390 } 391 392 /** 393 * Returns the listeners currently associated with this config map. 394 * 395 * @return The listeners currently associated with this config map. 396 */ 397 public Set<ConfigEventListener> getListeners() { return u(listeners); } 398 399 /** 400 * Returns the pre-lines on the specified section. 401 * 402 * <p> 403 * The pre-lines are all lines such as blank lines and comments that preceed a section. 404 * 405 * @param section 406 * The section name. 407 * <br>Must not be <jk>null</jk>. 408 * <br>Use blank to refer to the default section. 409 * @return 410 * An unmodifiable list of lines, or <jk>null</jk> if the section doesn't exist. 411 */ 412 public List<String> getPreLines(String section) { 413 checkSectionName(section); 414 try (var x = lock.read()) { 415 var cs = entries.get(section); 416 return cs == null ? null : cs.preLines; 417 } 418 } 419 420 /** 421 * Returns the keys of the entries in the specified section. 422 * 423 * @return 424 * An unmodifiable set of keys. 425 */ 426 public Set<String> getSections() { 427 var s = imports.isEmpty() ? entries.keySet() : setOf(String.class); 428 if (! imports.isEmpty()) { 429 imports.forEach(x -> s.addAll(x.getConfigMap().getSections())); 430 s.addAll(entries.keySet()); 431 } 432 return u(s); 433 } 434 435 /** 436 * Returns <jk>true</jk> if this config has the specified section. 437 * 438 * @param section 439 * The section name. 440 * <br>Must not be <jk>null</jk>. 441 * <br>Use blank to refer to the default section. 442 * @return <jk>true</jk> if this config has the specified section. 443 */ 444 public boolean hasSection(String section) { 445 checkSectionName(section); 446 return nn(entries.get(section)) || imports.stream().anyMatch(x -> x.getConfigMap().hasSection(section)); 447 } 448 449 /** 450 * Overwrites the contents of the config file. 451 * 452 * @param contents The new contents of the config file. 453 * @param synchronous Wait until the change has been persisted before returning this map. 454 * @return This object. 455 * @throws IOException Thrown by underlying stream. 456 * @throws InterruptedException Thread was interrupted. 457 */ 458 public ConfigMap load(String contents, boolean synchronous) throws IOException, InterruptedException { 459 460 if (synchronous) { 461 final var latch = new CountDownLatch(1); 462 var listener = (ConfigStoreListener)x -> latch.countDown(); 463 store.register(name, listener); 464 store.write(name, null, contents); 465 if (latch.await(30, TimeUnit.SECONDS)) { 466 store.unregister(name, listener); 467 } else { 468 throw new ConfigException("Unable to store contents of config to store."); 469 } 470 } else { 471 store.write(name, null, contents); 472 } 473 return this; 474 } 475 476 @SuppressWarnings("null") 477 @Override /* Overridden from ConfigStoreListener */ 478 public void onChange(String newContents) { 479 var changes2 = (ConfigEvents)null; 480 try (var x = lock.write()) { 481 if (neq(contents, newContents)) { 482 changes2 = findDiffs(newContents); 483 load(newContents); 484 485 // Reapply our changes on top of the modifications. 486 this.changes.forEach(y -> applyChange(false, y)); 487 } 488 } catch (IOException e) { 489 throw toRex(e); 490 } 491 if (nn(changes2) && ! changes2.isEmpty()) 492 signal(changes2); 493 } 494 495 /** 496 * Registers an event listener on this map. 497 * 498 * @param listener The new listener. 499 * @return This object. 500 */ 501 public ConfigMap register(ConfigEventListener listener) { 502 listeners.add(listener); 503 imports.forEach(x -> x.register(listener)); 504 return this; 505 } 506 507 /** 508 * Removes an entry. 509 * 510 * @param section 511 * The section name. 512 * <br>Must not be <jk>null</jk>. 513 * <br>Use blank to refer to the default section. 514 * @param key 515 * The entry key. 516 * <br>Must not be <jk>null</jk>. 517 * @return This object. 518 */ 519 public ConfigMap removeEntry(String section, String key) { 520 checkSectionName(section); 521 checkKeyName(key); 522 return applyChange(true, ConfigEvent.removeEntry(name, section, key)); 523 } 524 525 /** 526 * Not implemented. 527 * 528 * @param section 529 * The section name where to place the import statement. 530 * <br>Must not be <jk>null</jk>. 531 * <br>Use blank for the default section. 532 * @param importName 533 * The import name. 534 * <br>Must not be <jk>null</jk>. 535 * @return This object. 536 */ 537 public ConfigMap removeImport(String section, String importName) { 538 throw unsupportedOp(); 539 } 540 541 /** 542 * Removes a section. 543 * 544 * <p> 545 * This eliminates all entries in the section as well. 546 * 547 * @param section 548 * The section name. 549 * <br>Must not be <jk>null</jk>. 550 * <br>Use blank to refer to the default section. 551 * @return This object. 552 */ 553 public ConfigMap removeSection(String section) { 554 checkSectionName(section); 555 return applyChange(true, ConfigEvent.removeSection(name, section)); 556 } 557 558 /** 559 * Does a rollback of any changes on this map currently in memory. 560 * 561 * @return This object. 562 */ 563 public ConfigMap rollback() { 564 if (! changes.isEmpty()) { 565 try (var x = lock.write()) { 566 changes.clear(); 567 load(contents); 568 } catch (IOException e) { 569 throw toRex(e); 570 } 571 } 572 return this; 573 } 574 575 /** 576 * Adds or overwrites an existing entry. 577 * 578 * @param section 579 * The section name. 580 * <br>Must not be <jk>null</jk>. 581 * <br>Use blank to refer to the default section. 582 * @param key 583 * The entry key. 584 * <br>Must not be <jk>null</jk>. 585 * @param value 586 * The entry value. 587 * <br>If <jk>null</jk>, the previous value will not be overwritten. 588 * @param modifiers 589 * Optional modifiers. 590 * <br>If <jk>null</jk>, the previous value will not be overwritten. 591 * @param comment 592 * Optional comment. 593 * <br>If <jk>null</jk>, the previous value will not be overwritten. 594 * @param preLines 595 * Optional pre-lines. 596 * <br>If <jk>null</jk>, the previous value will not be overwritten. 597 * @return This object. 598 */ 599 public ConfigMap setEntry(String section, String key, String value, String modifiers, String comment, List<String> preLines) { 600 checkSectionName(section); 601 checkKeyName(key); 602 return applyChange(true, ConfigEvent.setEntry(name, section, key, value, modifiers, comment, preLines)); 603 } 604 605 /** 606 * Not implemented. 607 * 608 * @param section 609 * The section name where to place the import statement. 610 * <br>Must not be <jk>null</jk>. 611 * <br>Use blank for the default section. 612 * @param importName 613 * The import name. 614 * <br>Must not be <jk>null</jk>. 615 * @param preLines 616 * Optional comment and blank lines to add immediately before the import statement. 617 * <br>If <jk>null</jk>, previous pre-lines will not be replaced. 618 * @return This object. 619 */ 620 public ConfigMap setImport(String section, String importName, List<String> preLines) { 621 throw unsupportedOp(); 622 } 623 624 /** 625 * Adds a new section or replaces the pre-lines on an existing section. 626 * 627 * @param section 628 * The section name. 629 * <br>Must not be <jk>null</jk>. 630 * <br>Use blank to refer to the default section. 631 * @param preLines 632 * The pre-lines on the section. 633 * <br>If <jk>null</jk>, the previous value will not be overwritten. 634 * @return This object. 635 */ 636 public ConfigMap setSection(String section, List<String> preLines) { 637 checkSectionName(section); 638 return applyChange(true, ConfigEvent.setSection(name, section, preLines)); 639 } 640 641 @Override /* Overridden from Object */ 642 public String toString() { 643 try (var x = lock.read()) { 644 return asString(); 645 } 646 } 647 648 /** 649 * Unregisters an event listener from this map. 650 * 651 * @param listener The listener to remove. 652 * @return This object. 653 */ 654 public ConfigMap unregister(ConfigEventListener listener) { 655 listeners.remove(listener); 656 imports.forEach(x -> x.register(listener)); 657 return this; 658 } 659 660 /** 661 * Serializes this map to the specified writer. 662 * 663 * @param w The writer to serialize to. 664 * @return The same writer passed in. 665 * @throws IOException Thrown by underlying stream. 666 */ 667 public Writer writeTo(Writer w) throws IOException { 668 try (var x = lock.read()) { 669 for (var cs : entries.values()) 670 cs.writeTo(w); 671 } 672 return w; 673 } 674 675 private ConfigMap applyChange(boolean addToChangeList, ConfigEvent ce) { 676 if (ce == null) 677 return this; 678 try (var x = lock.write()) { 679 var section = ce.getSection(); 680 var cs = entries.get(section); 681 if (ce.getType() == SET_ENTRY) { 682 if (cs == null) { 683 cs = new ConfigSection(section); 684 entries.put(section, cs); 685 } 686 var oe = cs.entries.get(ce.getKey()); 687 if (oe == null) 688 oe = ConfigMapEntry.NULL; 689 // @formatter:off 690 cs.addEntry( 691 ce.getKey(), 692 ce.getValue() == null ? oe.value : ce.getValue(), 693 ce.getModifiers() == null ? oe.modifiers : ce.getModifiers(), 694 ce.getComment() == null ? oe.comment : ce.getComment(), 695 ce.getPreLines() == null ? oe.preLines : ce.getPreLines() 696 ); 697 // @formatter:on 698 } else if (ce.getType() == SET_SECTION) { 699 if (cs == null) { 700 cs = new ConfigSection(section); 701 entries.put(section, cs); 702 } 703 if (nn(ce.getPreLines())) 704 cs.setPreLines(ce.getPreLines()); 705 } else if (ce.getType() == REMOVE_ENTRY) { 706 if (nn(cs)) 707 cs.entries.remove(ce.getKey()); 708 } else if (ce.getType() == REMOVE_SECTION) { 709 if (nn(cs)) // NOSONAR - Intentional. 710 entries.remove(section); 711 } 712 if (addToChangeList) 713 changes.add(ce); 714 } 715 return this; 716 } 717 718 // This method should only be called from behind a lock. 719 private String asString() { 720 try { 721 var sw = new StringWriter(); 722 for (var cs : entries.values()) 723 cs.writeTo(sw); 724 return sw.toString(); 725 } catch (IOException e) { 726 throw toRex(e); // Not possible. 727 } 728 } 729 730 private ConfigEvents findDiffs(String updatedContents) throws IOException { 731 var changes2 = new ConfigEvents(); 732 var newMap = new ConfigMap(store, name, updatedContents); 733 734 // Imports added. 735 for (var i : newMap.imports) { 736 if (! imports.contains(i)) { 737 for (var s : i.getConfigMap().entries.values()) { 738 for (var e : s.oentries.values()) { 739 if (! newMap.hasEntry(s.name, e.key)) { 740 changes2.add(ConfigEvent.setEntry(name, s.name, e.key, e.value, e.modifiers, e.comment, e.preLines)); 741 } 742 } 743 } 744 } 745 } 746 747 // Imports removed. 748 for (var i : imports) { 749 if (! newMap.imports.contains(i)) { 750 for (var s : i.getConfigMap().entries.values()) { 751 for (var e : s.oentries.values()) { 752 if (! newMap.hasEntry(s.name, e.key)) { 753 changes2.add(ConfigEvent.removeEntry(name, s.name, e.key)); 754 } 755 } 756 } 757 } 758 } 759 760 for (var ns : newMap.oentries.values()) { 761 var s = oentries.get(ns.name); 762 if (s == null) { 763 for (var ne : ns.entries.values()) { 764 changes2.add(ConfigEvent.setEntry(name, ns.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines)); 765 } 766 } else { 767 for (var ne : ns.oentries.values()) { 768 var e = s.oentries.get(ne.key); 769 if (e == null || neq(e.value, ne.value)) { 770 changes2.add(ConfigEvent.setEntry(name, s.name, ne.key, ne.value, ne.modifiers, ne.comment, ne.preLines)); 771 } 772 } 773 for (var e : s.oentries.values()) { 774 var ne = ns.oentries.get(e.key); 775 if (ne == null) { 776 changes2.add(ConfigEvent.removeEntry(name, s.name, e.key)); 777 } 778 } 779 } 780 } 781 782 for (var s : oentries.values()) { 783 var ns = newMap.oentries.get(s.name); 784 if (ns == null) { 785 for (var e : s.oentries.values()) 786 changes2.add(ConfigEvent.removeEntry(name, s.name, e.key)); 787 } 788 } 789 790 return changes2; 791 } 792 793 private ConfigMap load(String contents) throws IOException { 794 if (contents == null) 795 contents = ""; 796 this.contents = contents; 797 798 entries.clear(); 799 oentries.clear(); 800 imports.forEach(Import::unregisterAll); 801 imports.clear(); 802 803 var imports2 = mapOf(String.class, ConfigMap.class); 804 805 List<String> lines = new LinkedList<>(); 806 try (var scanner = new Scanner(contents)) { 807 while (scanner.hasNextLine()) { 808 var line = scanner.nextLine(); 809 var c = firstChar(line); 810 var c2 = StringUtils.lastNonWhitespaceChar(line); 811 if (c == '[') { 812 var l = line.trim(); 813 if (c2 != ']' || ! isValidNewSectionName(l.substring(1, l.length() - 1))) 814 throw new ConfigException("Invalid section name found in configuration: {0}", line); 815 } else if (c == '<') { 816 var l = line.trim(); 817 var i = l.indexOf('>'); 818 if (i != -1) { 819 var l2 = l.substring(1, i); 820 if (! isValidConfigName(l2)) 821 throw new ConfigException("Invalid import config name found in configuration: {0}", line); 822 var l3 = l.substring(i + 1); 823 if (! (isEmpty(l3) || firstChar(l3) == '#')) 824 throw new ConfigException("Invalid import config name found in configuration: {0}", line); 825 var importName = l2.trim(); 826 try { 827 if (! imports2.containsKey(importName)) 828 imports2.put(importName, store.getMap(importName)); 829 } catch (@SuppressWarnings("unused") StackOverflowError e) { 830 throw ioex("Import loop detected in configuration ''{0}''->''{1}''", name, importName); 831 } 832 } 833 } 834 lines.add(line); 835 } 836 } 837 838 List<Import> irl = listOfSize(imports2.size()); 839 forEachReverse(toList(imports2.values()), x -> irl.add(new Import(x).register(listeners))); 840 this.imports.addAll(irl); 841 842 // Add [blank] section. 843 var inserted = false; 844 var foundComment = false; 845 for (var li = lines.listIterator(); li.hasNext();) { 846 var l = li.next(); 847 var c = firstNonWhitespaceChar(l); 848 if (c != '#') { 849 if (c == 0 && foundComment) { 850 li.set("[]"); 851 inserted = true; 852 } 853 break; 854 } 855 foundComment = true; 856 } 857 if (! inserted) 858 lines.add(0, "[]"); 859 860 // Collapse any multi-lines. 861 var li = lines.listIterator(lines.size()); 862 var accumulator = (String)null; 863 while (li.hasPrevious()) { 864 var l = li.previous(); 865 var c = firstChar(l); 866 if (c == '\t') { 867 c = firstNonWhitespaceChar(l); 868 if (c != '#') { 869 if (accumulator == null) 870 accumulator = l.substring(1); 871 else 872 accumulator = l.substring(1) + "\n" + accumulator; // NOSONAR - Intentionally not using StringBuilder. 873 li.remove(); 874 } 875 } else if (nn(accumulator)) { 876 li.set(l + "\n" + accumulator); 877 accumulator = null; 878 } 879 } 880 881 lines = copyOf(lines); 882 var last = lines.size() - 1; 883 884 // S1: Looking for section. 885 // S2: Found section, looking for start. 886 887 var state = S1; 888 889 var sections = listOf(ConfigSection.class); 890 891 for (var i = last; i >= 0; i--) { 892 var l = lines.get(i); 893 var c = firstChar(l); 894 895 if (state == S1) { 896 if (c == '[') { 897 state = S2; 898 } 899 } else { 900 if (c != '#' && (c == '[' || l.indexOf('=') != -1)) { 901 sections.add(new ConfigSection(lines.subList(i + 1, last + 1))); 902 last = i + 1; 903 state = (c == '[' ? S2 : S1); 904 } 905 } 906 } 907 908 sections.add(new ConfigSection(lines.subList(0, last + 1))); 909 910 for (var i = sections.size() - 1; i >= 0; i--) { 911 var cs = sections.get(i); 912 if (entries.containsKey(cs.name)) 913 throw new ConfigException("Duplicate section found in configuration: [{0}]", cs.name); 914 entries.put(cs.name, cs); 915 } 916 917 oentries.putAll(entries); 918 return this; 919 } 920 921 private void signal(ConfigEvents changes) { 922 if (ne(changes)) 923 listeners.forEach(x -> x.onConfigChange(changes)); 924 } 925 926 boolean hasEntry(String section, String key) { 927 var cs = entries.get(section); 928 var ce = cs == null ? null : cs.entries.get(key); 929 return nn(ce); 930 } 931}