diff --git a/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java b/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java index 4844752ec9..8451da05e9 100644 --- a/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java +++ b/logback-core/src/main/java/ch/qos/logback/core/ConsoleAppender.java @@ -48,7 +48,11 @@ public class ConsoleAppender extends OutputStreamAppender { protected ConsoleTarget target = ConsoleTarget.SystemOut; protected boolean withJansi = false; - private final static String AnsiConsole_CLASS_NAME = "org.fusesource.jansi.AnsiConsole"; + // Jansi was migrated from FuseSource (org.fusesource.jansi) to JLine (org.jline.jansi), which + // changed the package of AnsiConsole. Probe the JLine coordinates first, then fall back to the + // legacy FuseSource ones so that keeps working with both artifacts. See LOGBACK issue 1043. + private final static String[] ANSI_CONSOLE_CLASS_NAMES = { "org.jline.jansi.AnsiConsole", + "org.fusesource.jansi.AnsiConsole" }; private final static String JANSI2_OUT_METHOD_NAME = "out"; private final static String JANSI2_ERR_METHOD_NAME = "err"; private final static String WRAP_SYSTEM_OUT_METHOD_NAME = "wrapSystemOut"; @@ -114,7 +118,7 @@ private OutputStream wrapWithJansi(OutputStream targetStream) { try { addInfo("Enabling JANSI AnsiPrintStream for the console."); ClassLoader classLoader = Loader.getClassLoaderOfObject(context); - Class classObj = classLoader.loadClass(AnsiConsole_CLASS_NAME); + Class classObj = loadAnsiConsoleClass(classLoader); Method systemInstallMethod = classObj.getMethod(SYSTEM_INSTALL_METHOD_NAME); if(systemInstallMethod != null) { @@ -157,6 +161,26 @@ private OutputStream wrapWithJansi(OutputStream targetStream) { return targetStream; } + /** + * Loads the Jansi {@code AnsiConsole} class, probing the candidate class names in + * {@link #ANSI_CONSOLE_CLASS_NAMES} order (JLine's {@code org.jline.jansi} first, then the legacy + * FuseSource {@code org.fusesource.jansi}). This keeps {@code } working across the Jansi + * migration from FuseSource to JLine. + * + * @throws ClassNotFoundException if none of the candidate classes is available. + */ + Class loadAnsiConsoleClass(ClassLoader classLoader) throws ClassNotFoundException { + ClassNotFoundException lastException = null; + for (String className : ANSI_CONSOLE_CLASS_NAMES) { + try { + return classLoader.loadClass(className); + } catch (ClassNotFoundException e) { + lastException = e; + } + } + throw lastException; + } + /** * @return whether to use JANSI or not. */ diff --git a/logback-core/src/test/java/ch/qos/logback/core/ConsoleAppenderJansiClassResolutionTest.java b/logback-core/src/test/java/ch/qos/logback/core/ConsoleAppenderJansiClassResolutionTest.java new file mode 100644 index 0000000000..625b6ece83 --- /dev/null +++ b/logback-core/src/test/java/ch/qos/logback/core/ConsoleAppenderJansiClassResolutionTest.java @@ -0,0 +1,76 @@ +/* + * Logback: the reliable, generic, fast and flexible logging framework. + * Copyright (C) 1999-2026, QOS.ch. All rights reserved. + * + * This program and the accompanying materials are dual-licensed under + * either the terms of the Eclipse Public License v2.0 as published by + * the Eclipse Foundation + * + * or (per the licensee's choosing) + * + * under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation. + */ +package ch.qos.logback.core; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Verifies that {@link ConsoleAppender} probes the JLine Jansi {@code AnsiConsole} class name before + * the legacy FuseSource one, so that {@code } keeps working after the Jansi migration from + * FuseSource to JLine. See LOGBACK issue 1043. + */ +public class ConsoleAppenderJansiClassResolutionTest { + + static final String JLINE = "org.jline.jansi.AnsiConsole"; + static final String FUSESOURCE = "org.fusesource.jansi.AnsiConsole"; + + final ConsoleAppender ca = new ConsoleAppender<>(); + + /** + * ClassLoader that resolves only the AnsiConsole class names present in {@code known} (mapping each + * to a distinct stand-in class) and reports the others as absent. + */ + private ClassLoader loaderResolving(Map> known) { + return new ClassLoader(getClass().getClassLoader()) { + @Override + public Class loadClass(String name) throws ClassNotFoundException { + Class mapped = known.get(name); + if (mapped != null) { + return mapped; + } + if (JLINE.equals(name) || FUSESOURCE.equals(name)) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name); + } + }; + } + + @Test + public void prefersJLineWhenBothArePresent() throws ClassNotFoundException { + Map> known = new HashMap<>(); + known.put(JLINE, String.class); + known.put(FUSESOURCE, Integer.class); + assertEquals(String.class, ca.loadAnsiConsoleClass(loaderResolving(known))); + } + + @Test + public void fallsBackToFuseSourceWhenJLineIsAbsent() throws ClassNotFoundException { + Map> known = new HashMap<>(); + known.put(FUSESOURCE, Integer.class); + assertEquals(Integer.class, ca.loadAnsiConsoleClass(loaderResolving(known))); + } + + @Test + public void throwsWhenNoJansiIsAvailable() { + Map> none = new HashMap<>(); + assertThrows(ClassNotFoundException.class, () -> ca.loadAnsiConsoleClass(loaderResolving(none))); + } +}