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