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