propertyClass);
+}
diff --git a/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/PropertySource.java b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/PropertySource.java
new file mode 100644
index 00000000000..27f29bb723e
--- /dev/null
+++ b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/PropertySource.java
@@ -0,0 +1,47 @@
+/*
+ * 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.kit.env;
+
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Basic interface to retrieve property values.
+ *
+ * We can not reuse the property sources from 2.x, since those required some sort of {@code log4j} prefix to be
+ * included. In 3.x we want to use keys without a prefix.
+ *
+ */
+public interface PropertySource {
+ /**
+ * Provides the priority of the property source.
+ *
+ * Sources with higher priority override values from sources with lower priority.
+ *
+ *
+ * @return priority value
+ */
+ int getPriority();
+
+ /**
+ * 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.
+ */
+ @Nullable
+ String getProperty(String name);
+}
diff --git a/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/ContextualEnvironmentPropertySource.java b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/ContextualEnvironmentPropertySource.java
new file mode 100644
index 00000000000..54a4cdede6d
--- /dev/null
+++ b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/ContextualEnvironmentPropertySource.java
@@ -0,0 +1,71 @@
+/*
+ * 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.kit.env.internal;
+
+import java.util.Locale;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.PropertySource;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * PropertySource backed by the current environment variables.
+ *
+ * Should haves a slightly lower priority than global environment variables.
+ *
+ */
+public class ContextualEnvironmentPropertySource implements PropertySource {
+
+ private static final int DEFAULT_PRIORITY = 0;
+
+ private final String prefix;
+ private final int priority;
+
+ public ContextualEnvironmentPropertySource(final String contextName) {
+ this(contextName, DEFAULT_PRIORITY);
+ }
+
+ public ContextualEnvironmentPropertySource(final String contextName, final int priority) {
+ this.prefix = "log4j2." + contextName + ".";
+ this.priority = priority;
+ }
+
+ @Override
+ public int getPriority() {
+ return priority;
+ }
+
+ @Override
+ public @Nullable String getProperty(final String key) {
+ final String actualKey = key.replace('.', '_').toUpperCase(Locale.ROOT);
+ try {
+ return System.getenv(prefix + actualKey);
+ } catch (final SecurityException e) {
+ StatusLogger.getLogger()
+ .warn(
+ "{} lacks permissions to access system property {}.",
+ getClass().getName(),
+ actualKey,
+ e);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean containsProperty(final String key) {
+ return getProperty(key) != null;
+ }
+}
diff --git a/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/ContextualJavaPropsPropertySource.java b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/ContextualJavaPropsPropertySource.java
new file mode 100644
index 00000000000..16edc814b46
--- /dev/null
+++ b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/ContextualJavaPropsPropertySource.java
@@ -0,0 +1,70 @@
+/*
+ * 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.kit.env.internal;
+
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.PropertySource;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * PropertySource backed by the current system properties.
+ *
+ * Should have a slightly lower priority than global system properties.
+ *
+ */
+public class ContextualJavaPropsPropertySource implements PropertySource {
+
+ private static final int DEFAULT_PRIORITY = 100;
+
+ private final String prefix;
+ private final int priority;
+
+ public ContextualJavaPropsPropertySource(final String contextName) {
+ this(contextName, DEFAULT_PRIORITY);
+ }
+
+ public ContextualJavaPropsPropertySource(final String contextName, final int priority) {
+ this.prefix = "log4j.contexts." + contextName + ".";
+ this.priority = priority;
+ }
+
+ @Override
+ public int getPriority() {
+ return priority;
+ }
+
+ @Override
+ public @Nullable String getProperty(final String key) {
+ final String actualKey = prefix + key;
+ try {
+ return System.getProperty(actualKey);
+ } catch (final SecurityException e) {
+ StatusLogger.getLogger()
+ .warn(
+ "{} lacks permissions to access system property {}.",
+ getClass().getName(),
+ actualKey,
+ e);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean containsProperty(final String key) {
+ return getProperty(key) != null;
+ }
+}
diff --git a/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/PropertiesUtilPropertyEnvironment.java b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/PropertiesUtilPropertyEnvironment.java
new file mode 100644
index 00000000000..8d432204d01
--- /dev/null
+++ b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/PropertiesUtilPropertyEnvironment.java
@@ -0,0 +1,48 @@
+/*
+ * 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.kit.env.internal;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.kit.env.PropertyEnvironment;
+import org.apache.logging.log4j.kit.env.support.BasicPropertyEnvironment;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.PropertiesUtil;
+
+/**
+ * An adapter of the {@link PropertiesUtil} from Log4j API 2.x.
+ *
+ * @implNote Since {@link PropertiesUtil} requires all properties to start with {@code log4j2.}, we must add the prefix
+ * before querying for the property.
+ */
+public class PropertiesUtilPropertyEnvironment extends BasicPropertyEnvironment {
+
+ private static final String PREFIX = "log4j2.";
+ public static final PropertyEnvironment INSTANCE =
+ new PropertiesUtilPropertyEnvironment(PropertiesUtil.getProperties(), StatusLogger.getLogger());
+
+ private final PropertiesUtil propsUtil;
+
+ public PropertiesUtilPropertyEnvironment(final PropertiesUtil propsUtil, final Logger statusLogger) {
+ super(statusLogger);
+ this.propsUtil = propsUtil;
+ }
+
+ @Override
+ public String getStringProperty(final String name) {
+ return propsUtil.getStringProperty(PREFIX + name);
+ }
+}
diff --git a/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/package-info.java b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/package-info.java
new file mode 100644
index 00000000000..bb068e665d9
--- /dev/null
+++ b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/internal/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+@Export
+@NullMarked
+@Version("3.0.0")
+package org.apache.logging.log4j.kit.env.internal;
+
+import org.jspecify.annotations.NullMarked;
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/package-info.java b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/package-info.java
new file mode 100644
index 00000000000..e8ff76f849a
--- /dev/null
+++ b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+@Export
+@NullMarked
+@Version("3.0.0")
+package org.apache.logging.log4j.kit.env;
+
+import org.jspecify.annotations.NullMarked;
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/BasicPropertyEnvironment.java b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/BasicPropertyEnvironment.java
new file mode 100644
index 00000000000..abdc482c489
--- /dev/null
+++ b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/BasicPropertyEnvironment.java
@@ -0,0 +1,322 @@
+/*
+ * 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.kit.env.support;
+
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Parameter;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.kit.env.Log4jProperty;
+import org.apache.logging.log4j.kit.env.PropertyEnvironment;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * An implementation of {@link PropertyEnvironment} that only uses basic Java functions.
+ *
+ * Conversion problems are logged using a status logger.
+ *
+ */
+public abstract class BasicPropertyEnvironment implements PropertyEnvironment {
+
+ private final Logger statusLogger;
+
+ protected BasicPropertyEnvironment(final Logger statusLogger) {
+ this.statusLogger = statusLogger;
+ }
+
+ @Override
+ public Boolean getBooleanProperty(final String name, final Boolean defaultValue) {
+ return getObjectPropertyWithTypedDefault(name, this::toBoolean, defaultValue);
+ }
+
+ @Override
+ public Charset getCharsetProperty(final String name, final Charset defaultValue) {
+ return getObjectPropertyWithTypedDefault(name, this::toCharset, defaultValue);
+ }
+
+ @Override
+ public @Nullable Class extends T> getClassProperty(final String name, final Class upperBound) {
+ return getClassProperty(name, null, upperBound);
+ }
+
+ @Override
+ public Class extends T> getClassProperty(
+ final String name, final Class extends T> defaultValue, final Class upperBound) {
+ return getObjectPropertyWithTypedDefault(name, className -> toClass(className, upperBound), defaultValue);
+ }
+
+ @Override
+ public Duration getDurationProperty(final String name, final Duration defaultValue) {
+ return getObjectPropertyWithTypedDefault(name, this::toDuration, defaultValue);
+ }
+
+ @Override
+ public Integer getIntegerProperty(final String name, final Integer defaultValue) {
+ return getObjectPropertyWithTypedDefault(name, this::toInteger, defaultValue);
+ }
+
+ @Override
+ public Long getLongProperty(final String name, final Long defaultValue) {
+ return getObjectPropertyWithTypedDefault(name, this::toLong, defaultValue);
+ }
+
+ @Override
+ public abstract @Nullable String getStringProperty(String name);
+
+ @Override
+ public T getProperty(final Class propertyClass) {
+ if (!propertyClass.isAnnotationPresent(Log4jProperty.class)) {
+ throw new IllegalArgumentException("Unsupported configuration properties class '" + propertyClass.getName()
+ + "': missing '@Log4jProperty' annotation.");
+ }
+ return getRecordProperty(null, propertyClass);
+ }
+
+ protected Class> getClassForName(final String className) throws ReflectiveOperationException {
+ return Class.forName(className);
+ }
+
+ protected Boolean toBoolean(final String value) {
+ return Boolean.valueOf(value);
+ }
+
+ protected @Nullable Charset toCharset(final String value) {
+ try {
+ return Charset.forName(value);
+ } catch (final IllegalCharsetNameException | UnsupportedOperationException e) {
+ statusLogger.warn("Invalid Charset value '{}': {}", value, e.getMessage(), e);
+ }
+ return null;
+ }
+
+ protected @Nullable Duration toDuration(final CharSequence value) {
+ try {
+ return Duration.parse(value);
+ } catch (final DateTimeParseException e) {
+ statusLogger.warn("Invalid Duration value '{}': {}", value, e.getMessage(), e);
+ }
+ return null;
+ }
+
+ protected char[] toCharArray(final String value) {
+ return value.toCharArray();
+ }
+
+ @SuppressWarnings("unchecked")
+ protected @Nullable Class extends T> toClass(final String className, final Class upperBound) {
+ try {
+ final Class> clazz = getClassForName(className);
+ if (upperBound.isAssignableFrom(clazz)) {
+ return (Class extends T>) clazz;
+ }
+ statusLogger.warn("Invalid Class value '{}': class does not extend {}.", className, upperBound.getName());
+ } catch (final ReflectiveOperationException e) {
+ statusLogger.warn("Invalid Class value '{}': {}", className, e.getMessage(), e);
+ }
+ return null;
+ }
+
+ protected > @Nullable T toEnum(final String value, final Class enumClass) {
+ try {
+ return Enum.valueOf(enumClass, value);
+ } catch (final IllegalArgumentException e) {
+ statusLogger.warn("Invalid enum value '{}' of type {}.", value, enumClass.getName(), e);
+ }
+ return null;
+ }
+
+ protected @Nullable Integer toInteger(final String value) {
+ try {
+ return Integer.valueOf(value);
+ } catch (final NumberFormatException e) {
+ statusLogger.warn("Invalid integer value '{}': {}.", value, e.getMessage(), e);
+ }
+ return null;
+ }
+
+ protected @Nullable Long toLong(final String value) {
+ try {
+ return Long.valueOf(value);
+ } catch (final NumberFormatException e) {
+ statusLogger.warn("Invalid long value '{}': {}.", value, e.getMessage(), e);
+ }
+ return null;
+ }
+
+ protected @Nullable Level toLevel(final String value) {
+ return Level.toLevel(value, null);
+ }
+
+ private T getRecordProperty(final @Nullable String parentPrefix, final Class propertyClass) {
+ if (!propertyClass.isRecord()) {
+ throw new IllegalArgumentException("Unsupported configuration properties class '" + propertyClass.getName()
+ + "': class is not a record.");
+ }
+ final String prefix =
+ parentPrefix != null ? parentPrefix : getPropertyName(propertyClass, propertyClass::getSimpleName);
+
+ @SuppressWarnings("unchecked")
+ final Constructor[] constructors = (Constructor[]) propertyClass.getDeclaredConstructors();
+ if (constructors.length == 0) {
+ throw new IllegalArgumentException("Unsupported configuration properties class '" + propertyClass.getName()
+ + "': missing public constructor.");
+ } else if (constructors.length > 1) {
+ throw new IllegalArgumentException("Unsupported configuration properties class '" + propertyClass.getName()
+ + "': more than one constructor found.");
+ }
+ final Constructor constructor = constructors[0];
+
+ final Parameter[] parameters = constructor.getParameters();
+ final @Nullable Object[] initArgs = new Object[parameters.length];
+ for (int i = 0; i < initArgs.length; i++) {
+ final String name = prefix + "." + getPropertyName(parameters[i], parameters[i]::getName);
+ final String defaultValue = getPropertyDefaultAsString(parameters[i]);
+ initArgs[i] = getObjectProperty(name, parameters[i].getParameterizedType(), defaultValue);
+ }
+ try {
+ return constructor.newInstance(initArgs);
+ } catch (final ReflectiveOperationException e) {
+ throw new IllegalArgumentException(
+ "Unable to parse configuration properties class " + propertyClass.getName() + ": " + e.getMessage(),
+ e);
+ }
+ }
+
+ private @Nullable Object getObjectProperty(
+ final String name, final Type type, final @Nullable String defaultValue) {
+ if (type instanceof final ParameterizedType parameterizedType
+ && parameterizedType.getRawType().equals(Class.class)) {
+ final Type[] arguments = parameterizedType.getActualTypeArguments();
+ final Class> upperBound = arguments.length > 0 ? findUpperBound(arguments[0]) : Object.class;
+ return getObjectPropertyWithStringDefault(name, defaultValue, className -> toClass(className, upperBound));
+ }
+ if (type instanceof final Class> clazz) {
+ if (clazz.isRecord()) {
+ return getRecordProperty(name, clazz);
+ }
+ if (char[].class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(name, defaultValue, this::toCharArray);
+ }
+ if (boolean.class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(
+ name, Objects.toString(defaultValue, "false"), this::toBoolean);
+ }
+ if (Boolean.class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(name, defaultValue, this::toBoolean);
+ }
+ if (Charset.class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(name, defaultValue, this::toCharset);
+ }
+ if (Duration.class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(name, defaultValue, this::toDuration);
+ }
+ if (Enum.class.isAssignableFrom(clazz)) {
+ return getObjectPropertyWithStringDefault(
+ name, defaultValue, value -> toEnum(value, (Class extends Enum>) clazz));
+ }
+ if (int.class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(name, Objects.toString(defaultValue, "0"), this::toInteger);
+ }
+ if (Integer.class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(name, defaultValue, this::toInteger);
+ }
+ if (long.class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(name, Objects.toString(defaultValue, "0"), this::toLong);
+ }
+ if (Long.class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(name, defaultValue, this::toLong);
+ }
+ if (Level.class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(name, defaultValue, this::toLevel);
+ }
+ if (String.class.equals(clazz)) {
+ return getObjectPropertyWithStringDefault(name, defaultValue, x -> x);
+ }
+ }
+ throw new IllegalArgumentException("Unsupported property of type '" + type.getTypeName() + "'");
+ }
+
+ private Class> findUpperBound(final Type type) {
+ final Type[] bounds;
+ if (type instanceof final TypeVariable> typeVariable) {
+ bounds = typeVariable.getBounds();
+ } else if (type instanceof final WildcardType wildcardType) {
+ bounds = wildcardType.getUpperBounds();
+ } else {
+ bounds = new Type[0];
+ }
+ return bounds.length > 0 && bounds[0] instanceof final Class> clazz ? clazz : Object.class;
+ }
+
+ private String getPropertyName(final AnnotatedElement element, final Supplier fallback) {
+ if (element.isAnnotationPresent(Log4jProperty.class)) {
+ final String specifiedName =
+ element.getAnnotation(Log4jProperty.class).name();
+ if (!specifiedName.isEmpty()) {
+ return specifiedName;
+ }
+ }
+ return fallback.get();
+ }
+
+ private @Nullable String getPropertyDefaultAsString(final AnnotatedElement parameter) {
+ if (parameter.isAnnotationPresent(Log4jProperty.class)) {
+ final String defaultValue =
+ parameter.getAnnotation(Log4jProperty.class).defaultValue();
+ if (!defaultValue.isEmpty()) {
+ return defaultValue;
+ }
+ }
+ return null;
+ }
+
+ private @Nullable Object getObjectPropertyWithStringDefault(
+ final String name, final @Nullable String defaultValue, final Function super String, ?> converter) {
+ final String prop = getStringProperty(name);
+ if (prop != null) {
+ final @Nullable Object value = converter.apply(prop);
+ if (value != null) {
+ return value;
+ }
+ }
+ return defaultValue != null ? converter.apply(defaultValue) : null;
+ }
+
+ private T getObjectPropertyWithTypedDefault(
+ final String name, final Function super String, ? extends @Nullable T> converter, final T defaultValue) {
+ final String prop = getStringProperty(name);
+ if (prop != null) {
+ final @Nullable T value = converter.apply(prop);
+ if (value != null) {
+ return value;
+ }
+ }
+ return defaultValue;
+ }
+}
diff --git a/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/ClassLoaderPropertyEnvironment.java b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/ClassLoaderPropertyEnvironment.java
new file mode 100644
index 00000000000..e92ed5dff1a
--- /dev/null
+++ b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/ClassLoaderPropertyEnvironment.java
@@ -0,0 +1,37 @@
+/*
+ * 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.kit.env.support;
+
+import org.apache.logging.log4j.Logger;
+
+/**
+ * An environment implementation that uses a specific classloader to load classes.
+ */
+public abstract class ClassLoaderPropertyEnvironment extends BasicPropertyEnvironment {
+
+ private final ClassLoader loader;
+
+ public ClassLoaderPropertyEnvironment(final ClassLoader loader, final Logger statusLogger) {
+ super(statusLogger);
+ this.loader = loader;
+ }
+
+ @Override
+ protected Class> getClassForName(final String className) throws ReflectiveOperationException {
+ return Class.forName(className, true, loader);
+ }
+}
diff --git a/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/CompositePropertyEnvironment.java b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/CompositePropertyEnvironment.java
new file mode 100644
index 00000000000..a5047a33943
--- /dev/null
+++ b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/CompositePropertyEnvironment.java
@@ -0,0 +1,69 @@
+/*
+ * 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.kit.env.support;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.TreeSet;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.kit.env.PropertyEnvironment;
+import org.apache.logging.log4j.kit.env.PropertySource;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * An environment implementation that supports multiple {@link PropertySource}s.
+ */
+public class CompositePropertyEnvironment extends ClassLoaderPropertyEnvironment {
+
+ private final Collection sources =
+ new TreeSet<>(Comparator.comparing(PropertySource::getPriority).reversed());
+
+ public CompositePropertyEnvironment(
+ final @Nullable PropertyEnvironment parentEnvironment,
+ final Collection extends PropertySource> sources,
+ final ClassLoader loader,
+ final Logger statusLogger) {
+ super(loader, statusLogger);
+ this.sources.addAll(sources);
+ if (parentEnvironment != null) {
+ this.sources.add(new ParentEnvironmentPropertySource(parentEnvironment));
+ }
+ }
+
+ @Override
+ public @Nullable String getStringProperty(final String name) {
+ return sources.stream()
+ .map(source -> source.getProperty(name))
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElse(null);
+ }
+
+ private record ParentEnvironmentPropertySource(PropertyEnvironment parentEnvironment) implements PropertySource {
+
+ @Override
+ public int getPriority() {
+ return Integer.MIN_VALUE;
+ }
+
+ @Override
+ public @Nullable String getProperty(final String name) {
+ return parentEnvironment.getStringProperty(name);
+ }
+ }
+}
diff --git a/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/package-info.java b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/package-info.java
new file mode 100644
index 00000000000..af29b5a5a37
--- /dev/null
+++ b/log4j-kit/src/main/java/org/apache/logging/log4j/kit/env/support/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+@Export
+@NullMarked
+@Version("3.0.0")
+package org.apache.logging.log4j.kit.env.support;
+
+import org.jspecify.annotations.NullMarked;
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/log4j-kit/src/test/java/org/apache/logging/log4j/kit/env/support/BasicPropertyEnvironmentTest.java b/log4j-kit/src/test/java/org/apache/logging/log4j/kit/env/support/BasicPropertyEnvironmentTest.java
new file mode 100644
index 00000000000..4716ef565c1
--- /dev/null
+++ b/log4j-kit/src/test/java/org/apache/logging/log4j/kit/env/support/BasicPropertyEnvironmentTest.java
@@ -0,0 +1,226 @@
+/*
+ * 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.kit.env.support;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.nio.charset.Charset;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.kit.env.Log4jProperty;
+import org.apache.logging.log4j.kit.env.PropertyEnvironment;
+import org.apache.logging.log4j.kit.logger.TestListLogger;
+import org.apache.logging.log4j.spi.StandardLevel;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.assertj.core.api.Assertions;
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests the property values that are used as properties.
+ */
+class BasicPropertyEnvironmentTest {
+
+ @Log4jProperty
+ record BasicValues(boolean boolAttr, int intAttr, long longAttr) {}
+
+ @Log4jProperty
+ record DefaultBasicValues(
+ @Log4jProperty(defaultValue = "true") boolean boolAttr,
+ @Log4jProperty(defaultValue = "123") int intAttr,
+ @Log4jProperty(defaultValue = "123456") long longAttr) {}
+
+ @Log4jProperty(name = "BasicValues")
+ record BoxedBasicValues(@Nullable Boolean boolAttr, @Nullable Integer intAttr, @Nullable Long longAttr) {}
+
+ private static final Map BASIC_PROPS =
+ Map.of("BasicValues.boolAttr", "true", "BasicValues.intAttr", "123", "BasicValues.longAttr", "123456");
+
+ @Test
+ void should_support_basic_values() {
+ assertMapConvertsTo(Map.of(), new BasicValues(false, 0, 0L));
+ assertMapConvertsTo(BASIC_PROPS, new BasicValues(true, 123, 123456));
+ // Default values
+ assertMapConvertsTo(Map.of(), new DefaultBasicValues(true, 123, 123456));
+ }
+
+ @Test
+ void should_support_boxed_values() {
+ assertMapConvertsTo(Map.of(), new BoxedBasicValues(null, null, null));
+ assertMapConvertsTo(BASIC_PROPS, new BoxedBasicValues(true, 123, 123456L));
+ // No need to test default values, since properties with a default value should be primitives
+ }
+
+ @Log4jProperty
+ record ScalarValues(
+ @Nullable Charset charsetAttr,
+ @Nullable Duration durationAttr,
+ @Nullable String stringAttr,
+ @Nullable StandardLevel enumAttr,
+ @Nullable Level levelAttr) {}
+
+ @Log4jProperty
+ record DefaultScalarValues(
+ @Log4jProperty(defaultValue = "UTF-8") Charset charsetAttr,
+ @Log4jProperty(defaultValue = "PT8H") Duration durationAttr,
+ @Log4jProperty(defaultValue = "Hello child!") String stringAttr,
+ @Log4jProperty(defaultValue = "WARN") StandardLevel enumAttr,
+ @Log4jProperty(defaultValue = "INFO") Level levelAttr) {}
+
+ private static final Map SCALAR_PROPS = Map.of(
+ "ScalarValues.charsetAttr",
+ "UTF-8",
+ "ScalarValues.durationAttr",
+ "PT8H",
+ "ScalarValues.stringAttr",
+ "Hello child!",
+ "ScalarValues.enumAttr",
+ "WARN",
+ "ScalarValues.levelAttr",
+ "INFO");
+
+ @Test
+ void should_support_scalar_values() {
+ assertMapConvertsTo(Map.of(), new ScalarValues(null, null, null, null, null));
+ assertMapConvertsTo(
+ SCALAR_PROPS,
+ new ScalarValues(UTF_8, Duration.ofHours(8), "Hello child!", StandardLevel.WARN, Level.INFO));
+ // Default values
+ assertMapConvertsTo(
+ SCALAR_PROPS,
+ new DefaultScalarValues(UTF_8, Duration.ofHours(8), "Hello child!", StandardLevel.WARN, Level.INFO));
+ }
+
+ @Log4jProperty
+ record ArrayValues(char @Nullable [] password) {}
+
+ private static final Map ARRAY_PROPS = Map.of("ArrayValues.password", "changeit");
+
+ @Test
+ void should_support_arrays_of_scalars() {
+ final TestListLogger logger = new TestListLogger(BasicPropertyEnvironmentTest.class.getName());
+ // Missing properties
+ PropertyEnvironment env = new TestPropertyEnvironment(Map.of(), logger);
+ ArrayValues actual = env.getProperty(ArrayValues.class);
+ assertThat(actual.password()).isNull();
+ // With properties
+ env = new TestPropertyEnvironment(ARRAY_PROPS, logger);
+ actual = env.getProperty(ArrayValues.class);
+ assertThat(actual.password()).containsExactly("changeit".toCharArray());
+ // Check for warnings
+ assertThat(logger.getMessages()).isEmpty();
+ }
+
+ @Log4jProperty
+ record Component(@Nullable String type, SubComponent subComponent) {}
+
+ // Subcomponents shouldn't be annotated.
+ record SubComponent(@Nullable String type) {}
+
+ private static final Map COMPONENT_PROPS =
+ Map.of("Component.type", "COMPONENT", "Component.subComponent.type", "SUBCOMPONENT");
+
+ @Test
+ void should_support_nested_records() {
+ assertMapConvertsTo(Map.of(), new Component(null, new SubComponent(null)));
+ assertMapConvertsTo(COMPONENT_PROPS, new Component("COMPONENT", new SubComponent("SUBCOMPONENT")));
+ }
+
+ @Log4jProperty
+ record BoundedClass(Class extends Number> className) {}
+
+ @Log4jProperty
+ record BoundedClassParam(Class className) {}
+
+ static Stream should_support_classes_with_bounds() {
+ return Stream.of(
+ Arguments.of(
+ "BoundedClass.className",
+ "java.lang.String",
+ BoundedClass.class,
+ new BoundedClass(null),
+ List.of("Invalid Class value 'java.lang.String': class does not extend java.lang.Number.")),
+ Arguments.of(
+ "BoundedClassParam.className",
+ "java.lang.String",
+ BoundedClassParam.class,
+ new BoundedClassParam(null),
+ List.of("Invalid Class value 'java.lang.String': class does not extend java.lang.Number.")),
+ Arguments.of(
+ "BoundedClass.className",
+ "java.lang.Integer",
+ BoundedClass.class,
+ new BoundedClass(Integer.class),
+ Collections.emptyList()),
+ Arguments.of(
+ "BoundedClassParam.className",
+ "java.lang.Integer",
+ BoundedClassParam.class,
+ new BoundedClassParam(Integer.class),
+ Collections.emptyList()));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void should_support_classes_with_bounds(
+ final String key,
+ final String value,
+ final Class> clazz,
+ final Object expected,
+ final Iterable extends String> expectedMessages) {
+ final TestListLogger logger = new TestListLogger(BasicPropertyEnvironmentTest.class.getName());
+ final PropertyEnvironment env = new TestPropertyEnvironment(Map.of(key, value), logger);
+ assertThat(env.getProperty(clazz)).isEqualTo(expected);
+ Assertions.assertThat(logger.getMessages()).containsExactlyElementsOf(expectedMessages);
+ }
+
+ private void assertMapConvertsTo(final Map map, final Object expected) {
+ final TestListLogger logger = new TestListLogger(BasicPropertyEnvironmentTest.class.getName());
+ final PropertyEnvironment env = new TestPropertyEnvironment(map, logger);
+ final Object actual = env.getProperty(expected.getClass());
+ assertThat(actual).isEqualTo(expected);
+ assertThat(logger.getMessages()).isEmpty();
+ }
+
+ private static class TestPropertyEnvironment extends BasicPropertyEnvironment {
+
+ private final Map props;
+
+ public TestPropertyEnvironment(final Map props) {
+ this(props, StatusLogger.getLogger());
+ }
+
+ public TestPropertyEnvironment(final Map props, final Logger logger) {
+ super(logger);
+ this.props = props;
+ }
+
+ @Override
+ public String getStringProperty(final String name) {
+ return props.get(name);
+ }
+ }
+}
diff --git a/log4j-kit/src/test/java/org/apache/logging/log4j/kit/logger/AbstractLoggerTest.java b/log4j-kit/src/test/java/org/apache/logging/log4j/kit/logger/AbstractLoggerTest.java
index adc350922ea..a6138ecea12 100644
--- a/log4j-kit/src/test/java/org/apache/logging/log4j/kit/logger/AbstractLoggerTest.java
+++ b/log4j-kit/src/test/java/org/apache/logging/log4j/kit/logger/AbstractLoggerTest.java
@@ -26,7 +26,7 @@
import javassist.bytecode.MethodInfo;
import org.junit.jupiter.api.Test;
-public class AbstractLoggerTest {
+class AbstractLoggerTest {
private static final int MAX_INLINE_SIZE = 35;
/**
diff --git a/log4j-kit/src/test/java/org/apache/logging/log4j/kit/logger/TestListLogger.java b/log4j-kit/src/test/java/org/apache/logging/log4j/kit/logger/TestListLogger.java
new file mode 100644
index 00000000000..acf6f3689d0
--- /dev/null
+++ b/log4j-kit/src/test/java/org/apache/logging/log4j/kit/logger/TestListLogger.java
@@ -0,0 +1,73 @@
+/*
+ * 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.kit.logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.internal.recycler.DummyRecyclerFactoryProvider;
+import org.apache.logging.log4j.message.DefaultFlowMessageFactory;
+import org.apache.logging.log4j.message.FlowMessageFactory;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.MessageFactory;
+import org.apache.logging.log4j.message.ParameterizedNoReferenceMessageFactory;
+import org.apache.logging.log4j.spi.recycler.RecyclerFactory;
+import org.apache.logging.log4j.status.StatusLogger;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
+
+@NullMarked
+public class TestListLogger extends AbstractLogger {
+
+ private static final MessageFactory MESSAGE_FACTORY = ParameterizedNoReferenceMessageFactory.INSTANCE;
+ private static final FlowMessageFactory FLOW_MESSAGE_FACTORY = new DefaultFlowMessageFactory();
+ private static final RecyclerFactory RECYCLER_FACTORY =
+ new DummyRecyclerFactoryProvider().createForEnvironment(null);
+
+ private final List messages = new ArrayList<>();
+
+ public TestListLogger(final String name) {
+ super(name, MESSAGE_FACTORY, FLOW_MESSAGE_FACTORY, RECYCLER_FACTORY, StatusLogger.getLogger());
+ }
+
+ @Override
+ public Level getLevel() {
+ return Level.DEBUG;
+ }
+
+ @Override
+ public boolean isEnabled(final Level level, @Nullable final Marker marker) {
+ return Level.DEBUG.isLessSpecificThan(level);
+ }
+
+ @Override
+ protected void doLog(
+ final String fqcn,
+ final @Nullable StackTraceElement location,
+ final Level level,
+ final @Nullable Marker marker,
+ final @Nullable Message message,
+ final @Nullable Throwable throwable) {
+ messages.add(message != null ? message.getFormattedMessage() : "");
+ }
+
+ public List extends String> getMessages() {
+ return Collections.unmodifiableList(messages);
+ }
+}