LogProperties.java
package io.jstach.rainbowgum;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.lang.System.Logger.Level;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
import io.jstach.rainbowgum.LogAppender.AppenderFlag;
import io.jstach.rainbowgum.LogConfig.ChangePublisher.ChangeType;
import io.jstach.rainbowgum.LogProperties.Builder.AbstractLogProperties;
import io.jstach.rainbowgum.LogProperties.MutableLogProperties;
import io.jstach.rainbowgum.annotation.CaseChanging;
import io.jstach.rainbowgum.annotation.LogConfigurable;
/**
* Provides String based properties like {@link System#getProperty(String)} for default
* configuration of logging levels and output. Since Rainbow Gum is configuration agnostic
* LogProperties is a simple string key and string value interface to allow almost any
* configuration implementation. The only required method to implement is
* {@link #valueOrNull(String)} and thus LogProperties is a functional like interface but
* using {@link LogProperties#builder()} is preferred so that a
* {@linkplain #description(String) description} can be added.
* <p>
* If a custom {@link LogProperties} is not configured the default RainbowGum uses System
* properties.
* <p>
* The built-in properties to configure RainbowGum are modeled after <a href=
* "https://docs.spring.io/spring-boot/docs/3.1.0/reference/html/features.html#features.logging">
* Spring Boot logging </a>
* <p>
* LogProperties should generally be accessed with {@link #forKey(String)} (see
* {@link LogProperty}) which will allow safe <em>programmatic</em> data conversion of the
* flat key values and useful error messages to users if the property cannot be mapped or
* is missing. For more <em>declarative</em> injection of properties see
* {@link LogConfigurable} which will generate builders that can use LogProperties.
* <p>
* Rainbow Gum treats {@value #SEP} in the keys as special separator analogous to
* JavaScript/JSON and various other configuration systems. Furthermore to work with a
* wide set of configuration systems once a property key has a value it is
* <strong>RECOMMENDED</strong> that there should not be other property key value with a
* key that is a prefix of the previously mentioned key. For example allowing two
* properties <code>logging.example=stuff</code>, <code>logging.example.a=A</code> is not
* recommended as <code>logging.example</code> is defined as a leaf. The exception to this
* rule is logging levels mapped to logger names for consistency with Spring Boot.
* <p>
* Rainbow Gum out of the the box supports parsing two formats into properties:
* {@link Properties} text, and {@link URI#getRawQuery()} percent encoded query
* parameters. The two different formats have different behavior for
* {@link #listOrNull(String)} and {@link #mapOrNull(String)}. If a custom format of
* LogProperties is created those two methods may need to be extended.
* <p>
* A convention used through out Rainbow Gum is to have the property names as constants as
* it makes documentation easier. These constants name are suffixed with
* <code>_PROPERTY</code> or <code>_PREFIX</code> the former being a fully qualified key
* that maybe parameterized and the latter being a partial key ending with
* "<code>.</code>". The fully qualified keys are like leaves on a tree.
* <p>
* Property keys can be parameterized by using
* {@link LogProperties#interpolateKey(String, Map)} and a general pattern is
* <code>logging.component.{name}.prop</code> where {@value #NAME} is a parameter. Many
* property constants use this to allow multiple configuration of components by name.
*
* <table class="table">
* <caption><strong>Built-in Property Patterns</strong></caption>
* <tr>
* <th>Property Pattern</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>{@value #LEVEL_PREFIX} + {@value #SEP} + logger = LEVEL</td>
* <td>Sets the level for the logger. If logger name is missing then it is considered the
* root logger. {@link Level#INFO} is the default level for the root logger.</td>
* </tr>
* <tr>
* <td>{@value #CHANGE_PREFIX} + {@value #SEP} + logger = boolean |
* List<{@link ChangeType}></td>
* <td>Allows runtime changing of levels for the logger. By design RainbowGum does not
* allow loggers to change levels once initialized. This configuration will allow the
* level to be changed for the logger and its children and by default is false as the
* built-in {@link LogProperties} is generally static (system properties).</td>
* </tr>
* <tr>
* <td>{@value #ROUTES_PROPERTY} = <code>List<String></code></td>
* <td>A comma separated list of routes that should be enabled. If not set
* {@value LogRouter.Router#DEFAULT_ROUTER_NAME } is the default.</td>
* </tr>
* <tr>
* <td>{@value #ROUTE_APPENDERS_PROPERTY} = <code>List<String></code></td>
* <td>A comma separated list of appenders associated with the route. If not set
* {@value #APPENDERS_PROPERTY} is used.</td>
* </tr>
* <tr>
* <td>{@value #ROUTE_PUBLISHER_PROPERTY} = <code>URI</code></td>
* <td>Looks up a publisher by URI scheme using the {@link LogPublisherRegistry}. The
* publisher is then configured by the other properties starting with
* {@value #PUBLISHER_PREFIX}.</td>
* </tr>
* <tr>
* <td>{@value #PUBLISHER_PREFIX} + <code>scheme</code> + <code>.</code> +
* <code>propertyName</code></td>
* <td>Configures the route's publisher of the type selected by the URI scheme, for
* example <code>logging.route.default.publisher.async.bufferSize</code>. The scheme is
* the one written in the URI, so <code>async</code> settings apply to whichever publisher
* is registered as <code>async</code>.</td>
* </tr>
* <tr>
* <td>{@value #APPENDERS_PROPERTY} = <code>List<String></code></td>
* <td>A comma separated list of appenders that should be enabled. If not set
* {@value LogAppender#CONSOLE_APPENDER_NAME} is the default.</td>
* </tr>
* <tr>
* <td>{@value #APPENDER_OUTPUT_PROPERTY } = <code>URI</code></td>
* <td>Looks up an output by URI scheme using the {@link LogOutputRegistry}. The output is
* then configured by the other properties starting with {@value #OUTPUT_PREFIX}.</td>
* </tr>
* <tr>
* <td>{@value #APPENDER_ENCODER_PROPERTY } = <code>URI</code></td>
* <td>Looks up an encoder by URI scheme using the {@link LogEncoderRegistry}. The encoder
* is then configured by the other properties starting with {@value #ENCODER_PREFIX}.</td>
* </tr>
* <tr>
* <td>{@value #OUTPUT_PREFIX} + <code>scheme</code> + <code>.</code> +
* <code>propertyName</code></td>
* <td>Configures the appender's output of the type selected by the URI scheme, for
* example <code>logging.appender.file.output.file.bufferSize</code>. An output built
* without an appender uses its own name in place of the appender's.</td>
* </tr>
* <tr>
* <td>{@value #ENCODER_PREFIX} + <code>scheme</code> + <code>.</code> +
* <code>propertyName</code></td>
* <td>Configures the appender's encoder of the type selected by the URI scheme, for
* example <code>logging.appender.console.encoder.json5.keyValues</code>. Each type has
* its own keys, so switching an appender's encoder leaves the other type's settings
* unread rather than misread. An encoder built without an appender uses its own name in
* place of the appender's.</td>
* </tr>
* <tr>
* <td>{@value #FILE_PROPERTY} = URI</td>
* <td>A URI or file path to log to a file. This is for Spring Boot compatibility.</td>
* </tr>
* </table>
*
* @see LogConfigurable
*/
public interface LogProperties {
/**
* properties separator.
*/
static final String SEP = ".";
/**
* Logging properties prefix.
*/
static final String ROOT_PREFIX = "logging" + SEP;
/**
* Controls direct fail-safe bootstrap diagnostics. {@code OFF} (also {@code false})
* is the default; {@code ERROR} dumps on build failure, while {@code INFO} also dumps
* after successful config builds. {@code ALL} (also {@code true}) additionally prints
* a {@link LogReporter} report after {@link RainbowGum#start()} starts the logging
* components. Read from system properties before properties providers run, so it
* remains available when loading configuration fails. The system property overrides
* {@link LogConfig.Builder#debug(LogConfig.DebugModeType)}.
*/
static final String DEBUG_PROPERTY = ROOT_PREFIX + "debug";
/**
* Logging level properties prefix. The value should be the name of a
* {@linkplain java.lang.System.Logger.Level level}.
*/
static final String LEVEL_PREFIX = ROOT_PREFIX + "level";
/**
* Enabled level groups. The value should be a list of group names used on
* {@link #GROUP_PROPERTY}. This is to enable <a href=
* "https://docs.spring.io/spring-boot/3.3.2/reference/features/logging.html#features.logging.log-groups">
* Spring Boot like Log Groups </a>. <em>Note that unlike Spring Boot groups are not
* enabled unless this property contains the group.</em>
*/
static final String GROUPS_PROPERTY = ROOT_PREFIX + "groups";
/**
* Group property with list of logger names. The level of the group is set with
* <code>{@value #LEVEL_PREFIX} + .{name} = Level</code> or on the router itself with
* <code>{@value #ROUTE_LEVEL_PREFIX} + .{name} = Level</code>. This is largely
* compatible with <a href=
* "https://docs.spring.io/spring-boot/3.3.2/reference/features/logging.html#features.logging.log-groups">
* Spring Boot Log Groups </a>.
* @see LevelResolver#parseLevel(String)
*/
static final String GROUP_PROPERTY = ROOT_PREFIX + "group.{name}";
/**
* Logging change configuration allowed property.
*/
static final String GLOBAL_CHANGE_PROPERTY = ROOT_PREFIX + "global.change";
/**
* The level threshold of events captured before Rainbow Gum is fully bound. Before
* Rainbow Gum is bound events are queued. By default events that are below
* <code>INFO</code> are not queued.
*/
static final String GLOBAL_QUEUE_LEVEL_PROPERTY = ROOT_PREFIX + "global.queue.level";
/**
* The level threshold of queued events reported to standard error before Rainbow Gum
* is bound. Defaults to <code>ERROR</code>.
*/
static final String GLOBAL_QUEUE_ERROR_PROPERTY = ROOT_PREFIX + "global.queue.error";
/**
* Will globally turn of any ANSI escape output as well disable extensions that do
* things for ANSI escape like JANSI. If not set this is derived automatically from
* {@link io.jstach.rainbowgum.format.AnsiSupport#isAnsiSupported()} which checks
* whether the process is attached to an ANSI capable terminal.
*/
static final String GLOBAL_ANSI_DISABLE_PROPERTY = ROOT_PREFIX + "global.ansi.disable";
/**
* If true guarantees no appender will use {@code synchronized} for its locking,
* regardless of an explicitly requested
* {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER}: an explicit
* request for that type is downgraded to
* {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} instead of being honored.
* Intended for deployments that want a hard guarantee independent of per-appender
* configuration - e.g. still running JDK versions before
* <a href="https://openjdk.org/jeps/491">JEP 491</a> (JDK 24), where
* {@code synchronized} pins the carrier platform thread when called from a virtual
* thread, and that would rather not rely on per-appender configuration to avoid it.
*/
static final String GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY = ROOT_PREFIX + "global.appender.reentrantLock";
/**
* A global, process-wide guarantee of no {@link ThreadLocal} anywhere in the logging
* path, for deployments that want that guaranteed independent of
* per-appender/per-module configuration. When active:
* <ul>
* <li>Any appender that would otherwise use
* {@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER} or
* {@link LogAppender.AppenderType#SYNCHRONIZED_THREAD_LOCAL_BUFFER} (the default,
* plus its {@code synchronized}-locking sibling - both {@link ThreadLocal}-backed) is
* downgraded to {@link LogAppender.AppenderType#LOCK_NEW_BUFFER} instead - an
* explicit request for {@link LogAppender.AppenderType#REUSE_BUFFER} (already
* {@link ThreadLocal}-free) is left as-is rather than also forced to
* {@code LOCK_NEW_BUFFER}.</li>
* <li>{@code rainbowgum-slf4j}'s MDC support is disabled, the same as an explicit
* {@code logging.mdc.type=NOOP} - see {@code RainbowGumSLF4JServiceProvider}.</li>
* </ul>
* @apiNote unlike {@link #GLOBAL_APPENDER_REENTRANT_LOCK_PROPERTY} this reaches
* beyond {@code core} into {@code rainbowgum-slf4j} - both independently read this
* same property key rather than one module exposing a shared static flag the other
* consumes, since there is no other cross-module wiring between them today.
*/
static final String GLOBAL_THREADLOCAL_DISABLED_PROPERTY = ROOT_PREFIX + "global.threadlocalDisabled";
/**
* Opts every appender/encoder that has not been given an explicit choice into
* whatever this release currently believes is the best buffering/locking strategy for
* the platform the process is actually running on, instead of the fixed defaults
* ({@link LogAppender.AppenderType#LOCK_THREAD_LOCAL_BUFFER}, a
* {@link java.nio.charset.CharsetEncoder}-based encode) used otherwise. An explicit
* choice - programmatic ({@code appenderType(...)}, {@code useGetBytes(...)}) or via
* a more specific property ({@link LogAppender#APPENDER_TYPE_PROPERTY}) - always wins
* over this property; it only ever fills in what would otherwise be unset.
* <p>
* <strong>What "best" resolves to today, and why that is not a contract:</strong>
* currently this only has an effect under GraalVM native-image (detected the same way
* {@link LogAppender.AppenderType#AUTO_DETECT} is - see its javadoc), where it sets
* the default appender type to {@code AUTO_DETECT} (this benchmark's own measurements
* found {@code SYNCHRONIZED_THREAD_LOCAL_BUFFER} a real, repeatable win over the
* plain-JVM default there) and defaults text encoders to
* {@code String.getBytes(Charset)}-based encoding over the default
* {@code CharsetEncoder}-based one. On a plain JVM this property currently changes
* nothing at all - the measurements that motivated the native-image behavior above
* pointed the other way on HotSpot. None of this is a promise: as this project
* benchmarks more platforms, JDK versions, and encode strategies, what "optimize"
* resolves to on a given platform - including whether it does anything on a plain JVM
* at all - can and will change between releases without notice. Pin an explicit
* appender type/encoder strategy instead if a deployment needs a specific one to
* always be used.
*/
static final String GLOBAL_OPTIMIZE_PROPERTY = ROOT_PREFIX + "global.optimize";
/**
* Key values namespace, for properties that configure where an event's key values
* come from.
*/
static final String KEY_VALUES_PREFIX = ROOT_PREFIX + "keyvalues" + SEP;
/**
* A comma separated list of key value sources (the lowercase names of
* {@link LogEventFactory.KeyValuesContributor.Source.Standard}, for example
* <code>defaults,slf4j</code>) whose contributors are left out of every event's key
* values. A logging facade's own context store is still used for events logged
* through that facade, and {@link LogEventFactory.KeyValuesContributor#clear()} still
* clears it according to
* {@link LogEventFactory.KeyValuesContributor.Source.Standard#clearWhenDisabled()}.
*/
static final String KEY_VALUES_DISABLED_PROPERTY = KEY_VALUES_PREFIX + "disabled";
/**
* Maximum number of alerts {@link LogAlerts} holds in its ring buffer. Read once,
* from whatever {@link LogProperties} {@link LogConfig.Builder#build()} has already
* resolved by the time it constructs {@link LogAlerts} - before any
* {@link io.jstach.rainbowgum.spi.RainbowGumServiceProvider.Configurator} runs.
* @see LogAlerts#DEFAULT_CAPACITY
*/
static final String ALERTS_CAPACITY_PROPERTY = ROOT_PREFIX + "alerts.capacity";
/**
* What {@link LogAlerts} does, once on {@link LogLifecycle#start(LogConfig) start},
* if it has recorded at least one error alert and still has zero
* {@link LogAlerts.Listener}s registered - see
* {@link LogAlerts.UnobservedErrorsAction} for the three choices and
* {@link LogAlerts.UnobservedErrorsAction#DUMP} for the default.
*/
static final String ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY = ROOT_PREFIX + "alerts.unobservedErrorsAction";
/**
* Alert level at which starting Rainbow Gum fails, like treating compiler warnings as
* errors: see {@link LogAlerts.FailLevel}. Unlike
* {@value #ALERTS_UNOBSERVED_ERRORS_ACTION_PROPERTY} it fails whether or not a
* {@link LogAlerts.Listener} is registered.
*/
static final String ALERTS_FAIL_PROPERTY = ROOT_PREFIX + "alerts.fail";
/**
* Alert level for property keys that were set but not read while starting: see
* {@link LogAlerts.UnusedKeyLevel}.
*/
static final String ALERTS_UNUSED_KEY_PROPERTY = ROOT_PREFIX + "alerts.unusedKey";
/**
* Logging change properties prefix.
*/
static final String CHANGE_PREFIX = ROOT_PREFIX + "change";
/**
* Logging caller info properties prefix. Independent of {@link #CHANGE_PREFIX} -
* resolved through its own logger-name hierarchy walk, so a value here can never be
* silently overridden by a more specific {@link #CHANGE_PREFIX} entry or vice versa.
*/
static final String CALLER_PREFIX = ROOT_PREFIX + "caller";
/**
* Logging file property for default single file appending. The value should be a URI.
*/
static final String FILE_PROPERTY = ROOT_PREFIX + "file.name";
/**
* A common key parameter is called name.
*/
static final String NAME = "name";
/**
* For properties keys that are parameterized with {@value #NAME} the name often used
* if not specified is: {@value #DEFAULT_NAME}.
*/
static final String DEFAULT_NAME = "default";
/**
* Enabled Logging appenders. The value should be a list of names. This is equivalent
* to {@value #ROUTE_APPENDERS_PROPERTY} with name "default".
*/
static final String APPENDERS_PROPERTY = ROOT_PREFIX + "appenders";
/**
* Logging appender prefix for configuration.
*/
static final String APPENDER_PREFIX = ROOT_PREFIX + "appender.{" + NAME + "}.";
/**
* Prefix of the properties configuring an appender's output, followed by the output's
* URI scheme, for example <code>logging.appender.{name}.output.file.</code>. Also
* used for an output built without an appender.
*/
static final String OUTPUT_PREFIX = APPENDER_PREFIX + "output.";
/**
* Prefix of the properties configuring an appender's encoder, followed by the
* encoder's URI scheme, for example
* <code>logging.appender.{name}.encoder.json5.</code>. Also used for an encoder built
* without an appender.
*/
static final String ENCODER_PREFIX = APPENDER_PREFIX + "encoder.";
/**
* Output appender property.
*/
static final String APPENDER_OUTPUT_PROPERTY = OUTPUT_PREFIX + "uri";
/**
* Encoder appender property.
*/
static final String APPENDER_ENCODER_PROPERTY = ENCODER_PREFIX + "uri";
/**
* Appender flags. A list of flags (usually comma separated).
* @see AppenderFlag
*/
static final String APPENDER_FLAGS_PROPERTY = LogProperties.APPENDER_PREFIX + "flags";
/**
* Appender type (buffer/locking strategy). Unlike {@value #APPENDER_FLAGS_PROPERTY}
* this is a single value, not a list.
* @see LogAppender.AppenderType
*/
static final String APPENDER_TYPE_PROPERTY = LogProperties.APPENDER_PREFIX + "type";
/**
* Routes enabled with names comma separated.
*/
static final String ROUTES_PROPERTY = ROOT_PREFIX + "routes";
/**
* Route prefix for configuration.
*/
static final String ROUTE_PREFIX = ROOT_PREFIX + "route.{" + NAME + "}.";
/**
* Prefix of the properties configuring a route's publisher, followed by the URI
* scheme of {@value #ROUTE_PUBLISHER_PROPERTY}, for example
* <code>logging.route.{name}.publisher.async.</code>. Also used for a publisher built
* without a route.
*/
static final String PUBLISHER_PREFIX = ROUTE_PREFIX + "publisher.";
/**
* Logging publisher URI property.
*/
static final String ROUTE_PUBLISHER_PROPERTY = PUBLISHER_PREFIX + "uri";
/**
* Appenders associated with a route comma separated.
*/
static final String ROUTE_APPENDERS_PROPERTY = ROUTE_PREFIX + "appenders";
/**
* Logging level properties prefix. The value should be the name of a
* {@linkplain java.lang.System.Logger.Level level}.
*/
static final String ROUTE_LEVEL_PREFIX = ROUTE_PREFIX + "level";
/**
* Route flags.
*/
static final String ROUTE_FLAGS_PROPERTY = ROUTE_PREFIX + "flags";
/**
* Analogous to {@link System#getProperty(String)}.
* @param key property name.
* @return property value.
*/
public @Nullable String valueOrNull(String key);
/**
* Analogous to {@link System#getProperty(String, String)} with the exception that the
* fallback cannot be null and the return cannot be null.
* @param key property name.
* @param fallback returned if the value is not found and may not be null.
* @return value found or fallback.
*/
default String value(String key, String fallback) {
Objects.requireNonNull(fallback);
String v = valueOrNull(key);
if (v != null) {
return v;
}
return fallback;
}
/**
* Gets a list or null if the key is missing and by default uses
* {@link #parseList(String)}. The default implementation will parse the string value
* as a list using {@link LogProperties#parseList(String)} but custom implementations
* may rely on their own parsing or the data type is built-in to the backing
* configuration system.
* <p>
* The default format is a single property value that is comma or ampersand separated
* and percent-encoded (<a href=
* "https://www.w3.org/TR/2014/REC-html5-20141028/forms.html#url-encoded-form-data">
* application/x-www-form-urlencoded</a>) for entries needing to contain the separator
* or an equals sign, for example: <pre><code>
* logging.appenders=console,file
* </code></pre> resolves to <code>["console", "file"]</code>, and: <pre><code>
* logging.appenders=one%20two,three%2Cfour
* </code></pre> (a space and an escaped comma) resolves to
* <code>["one two", "three,four"]</code>. See {@link #parseList(String)} for empty
* elements.
* @param key property name.
* @return list or <code>null</code>.
* @apiNote the reason empty list is not returned for missing key is that it creates
* ambiguity so null is returned when key is missing.
*/
default @Nullable List<String> listOrNull(String key) {
String s = valueOrNull(key);
if (s == null) {
return null;
}
return parseList(s);
}
/**
* Gets a map or null if the key is missing and by default uses
* {@link #parseMap(String)}. The default implementation will parse the string value
* as map using {@link LogProperties#parseMap(String)} but custom implementations may
* rely on their own parsing or the data type is built-in to the backing configuration
* system.
* <p>
* The returned map must iterate in a stable order, the same every time for the same
* configuration, preferably the order its entries were declared.
* <p>
* The default format is a single property value that is comma or ampersand separated
* <code>key=value</code> pairs, percent-encoded (<a href=
* "https://www.w3.org/TR/2014/REC-html5-20141028/forms.html#url-encoded-form-data">
* application/x-www-form-urlencoded</a>) for entries needing to contain the separator
* or an equals sign, for example: <pre><code>
* logging.appender.console.encoder.gelf.headers=environment=prod,region=us-east
* </code></pre> resolves to
* <code>{"environment": "prod", "region": "us-east"}</code>, and: <pre><code>
* logging.appender.console.encoder.gelf.headers=region=us%20east
* </code></pre> (a percent-encoded space) resolves to
* <code>{"region": "us east"}</code>.
* <p>
* When that property is not set, the map can instead be given as a list of its keys
* in <code>key.keys</code> and each value in <code>key.</code><em>mapKey</em>, which
* works with sources that only hold flat values such as system properties or
* environment variables, keeps the declared order, and needs no encoding: <pre><code>
* logging.appender.console.encoder.gelf.headers.keys=environment,region
* logging.appender.console.encoder.gelf.headers.environment=prod
* logging.appender.console.encoder.gelf.headers.region=us east
* </code></pre> A key listed without a value is an error.
* @param key property name.
* @return map or <code>null</code>.
* @throws IllegalArgumentException if a key listed in <code>key.keys</code> is empty
* or has no value.
* @apiNote the reason empty map is not returned for missing key is that it creates
* ambiguity so null is returned when key is missing.
*/
default @Nullable Map<String, String> mapOrNull(String key) {
String s = valueOrNull(key);
if (s == null) {
return PropertyMaps.fromKeys(this, key);
}
return parseMap(s);
}
/**
* Suffix of the property listing a map's keys, see {@link #mapOrNull(String)}.
*/
static final String MAP_KEYS_SUFFIX = "keys";
/**
* Visits this properties with visitor, by default just applying visitor to this and
* key directly. A composite/aggregate {@link LogProperties} (one backed by more than
* one underlying source) overrides this to recursively visit each of its members in
* order, returning the first non-<code>null</code> result - which member actually
* produced it is exactly what visitor is handed as its first argument, so a composite
* never needs to know anything about what visitor is trying to build (a raw value, a
* resolved property, or anything else); it just walks its members and stops at the
* first hit.
* @param <R> result type.
* @param key property key.
* @param visitor applied to (the member of this properties that ends up handling the
* visit, key); a <code>null</code> return means keep visiting.
* @return first non-<code>null</code> result, or <code>null</code> if nothing visited
* produced one.
*/
default <R extends @Nullable Object> @Nullable R visit(String key,
BiFunction<LogProperties, String, @Nullable R> visitor) {
return visitor.apply(this, key);
}
/**
* Searches up a {@value #SEP} separated path using this properties to check for
* values. The first value not missing (not <code>null</code>) will be returned.
* @param <T> result type
* @param root prefix.
* @param key partial key that will have prefix concated on each try of func.
* @param func convert function where <code>null</code> return means to keep
* searching.
* @return closest value or <code>null</code> if no value can be found.
* @see #findUpPathOrNull(String, Function)
*/
@SuppressWarnings("exports")
default <T extends @Nullable Object> @Nullable T findOrNull(String root, String key,
BiFunction<LogProperties, String, @Nullable T> func) {
return findUpPathOrNull(key, k -> func.apply(this, concatKey(root, k)));
}
/**
* Starts resolving a property under key against this properties - the preferred entry
* point for looking up and converting a property, see {@link LogProperty}.
* @param key key, must start with {@value #ROOT_PREFIX} and not start or end with
* {@value #SEP}.
* @return a LogProperty ready to be resolved (e.g. via
* {@link LogProperty#ofString()}) or given fallback keys to try (via
* {@link LogProperty#or(String)}).
* @throws IllegalArgumentException if key is malformed.
*/
default LogProperty forKey(String key) {
return LogProperty.of(this, key);
}
/**
* Starts resolving a property under key, with a <code>{name}</code> placeholder in
* key interpolated to nameParam, against this properties.
* @param key key, containing a <code>{name}</code> placeholder, and once interpolated
* must start with {@value #ROOT_PREFIX} and not start or end with {@value #SEP}.
* @param nameParam value to interpolate for <code>{name}</code> in key.
* @return a LogProperty ready to be resolved (e.g. via
* {@link LogProperty#ofString()}) or given fallback keys to try (via
* {@link LogProperty#or(String, String)}).
* @throws IllegalArgumentException if the interpolated key is malformed.
*/
default LogProperty forKey(String key, String nameParam) {
return LogProperty.of(this, key, nameParam);
}
/**
* Describes a key and by default is usually just the key.
* @param key key.
* @return usually the key.
*/
default String description(String key) {
return key;
}
/**
* When log properties are coalesced this method is used to resolve order. A higher
* number gives higher precedence.
* @return zero by default.
*/
default int order() {
return 0;
}
/**
* Abstract log properties builder.
*
* @param <T> builder.
* @apiNote this abstract class can largely be ignored hence why it is sealed.
*/
public sealed abstract class AbstractBuilder<T> {
/**
* description
*/
protected @Nullable String description = null;
/**
* order
*/
protected int order = 0;
/**
* rename key
*/
protected Function<String, String> renameKey = s -> s;
/**
* fallbacks
*/
protected List<LogProperties> fallbacks = new ArrayList<>();
/**
* Ignored.
*/
protected AbstractBuilder() {
}
/**
* Add a fallback properties.
* @param properties fallback not null.
* @return this.
*/
public T with(LogProperties properties) {
this.fallbacks.add(properties);
return self();
}
/**
* For returning this.
* @return this builder.
*/
protected abstract T self();
/**
* Description for properties.
* @param description not null.
* @return this.
*/
public T description(String description) {
this.description = description;
return self();
}
/**
* When log properties are coalesced this method is used to resolve order. A
* higher number gives higher precedence.
* @param order order.
* @return this.
*/
public T order(int order) {
this.order = order;
return self();
}
/**
* Renames the key before it accesses function.
* @param renameKey rename key function.
* @return this.
*/
public T renameKey(Function<String, String> renameKey) {
this.renameKey = renameKey;
return self();
}
/**
* Removes the prefix from a key before it accesses the underlying properties.
* @param prefix to be removed from start of key.
* @return this.
*/
public T removeKeyPrefix(String prefix) {
return renameKey(k -> LogProperties.removeKeyPrefix(k, prefix));
}
}
/**
* Builder for properties.
*/
public final static class Builder extends AbstractBuilder<Builder> {
private @Nullable Function<Builder, LogProperties> provider = null;
private Builder() {
}
@Override
protected Builder self() {
return this;
}
private Builder provider(Function<Builder, LogProperties> provider) {
if (this.provider != null) {
throw new IllegalArgumentException("from already set");
}
this.provider = provider;
return this;
}
/**
* Sets what is called on {@link LogProperties#valueOrNull(String)}.
* @param function valueOrNull func.
* @return this.
*/
public Builder fromFunction(@SuppressWarnings("exports") Function<String, @Nullable String> function) {
return provider((b) -> new DefaultLogProperties(function,
Objects.requireNonNullElse(description, "function"), renameKey, order));
}
/**
* Parses a string as {@link Properties}.
* <p>
* The parameter is the <strong>contents</strong> of a properties file, not a
* resource, path, or file name. Passing a name such as
* {@code "logging.properties"} does not load that file; it is parsed as a single
* key with an empty value.
* @param properties properties content as a string, for example
* {@code "logging.level=INFO"}.
* @return this.
* @apiNote Rainbow Gum core does not read resources from the filesystem or the
* classpath. If external properties are needed, it is the caller's responsibility
* to read them into a string (or use a module that does, like
* {@code rainbowgum-simple-props}).
*/
public Builder fromProperties(String properties) {
if (description == null) {
description = "PROPERTIES_STRING";
}
return fromFunction(PropertiesParser.readProperties(properties)::get);
}
/**
* Parses a string as a URI query string. If the URI has no query portion then it
* will be equivalent to empty properties.
* @param uri percent encoded uri with separator as "<code>&</code>" and key
* value separator of "<code>=</code>".
* @return this.
*/
public Builder fromURIQuery(URI uri) {
String query = uri.getRawQuery();
return provider((b) -> queryToProperties(query,
Objects.requireNonNullElse(description, "URI_QUERY(" + LogRedactor.redactUri(uri) + ")")));
}
private MultiMapProperties queryToProperties(@Nullable String query, String description) {
Map<String, List<String>> multiMap;
if (query == null) {
multiMap = Map.of();
}
else {
multiMap = parseMultiMap(query);
}
var properties = new MultiMapProperties(multiMap, description, renameKey, order);
return properties;
}
/**
* Builds LogProperties based on builder config.
* @return this.
*/
public LogProperties build() {
var f = this.provider;
if (f == null) {
if (!fallbacks.isEmpty()) {
return LogProperties.of(fallbacks);
}
throw new IllegalStateException("from was not set");
}
var first = f.apply(this);
if (fallbacks.isEmpty()) {
return first;
}
List<LogProperties> combined = new ArrayList<>();
combined.add(first);
combined.addAll(fallbacks);
return LogProperties.of(combined);
}
static abstract class AbstractLogProperties implements LogProperties, LogReporter.Reportable {
protected final String description;
protected final Function<String, String> renameKey;
protected final int order;
public AbstractLogProperties(String description, Function<String, String> renameKey, int order) {
super();
this.description = description;
this.renameKey = renameKey;
this.order = order;
}
@Override
public int order() {
return this.order;
}
@Override
public String description(String key) {
String rename = renameKey.apply(key);
return descriptionForKey(description, rename);
}
@Override
public void report(Appendable out) throws IOException {
out.append(description);
}
@Override
public String toString() {
return getClass().getSimpleName() + "[description='" + description + "', " + "order=" + order + "]";
}
}
private static final class DefaultLogProperties extends AbstractLogProperties {
private final Function<String, @Nullable String> func;
DefaultLogProperties(Function<String, @Nullable String> func, String description,
Function<String, String> renameKey, int order) {
super(description, renameKey, order);
this.func = func;
}
@Override
public @Nullable String valueOrNull(String key) {
return func.apply(renameKey.apply(key));
}
}
}
/**
* Properties that can list the keys they hold, so that with
* {@link LogConfig.DebugModeType#HELP} (or {@link LogConfig.DebugModeType#ALL}) keys
* that were set but never read can be reported.
*/
public interface Listable extends LogProperties {
/**
* The keys this source holds.
* @return keys.
*/
Collection<String> keys();
}
/**
* A log properties that can be mutated like a map.
*/
public interface MutableLogProperties extends LogProperties {
/**
* Assigns key to a value.
* @param key key.
* @param value value if null will remove the key.
* @return this.
*/
MutableLogProperties put(String key, @Nullable String value);
/**
* Builder for MutableLogProperties.
* @return builder.
*/
public static Builder builder() {
return new Builder();
}
/**
* Mutable Log properties builder.
*/
public final class Builder extends AbstractBuilder<Builder> {
private Map<String, String> map = new LinkedHashMap<>();
private Builder() {
}
@Override
protected Builder self() {
return this;
}
/**
* Sets the backing mutable map.
* @param map backing map.
* @return this.
*/
public Builder with(Map<String, String> map) {
this.map = map;
return this;
}
/**
* Parses a string as {@link Properties} useful for unit testing with Java
* multiline string literal.
* @param properties properties as a string.
* @return this.
*/
public Builder copyProperties(String properties) {
map.putAll(PropertiesParser.readProperties(properties));
return this;
}
/**
* Builds a mutable log properties.
* @return mutable log properties.
*/
public MutableLogProperties build() {
String description = this.description;
if (description == null) {
description = "custom mutable";
}
var first = new MapLogProperties(map, description, renameKey, order);
if (fallbacks.isEmpty()) {
return first;
}
List<LogProperties> combined = new ArrayList<>();
combined.add(first);
combined.addAll(fallbacks);
return new CompositeMutableLogProperties(sort(combined));
}
private static final class MapLogProperties extends LogProperties.Builder.AbstractLogProperties
implements MutableLogProperties {
private final Map<String, String> map;
MapLogProperties(Map<String, String> map, String description, Function<String, String> renameKey,
int order) {
super(description, renameKey, order);
this.map = map;
}
@Override
public @Nullable String valueOrNull(String key) {
return map.get(renameKey.apply(key));
}
@Override
public MutableLogProperties put(String key, @Nullable String value) {
Objects.requireNonNull(key);
key = renameKey.apply(key);
if (value == null) {
map.remove(key);
return this;
}
map.put(key, value);
return this;
}
}
}
}
/**
* LogProperties builder.
* @return builder.
*/
public static Builder builder() {
return new Builder();
}
/**
* Finds global properties by checking the currently bound rainbow gum and if not set
* use system properties. <strong>This method should be used sparingly and is
* preferred that you get the properties through a config instance instead.</strong>
* @return currently bound properties or system properties.
* @apiNote the intention of this method is to be used for ServiceLoader singleton
* components and other systems that maybe bound prior to rainbow gum loading that
* need to determine if they should be enabled.
*/
public static LogProperties findGlobalProperties() {
return findGlobalProperties(() -> StandardProperties.SYSTEM_PROPERTIES);
}
/**
* Finds global properties by checking the currently bound rainbow gum and if not set
* use the supplier as the fallback. <strong>This method should be used sparingly and
* is preferred that you get the properties through a config instance
* instead.</strong>
* @param fallback supplier to use if no rainbow gum is bound.
* @return currently bound properties or fallback.
* @apiNote the intention of this method is to be used for ServiceLoader singleton
* components and other systems that maybe bound prior to rainbow gum loading that
* need to determine if they should be enabled.
*/
public static LogProperties findGlobalProperties(Supplier<? extends LogProperties> fallback) {
var gum = RainbowGum.getOrNull();
if (gum != null) {
return gum.config().properties();
}
return Objects.requireNonNull(fallback.get());
}
/**
* Creates log properties from many log properties. Equal properties are only included
* once.
* @param logProperties list of properties.
* @param fallback if the logProperties list is empty.
* @return log properties
*/
public static LogProperties of(List<? extends LogProperties> logProperties, LogProperties fallback) {
if (logProperties.isEmpty()) {
return fallback;
}
if (logProperties.size() == 1) {
return Objects.requireNonNull(logProperties.get(0));
}
var array = sort(logProperties);
return new CompositeLogProperties(array);
}
private static LogProperties[] sort(List<? extends LogProperties> logProperties) {
@SuppressWarnings("null") // TODO eclipse array issue
LogProperties[] array = logProperties.stream()
.filter(p -> p != StandardProperties.EMPTY)
// The same source listed twice adds nothing but noise in reports and errors.
.distinct()
.toArray(size -> new LogProperties[size]);
Arrays.sort(array, Comparator.comparingInt(LogProperties::order).reversed());
return array;
}
/**
* Creates log properties from many log properties.
* @param logProperties the list of properties.
* @return if the logProperties is empty {@link StandardProperties#EMPTY} will be
* used.
*/
public static LogProperties of(List<? extends LogProperties> logProperties) {
return of(logProperties, StandardProperties.EMPTY);
}
/**
* Creates log properties from a URI query and provided properties. This is useful for
* URI based providers where the URI query is contributing properties for the plugin.
* @param uri query will be used to pull properties from using
* {@link #parseUriQuery(String, BiConsumer)}.
* @param prefix part of the key that will stripped before accessing the URI
* properties.
* @param properties usually this is the properties coming from {@link LogConfig}.
* @return combined properties where properties is tried first than the URI query
* parameters.
*/
public static LogProperties of(URI uri, String prefix, LogProperties properties) {
return of(uri, prefix, properties, null);
}
/**
* Creates log properties from a URI query and provided properties. This is useful for
* URI based providers where the URI query is contributing properties for the plugin.
* @param uri query will be used to pull properties from using
* {@link #parseUriQuery(String, BiConsumer)}.
* @param prefix part of the key that will stripped before accessing the URI
* properties.
* @param properties usually this is the properties coming from {@link LogConfig}.
* @param uriKey where the URI came from.
* @return combined properties where properties is tried first than the URI query
* parameters.
*/
public static LogProperties of(URI uri, String prefix, LogProperties properties, @Nullable String uriKey) {
/*
* TODO this should be integrated with the whole providers and registries.
*/
String propDesc = uriKey != null ? "[" + uriKey + "]->" : "";
String description = propDesc + "URI(" + LogRedactor.redactUri(uri) + ")";
var uriProperties = LogProperties.builder()
.description(description)
.fromURIQuery(uri)
.renameKey(s -> LogProperties.removeKeyPrefix(s, prefix))
.build();
return LogProperties.of(List.of(properties, uriProperties));
}
/**
* Parse a {@linkplain URI#getRawQuery() URI query} in <a href=
* "https://www.w3.org/TR/2014/REC-html5-20141028/forms.html#url-encoded-form-data">
* application/x-www-form-urlencoded </a> format useful for parsing properties values
* that have key values embedded in them. <strong>This parser unlike form encoding
* uses <code>%20</code> for space as the data is coming from a URI.</strong>
* @param query raw query component of URI.
* @param consumer accept will be called for each entry and if key (left hand) does
* not have a "<code>=</code>" following it the second parameter on the consumer
* accept will be passed <code>null</code>.
*/
public static void parseUriQuery(String query,
@SuppressWarnings("exports") BiConsumer<String, @Nullable String> consumer) {
parseUriQuery(query, true, consumer);
}
/**
* Parses a URI query formatted string to a Map.
* @param query raw query component of URI.
* @return decoded key values with last key winning over previous equal keys.
* @see #parseUriQuery(String, BiConsumer)
*/
public static Map<String, String> parseMap(String query) {
Map<String, String> m = new LinkedHashMap<>();
parseUriQuery(query, (k, v) -> {
if (v != null) {
m.put(k, v);
}
});
return m;
}
/**
* Parses a URI query for a multiple value map. The list values of the map maybe empty
* if the query parameter does not have any values associated with it which would be
* the case if there is a parameter (key) with no "<code>=</code>" following it. For
* example the following would have three entries of <code>a,b,c</code> all with empty
* list: <pre>
* <code>
* a&b&c&
* </code> </pre>
* @param query raw query component of URI.
* @return decoded key values with multiple keys grouped together in order found.
* @see #parseUriQuery(String, BiConsumer)
*/
public static Map<String, List<String>> parseMultiMap(String query) {
Map<String, List<String>> m = new LinkedHashMap<>();
BiConsumer<String, @Nullable String> f = (k, v) -> {
List<String> list = Objects.requireNonNull(m.computeIfAbsent(k, _k -> new ArrayList<String>()));
if (v != null) {
list.add(v);
}
};
parseUriQuery(query, f);
return m;
}
/**
* Parses a list of strings from a string that is <a href=
* "https://www.w3.org/TR/2014/REC-html5-20141028/forms.html#url-encoded-form-data">
* percent encoded for escaping (application/x-www-form-urlencoded)</a> where the
* separator can be either "<code>&</code>" or "<code>,</code>".
* <p>
* An example would be using ampersand: <pre><code>
* "a&b%20B&c" -> ["a","b B","c"]
* </code> </pre> and comma: <pre><code>
* "a,b,c" -> ["a","b","c"]
* </code> </pre>
* <p>
* An element may be empty. One empty element at the start and one at the end are
* dropped, so a value assembled from parts that may be missing still works
* (<code>"a,"</code> and <code>",a,"</code> are <code>["a"]</code>), while every
* other empty element is kept: <pre><code>
* "" -> []
* ",," -> [""]
* "a,,b" -> ["a","","b"]
* "a,," -> ["a",""]
* </code> </pre> An equals sign is part of the element: <code>"a=b"</code> is
* <code>["a=b"]</code>.
* @param query the comma or ampersand delimited string.
* @return list of strings
*/
public static List<String> parseList(String query) {
List<String> list = new ArrayList<>();
int start = 0;
for (int i = 0; i < query.length(); i++) {
char c = query.charAt(i);
if (c == ',' || c == '&') {
list.add(query.substring(start, i));
start = i + 1;
}
}
list.add(query.substring(start));
if (list.get(0).isEmpty()) {
list.remove(0);
}
if (!list.isEmpty() && list.get(list.size() - 1).isEmpty()) {
list.remove(list.size() - 1);
}
list.replaceAll(e -> PercentCodec.decode(e, StandardCharsets.UTF_8));
return list;
}
private static void parseUriQuery(String query, boolean decode, BiConsumer<String, @Nullable String> consumer) {
parseUriQuery(query, decode, "[&,]", consumer);
}
private static void parseUriQuery(String query, boolean decode, String sep,
BiConsumer<String, @Nullable String> consumer) {
/*
* TODO default java split has issues but is very fast
*/
@SuppressWarnings("StringSplitter")
String[] pairs = query.split(sep);
for (String pair : pairs) {
int idx = pair.indexOf("=");
String key;
String value;
if (idx == 0) {
continue;
}
else if (idx < 0) {
key = pair;
value = null;
}
else {
key = pair.substring(0, idx);
value = pair.substring(idx + 1);
}
if (decode) {
key = PercentCodec.decode(key, StandardCharsets.UTF_8);
if (value != null) {
value = PercentCodec.decode(value, StandardCharsets.UTF_8);
}
}
if (key.isBlank()) {
continue;
}
consumer.accept(key, value);
}
}
/**
* Common static log properties such as System properties ane environment variables.
*/
@CaseChanging
enum StandardProperties implements LogProperties, LogReporter.Reportable {
/**
* Empty properties. The order is <code>-1</code>.
*/
EMPTY {
@Override
public @Nullable String valueOrNull(String key) {
return null;
}
@Override
public int order() {
return -1;
}
},
/**
* Uses {@link System#getProperty(String)} for {@link #valueOrNull(String)}. The
* {@link #order()} is <code>400</code> which the same value as MicroProfile
* config ordinal.
*/
SYSTEM_PROPERTIES {
@Override
public @Nullable String valueOrNull(String key) {
return System.getProperty(key);
}
@Override
public int order() {
return 400;
}
},
/**
* Uses {@link System#getenv(String)} for {@link #valueOrNull(String)}.The
* {@link #order()} is <code>300</code> which is the same value as MicroProfile
* config ordinal.
*/
ENVIRONMENT_VARIABLES {
@Override
public @Nullable String valueOrNull(String key) {
return System.getenv(translateKey(key));
}
@Override
protected String translateKey(String key) {
return key.replace(".", "_");
}
@Override
public int order() {
return 300;
}
};
@Override
public String description(String key) {
String k = translateKey(key);
return name() + "[" + k + "]";
}
/*
* Each constant's own per-constant class body (see EMPTY/SYSTEM_PROPERTIES/
* ENVIRONMENT_VARIABLES above) makes getClass() an anonymous subclass with no
* canonical name, so without this LogPropertiesReporting's fallback chain would
* land on the description(dummyKey) branch instead: name() alone is a cleaner
* report for the single most common LogProperties in practice (the default when
* nothing else is configured).
*/
@Override
public void report(Appendable out) throws IOException {
out.append(name());
}
/**
* Translate the key.
* @param key input
* @return output
*/
protected String translateKey(String key) {
return key;
}
}
/**
* Searches a {@value LogProperties#SEP} separated property path recursing up the path
* till a non null value (not missing) is found.
* <p>
* For example assume we have a single property set like
* <code>logging.level.com.stuff=DEBUG</code> and we call this function with
* <code>logging.level.com.stuff.a.b</code>.
* <ol>
* <li><code>logging.level.com.stuff.a.b</code> will resolve to <code>null</code></li>
* <li><code>logging.level.com.stuff.a</code> will resolve to <code>null</code></li>
* <li><code>logging.level.com.stuff</code> will resolve to <code>"DEBUG"</code>, stop
* and return <code>"DEBUG"</code>.</li>
* </ol>
* @param <T> type to return.
* @param key the initial path.
* @param resolveFunc function that returns the value at path or <code>null</code>.
* @return value or <code>null</code> if no non null value can be found.
*/
@SuppressWarnings("exports")
public static <T extends @Nullable Object> @Nullable T findUpPathOrNull(String key,
Function<String, @Nullable T> resolveFunc) {
return searchPath(key, resolveFunc, SEP);
}
private static <T> @Nullable T searchPath(String key, Function<String, @Nullable T> resolveFunc, String sep) {
String tempName = key;
@Nullable T level = null;
int indexOfLastDot = tempName.length();
while ((level == null) && (indexOfLastDot > -1)) {
tempName = tempName.substring(0, indexOfLastDot);
level = resolveFunc.apply(tempName);
indexOfLastDot = tempName.lastIndexOf(sep);
}
if (level != null) {
return level;
}
return resolveFunc.apply("");
}
/**
* Concats a key using {@link #SEP} to the root prefix.
* @param name key.
* @return concat key.
*/
static String concatKey(String name) {
return concatKey(LogProperties.ROOT_PREFIX, name);
}
/**
* Concats a keys using {@link #SEP} as the separator.
* @param prefix start of key.
* @param name second part of key.
* @return key.
*/
static String concatKey(String prefix, String name) {
if (name.equals("")) {
return prefix;
}
if (prefix.equals("")) {
return name;
}
if (prefix.endsWith(".")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
if (name.startsWith(SEP)) {
return prefix + name;
}
return prefix + SEP + name;
}
/**
* Replace "{name}" tokens in property names.
* @param key property name.
* @param parameters tokens to replace with entry values.
* @return interpolated property key.
* @see #interpolateKey(String, Function)
*/
static String interpolateKey(String key, Map<String, String> parameters) {
return interpolateKey(key, parameters::get);
}
/**
* Replaces property <strong>key</strong> parameters by using the lookup function.
* Property keys are often parameterized usually by some name. The parameters are
* surrounded by curly braces and replaced by the lookup function. An example of the
* format is <code>a.{PARAM}</code> which has a parameter named <code>PARAM</code>. If
* the lookup function has <code>PARAM->b</code> then the result will be
* <code>a.b</code>. If a value for a parameter is null (indicates not found) an
* exception is thrown.
* @param key to be interpolated.
* @param lookup function that may return null for a parameter.
* @return interpolated key with all parameters replaced.
* @throws IllegalArgumentException if the lookup function does not have a value for a
* parameter.
*/
public static String interpolateKey(String key,
@SuppressWarnings("exports") Function<String, ? extends @Nullable String> lookup)
throws IllegalArgumentException {
var parameters = keyParameters(key);
String result = key;
for (String k : parameters) {
var v = lookup.apply(k);
if (v == null) {
throw new IllegalArgumentException(
"Keyed parameter missing. key: '" + key + "' parameter: '" + k + "'");
}
LogProperty.Validator.validateKeyParameterValue(key, k, v);
result = result.replace("{" + k + "}", v);
}
return result;
}
/**
* Interpolates a named key which is a property key that has a {@value #NAME}.
* @param key with name parameters.
* @param name replace name token.
* @return interpolated property key.
*/
static String interpolateNamedKey(String key, String name) {
return interpolateKey(key, Map.of(NAME, name));
}
/**
* Remove key prefix from the key. Prefix should end in "." but this method does not
* check that.
* @param key key whose prefix will be removed.
* @param prefix property name prefix.
* @return prefix removed if the string is prefixed.
*/
static String removeKeyPrefix(String key, String prefix) {
if (key.startsWith(prefix)) {
return key.substring(prefix.length());
}
return key;
}
/**
* Property prefix parameter pattern.
*/
static final Pattern PARAMETER_PATTERN = Pattern.compile("\\{(.*?)\\}");
/**
* Extract key parameters with {@link #PARAMETER_PATTERN}.
* @param key also known as property name.
* @return parameters in key.
*/
static Set<String> keyParameters(String key) {
Set<String> tokens = new LinkedHashSet<>();
Matcher matcher = PARAMETER_PATTERN.matcher(key);
while (matcher.find()) {
var t = matcher.group(1);
if (t == null) {
throw new IllegalStateException("bug with parameter pattern regex.");
}
tokens.add(t);
}
return tokens;
}
/**
* Validate key parameters.
* @param key key to be interpolated with parameters.
* @param parameters names of parameters which must <strong>equal</strong> the set of
* the found parameters. while the order obviously does not matter the set cannot have
* more parameters than found in the key.
*/
static void validateKeyParameters(String key, Set<String> parameters) {
var keyParameters = keyParameters(key);
if (!parameters.equals(keyParameters)) {
throw new IllegalArgumentException("Keyed parameter mismatch. key: '" + key + "' , key parameters: "
+ keyParameters + " provided parameters: " + parameters);
}
}
/**
* Standard way to produce description for a key.
* @param type the properties type name.
* @param key the properties key translated.
* @return description for key.
*/
static String descriptionForKey(String type, String key) {
return type + "[" + key + "]";
}
/**
* Standard way to produce description for a key found in a resource (a file or
* similar), without a line number.
* @param type the properties type name.
* @param resource file like name.
* @param key the properties key translated.
* @return description for key.
*/
static String descriptionForResource(String type, String resource, String key) {
return type + "[" + resource + "][" + key + "]";
}
/**
* Standard way to produce description for a key found in a resource (a file or
* similar) at a known index, usually a line number.
* @param type the properties type name.
* @param resource file like name.
* @param key the properties key translated.
* @param index usually line number of where key found.
* @return description for key.
*/
static String descriptionForResource(String type, String resource, String key, long index) {
return type + "[" + resource + ":" + index + "][" + key + "]";
}
}
final class MultiMapProperties extends AbstractLogProperties {
private final Map<String, List<String>> multiMap;
public MultiMapProperties(Map<String, List<String>> multiMap, String description,
Function<String, String> renameKey, int order) {
super(description, renameKey, order);
this.multiMap = multiMap;
}
@Override
public @Nullable String valueOrNull(String key) {
return _valueOrNull(renameKey.apply(key));
}
private @Nullable String _valueOrNull(String key) {
var list = multiMap.get(key);
if (list == null || list.isEmpty()) {
return null;
}
return list.get(0);
}
@Override
public @Nullable List<String> listOrNull(String key) {
return multiMap.get(renameKey.apply(key));
}
@Override
public @Nullable Map<String, String> mapOrNull(String key) {
String s = valueOrNull(key);
if (s != null) {
return LogProperties.parseMap(s);
}
var map = PropertyMaps.fromKeys(this, key);
if (map != null) {
return map;
}
/*
* Now we check for dotted notation since key was not set. key.subkey=value
*/
String prefix = renameKey.apply(key) + LogProperties.SEP;
LinkedHashMap<String, String> m = new LinkedHashMap<>();
for (var k : multiMap.keySet()) {
if (k.startsWith(prefix) && !k.equals(prefix)) {
String v = _valueOrNull(k);
if (v != null) {
String _k = k.substring(prefix.length());
m.put(_k, v);
}
}
}
return m.isEmpty() ? null : m;
}
}
interface ListLogProperties extends LogProperties, LogReporter.Reportable {
@Override
default @Nullable String valueOrNull(String key) {
for (var props : properties()) {
var value = props.valueOrNull(key);
if (value != null) {
return value;
}
}
return null;
}
@Override
default <R extends @Nullable Object> @Nullable R visit(String key,
BiFunction<LogProperties, String, @Nullable R> visitor) {
for (var props : properties()) {
var value = props.visit(key, visitor);
if (value != null) {
return value;
}
}
return null;
}
@Override
default String description(String key) {
var descriptions = new ArrayList<String>();
visit(key, (source, sourceKey) -> {
descriptions.add(source.description(sourceKey));
return null;
});
String description = String.join(", ", descriptions);
if (description.length() > 120) {
return "\n " + String.join(",\n ", descriptions);
}
return description;
}
LogProperties[] properties();
default StringBuilder toString(StringBuilder sb) {
sb.append(getClass().getSimpleName());
sb.append("[properties=").append(Arrays.deepToString(properties())).append("]");
return sb;
}
/*
* Reports components separated by commas without the error description's line
* wrapping (via LogPropertiesReporting.describe, the same fallback chain
* LogReporter#appendGlobalProperties uses for the top level LogProperties a LogConfig
* was built with) rather than per key.
*/
@Override
default void report(Appendable out) throws IOException {
boolean first = true;
for (var p : properties()) {
if (first) {
first = false;
}
else {
out.append(", ");
}
LogPropertiesReporting.describe(out, p);
}
}
}
/*
* Shared by ListLogProperties#report(Appendable) (recursively, per component) and
* LogReporter's own appendGlobalProperties(Appendable, LogProperties) (for the top level
* LogProperties a LogConfig was built with), so there is exactly one fallback chain for
* "how do we describe an arbitrary LogProperties for a report": an explicit
* Reportable#report(...) if implemented, else the concrete class' fully qualified name
* (stable and free: no override needed), else, only for the rare case where the FQCN
* itself is null (e.g. one of StandardProperties' own per constant enum bodies, which are
* anonymous classes under the hood, before it implemented Reportable directly),
* description(key) with a dummy key, since that is the only description() such a
* LogProperties is guaranteed to answer at all.
*/
final class LogPropertiesReporting {
static final String DESCRIBE_KEY = "logging.reporter.describe";
private LogPropertiesReporting() {
}
static void describe(Appendable out, LogProperties properties) throws IOException {
if (properties instanceof LogReporter.Reportable r) {
r.report(out);
return;
}
String fqcn = properties.getClass().getCanonicalName();
if (fqcn != null) {
out.append(fqcn);
return;
}
out.append(properties.description(DESCRIBE_KEY));
}
}
@SuppressWarnings("ArrayRecordComponent") // TODO we did this originall because it was hot
// path but not anymore
record CompositeMutableLogProperties(LogProperties[] properties) implements ListLogProperties, MutableLogProperties {
@Override
@SuppressWarnings("ReferenceEquality") // identity check to avoid self-recursion if a
// composite contains itself
public MutableLogProperties put(String key, @Nullable String value) {
for (var p : properties()) {
if (p instanceof MutableLogProperties mp && mp != this) {
mp.put(key, value);
return this;
}
}
return this;
}
@Override
public String toString() {
return toString(new StringBuilder()).toString();
}
}
@SuppressWarnings("ArrayRecordComponent") // TODO we did this originall because it was hot
// path but not anymore
record CompositeLogProperties(LogProperties[] properties) implements ListLogProperties {
@Override
public String toString() {
return toString(new StringBuilder()).toString();
}
}
final class PropertiesParser {
private PropertiesParser() {
}
static Map<String, String> readProperties(String input) {
Map<String, String> m = new LinkedHashMap<>();
StringReader sr = new StringReader(input);
try {
readProperties(sr, m::put);
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
return m;
}
static void readProperties(Reader reader, BiConsumer<String, String> consumer) throws IOException {
Properties bp = prepareProperties(consumer);
bp.load(reader);
}
private static Properties prepareProperties(BiConsumer<String, String> consumer) {
// Hack to use properties class to load but our map for preserved order
@SuppressWarnings({ "serial", "nullness" })
Properties bp = new Properties() {
@Override
@SuppressWarnings({ "nullness", "keyfor", "UnsynchronizedOverridesSynchronized" }) // checker
// bug
public @Nullable Object put(Object key, Object value) {
Objects.requireNonNull(key);
Objects.requireNonNull(value);
consumer.accept((String) key, (String) value);
return null;
}
};
return bp;
}
}
/*
* The key.keys form of LogProperties.mapOrNull, shared with implementations that override
* it.
*/
final class PropertyMaps {
private PropertyMaps() {
}
static @Nullable Map<String, String> fromKeys(LogProperties properties, String key) {
String keysKey = key + LogProperties.SEP + LogProperties.MAP_KEYS_SUFFIX;
List<String> keys = properties.listOrNull(keysKey);
if (keys == null) {
return null;
}
Map<String, String> m = new LinkedHashMap<>();
for (String k : keys) {
if (k.isEmpty()) {
throw new IllegalArgumentException("'" + keysKey + "' has an empty map key");
}
String valueKey = key + LogProperties.SEP + k;
String v = properties.valueOrNull(valueKey);
if (v == null) {
throw new IllegalArgumentException(
"map key '" + k + "' is listed in '" + keysKey + "' but '" + valueKey + "' is missing");
}
m.put(k, v);
}
return m;
}
}