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