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