diff --git a/jpl-to-log4j-api/pom.xml b/jpl-to-log4j-api/pom.xml
new file mode 100644
index 0000000..05f46b2
--- /dev/null
+++ b/jpl-to-log4j-api/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ 4.0.0
+
+ org.apache.logging.log4j
+ log4j-jdk-parent
+ ${revision}
+ ../parent
+
+
+ jpl-to-log4j-api
+ JPL to Log4j API logging bridge
+ The Apache Log4j implementation of java.lang.System.LoggerFinder
+
+
+
+ false
+
+ org.apache.logging.jpl.log4j.api
+ 11
+
+
+
+
+
+ org.apache.logging.log4j
+ log4j-api
+
+
+
+ org.apache.logging.log4j
+ log4j-core
+ test
+
+
+
+ org.apache.logging.log4j
+ log4j-core-test
+ test
+
+
+
+ org.hamcrest
+ hamcrest
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+
diff --git a/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/Log4jSystemLogger.java b/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/Log4jSystemLogger.java
new file mode 100644
index 0000000..cc384bd
--- /dev/null
+++ b/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/Log4jSystemLogger.java
@@ -0,0 +1,145 @@
+/*
+ * 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.jpl;
+
+import java.lang.System.Logger;
+import java.util.MissingResourceException;
+import java.util.Objects;
+import java.util.ResourceBundle;
+import java.util.function.Supplier;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.MessageFormatMessage;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.apache.logging.log4j.spi.ExtendedLogger;
+
+/**
+ * JPL {@link Logger logger} implementation that uses Log4j.
+ * Implement all default {@link Logger} methods to ensure proper class resolution
+ *
+ * @since 2.14
+ */
+public class Log4jSystemLogger implements Logger {
+
+ private final ExtendedLogger logger;
+
+ private static final String FQCN = Log4jSystemLogger.class.getName();
+
+ public Log4jSystemLogger(final ExtendedLogger logger) {
+ this.logger = logger;
+ }
+
+ @Override
+ public String getName() {
+ return logger.getName();
+ }
+
+ @Override
+ public boolean isLoggable(final Level level) {
+ return logger.isEnabled(getLevel(level));
+ }
+
+ @Override
+ public void log(final Level level, final String msg) {
+ log(level, (ResourceBundle) null, msg, (Throwable) null);
+ }
+
+ @Override
+ public void log(final Level level, final Supplier msgSupplier) {
+ Objects.requireNonNull(msgSupplier);
+ if (isLoggable(Objects.requireNonNull(level))) {
+ log(level, (ResourceBundle) null, msgSupplier.get(), (Throwable) null);
+ }
+ }
+
+ @Override
+ public void log(final Level level, final Object obj) {
+ Objects.requireNonNull(obj);
+ if (isLoggable(Objects.requireNonNull(level))) {
+ log(level, (ResourceBundle) null, obj.toString(), (Throwable) null);
+ }
+ }
+
+ @Override
+ public void log(final Level level, final String msg, final Throwable thrown) {
+ log(level, null, msg, thrown);
+ }
+
+ @Override
+ public void log(final Level level, final Supplier msgSupplier, final Throwable thrown) {
+ Objects.requireNonNull(msgSupplier);
+ if (isLoggable(Objects.requireNonNull(level))) {
+ log(level, null, msgSupplier.get(), thrown);
+ }
+ }
+
+ @Override
+ public void log(final Level level, final String format, final Object... params) {
+ log(level, null, format, params);
+ }
+
+ @Override
+ public void log(final Level level, final ResourceBundle bundle, final String msg, final Throwable thrown) {
+ logger.logIfEnabled(FQCN, getLevel(level), null, getResource(bundle, msg), thrown);
+ }
+
+ @Override
+ public void log(final Level level, final ResourceBundle bundle, final String format, final Object... params) {
+ final Message message = createMessage(getResource(bundle, format), params);
+ logger.logIfEnabled(FQCN, getLevel(level), null, message, message.getThrowable());
+ }
+
+ private static Message createMessage(final String format, final Object... params) {
+ if (params == null || params.length == 0) {
+ return new SimpleMessage(format);
+ }
+ return new MessageFormatMessage(format, params);
+ }
+
+ private static org.apache.logging.log4j.Level getLevel(final Level level) {
+ switch (level) {
+ case OFF:
+ return org.apache.logging.log4j.Level.OFF;
+ case ERROR:
+ return org.apache.logging.log4j.Level.ERROR;
+ case WARNING:
+ return org.apache.logging.log4j.Level.WARN;
+ case INFO:
+ return org.apache.logging.log4j.Level.INFO;
+ case DEBUG:
+ return org.apache.logging.log4j.Level.DEBUG;
+ case TRACE:
+ return org.apache.logging.log4j.Level.TRACE;
+ case ALL:
+ return org.apache.logging.log4j.Level.ALL;
+ }
+ return org.apache.logging.log4j.Level.ERROR;
+ }
+
+ private static String getResource(final ResourceBundle bundle, final String msg) {
+ if (bundle == null || msg == null) {
+ return msg;
+ }
+ try {
+ return bundle.getString(msg);
+ } catch (MissingResourceException e) {
+ // ignore
+ return msg;
+ } catch (ClassCastException ex) {
+ return bundle.getObject(msg).toString();
+ }
+ }
+}
diff --git a/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/Log4jSystemLoggerAdapter.java b/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/Log4jSystemLoggerAdapter.java
new file mode 100644
index 0000000..ec41841
--- /dev/null
+++ b/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/Log4jSystemLoggerAdapter.java
@@ -0,0 +1,45 @@
+/*
+ * 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.jpl;
+
+import java.lang.System.Logger;
+import java.lang.System.LoggerFinder;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.spi.AbstractLoggerAdapter;
+import org.apache.logging.log4j.spi.LoggerContext;
+import org.apache.logging.log4j.util.StackLocatorUtil;
+
+/**
+ * {@link Logger} registry implementation using just log4j-api.
+ *
+ * @since 2.14
+ */
+public class Log4jSystemLoggerAdapter extends AbstractLoggerAdapter {
+
+ @Override
+ protected Logger newLogger(final String name, final LoggerContext context) {
+ return new Log4jSystemLogger(context.getLogger(name));
+ }
+
+ @Override
+ protected LoggerContext getContext() {
+ return getContext(
+ LogManager.getFactory().isClassLoaderDependent()
+ ? StackLocatorUtil.getCallerClass(LoggerFinder.class)
+ : null);
+ }
+}
diff --git a/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/Log4jSystemLoggerFinder.java b/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/Log4jSystemLoggerFinder.java
new file mode 100644
index 0000000..4cc7ae1
--- /dev/null
+++ b/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/Log4jSystemLoggerFinder.java
@@ -0,0 +1,35 @@
+/*
+ * 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.jpl;
+
+import aQute.bnd.annotation.Resolution;
+import aQute.bnd.annotation.spi.ServiceProvider;
+import java.lang.System.Logger;
+
+/**
+ * @since 2.14
+ */
+@ServiceProvider(value = System.LoggerFinder.class, resolution = Resolution.OPTIONAL)
+public class Log4jSystemLoggerFinder extends System.LoggerFinder {
+
+ private final Log4jSystemLoggerAdapter loggerAdapter = new Log4jSystemLoggerAdapter();
+
+ @Override
+ public Logger getLogger(final String name, final Module module) {
+ return loggerAdapter.getLogger(name);
+ }
+}
diff --git a/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/package-info.java b/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/package-info.java
new file mode 100644
index 0000000..f2db704
--- /dev/null
+++ b/jpl-to-log4j-api/src/main/java/org/apache/logging/log4j/jpl/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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
+@Version("2.20.1")
+package org.apache.logging.log4j.jpl;
+
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/jpl-to-log4j-api/src/main/resources/META-INF/services/java.lang.System$LoggerFinder b/jpl-to-log4j-api/src/main/resources/META-INF/services/java.lang.System$LoggerFinder
new file mode 100644
index 0000000..0f0f23e
--- /dev/null
+++ b/jpl-to-log4j-api/src/main/resources/META-INF/services/java.lang.System$LoggerFinder
@@ -0,0 +1,17 @@
+# 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.
+org.apache.logging.log4j.jpl.Log4jSystemLoggerFinder
\ No newline at end of file
diff --git a/jpl-to-log4j-api/src/test/java/org/apache/logging/log4j/jpl/Log4jSystemLoggerTest.java b/jpl-to-log4j-api/src/test/java/org/apache/logging/log4j/jpl/Log4jSystemLoggerTest.java
new file mode 100644
index 0000000..cac01a7
--- /dev/null
+++ b/jpl-to-log4j-api/src/test/java/org/apache/logging/log4j/jpl/Log4jSystemLoggerTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.jpl;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.lang.System.Logger;
+import java.util.List;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class Log4jSystemLoggerTest {
+
+ public static final String LOGGER_NAME = "Test";
+ protected Logger logger;
+ protected ListAppender eventAppender;
+ protected ListAppender stringAppender;
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ logger = System.getLogger(LOGGER_NAME);
+ assertThat(logger, instanceOf(Log4jSystemLogger.class));
+ eventAppender = ListAppender.getListAppender("TestAppender");
+ stringAppender = ListAppender.getListAppender("StringAppender");
+ assertNotNull(eventAppender);
+ assertNotNull(stringAppender);
+ }
+
+ @AfterEach
+ public void tearDown() throws Exception {
+ if (eventAppender != null) {
+ eventAppender.clear();
+ }
+ if (stringAppender != null) {
+ stringAppender.clear();
+ }
+ }
+
+ @Test
+ public void testGetName() throws Exception {
+ assertThat(logger.getName(), equalTo(LOGGER_NAME));
+ }
+
+ @Test
+ public void testIsLoggable() throws Exception {
+ assertThat(logger.isLoggable(Logger.Level.ERROR), equalTo(true));
+ }
+
+ @Test
+ public void testLog() throws Exception {
+ logger.log(Logger.Level.INFO, "Informative message here.");
+ final List events = eventAppender.getEvents();
+ assertThat(events, hasSize(1));
+ final LogEvent event = events.get(0);
+ assertThat(event, instanceOf(Log4jLogEvent.class));
+ assertEquals(Level.INFO, event.getLevel());
+ assertEquals(LOGGER_NAME, event.getLoggerName());
+ assertEquals("Informative message here.", event.getMessage().getFormattedMessage());
+ assertEquals(Log4jSystemLogger.class.getName(), event.getLoggerFqcn());
+ }
+
+ @Test
+ public void testParameterizedLogging() {
+ logger.log(Logger.Level.INFO, "Hello, {0}!", "World");
+ final List events = eventAppender.getEvents();
+ assertThat(events, hasSize(1));
+ final LogEvent event = events.get(0);
+ assertThat(event, instanceOf(Log4jLogEvent.class));
+ assertEquals(Level.INFO, event.getLevel());
+ assertEquals(LOGGER_NAME, event.getLoggerName());
+ assertEquals("Hello, World!", event.getMessage().getFormattedMessage());
+ assertEquals(Log4jSystemLogger.class.getName(), event.getLoggerFqcn());
+ }
+
+ @Test
+ public void testParameterizedLoggingWithThrowable() {
+ final Throwable throwable = new RuntimeException();
+ logger.log(Logger.Level.INFO, "Hello, {0}!", "World", throwable);
+ final List events = eventAppender.getEvents();
+ assertThat(events, hasSize(1));
+ final LogEvent event = events.get(0);
+ assertThat(event, instanceOf(Log4jLogEvent.class));
+ assertEquals(Level.INFO, event.getLevel());
+ assertEquals(LOGGER_NAME, event.getLoggerName());
+ assertEquals("Hello, World!", event.getMessage().getFormattedMessage());
+ assertEquals(Log4jSystemLogger.class.getName(), event.getLoggerFqcn());
+ assertSame(throwable, event.getThrown());
+ }
+
+ @Test
+ public void testLogWithCallingClass() throws Exception {
+ final Logger log = System.getLogger("Test.CallerClass");
+ log.log(Logger.Level.INFO, "Calling from LoggerTest");
+ final List messages = stringAppender.getMessages();
+ assertThat(messages, hasSize(1));
+ final String message = messages.get(0);
+ assertEquals(Log4jSystemLoggerTest.class.getName(), message);
+ }
+
+ @Test
+ public void testCurlyBraces() {
+ testMessage("{message}");
+ }
+
+ @Test
+ public void testPercent() {
+ testMessage("message%s");
+ }
+
+ @Test
+ public void testPercentAndCurlyBraces() {
+ testMessage("message{%s}");
+ }
+
+ private void testMessage(final String string) {
+ logger.log(Logger.Level.INFO, "Test info " + string);
+ final List events = eventAppender.getEvents();
+ assertThat(events, hasSize(1));
+ for (final LogEvent event : events) {
+ final String message = event.getMessage().getFormattedMessage();
+ assertThat(message, equalTo("Test info " + string));
+ }
+ }
+}
diff --git a/jpl-to-log4j-api/src/test/resources/log4j2-test.xml b/jpl-to-log4j-api/src/test/resources/log4j2-test.xml
new file mode 100644
index 0000000..79c22e7
--- /dev/null
+++ b/jpl-to-log4j-api/src/test/resources/log4j2-test.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/parent/pom.xml b/parent/pom.xml
index 65886d1..01efcec 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -71,6 +71,12 @@
import
+
+ org.hamcrest
+ hamcrest
+ ${hamcrest.version}
+
+
org.junit
junit-bom
diff --git a/pom.xml b/pom.xml
index 65a200b..1b8ff72 100644
--- a/pom.xml
+++ b/pom.xml
@@ -59,6 +59,7 @@
parent
+ jpl-to-log4j-api
log4j-api-to-jul
@@ -100,6 +101,12 @@
+
+ org.apache.logging.log4j
+ jpl-to-log4j-api
+ ${project.version}
+
+
org.apache.logging.log4j
log4j-api-to-jul