blob: 1efd7717388bced71c4b8918201ab81a517b7bdd [file] [log] [blame]
/*
* 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 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.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
/**
* <em>Consider this class private.</em>
* <p>
* 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.
* </p>
*
* @see PropertySource
*/
public final class PropertiesUtil {
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 PropertiesUtil LOG4J_PROPERTIES = new PropertiesUtil(LOG4J_PROPERTIES_FILE_NAME);
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.environment = new Environment(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.environment = new Environment(new PropertyFilePropertySource(propertiesFileName));
}
/**
* 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 e) {
LowLevelLogUtil.logException("Unable to read " + source, e);
} finally {
try {
in.close();
} catch (final IOException e) {
LowLevelLogUtil.logException("Unable to close " + source, e);
}
}
}
return props;
}
/**
* Returns the PropertiesUtil used by Log4j.
*
* @return the main Log4j PropertiesUtil instance.
*/
public static PropertiesUtil getProperties() {
return LOG4J_PROPERTIES;
}
/**
* 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<Boolean> 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);
}
}
LowLevelLogUtil.log("Unable to get Charset '" + charsetName + "' for property '" + name + "', using default "
+ defaultValue + " and continuing.");
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 Exception ignored) {
return defaultValue;
}
}
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);
} catch (final Exception ignored) {
return defaultValue;
}
}
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<Integer> 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 Exception ignored) {
return defaultValue;
}
}
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<Long> 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);
if (prop != null) {
return TimeUnit.getDuration(prop);
}
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<Duration> 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<String> 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 ex) {
LowLevelLogUtil.logException("Unable to access system properties.", ex);
// Sandboxed - can't read System Properties
return new Properties();
}
}
/**
* Reloads all properties. This is primarily useful for unit tests.
*
* @since 2.10.0
*/
public void reload() {
environment.reload();
}
/**
* Provides support for looking up global configuration properties via environment variables, property files,
* and system properties, in three variations:
* <p>
* 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.
* <p>
* Legacy: the original property name as defined in the source pre-2.10.0.
* <p>
* Tokenized: loose matching based on word boundaries.
*
* @since 2.10.0
*/
private static class Environment {
private final Set<PropertySource> sources = new TreeSet<>(new PropertySource.Comparator());
private final Map<CharSequence, String> literal = new ConcurrentHashMap<>();
private final Map<CharSequence, String> normalized = new ConcurrentHashMap<>();
private final Map<List<CharSequence>, String> tokenized = new ConcurrentHashMap<>();
private Environment(final PropertySource propertySource) {
final PropertyFilePropertySource sysProps = new PropertyFilePropertySource(LOG4J_SYSTEM_PROPERTIES_FILE_NAME);
try {
sysProps.forEach((key, value) -> {
if (System.getProperty(key) == null) {
System.setProperty(key, value);
}
});
} catch (final SecurityException ex) {
// Access to System Properties is restricted so just skip it.
}
sources.add(propertySource);
for (final ClassLoader classLoader : LoaderUtil.getClassLoaders()) {
try {
for (final PropertySource source : ServiceLoader.load(PropertySource.class, classLoader)) {
sources.add(source);
}
} catch (final Throwable ex) {
/* Don't log anything to the console. It may not be a problem that a PropertySource
* isn't accessible.
*/
}
}
reload();
}
private synchronized void reload() {
literal.clear();
normalized.clear();
tokenized.clear();
for (final PropertySource source : sources) {
source.forEach((key, value) -> {
if (key != null && value != null) {
literal.put(key, value);
final List<CharSequence> tokens = PropertySource.Util.tokenize(key);
if (tokens.isEmpty()) {
normalized.put(source.getNormalForm(Collections.singleton(key)), value);
} else {
normalized.put(source.getNormalForm(tokens), value);
tokenized.put(tokens, value);
}
}
});
}
}
private static boolean hasSystemProperty(final String key) {
try {
return System.getProperties().containsKey(key);
} catch (final SecurityException ignored) {
return false;
}
}
private String get(final String key) {
if (normalized.containsKey(key)) {
return normalized.get(key);
}
if (literal.containsKey(key)) {
return literal.get(key);
}
if (hasSystemProperty(key)) {
return System.getProperty(key);
}
for (final PropertySource source : sources) {
if (source.containsProperty(key)) {
return source.getProperty(key);
}
}
return tokenized.get(PropertySource.Util.tokenize(key));
}
private boolean containsKey(final String key) {
return normalized.containsKey(key) ||
literal.containsKey(key) ||
hasSystemProperty(key) ||
tokenized.containsKey(PropertySource.Util.tokenize(key));
}
}
/**
* 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.length() == 0) {
return subset;
}
final String prefixToMatch = prefix.charAt(prefix.length() - 1) != '.' ? prefix + '.' : prefix;
final List<String> 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<String, Properties> partitionOnCommonPrefixes(final Properties properties) {
final Map<String, Properties> parts = new ConcurrentHashMap<>();
for (final String key : properties.stringPropertyNames()) {
final String prefix = key.substring(0, key.indexOf('.'));
if (!parts.containsKey(prefix)) {
parts.put(prefix, new Properties());
}
parts.get(prefix).setProperty(key.substring(key.indexOf('.') + 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 getStringProperty("os.name", "").startsWith("Windows");
}
private enum TimeUnit {
NANOS("ns,nano,nanos,nanosecond,nanoseconds", ChronoUnit.NANOS),
MICROS("us,micro,micros,microsecond,microseconds", ChronoUnit.MICROS),
MILLIS("ms,milli,millis,millsecond,milliseconds", ChronoUnit.MILLIS),
SECONDS("s,second,seconds", ChronoUnit.SECONDS),
MINUTES("m,minute,minutes", ChronoUnit.MINUTES),
HOURS("h,hour,hours", ChronoUnit.HOURS),
DAYS("d,day,days", ChronoUnit.DAYS);
private final String[] descriptions;
private final ChronoUnit timeUnit;
TimeUnit(final String descriptions, final ChronoUnit timeUnit) {
this.descriptions = descriptions.split(",");
this.timeUnit = timeUnit;
}
ChronoUnit getTimeUnit() {
return this.timeUnit;
}
static Duration getDuration(final String time) {
final String value = time.trim();
TemporalUnit temporalUnit = ChronoUnit.MILLIS;
long timeVal = 0;
for (final TimeUnit timeUnit : values()) {
for (final String suffix : timeUnit.descriptions) {
if (value.endsWith(suffix)) {
temporalUnit = timeUnit.timeUnit;
timeVal = Long.parseLong(value.substring(0, value.length() - suffix.length()));
}
}
}
return Duration.of(timeVal, temporalUnit);
}
}
}