Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ public class ConsoleAppender<E> extends OutputStreamAppender<E> {
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 <withJansi> 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";
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 <withJansi>} 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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <withJansi>} 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<Object> 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<String, Class<?>> 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<String, Class<?>> 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<String, Class<?>> known = new HashMap<>();
known.put(FUSESOURCE, Integer.class);
assertEquals(Integer.class, ca.loadAnsiConsoleClass(loaderResolving(known)));
}

@Test
public void throwsWhenNoJansiIsAvailable() {
Map<String, Class<?>> none = new HashMap<>();
assertThrows(ClassNotFoundException.class, () -> ca.loadAnsiConsoleClass(loaderResolving(none)));
}
}