org.apache.logging.log4j.util.PropertiesUtil Maven / Gradle / Ivy
The newest version!
/*
* 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.spi.ServiceConsumer;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
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.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
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.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.apache.logging.log4j.internal.CopyOnWriteNavigableSet;
/**
* 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_SYSTEM_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
*/
@InternalApi
@ServiceConsumer(value = PropertySource.class, cardinality = Cardinality.MULTIPLE)
public class PropertiesUtil implements PropertyEnvironment {
private static final String LOG4J_NAMESPACE = "component";
private static final String LOG4J_CONTEXT_PREFIX = "log4j2.context.";
private static final String DELIMITER = ".";
private static final String META_INF = "META-INF/";
private static final String LOG4J_SYSTEM_PROPERTIES_FILE_NAME = "log4j2.system.properties";
private static final Lazy> SERVICE_PROPERTY_SOURCES =
Lazy.lazy(() -> ServiceLoaderUtil.safeStream(
ServiceLoader.load(PropertySource.class, PropertiesUtil.class.getClassLoader()))
.collect(Collectors.toList()));
private static final Lazy COMPONENT_PROPERTIES = Lazy.lazy(PropertiesUtil::new);
private static final ThreadLocal environments = new InheritableThreadLocal<>();
private final Environment environment;
private final Lock reloadLock = new ReentrantLock();
/**
* 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 using a given Properties object as its source of defined properties.
*
* @param props the Properties to use by default
* @param includeInvalid includes invalid properties.
*/
public PropertiesUtil(final Properties props, final boolean includeInvalid) {
this(new PropertiesPropertySource(
props, PropertySource.SYSTEM_CONTEXT, PropertySource.DEFAULT_PRIORITY, includeInvalid));
}
/**
* 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(PropertySource.SYSTEM_CONTEXT, propertiesFileName);
}
/**
* 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 contextName, final String propertiesFileName) {
final List sources = new ArrayList<>();
if (propertiesFileName.endsWith(".json") || propertiesFileName.endsWith(".jsn")) {
final PropertySource source = getJsonPropertySource(propertiesFileName, 50);
if (source != null) {
sources.add(source);
}
} else {
final Properties properties = PropertyFilePropertySource.loadPropertiesFile(propertiesFileName);
final PropertySource source = new PropertiesPropertySource(properties, null, 60, true);
sources.add(source);
}
this.environment = new Environment(contextName, sources);
}
private PropertiesUtil() {
this.environment = getEnvironment(LOG4J_NAMESPACE);
}
/**
* Constructs a PropertiesUtil for a given property source as source of additional properties.
* @param source a property source
*/
public PropertiesUtil(final PropertySource source) {
final List sources = Collections.singletonList(source);
this.environment = new Environment(PropertySource.SYSTEM_CONTEXT, sources);
}
private PropertiesUtil(final String contextName, final List sources) {
this.environment = new Environment(contextName, sources);
}
public static boolean hasThreadProperties() {
return environments.get() != null;
}
public static void setThreadProperties(final PropertiesUtil properties) {
environments.set(properties);
}
public static void clearThreadProperties() {
environments.remove();
}
/**
* Returns the PropertiesUtil used by Log4j.
*
* @return the main Log4j PropertiesUtil instance.
*/
public static PropertiesUtil getProperties() {
final PropertiesUtil props = environments.get();
if (props != null) {
return props;
}
return COMPONENT_PROPERTIES.get();
}
public static PropertyEnvironment getProperties(final String namespace) {
return getEnvironment(namespace);
}
private static Environment getEnvironment(final String namespace) {
final List sources = new ArrayList<>();
final String fileName = String.format("log4j2.%s.properties", namespace);
final Properties properties = PropertyFilePropertySource.loadPropertiesFile(fileName);
PropertySource source = new PropertiesPropertySource(properties, 50);
sources.add(source);
source = getJsonPropertySource(String.format("log4j2.%s.json", namespace), 60);
if (source != null) {
sources.add(source);
}
return new Environment(PropertySource.SYSTEM_CONTEXT, sources);
}
public static ResourceBundle getCharsetsResourceBundle() {
return ResourceBundle.getBundle("Log4j-charsets");
}
@Override
public void addPropertySource(final PropertySource propertySource) {
if (environment != null) {
environment.addPropertySource(propertySource);
}
}
@Override
public void removePropertySource(final PropertySource propertySource) {
if (environment != null) {
environment.removePropertySource(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
*/
@Override
public boolean hasProperty(final String name) {
return environment.hasProperty(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
*/
@SuppressWarnings("deprecation")
public Boolean getBooleanProperty(final String[] prefixes, final String key, final Supplier supplier) {
for (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) {
// returns default value
}
}
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 Exception ignored) {
// ignore
}
}
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
*/
@SuppressWarnings("deprecation")
public Integer getIntegerProperty(final String[] prefixes, final String key, final Supplier supplier) {
for (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) {
// returns the default value
}
}
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
*/
@SuppressWarnings("deprecation")
public Long getLongProperty(final String[] prefixes, final String key, final Supplier supplier) {
for (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
*/
@SuppressWarnings("deprecation")
public Duration getDurationProperty(final String[] prefixes, final String key, final Supplier supplier) {
for (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
*/
@SuppressWarnings("deprecation")
public String getStringProperty(final String[] prefixes, final String key, final Supplier supplier) {
for (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.
*/
@Override
public String getStringProperty(final String name) {
return environment.getStringProperty(name);
}
/**
* Reloads all properties. This is primarily useful for unit tests.
*
* @since 2.10.0
*/
public void reload() {
reloadLock.lock();
try {
environment.reload();
} finally {
reloadLock.unlock();
}
}
/**
* Get the properties for a LoggerContext just by the name. This ALWAYS creates a new PropertiesUtil. Use
* locateProperties to obtain the current properties.
* @param contextName The context name.
* @return The PropertiesUtil that was created.
*/
public static PropertiesUtil getContextProperties(final String contextName) {
return getContextProperties(contextName, getProperties());
}
/**
* Get the properties for a LoggerContext just by the name. This method is used for testing.
* @param contextName The context name.
* @return The PropertiesUtil that was created.
*/
public static PropertiesUtil getContextProperties(final String contextName, final PropertiesUtil propertiesUtil) {
final ClassLoader classLoader = PropertiesUtil.class.getClassLoader();
final String filePrefix = META_INF + PropertySource.PREFIX + contextName + DELIMITER;
final List contextSources = new ArrayList<>();
contextSources.addAll(getContextPropertySources(classLoader, filePrefix, contextName));
contextSources.addAll(propertiesUtil.environment.sources);
return new PropertiesUtil(contextName, contextSources);
}
/**
* Get the properties associated with a LoggerContext that is associated with a ClassLoader. This ALWAYS creates
* a new PropertiesUtil. Use locateProperties to obtain the current properties.
* @param classLoader The ClassLoader.
* @param contextName The context name.
* @return The PropertiesUtil created.
*/
public static PropertiesUtil getContextProperties(final ClassLoader classLoader, final String contextName) {
String filePrefix = META_INF + PropertySource.PREFIX + contextName + DELIMITER;
final List contextSources =
new ArrayList<>(getContextPropertySources(classLoader, filePrefix, contextName));
filePrefix = META_INF + LOG4J_CONTEXT_PREFIX;
contextSources.addAll(getContextPropertySources(classLoader, filePrefix, contextName));
contextSources.addAll(getProperties().environment.sources);
return new PropertiesUtil(contextName, contextSources);
}
private static List getContextPropertySources(
final ClassLoader classLoader, final String filePrefix, final String contextName) {
final List sources = new ArrayList<>();
String fileName = filePrefix + "json";
try {
final Enumeration urls = classLoader.getResources(fileName);
while (urls.hasMoreElements()) {
final URL url = urls.nextElement();
try (final InputStream is = url.openStream()) {
final String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
final PropertySource propertySource = parseJsonProperties(json, contextName, 110);
sources.add(propertySource);
} catch (Exception ex) {
LowLevelLogUtil.logException("Unable to parse JSON for " + url.toString(), ex);
}
}
} catch (Exception ex) {
LowLevelLogUtil.logException("Unable to access " + fileName, ex);
}
fileName = filePrefix + "properties";
try {
final Enumeration urls = classLoader.getResources(fileName);
while (urls.hasMoreElements()) {
final URL url = urls.nextElement();
try (final InputStream is = url.openStream()) {
final Properties props = new Properties();
props.load(is);
final PropertySource source = new PropertiesPropertySource(props, contextName, 120);
sources.add(source);
} catch (Exception ex) {
LowLevelLogUtil.logException("Unable to access properties for " + url.toString(), ex);
}
}
} catch (Exception ex) {
LowLevelLogUtil.logException("Unable to access " + fileName, ex);
}
return sources;
}
private static PropertySource getJsonPropertySource(final String fileName, int priority) {
if (fileName.startsWith("file://")) {
try {
final URL url = new URL(fileName);
try (final InputStream is = url.openStream()) {
return parseJsonProperties(
new String(is.readAllBytes(), StandardCharsets.UTF_8),
PropertySource.SYSTEM_CONTEXT,
priority);
}
} catch (Exception ex) {
LowLevelLogUtil.logException("Unable to read " + fileName, ex);
}
} else {
final File file = new File(fileName);
if (file.exists()) {
try (var is = new FileInputStream(file)) {
return parseJsonProperties(
new String(is.readAllBytes(), StandardCharsets.UTF_8),
PropertySource.SYSTEM_CONTEXT,
priority);
} catch (IOException ioe) {
LowLevelLogUtil.logException("Unable to read " + fileName, ioe);
}
} else {
for (final URL url : LoaderUtil.findResources(fileName)) {
try (final InputStream in = url.openStream()) {
return parseJsonProperties(
new String(in.readAllBytes(), StandardCharsets.UTF_8),
PropertySource.SYSTEM_CONTEXT,
priority);
} catch (final IOException e) {
LowLevelLogUtil.logException("Unable to read " + url, e);
}
}
}
}
return null;
}
private static PropertySource parseJsonProperties(final String json, final String contextName, final int priority) {
final Map root = Cast.cast(JsonReader.read(json));
final Properties props = new Properties();
populateProperties(props, "", root);
return new PropertiesPropertySource(props, contextName, priority);
}
private static void populateProperties(
final Properties props, final String prefix, final Map root) {
if (!root.isEmpty()) {
for (Map.Entry entry : root.entrySet()) {
if (entry.getValue() instanceof String) {
props.setProperty(createKey(prefix, entry.getKey()), (String) entry.getValue());
} else if (entry.getValue() instanceof List) {
final StringBuilder sb = new StringBuilder();
final List