org.apache.logging.log4j.util.PropertiesUtil Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.logging.log4j.util;
import aQute.bnd.annotation.Cardinality;
import aQute.bnd.annotation.Resolution;
import aQute.bnd.annotation.spi.ServiceConsumer;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.status.StatusLogger;
/**
* Consider this class private.
*
* Provides utility methods for managing {@link Properties} instances as well as access to the global configuration
* system. Properties by default are loaded from the system properties, system environment, and a classpath resource
* file named {@value #LOG4J_PROPERTIES_FILE_NAME}. Additional properties can be loaded by implementing a custom
* {@link PropertySource} service and specifying it via a {@link ServiceLoader} file called
* {@code META-INF/services/org.apache.logging.log4j.util.PropertySource} with a list of fully qualified class names
* implementing that interface.
*
*
* @see PropertySource
*/
@ServiceConsumer(value = PropertySource.class, resolution = Resolution.OPTIONAL, cardinality = Cardinality.MULTIPLE)
public final class PropertiesUtil {
private static final Logger LOGGER = StatusLogger.getLogger();
private static final String LOG4J_PROPERTIES_FILE_NAME = "log4j2.component.properties";
private static final String LOG4J_SYSTEM_PROPERTIES_FILE_NAME = "log4j2.system.properties";
private static final Lazy COMPONENT_PROPERTIES =
Lazy.lazy(() -> new PropertiesUtil(LOG4J_PROPERTIES_FILE_NAME, false));
private static final Pattern DURATION_PATTERN = Pattern.compile("([+-]?\\d+)\\s*(\\w+)?", Pattern.CASE_INSENSITIVE);
private final Environment environment;
/**
* Constructs a PropertiesUtil using a given Properties object as its source of defined properties.
*
* @param props the Properties to use by default
*/
public PropertiesUtil(final Properties props) {
this(new PropertiesPropertySource(props));
}
/**
* Constructs a PropertiesUtil for a given properties file name on the classpath. The properties specified in this
* file are used by default. If a property is not defined in this file, then the equivalent system property is used.
*
* @param propertiesFileName the location of properties file to load
*/
public PropertiesUtil(final String propertiesFileName) {
this(propertiesFileName, true);
}
private PropertiesUtil(final String propertiesFileName, final boolean useTccl) {
this(new PropertyFilePropertySource(propertiesFileName, useTccl));
}
/**
* Constructs a PropertiesUtil for a give property source as source of additional properties.
* @param source a property source
*/
PropertiesUtil(final PropertySource source) {
environment = new Environment(source);
}
/**
* Loads and closes the given property input stream. If an error occurs, log to the status logger.
*
* @param in a property input stream.
* @param source a source object describing the source, like a resource string or a URL.
* @return a new Properties object
*/
static Properties loadClose(final InputStream in, final Object source) {
final Properties props = new Properties();
if (null != in) {
try {
props.load(in);
} catch (final IOException error) {
LOGGER.error("Unable to read source `{}`", source, error);
} finally {
try {
in.close();
} catch (final IOException error) {
LOGGER.error("Unable to close source `{}`", source, error);
}
}
}
return props;
}
/**
* Returns the PropertiesUtil used by Log4j.
*
* @return the main Log4j PropertiesUtil instance.
*/
public static PropertiesUtil getProperties() {
return COMPONENT_PROPERTIES.get();
}
/**
* Allows a {@link PropertySource} to be added after {@code PropertiesUtil} has been created.
* @param propertySource the {@code PropertySource} to add.
* @since 2.19.0
*/
public void addPropertySource(final PropertySource propertySource) {
environment.addPropertySource(Objects.requireNonNull(propertySource));
}
/**
* Removes a {@link PropertySource}.
* @param propertySource the {@code PropertySource} to remove.
* @since 2.24.0
*/
public void removePropertySource(final PropertySource propertySource) {
environment.removePropertySource(Objects.requireNonNull(propertySource));
}
/**
* Returns {@code true} if the specified property is defined, regardless of its value (it may not have a value).
*
* @param name the name of the property to verify
* @return {@code true} if the specified property is defined, regardless of its value
*/
public boolean hasProperty(final String name) {
return environment.containsKey(name);
}
/**
* Gets the named property as a boolean value. If the property matches the string {@code "true"} (case-insensitive),
* then it is returned as the boolean value {@code true}. Any other non-{@code null} text in the property is
* considered {@code false}.
*
* @param name the name of the property to look up
* @return the boolean value of the property or {@code false} if undefined.
*/
public boolean getBooleanProperty(final String name) {
return getBooleanProperty(name, false);
}
/**
* Gets the named property as a boolean value.
*
* @param name the name of the property to look up
* @param defaultValue the default value to use if the property is undefined
* @return the boolean value of the property or {@code defaultValue} if undefined.
*/
public boolean getBooleanProperty(final String name, final boolean defaultValue) {
final String prop = getStringProperty(name);
return prop == null ? defaultValue : "true".equalsIgnoreCase(prop);
}
/**
* Gets the named property as a boolean value.
*
* @param name the name of the property to look up
* @param defaultValueIfAbsent the default value to use if the property is undefined
* @param defaultValueIfPresent the default value to use if the property is defined but not assigned
* @return the boolean value of the property or {@code defaultValue} if undefined.
*/
public boolean getBooleanProperty(
final String name, final boolean defaultValueIfAbsent, final boolean defaultValueIfPresent) {
final String prop = getStringProperty(name);
return prop == null
? defaultValueIfAbsent
: prop.isEmpty() ? defaultValueIfPresent : "true".equalsIgnoreCase(prop);
}
/**
* Retrieves a property that may be prefixed by more than one string.
* @param prefixes The array of prefixes.
* @param key The key to locate.
* @param supplier The method to call to derive the default value. If the value is null, null will be returned
* if no property is found.
* @return The value or null if it is not found.
* @since 2.13.0
*/
public Boolean getBooleanProperty(final String[] prefixes, final String key, final Supplier supplier) {
for (final String prefix : prefixes) {
if (hasProperty(prefix + key)) {
return getBooleanProperty(prefix + key);
}
}
return supplier != null ? supplier.get() : null;
}
/**
* Gets the named property as a Charset value.
*
* @param name the name of the property to look up
* @return the Charset value of the property or {@link Charset#defaultCharset()} if undefined.
*/
public Charset getCharsetProperty(final String name) {
return getCharsetProperty(name, Charset.defaultCharset());
}
/**
* Gets the named property as a Charset value. If we cannot find the named Charset, see if it is mapped in
* file {@code Log4j-charsets.properties} on the class path.
*
* @param name the name of the property to look up
* @param defaultValue the default value to use if the property is undefined
* @return the Charset value of the property or {@code defaultValue} if undefined.
*/
public Charset getCharsetProperty(final String name, final Charset defaultValue) {
final String charsetName = getStringProperty(name);
if (charsetName == null) {
return defaultValue;
}
if (Charset.isSupported(charsetName)) {
return Charset.forName(charsetName);
}
final ResourceBundle bundle = getCharsetsResourceBundle();
if (bundle.containsKey(name)) {
final String mapped = bundle.getString(name);
if (Charset.isSupported(mapped)) {
return Charset.forName(mapped);
}
}
LOGGER.warn(
"Unable to read charset `{}` from property `{}`. Falling back to the default: `{}`",
charsetName,
name,
defaultValue);
return defaultValue;
}
/**
* Gets the named property as a double.
*
* @param name the name of the property to look up
* @param defaultValue the default value to use if the property is undefined
* @return the parsed double value of the property or {@code defaultValue} if it was undefined or could not be parsed.
*/
public double getDoubleProperty(final String name, final double defaultValue) {
final String prop = getStringProperty(name);
if (prop != null) {
try {
return Double.parseDouble(prop);
} catch (final NumberFormatException e) {
LOGGER.warn(
"Unable to read double `{}` from property `{}`. Falling back to the default: `{}`",
prop,
name,
defaultValue,
e);
}
}
return defaultValue;
}
/**
* Gets the named property as an integer.
*
* @param name the name of the property to look up
* @param defaultValue the default value to use if the property is undefined
* @return the parsed integer value of the property or {@code defaultValue} if it was undefined or could not be
* parsed.
*/
public int getIntegerProperty(final String name, final int defaultValue) {
final String prop = getStringProperty(name);
if (prop != null) {
try {
return Integer.parseInt(prop.trim());
} catch (final NumberFormatException e) {
LOGGER.warn(
"Unable to read int `{}` from property `{}`. Falling back to the default: `{}`",
prop,
name,
defaultValue,
e);
}
}
return defaultValue;
}
/**
* Retrieves a property that may be prefixed by more than one string.
* @param prefixes The array of prefixes.
* @param key The key to locate.
* @param supplier The method to call to derive the default value. If the value is null, null will be returned
* if no property is found.
* @return The value or null if it is not found.
* @since 2.13.0
*/
public Integer getIntegerProperty(final String[] prefixes, final String key, final Supplier supplier) {
for (final String prefix : prefixes) {
if (hasProperty(prefix + key)) {
return getIntegerProperty(prefix + key, 0);
}
}
return supplier != null ? supplier.get() : null;
}
/**
* Gets the named property as a long.
*
* @param name the name of the property to look up
* @param defaultValue the default value to use if the property is undefined
* @return the parsed long value of the property or {@code defaultValue} if it was undefined or could not be parsed.
*/
public long getLongProperty(final String name, final long defaultValue) {
final String prop = getStringProperty(name);
if (prop != null) {
try {
return Long.parseLong(prop);
} catch (final NumberFormatException e) {
LOGGER.warn(
"Unable to read long `{}` from property `{}`. Falling back to the default: `{}`",
prop,
name,
defaultValue,
e);
}
}
return defaultValue;
}
/**
* Retrieves a property that may be prefixed by more than one string.
* @param prefixes The array of prefixes.
* @param key The key to locate.
* @param supplier The method to call to derive the default value. If the value is null, null will be returned
* if no property is found.
* @return The value or null if it is not found.
* @since 2.13.0
*/
public Long getLongProperty(final String[] prefixes, final String key, final Supplier supplier) {
for (final String prefix : prefixes) {
if (hasProperty(prefix + key)) {
return getLongProperty(prefix + key, 0);
}
}
return supplier != null ? supplier.get() : null;
}
/**
* Retrieves a Duration where the String is of the format nnn[unit] where nnn represents an integer value
* and unit represents a time unit.
* @param name The property name.
* @param defaultValue The default value.
* @return The value of the String as a Duration or the default value, which may be null.
* @since 2.13.0
*/
public Duration getDurationProperty(final String name, final Duration defaultValue) {
final String prop = getStringProperty(name);
try {
return parseDuration(prop);
} catch (final IllegalArgumentException e) {
LOGGER.warn(
"Unable to read duration `{}` from property `{}`.\nExpected format 'n unit', where 'n' is an "
+ "integer and 'unit' is one of: {}.",
prop,
name,
TimeUnit.getValidUnits().collect(Collectors.joining(", ")),
e);
}
return defaultValue;
}
/**
* Retrieves a property that may be prefixed by more than one string.
* @param prefixes The array of prefixes.
* @param key The key to locate.
* @param supplier The method to call to derive the default value. If the value is null, null will be returned
* if no property is found.
* @return The value or null if it is not found.
* @since 2.13.0
*/
public Duration getDurationProperty(final String[] prefixes, final String key, final Supplier supplier) {
for (final String prefix : prefixes) {
if (hasProperty(prefix + key)) {
return getDurationProperty(prefix + key, null);
}
}
return supplier != null ? supplier.get() : null;
}
/**
* Retrieves a property that may be prefixed by more than one string.
* @param prefixes The array of prefixes.
* @param key The key to locate.
* @param supplier The method to call to derive the default value. If the value is null, null will be returned
* if no property is found.
* @return The value or null if it is not found.
* @since 2.13.0
*/
public String getStringProperty(final String[] prefixes, final String key, final Supplier supplier) {
for (final String prefix : prefixes) {
final String result = getStringProperty(prefix + key);
if (result != null) {
return result;
}
}
return supplier != null ? supplier.get() : null;
}
/**
* Gets the named property as a String.
*
* @param name the name of the property to look up
* @return the String value of the property or {@code null} if undefined.
*/
public String getStringProperty(final String name) {
return environment.get(name);
}
/**
* Gets the named property as a String.
*
* @param name the name of the property to look up
* @param defaultValue the default value to use if the property is undefined
* @return the String value of the property or {@code defaultValue} if undefined.
*/
public String getStringProperty(final String name, final String defaultValue) {
final String prop = getStringProperty(name);
return prop == null ? defaultValue : prop;
}
/**
* Return the system properties or an empty Properties object if an error occurs.
*
* @return The system properties.
*/
public static Properties getSystemProperties() {
try {
return new Properties(System.getProperties());
} catch (final SecurityException error) {
LOGGER.error("Unable to access system properties.", error);
// Sandboxed - can't read System Properties
return new Properties();
}
}
/**
* Reloads all properties. This is primarily useful for unit tests.
*
* @since 2.10.0
* @deprecated since 2.24.0 caching of property values is disabled.
*/
@Deprecated
public void reload() {}
/**
* Provides support for looking up global configuration properties via environment variables, property files,
* and system properties, in three variations:
*
* Normalized: all log4j-related prefixes removed, remaining property is camelCased with a log4j2 prefix for
* property files and system properties, or follows a LOG4J_FOO_BAR format for environment variables.
*
* Legacy: the original property name as defined in the source pre-2.10.0.
*
* Tokenized: loose matching based on word boundaries.
*
* @since 2.10.0
*/
private static final class Environment {
private final Set sources = ConcurrentHashMap.newKeySet();
private Environment(final PropertySource propertySource) {
final PropertySource sysProps = new PropertyFilePropertySource(LOG4J_SYSTEM_PROPERTIES_FILE_NAME, false);
try {
sysProps.forEach((key, value) -> {
if (System.getProperty(key) == null) {
System.setProperty(key, value);
}
});
} catch (final SecurityException e) {
LOGGER.warn(
"Unable to set Java system properties from {} file, due to security restrictions.",
LOG4J_SYSTEM_PROPERTIES_FILE_NAME,
e);
}
sources.add(propertySource);
// We don't log anything on the status logger.
ServiceLoaderUtil.safeStream(
PropertySource.class,
ServiceLoader.load(PropertySource.class, PropertiesUtil.class.getClassLoader()),
LOGGER)
.forEach(sources::add);
}
private void addPropertySource(final PropertySource propertySource) {
sources.add(propertySource);
}
private void removePropertySource(final PropertySource propertySource) {
sources.remove(propertySource);
}
private String get(final String key) {
final List tokens = PropertySource.Util.tokenize(key);
return sources.stream()
.sorted(PropertySource.Comparator.INSTANCE)
.map(source -> {
if (!tokens.isEmpty()) {
final String normalKey = Objects.toString(source.getNormalForm(tokens), null);
if (normalKey != null && sourceContainsProperty(source, normalKey)) {
return sourceGetProperty(source, normalKey);
}
}
return sourceGetProperty(source, key);
})
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
private boolean sourceContainsProperty(final PropertySource source, final String key) {
try {
return source.containsProperty(key);
} catch (final Exception e) {
LOGGER.warn("Failed to retrieve Log4j property {} from property source {}.", key, source, e);
return false;
}
}
private String sourceGetProperty(final PropertySource source, final String key) {
try {
return source.getProperty(key);
} catch (final Exception e) {
LOGGER.warn("Failed to retrieve Log4j property {} from property source {}.", key, source, e);
return null;
}
}
private boolean containsKey(final String key) {
final List tokens = PropertySource.Util.tokenize(key);
return sources.stream().anyMatch(s -> {
final CharSequence normalizedKey = tokens.isEmpty() ? null : s.getNormalForm(tokens);
return sourceContainsProperty(s, key)
|| (normalizedKey != null && sourceContainsProperty(s, normalizedKey.toString()));
});
}
}
/**
* Extracts properties that start with or are equals to the specific prefix and returns them in a new Properties
* object with the prefix removed.
*
* @param properties The Properties to evaluate.
* @param prefix The prefix to extract.
* @return The subset of properties.
*/
public static Properties extractSubset(final Properties properties, final String prefix) {
final Properties subset = new Properties();
if (prefix == null || prefix.isEmpty()) {
return subset;
}
final String prefixToMatch = prefix.charAt(prefix.length() - 1) != '.' ? prefix + '.' : prefix;
final Collection keys = new ArrayList<>();
for (final String key : properties.stringPropertyNames()) {
if (key.startsWith(prefixToMatch)) {
subset.setProperty(key.substring(prefixToMatch.length()), properties.getProperty(key));
keys.add(key);
}
}
for (final String key : keys) {
properties.remove(key);
}
return subset;
}
static ResourceBundle getCharsetsResourceBundle() {
return ResourceBundle.getBundle("Log4j-charsets");
}
/**
* Partitions a properties map based on common key prefixes up to the first period.
*
* @param properties properties to partition
* @return the partitioned properties where each key is the common prefix (minus the period) and the values are
* new property maps without the prefix and period in the key
* @since 2.6
*/
public static Map partitionOnCommonPrefixes(final Properties properties) {
return partitionOnCommonPrefixes(properties, false);
}
/**
* Partitions a properties map based on common key prefixes up to the first period.
*
* @param properties properties to partition
* @param includeBaseKey when true if a key exists with no '.' the key will be included.
* @return the partitioned properties where each key is the common prefix (minus the period) and the values are
* new property maps without the prefix and period in the key
* @since 2.17.2
*/
public static Map partitionOnCommonPrefixes(
final Properties properties, final boolean includeBaseKey) {
final Map parts = new ConcurrentHashMap<>();
for (final String key : properties.stringPropertyNames()) {
final int idx = key.indexOf('.');
if (idx < 0) {
if (includeBaseKey) {
if (!parts.containsKey(key)) {
parts.put(key, new Properties());
}
parts.get(key).setProperty("", properties.getProperty(key));
}
continue;
}
final String prefix = key.substring(0, idx);
if (!parts.containsKey(prefix)) {
parts.put(prefix, new Properties());
}
parts.get(prefix).setProperty(key.substring(idx + 1), properties.getProperty(key));
}
return parts;
}
/**
* Returns true if system properties tell us we are running on Windows.
*
* @return true if system properties tell us we are running on Windows.
*/
public boolean isOsWindows() {
return SystemPropertiesPropertySource.getSystemProperty("os.name", "").startsWith("Windows");
}
static Duration parseDuration(final CharSequence value) {
final Matcher matcher = DURATION_PATTERN.matcher(value);
if (matcher.matches()) {
return Duration.of(parseDurationAmount(matcher.group(1)), TimeUnit.parseUnit(matcher.group(2)));
}
throw new IllegalArgumentException("Invalid duration value '" + value + "'.");
}
private static long parseDurationAmount(final String amount) {
try {
return Long.parseLong(amount);
} catch (final NumberFormatException e) {
throw new IllegalArgumentException("Invalid duration amount '" + amount + "'", e);
}
}
private enum TimeUnit {
NANOS(new String[] {"ns", "nano", "nanos", "nanosecond", "nanoseconds"}, ChronoUnit.NANOS),
MICROS(new String[] {"us", "micro", "micros", "microsecond", "microseconds"}, ChronoUnit.MICROS),
MILLIS(new String[] {"ms", "milli", "millis", "millisecond", "milliseconds"}, ChronoUnit.MILLIS),
SECONDS(new String[] {"s", "second", "seconds"}, ChronoUnit.SECONDS),
MINUTES(new String[] {"m", "minute", "minutes"}, ChronoUnit.MINUTES),
HOURS(new String[] {"h", "hour", "hours"}, ChronoUnit.HOURS),
DAYS(new String[] {"d", "day", "days"}, ChronoUnit.DAYS);
private final String[] descriptions;
private final TemporalUnit timeUnit;
TimeUnit(final String[] descriptions, final TemporalUnit timeUnit) {
this.descriptions = descriptions;
this.timeUnit = timeUnit;
}
private static Stream getValidUnits() {
return Arrays.stream(values()).flatMap(unit -> Arrays.stream(unit.descriptions));
}
private static TemporalUnit parseUnit(final String unit) {
if (unit != null) {
for (final TimeUnit value : values()) {
for (final String description : value.descriptions) {
if (unit.equals(description)) {
return value.timeUnit;
}
}
}
throw new IllegalArgumentException("Invalid duration unit '" + unit + "'");
}
return ChronoUnit.MILLIS;
}
}
}