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; 018 019import static org.apache.juneau.common.utils.IOUtils.*; 020import static org.apache.juneau.common.utils.StringUtils.*; 021import static org.apache.juneau.common.utils.ThrowableUtils.*; 022import static org.apache.juneau.common.utils.Utils.*; 023import static org.apache.juneau.internal.CollectionUtils.*; 024import static org.apache.juneau.internal.CollectionUtils.map; 025 026import java.io.*; 027import java.lang.annotation.*; 028import java.lang.reflect.*; 029import java.util.*; 030import java.util.concurrent.atomic.*; 031 032import org.apache.juneau.*; 033import org.apache.juneau.collections.*; 034import org.apache.juneau.common.utils.*; 035import org.apache.juneau.config.event.*; 036import org.apache.juneau.config.internal.*; 037import org.apache.juneau.config.mod.*; 038import org.apache.juneau.config.store.*; 039import org.apache.juneau.config.vars.*; 040import org.apache.juneau.internal.*; 041import org.apache.juneau.json.*; 042import org.apache.juneau.parser.*; 043import org.apache.juneau.serializer.*; 044import org.apache.juneau.svl.*; 045import org.apache.juneau.utils.*; 046 047/** 048 * Main configuration API class. 049 * 050 * <h5 class='section'>Notes:</h5><ul> 051 * <li class='note'>This class is thread safe and reusable. 052 * </ul> 053 * 054 * <h5 class='section'>See Also:</h5><ul> 055 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/JuneauConfigBasics">juneau-config Basics</a> 056 * </ul> 057 */ 058public class Config extends Context implements ConfigEventListener { 059 060 //----------------------------------------------------------------------------------------------------------------- 061 // Static 062 //----------------------------------------------------------------------------------------------------------------- 063 064 private static final boolean DISABLE_AUTO_SYSTEM_PROPS = Boolean.getBoolean("juneau.disableAutoSystemProps"); 065 private static final AtomicReference<Config> SYSTEM_DEFAULT = new AtomicReference<>(findSystemDefault()); 066 067 /** 068 * Sets a system default configuration. 069 * 070 * @param systemDefault The new system default configuration. 071 */ 072 public static synchronized void setSystemDefault(Config systemDefault) { 073 SYSTEM_DEFAULT.set(systemDefault); 074 } 075 076 /** 077 * Returns the system default configuration. 078 * 079 * @return The system default configuration, or <jk>null</jk> if it doesn't exist. 080 */ 081 public static synchronized Config getSystemDefault() { 082 return SYSTEM_DEFAULT.get(); 083 } 084 085 private static synchronized Config findSystemDefault() { 086 087 for (var n : getCandidateSystemDefaultConfigNames()) { 088 var config = find(n); 089 if (config != null) { 090 if (! DISABLE_AUTO_SYSTEM_PROPS) 091 config.setSystemProperties(); 092 return config; 093 } 094 } 095 096 return null; 097 } 098 099 /** 100 * Returns the list of candidate system default configuration file names. 101 * 102 * <p> 103 * If the <js>"juneau.configFile"</js> system property is set, returns a singleton of that value. 104 * <br>Otherwise, returns a list consisting of the following values: 105 * <ol> 106 * <li>File with same name as jar file but with <js>".cfg"</js> extension. (e.g. <js>"myjar.cfg"</js>) 107 * <li>Any file ending in <js>".cfg"</js> in the home directory (names ordered alphabetically). 108 * <li><js>"juneau.cfg"</js> 109 * <li><js>"default.cfg"</js> 110 * <li><js>"application.cfg"</js> 111 * <li><js>"app.cfg"</js> 112 * <li><js>"settings.cfg"</js> 113 * <li><js>"application.properties"</js> 114 * </ol> 115 * <p> 116 * 117 * @return 118 * A list of candidate file names. 119 * <br>The returned list is modifiable. 120 * <br>Each call constructs a new list. 121 */ 122 public static synchronized List<String> getCandidateSystemDefaultConfigNames() { 123 var l = listOf(String.class); 124 125 var s = System.getProperty("juneau.configFile"); 126 if (s != null) { 127 l.add(s); 128 return l; 129 } 130 131 var cmd = System.getProperty("sun.java.command", "not_found").split("\\s+")[0]; 132 if (cmd.endsWith(".jar") && ! cmd.contains("surefirebooter")) { 133 cmd = cmd.replaceAll(".*?([^\\\\\\/]+)\\.jar$", "$1"); 134 l.add(cmd + ".cfg"); 135 cmd = cmd.replaceAll("[\\.\\_].*$", ""); // Try also without version in jar name. 136 l.add(cmd + ".cfg"); 137 } 138 139 var files = sortedSet(new File(".").listFiles()); 140 for (var f : files) 141 if (f.getName().endsWith(".cfg")) 142 l.add(f.getName()); 143 144 l.add("juneau.cfg"); 145 l.add("default.cfg"); 146 l.add("application.cfg"); 147 l.add("app.cfg"); 148 l.add("settings.cfg"); 149 l.add("application.properties"); 150 151 return l; 152 } 153 154 private static synchronized Config find(String name) { 155 if (name == null) 156 return null; 157 if (FileStore.DEFAULT.exists(name)) 158 return Config.create(name).store(FileStore.DEFAULT).build(); 159 if (ClasspathStore.DEFAULT.exists(name)) 160 return Config.create(name).store(ClasspathStore.DEFAULT).build(); 161 return null; 162 } 163 164 /** 165 * Creates a new builder for this object. 166 * 167 * @return A new builder. 168 */ 169 public static Builder create() { 170 return new Builder(); 171 } 172 173 /** 174 * Same as {@link #create()} but initializes the builder with the specified config name. 175 * 176 * @param name The configuration name. 177 * @return A new builder. 178 */ 179 public static Builder create(String name) { 180 return new Builder().name(name); 181 } 182 183 //------------------------------------------------------------------------------------------------------------------- 184 // Builder 185 //------------------------------------------------------------------------------------------------------------------- 186 187 /** 188 * Builder class. 189 */ 190 public static class Builder extends Context.Builder { 191 192 String name; 193 ConfigStore store; 194 WriterSerializer serializer; 195 ReaderParser parser; 196 Map<Character,Mod> mods; 197 VarResolver varResolver; 198 int binaryLineLength; 199 BinaryFormat binaryFormat; 200 boolean multiLineValuesOnSeparateLines, readOnly; 201 202 /** 203 * Constructor, default settings. 204 */ 205 protected Builder() { 206 name = env("Config.name", "Configuration.cfg"); 207 store = FileStore.DEFAULT; 208 serializer = Json5Serializer.DEFAULT; 209 parser = JsonParser.DEFAULT; 210 mods = map(); 211 mods(XorEncodeMod.INSTANCE); 212 varResolver = VarResolver.DEFAULT; 213 binaryLineLength = env("Config.binaryLineLength", -1); 214 binaryFormat = env("Config.binaryFormat", BinaryFormat.BASE64); 215 multiLineValuesOnSeparateLines = env("Config.multiLineValuesOnSeparateLines", false); 216 readOnly = env("Config.readOnly", false); 217 } 218 219 /** 220 * Copy constructor. 221 * 222 * @param copyFrom The bean to copy from. 223 */ 224 protected Builder(Config copyFrom) { 225 super(copyFrom); 226 name = copyFrom.name; 227 store = copyFrom.store; 228 serializer = copyFrom.serializer; 229 parser = copyFrom.parser; 230 mods = copyOf(copyFrom.mods); 231 varResolver = copyFrom.varResolver; 232 binaryLineLength = copyFrom.binaryLineLength; 233 binaryFormat = copyFrom.binaryFormat; 234 multiLineValuesOnSeparateLines = copyFrom.multiLineValuesOnSeparateLines; 235 readOnly = copyFrom.readOnly; 236 } 237 238 /** 239 * Copy constructor. 240 * 241 * @param copyFrom The builder to copy from. 242 */ 243 protected Builder(Builder copyFrom) { 244 super(copyFrom); 245 name = copyFrom.name; 246 store = copyFrom.store; 247 serializer = copyFrom.serializer; 248 parser = copyFrom.parser; 249 mods = copyOf(copyFrom.mods); 250 varResolver = copyFrom.varResolver; 251 binaryLineLength = copyFrom.binaryLineLength; 252 binaryFormat = copyFrom.binaryFormat; 253 multiLineValuesOnSeparateLines = copyFrom.multiLineValuesOnSeparateLines; 254 readOnly = copyFrom.readOnly; 255 } 256 257 @Override /* Context.Builder */ 258 public Builder copy() { 259 return new Builder(this); 260 } 261 262 @Override /* Context.Builder */ 263 public Config build() { 264 return build(Config.class); 265 } 266 267 //----------------------------------------------------------------------------------------------------------------- 268 // Properties 269 //----------------------------------------------------------------------------------------------------------------- 270 271 /** 272 * Configuration name. 273 * 274 * <p> 275 * Specifies the configuration name. 276 * <br>This is typically the configuration file name, although 277 * the name can be anything identifiable by the {@link ConfigStore} used for retrieving and storing the configuration. 278 * 279 * @param value 280 * The new value for this property. 281 * <br>The default is the first value found: 282 * <ul> 283 * <li>System property <js>"Config.name" 284 * <li>Environment variable <js>"CONFIG_NAME" 285 * <li><js>"Configuration.cfg"</js> 286 * </ul> 287 * @return This object. 288 */ 289 public Builder name(String value) { 290 name = value; 291 return this; 292 } 293 294 /** 295 * Configuration store. 296 * 297 * <p> 298 * The configuration store used for retrieving and storing configurations. 299 * 300 * @param value 301 * The new value for this property. 302 * <br>The default is {@link FileStore#DEFAULT}. 303 * @return This object. 304 */ 305 public Builder store(ConfigStore value) { 306 store = value; 307 return this; 308 } 309 310 /** 311 * Configuration store. 312 * 313 * <p> 314 * Convenience method for calling <code>store(ConfigMemoryStore.<jsf>DEFAULT</jsf>)</code>. 315 * 316 * @return This object. 317 */ 318 public Builder memStore() { 319 store = MemoryStore.DEFAULT; 320 return this; 321 } 322 323 /** 324 * POJO serializer. 325 * 326 * <p> 327 * The serializer to use for serializing POJO values. 328 * 329 * @param value 330 * The new value for this property. 331 * <br>The default is {@link Json5Serializer#DEFAULT} 332 * @return This object. 333 */ 334 public Builder serializer(WriterSerializer value) { 335 serializer = value; 336 return this; 337 } 338 339 /** 340 * POJO parser. 341 * 342 * <p> 343 * The parser to use for parsing values to POJOs. 344 * 345 * @param value 346 * The new value for this property. 347 * <br>The default is {@link JsonParser#DEFAULT}. 348 * @return This object. 349 */ 350 public Builder parser(ReaderParser value) { 351 parser = value; 352 return this; 353 } 354 355 /** 356 * Adds a value modifier. 357 * 358 * <p> 359 * Modifiers are used to modify entry value before being persisted. 360 * 361 * @param values 362 * The mods to apply to this config. 363 * @return This object. 364 */ 365 public Builder mods(Mod...values) { 366 for (var value : values) 367 mods.put(value.getId(), value); 368 return this; 369 } 370 371 /** 372 * SVL variable resolver. 373 * 374 * <p> 375 * The resolver to use for resolving SVL variables. 376 * 377 * @param value 378 * The new value for this property. 379 * <br>The default is {@link VarResolver#DEFAULT}. 380 * @return This object. 381 */ 382 public Builder varResolver(VarResolver value) { 383 varResolver = value; 384 return this; 385 } 386 387 /** 388 * Binary value line length. 389 * 390 * <p> 391 * When serializing binary values, lines will be split after this many characters. 392 * <br>Use <c>-1</c> to represent no line splitting. 393 * 394 * @param value 395 * The new value for this property. 396 * <br>The default is the first value found: 397 * <ul> 398 * <li>System property <js>"Config.binaryLineLength" 399 * <li>Environment variable <js>"CONFIG_BINARYLINELENGTH" 400 * <li><c>-1</c> 401 * </ul> 402 * @return This object. 403 */ 404 public Builder binaryLineLength(int value) { 405 binaryLineLength = value; 406 return this; 407 } 408 409 /** 410 * Binary value format. 411 * 412 * <p> 413 * The format to use when persisting byte arrays. 414 * 415 * <ul class='values'> 416 * <li>{@link BinaryFormat#BASE64} - BASE64-encoded string. 417 * <li>{@link BinaryFormat#HEX} - Hexadecimal. 418 * <li>{@link BinaryFormat#SPACED_HEX} - Hexadecimal with spaces between bytes. 419 * </ul> 420 * 421 * @param value 422 * The new value for this property. 423 * <br>The default is the first value found: 424 * <ul> 425 * <li>System property <js>"Config.binaryFormat" 426 * <li>Environment variable <js>"CONFIG_BINARYFORMAT" 427 * <li>{@link BinaryFormat#BASE64} 428 * </ul> 429 * @return This object. 430 */ 431 public Builder binaryFormat(BinaryFormat value) { 432 binaryFormat = value; 433 return this; 434 } 435 436 /** 437 * Multi-line values on separate lines. 438 * 439 * <p> 440 * When enabled, multi-line values will always be placed on a separate line from the key. 441 * 442 * <p> 443 * The default is the first value found: 444 * <ul> 445 * <li>System property <js>"Config.multiLineValuesOnSeparateLine" 446 * <li>Environment variable <js>"CONFIG_MULTILINEVALUESONSEPARATELINE" 447 * <li><jk>false</jk> 448 * </ul> 449 * 450 * @return This object. 451 */ 452 public Builder multiLineValuesOnSeparateLines() { 453 multiLineValuesOnSeparateLines = true; 454 return this; 455 } 456 457 /** 458 * Read-only mode. 459 * 460 * <p> 461 * When enabled, attempts to call any setters on this object will throw an {@link UnsupportedOperationException}. 462 * 463 * <p> 464 * The default is the first value found: 465 * <ul> 466 * <li>System property <js>"Config.readOnly" 467 * <li>Environment variable <js>"CONFIG_READONLY" 468 * <li><jk>false</jk> 469 * </ul> 470 * 471 * @return This object. 472 */ 473 public Builder readOnly() { 474 readOnly = true; 475 return this; 476 } 477 @Override /* Overridden from Builder */ 478 public Builder annotations(Annotation...values) { 479 super.annotations(values); 480 return this; 481 } 482 483 @Override /* Overridden from Builder */ 484 public Builder annotations(List<Annotation> values) { 485 super.annotations(values); 486 return this; 487 } 488 489 @Override /* Overridden from Builder */ 490 public Builder apply(AnnotationWorkList work) { 491 super.apply(work); 492 return this; 493 } 494 495 @Override /* Overridden from Builder */ 496 public Builder applyAnnotations(Object...from) { 497 super.applyAnnotations(from); 498 return this; 499 } 500 501 @Override /* Overridden from Builder */ 502 public Builder applyAnnotations(Class<?>...from) { 503 super.applyAnnotations(from); 504 return this; 505 } 506 507 @Override /* Overridden from Builder */ 508 public Builder cache(Cache<HashKey,? extends org.apache.juneau.Context> value) { 509 super.cache(value); 510 return this; 511 } 512 513 @Override /* Overridden from Builder */ 514 public Builder debug() { 515 super.debug(); 516 return this; 517 } 518 519 @Override /* Overridden from Builder */ 520 public Builder debug(boolean value) { 521 super.debug(value); 522 return this; 523 } 524 525 @Override /* Overridden from Builder */ 526 public Builder impl(Context value) { 527 super.impl(value); 528 return this; 529 } 530 531 @Override /* Overridden from Builder */ 532 public Builder type(Class<? extends org.apache.juneau.Context> value) { 533 super.type(value); 534 return this; 535 } 536 } 537 538 //------------------------------------------------------------------------------------------------------------------- 539 // Instance 540 //------------------------------------------------------------------------------------------------------------------- 541 542 final String name; 543 final ConfigStore store; 544 final WriterSerializer serializer; 545 final ReaderParser parser; 546 final Map<Character,Mod> mods; 547 final VarResolver varResolver; 548 final int binaryLineLength; 549 final BinaryFormat binaryFormat; 550 final boolean multiLineValuesOnSeparateLines, readOnly; 551 final BeanSession beanSession; 552 final VarResolverSession varSession; 553 554 private final ConfigMap configMap; 555 private final List<ConfigEventListener> listeners = synced(new LinkedList<>()); 556 557 558 @Override /* Context */ 559 public Builder copy() { 560 return new Builder(this); 561 } 562 563 /** 564 * Constructor. 565 * 566 * @param builder The builder for this object. 567 * @throws IOException Thrown by underlying stream. 568 */ 569 public Config(Builder builder) throws IOException { 570 super(builder); 571 572 name = builder.name; 573 store = builder.store; 574 configMap = store.getMap(name); 575 configMap.register(this); 576 serializer = builder.serializer; 577 parser = builder.parser; 578 beanSession = parser.getBeanContext().getSession(); 579 mods = u(copyOf(builder.mods)); 580 varResolver = builder.varResolver; 581 varSession = varResolver 582 .copy() 583 .vars(ConfigVar.class) 584 .bean(Config.class, this) 585 .build() 586 .createSession(); 587 binaryLineLength = builder.binaryLineLength; 588 binaryFormat = builder.binaryFormat; 589 multiLineValuesOnSeparateLines = builder.multiLineValuesOnSeparateLines; 590 readOnly = builder.readOnly; 591 } 592 593 Config(Config copyFrom, VarResolverSession varSession) { 594 super(copyFrom); 595 name = copyFrom.name; 596 store = copyFrom.store; 597 configMap = copyFrom.configMap; 598 configMap.register(this); 599 serializer = copyFrom.serializer; 600 parser = copyFrom.parser; 601 mods = copyFrom.mods; 602 varResolver = copyFrom.varResolver; 603 this.varSession = varSession; 604 binaryLineLength = copyFrom.binaryLineLength; 605 binaryFormat = copyFrom.binaryFormat; 606 multiLineValuesOnSeparateLines = copyFrom.multiLineValuesOnSeparateLines; 607 readOnly = copyFrom.readOnly; 608 beanSession = copyFrom.beanSession; 609 } 610 611 /** 612 * Creates a copy of this config using the specified var session for resolving variables. 613 * 614 * <p> 615 * This creates a shallow copy of the config but replacing the variable resolver. 616 * 617 * @param varSession The var session used for resolving string variables. 618 * @return A new config object. 619 */ 620 public Config resolving(VarResolverSession varSession) { 621 return new Config(this, varSession); 622 } 623 624 /** 625 * Returns the name associated with this config (usually a file name). 626 * 627 * @return The name associated with this config, or <jk>null</jk> if it has no name. 628 */ 629 public String getName() { 630 return name; 631 } 632 633 //----------------------------------------------------------------------------------------------------------------- 634 // Workhorse getters 635 //----------------------------------------------------------------------------------------------------------------- 636 637 /** 638 * Returns the specified value as a string from the config file. 639 * 640 * <p> 641 * Unlike {@link #getString(String)}, this method doesn't replace SVL variables. 642 * 643 * @param key The key. 644 * @return The value, or <jk>null</jk> if the section or value doesn't exist. 645 */ 646 private String getRaw(String key) { 647 648 var sname = sname(key); 649 var skey = skey(key); 650 651 var ce = configMap.getEntry(sname, skey); 652 653 if (ce == null) 654 return null; 655 656 return removeMods(ce.getModifiers(), ce.getValue()); 657 } 658 659 String applyMods(String mods, String x) { 660 if (mods != null && x != null) 661 for (var i = 0; i < mods.length(); i++) 662 x = getMod(mods.charAt(i)).doApply(x); 663 return x; 664 } 665 666 String removeMods(String mods, String x) { 667 if (mods != null && x != null) 668 for (int i = mods.length()-1; i > -1; i--) 669 x = getMod(mods.charAt(i)).doRemove(x); 670 return x; 671 } 672 673 Mod getMod(char id) { 674 var x = mods.get(id); 675 return x == null ? Mod.NO_OP : x; 676 } 677 678 //----------------------------------------------------------------------------------------------------------------- 679 // Utility methods 680 //----------------------------------------------------------------------------------------------------------------- 681 682 /** 683 * Takes the settings defined in this configuration and sets them as system properties. 684 * 685 * @return This object. 686 */ 687 public Config setSystemProperties() { 688 for (var section : getSectionNames()) { 689 for (var key : getKeys(section)) { 690 var k = (section.isEmpty() ? key : section + '/' + key); 691 System.setProperty(k, getRaw(k)); 692 } 693 } 694 return this; 695 } 696 697 //----------------------------------------------------------------------------------------------------------------- 698 // Workhorse setters 699 //----------------------------------------------------------------------------------------------------------------- 700 701 /** 702 * Sets a value in this config. 703 * 704 * @param key The key. 705 * @param value The value. 706 * @return This object. 707 * @throws UnsupportedOperationException If configuration is read only. 708 */ 709 public Config set(String key, String value) { 710 checkWrite(); 711 Utils.assertArgNotNull("key", key); 712 var sname = sname(key); 713 var skey = skey(key); 714 715 var ce = configMap.getEntry(sname, skey); 716 if (ce == null && value == null) 717 return this; 718 719 var s = applyMods(ce == null ? null : ce.getModifiers(), s(value)); 720 721 configMap.setEntry(sname, skey, s, null, null, null); 722 return this; 723 } 724 725 /** 726 * Adds or replaces an entry with the specified key with a POJO serialized to a string using the registered 727 * serializer. 728 * 729 * <p> 730 * Equivalent to calling <c>put(key, value, isEncoded(key))</c>. 731 * 732 * @param key The key. 733 * @param value The new value POJO. 734 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist. 735 * @throws SerializeException 736 * If serializer could not serialize the value or if a serializer is not registered with this config file. 737 * @throws UnsupportedOperationException If configuration is read only. 738 */ 739 public Config set(String key, Object value) throws SerializeException { 740 return set(key, value, null); 741 } 742 743 /** 744 * Same as {@link #set(String, Object)} but allows you to specify the serializer to use to serialize the 745 * value. 746 * 747 * @param key The key. 748 * @param value The new value. 749 * @param serializer 750 * The serializer to use for serializing the object. 751 * If <jk>null</jk>, then uses the predefined serializer on the config file. 752 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist. 753 * @throws SerializeException 754 * If serializer could not serialize the value or if a serializer is not registered with this config file. 755 * @throws UnsupportedOperationException If configuration is read only. 756 */ 757 public Config set(String key, Object value, Serializer serializer) throws SerializeException { 758 return set(key, serialize(value, serializer)); 759 } 760 761 /** 762 * Same as {@link #set(String, Object)} but allows you to specify all aspects of a value. 763 * 764 * @param key The key. 765 * @param value The new value. 766 * @param serializer 767 * The serializer to use for serializing the object. 768 * If <jk>null</jk>, then uses the predefined serializer on the config file. 769 * @param modifiers 770 * Optional modifiers to apply to the value. 771 * <br>If <jk>null</jk>, then previous value will not be replaced. 772 * @param comment 773 * Optional same-line comment to add to this value. 774 * <br>If <jk>null</jk>, then previous value will not be replaced. 775 * @param preLines 776 * Optional comment or blank lines to add before this entry. 777 * <br>If <jk>null</jk>, then previous value will not be replaced. 778 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist. 779 * @throws SerializeException 780 * If serializer could not serialize the value or if a serializer is not registered with this config file. 781 * @throws UnsupportedOperationException If configuration is read only. 782 */ 783 public Config set(String key, Object value, Serializer serializer, String modifiers, String comment, List<String> preLines) throws SerializeException { 784 checkWrite(); 785 Utils.assertArgNotNull("key", key); 786 var sname = sname(key); 787 var skey = skey(key); 788 modifiers = Utils.nullIfEmpty(modifiers); 789 790 var s = applyMods(modifiers, serialize(value, serializer)); 791 792 configMap.setEntry(sname, skey, s, modifiers, comment, preLines); 793 return this; 794 } 795 796 /** 797 * Removes an entry with the specified key. 798 * 799 * @param key The key. 800 * @return The previous value, or <jk>null</jk> if the section or key did not previously exist. 801 * @throws UnsupportedOperationException If configuration is read only. 802 */ 803 public Config remove(String key) { 804 checkWrite(); 805 var sname = sname(key); 806 var skey = skey(key); 807 configMap.removeEntry(sname, skey); 808 return this; 809 } 810 811 /** 812 * Encodes and unencoded entries in this config. 813 * 814 * <p> 815 * If any entries in the config are marked as encoded but not actually encoded, 816 * this will encode them. 817 * 818 * @return This object. 819 * @throws UnsupportedOperationException If configuration is read only. 820 */ 821 public Config applyMods() { 822 checkWrite(); 823 for (var section : configMap.getSections()) { 824 for (var key : configMap.getKeys(section)) { 825 var ce = configMap.getEntry(section, key); 826 if (ce.getModifiers() != null) { 827 var mods2 = ce.getModifiers(); 828 var value = ce.getValue(); 829 for (var i = 0; i < mods2.length(); i++) { 830 var mod = getMod(mods2.charAt(i)); 831 if (! mod.isApplied(value)) { 832 configMap.setEntry(section, key, mod.apply(value), null, null, null); 833 } 834 } 835 } 836 } 837 } 838 839 return this; 840 } 841 842 //----------------------------------------------------------------------------------------------------------------- 843 // API methods 844 //----------------------------------------------------------------------------------------------------------------- 845 846 /** 847 * Gets the entry with the specified key. 848 * 849 * <p> 850 * The key can be in one of the following formats... 851 * <ul class='spaced-list'> 852 * <li> 853 * <js>"key"</js> - A value in the default section (i.e. defined above any <c>[section]</c> header). 854 * <li> 855 * <js>"section/key"</js> - A value from the specified section. 856 * </ul> 857 * 858 * <p> 859 * If entry does not exist, returns an empty {@link Entry} object. 860 * 861 * @param key The key. 862 * @return The entry bean, never <jk>null</jk>. 863 */ 864 public Entry get(String key) { 865 return new Entry(this, configMap, sname(key), skey(key)); 866 } 867 868 /** 869 * Gets the entry with the specified key. 870 * 871 * <p> 872 * The key can be in one of the following formats... 873 * <ul class='spaced-list'> 874 * <li> 875 * <js>"key"</js> - A value in the default section (i.e. defined above any <c>[section]</c> header). 876 * <li> 877 * <js>"section/key"</js> - A value from the specified section. 878 * </ul> 879 * 880 * <p> 881 * If entry does not exist, returns <jk>null</jk>. 882 * 883 * <h5 class='section'>Notes:</h5><ul> 884 * <li class='note'>This method is equivalent to calling <c>get(<jv>key</jv>).orElse(<jk>null</jk>);</c>. 885 * </ul> 886 * 887 * @param key The key. 888 * @return The entry value, or <jk>null</jk> if it doesn't exist. 889 */ 890 public String getString(String key) { 891 return new Entry(this, configMap, sname(key), skey(key)).orElse(null); 892 } 893 894 /** 895 * Gets the section with the specified name. 896 * 897 * <p> 898 * If section does not exist, returns an empty {@link Section} object. 899 * 900 * @param name The section name. <jk>null</jk> and blank refer to the default section. 901 * @return The section bean, never <jk>null</jk>. 902 */ 903 public Section getSection(String name) { 904 return new Section(this, configMap, Utils.emptyIfNull(name)); 905 } 906 907 /** 908 * Returns the keys of the entries in the specified section. 909 * 910 * @param section 911 * The section name to write from. 912 * <br>If empty, refers to the default section. 913 * <br>Must not be <jk>null</jk>. 914 * @return 915 * An unmodifiable set of keys, or an empty set if the section doesn't exist. 916 */ 917 public Set<String> getKeys(String section) { 918 return configMap.getKeys(section(section)); 919 } 920 921 /** 922 * Returns the section names defined in this config. 923 * 924 * @return The section names defined in this config. 925 */ 926 public Set<String> getSectionNames() { 927 return u(configMap.getSections()); 928 } 929 930 /** 931 * Returns <jk>true</jk> if this section contains the specified key and the key has a non-blank value. 932 * 933 * @param key The key. 934 * @return <jk>true</jk> if this section contains the specified key and the key has a non-blank value. 935 */ 936 public boolean exists(String key) { 937 return Utils.isNotEmpty(get(key).as(String.class).orElse(null)); 938 } 939 940 /** 941 * Creates the specified section if it doesn't exist. 942 * 943 * <p> 944 * Returns the existing section if it already exists. 945 * 946 * @param name 947 * The section name. 948 * <br>Must not be <jk>null</jk>. 949 * <br>Use blank for the default section. 950 * @param preLines 951 * Optional comment and blank lines to add immediately before the section. 952 * <br>If <jk>null</jk>, previous pre-lines will not be replaced. 953 * @return The appended or existing section. 954 * @throws UnsupportedOperationException If configuration is read only. 955 */ 956 public Config setSection(String name, List<String> preLines) { 957 try { 958 return setSection(section(name), preLines, null); 959 } catch (SerializeException e) { 960 throw asRuntimeException(e); // Impossible. 961 } 962 } 963 964 /** 965 * Creates the specified section if it doesn't exist. 966 * 967 * @param name 968 * The section name. 969 * <br>Must not be <jk>null</jk>. 970 * <br>Use blank for the default section. 971 * @param preLines 972 * Optional comment and blank lines to add immediately before the section. 973 * <br>If <jk>null</jk>, previous pre-lines will not be replaced. 974 * @param contents 975 * Values to set in the new section. 976 * <br>Can be <jk>null</jk>. 977 * @return The appended or existing section. 978 * @throws SerializeException Contents could not be serialized. 979 * @throws UnsupportedOperationException If configuration is read only. 980 */ 981 public Config setSection(String name, List<String> preLines, Map<String,Object> contents) throws SerializeException { 982 checkWrite(); 983 configMap.setSection(section(name), preLines); 984 985 if (contents != null) 986 for (var e : contents.entrySet()) 987 set(section(name) + '/' + e.getKey(), e.getValue()); 988 989 return this; 990 } 991 992 /** 993 * Removes the section with the specified name. 994 * 995 * @param name The name of the section to remove 996 * @return This object. 997 * @throws UnsupportedOperationException If configuration is read only. 998 */ 999 public Config removeSection(String name) { 1000 checkWrite(); 1001 configMap.removeSection(name); 1002 return this; 1003 } 1004 1005 /** 1006 * Creates the specified import statement if it doesn't exist. 1007 * 1008 * @param sectionName 1009 * The section name where to place the import statement. 1010 * <br>Must not be <jk>null</jk>. 1011 * <br>Use blank for the default section. 1012 * @param importName 1013 * The import name. 1014 * <br>Must not be <jk>null</jk>. 1015 * @param preLines 1016 * Optional comment and blank lines to add immediately before the import statement. 1017 * <br>If <jk>null</jk>, previous pre-lines will not be replaced. 1018 * @return The appended or existing import statement. 1019 * @throws UnsupportedOperationException If configuration is read only. 1020 */ 1021 public Config setImport(String sectionName, String importName, List<String> preLines) { 1022 checkWrite(); 1023 configMap.setImport(section(name), importName, preLines); 1024 return this; 1025 } 1026 1027 /** 1028 * Removes the import statement with the specified name from the specified section. 1029 * 1030 * @param sectionName 1031 * The section name where to place the import statement. 1032 * <br>Must not be <jk>null</jk>. 1033 * <br>Use blank for the default section. 1034 * @param importName 1035 * The import name. 1036 * <br>Must not be <jk>null</jk>. 1037 * @return This object. 1038 * @throws UnsupportedOperationException If configuration is read only. 1039 */ 1040 public Config removeImport(String sectionName, String importName) { 1041 checkWrite(); 1042 configMap.removeImport(sectionName, importName); 1043 return this; 1044 } 1045 1046 /** 1047 * Loads the contents of the specified map of maps into this config. 1048 * 1049 * @param m The maps to load. 1050 * @return This object. 1051 * @throws SerializeException Value could not be serialized. 1052 */ 1053 public Config load(Map<String,Map<String,Object>> m) throws SerializeException { 1054 if (m != null) 1055 for (var e : m.entrySet()) { 1056 setSection(e.getKey(), null, e.getValue()); 1057 } 1058 return this; 1059 } 1060 1061 /** 1062 * Commit the changes in this config to the store. 1063 * 1064 * @return This object. 1065 * @throws IOException Thrown by underlying stream. 1066 * @throws UnsupportedOperationException If configuration is read only. 1067 */ 1068 public Config commit() throws IOException { 1069 checkWrite(); 1070 configMap.commit(); 1071 return this; 1072 } 1073 1074 /** 1075 * Saves this config file to the specified writer as an INI file. 1076 * 1077 * <p> 1078 * The writer will automatically be closed. 1079 * 1080 * @param w The writer to send the output to. 1081 * @return This object. 1082 * @throws IOException If a problem occurred trying to send contents to the writer. 1083 */ 1084 public Writer writeTo(Writer w) throws IOException { 1085 return configMap.writeTo(w); 1086 } 1087 1088 /** 1089 * Add a listener to this config to react to modification events. 1090 * 1091 * <p> 1092 * Listeners should be removed using {@link #removeListener(ConfigEventListener)}. 1093 * 1094 * @param listener The new listener to add. 1095 * @return This object. 1096 */ 1097 public synchronized Config addListener(ConfigEventListener listener) { 1098 listeners.add(listener); 1099 return this; 1100 } 1101 1102 /** 1103 * Removes a listener from this config. 1104 * 1105 * @param listener The listener to remove. 1106 * @return This object. 1107 */ 1108 public synchronized Config removeListener(ConfigEventListener listener) { 1109 listeners.remove(listener); 1110 return this; 1111 } 1112 1113 /** 1114 * Closes this configuration object by unregistering it from the underlying config map. 1115 */ 1116 public void close() { 1117 configMap.unregister(this); 1118 } 1119 1120 /** 1121 * Overwrites the contents of the config file. 1122 * 1123 * @param contents The new contents of the config file. 1124 * @param synchronous Wait until the change has been persisted before returning this map. 1125 * @return This object. 1126 * @throws IOException Thrown by underlying stream. 1127 * @throws InterruptedException Thread was interrupted. 1128 * @throws UnsupportedOperationException If configuration is read only. 1129 */ 1130 public Config load(Reader contents, boolean synchronous) throws IOException, InterruptedException { 1131 checkWrite(); 1132 configMap.load(read(contents), synchronous); 1133 return this; 1134 } 1135 1136 /** 1137 * Overwrites the contents of the config file. 1138 * 1139 * @param contents The new contents of the config file. 1140 * @param synchronous Wait until the change has been persisted before returning this map. 1141 * @return This object. 1142 * @throws IOException Thrown by underlying stream. 1143 * @throws InterruptedException Thread was interrupted. 1144 * @throws UnsupportedOperationException If configuration is read only. 1145 */ 1146 public Config load(String contents, boolean synchronous) throws IOException, InterruptedException { 1147 checkWrite(); 1148 configMap.load(contents, synchronous); 1149 return this; 1150 } 1151 1152 /** 1153 * Does a rollback of any changes on this config currently in memory. 1154 * 1155 * @return This object. 1156 * @throws UnsupportedOperationException If configuration is read only. 1157 */ 1158 public Config rollback() { 1159 checkWrite(); 1160 configMap.rollback(); 1161 return this; 1162 } 1163 1164 /** 1165 * Returns the contents of this config as a simple map. 1166 * 1167 * @return The contents of this config as a simple map. 1168 */ 1169 public JsonMap toMap() { 1170 return configMap.asMap(); 1171 } 1172 1173 //----------------------------------------------------------------------------------------------------------------- 1174 // Test methods 1175 //----------------------------------------------------------------------------------------------------------------- 1176 1177 ConfigMap getConfigMap() { 1178 return configMap; 1179 } 1180 1181 List<ConfigEventListener> getListeners() { 1182 return u(listeners); 1183 } 1184 1185 //----------------------------------------------------------------------------------------------------------------- 1186 // Interface methods 1187 //----------------------------------------------------------------------------------------------------------------- 1188 1189 @Override /* ConfigEventListener */ 1190 public synchronized void onConfigChange(ConfigEvents events) { 1191 listeners.forEach(x -> x.onConfigChange(events)); 1192 } 1193 1194 //----------------------------------------------------------------------------------------------------------------- 1195 // Private methods 1196 //----------------------------------------------------------------------------------------------------------------- 1197 1198 private String serialize(Object value, Serializer serializer) throws SerializeException { 1199 if (value == null) 1200 return ""; 1201 if (serializer == null) 1202 serializer = this.serializer; 1203 var c = value.getClass(); 1204 if (value instanceof CharSequence cs) 1205 return nlIfMl(cs); 1206 if (isSimpleType(c)) 1207 return value.toString(); 1208 1209 if (value instanceof byte[] b) { 1210 String s = null; 1211 if (binaryFormat == BinaryFormat.HEX) 1212 s = toHex(b); 1213 else if (binaryFormat == BinaryFormat.SPACED_HEX) 1214 s = toSpacedHex(b); 1215 else 1216 s = base64Encode(b); 1217 var l = binaryLineLength; 1218 if (l <= 0 || s.length() <= l) 1219 return s; 1220 var sb = new StringBuilder(); 1221 for (var i = 0; i < s.length(); i += l) 1222 sb.append(binaryLineLength > 0 ? "\n" : "").append(s.substring(i, Math.min(s.length(), i + l))); 1223 return sb.toString(); 1224 } 1225 1226 String r = null; 1227 if (multiLineValuesOnSeparateLines) 1228 r = "\n" + (String)serializer.serialize(value); 1229 else 1230 r = (String)serializer.serialize(value); 1231 1232 if (r.startsWith("'")) 1233 return r.substring(1, r.length()-1); 1234 return r; 1235 } 1236 1237 private String nlIfMl(CharSequence cs) { 1238 var s = cs.toString(); 1239 if (s.indexOf('\n') != -1 && multiLineValuesOnSeparateLines) 1240 return "\n" + s; 1241 return s; 1242 } 1243 1244 private boolean isSimpleType(Type t) { 1245 if (! (t instanceof Class)) 1246 return false; 1247 var c = (Class<?>)t; 1248 return (c == String.class || c.isPrimitive() || c.isAssignableFrom(Number.class) || c == Boolean.class || c.isEnum()); 1249 } 1250 1251 private String sname(String key) { 1252 Utils.assertArgNotNull("key", key); 1253 var i = key.indexOf('/'); 1254 if (i == -1) 1255 return ""; 1256 return key.substring(0, i); 1257 } 1258 1259 private String skey(String key) { 1260 var i = key.indexOf('/'); 1261 if (i == -1) 1262 return key; 1263 return key.substring(i+1); 1264 } 1265 1266 private String section(String section) { 1267 Utils.assertArgNotNull("section", section); 1268 if (Utils.isEmpty(section)) 1269 return ""; 1270 return section; 1271 } 1272 1273 void checkWrite() { 1274 if (readOnly) 1275 throw new UnsupportedOperationException("Cannot call this method on a read-only configuration."); 1276 } 1277 1278 //----------------------------------------------------------------------------------------------------------------- 1279 // Other methods 1280 //----------------------------------------------------------------------------------------------------------------- 1281 1282 @Override /* Object */ 1283 public String toString() { 1284 return configMap.toString(); 1285 } 1286}