package org.springframework.roo.shell.jline;
import static org.apache.commons.io.IOUtils.LINE_SEPARATOR;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jline.ANSIBuffer;
import jline.ConsoleReader;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.Validate;
import org.springframework.roo.shell.ShellPromptAccessor;
import org.springframework.roo.support.util.AnsiEscapeCode;
/**
* JDK logging {@link Handler} that emits log messages to a JLine
* {@link ConsoleReader}.
*
* @author Ben Alex
* @since 1.0
*/
public class JLineLogHandler extends Handler {
private static final boolean BRIGHT_COLORS = Boolean.getBoolean("roo.bright");
private static boolean includeThreadName = false;
private static String lastMessage;
private static ThreadLocal<Boolean> redrawProhibit = new ThreadLocal<Boolean>();
private static boolean suppressDuplicateMessages = true;
public static void cancelRedrawProhibition() {
redrawProhibit.remove();
}
/**
* Makes text brighter if requested through system property 'roo.bright' and
* works around issue on Windows in using reverse() in combination with the
* Jansi lib, which leaves its 'negative' flag set unless reset explicitly.
*
* @return new patched ANSIBuffer
*/
static ANSIBuffer getANSIBuffer() {
final char esc = (char) 27;
return new ANSIBuffer() {
@Override
public ANSIBuffer attrib(final String str, final int code) {
if (BRIGHT_COLORS && 30 <= code && code <= 37) {
// This is a color code: add a 'bright' code
return append(esc + "[" + code + ";1m").append(str).append(ANSICodes.attrib(0));
}
return super.attrib(str, code);
};
@Override
public ANSIBuffer reverse(final String str) {
if (SystemUtils.IS_OS_WINDOWS) {
return super.reverse(str).append(ANSICodes.attrib(esc));
}
return super.reverse(str);
}
};
}
public static boolean isSuppressDuplicateMessages() {
return suppressDuplicateMessages;
}
public static void prohibitRedraw() {
redrawProhibit.set(true);
}
public static void resetMessageTracking() {
lastMessage = null; // see ROO-251
}
public static void setIncludeThreadName(final boolean include) {
includeThreadName = include;
}
public static void setSuppressDuplicateMessages(final boolean suppressDuplicateMessages) {
JLineLogHandler.suppressDuplicateMessages = suppressDuplicateMessages;
}
private boolean ansiSupported;
private ConsoleReader reader;
private ShellPromptAccessor shellPromptAccessor;
private String userInterfaceThreadName;
public JLineLogHandler(final ConsoleReader reader, final ShellPromptAccessor shellPromptAccessor) {
Validate.notNull(reader, "Console reader required");
Validate.notNull(shellPromptAccessor, "Shell prompt accessor required");
this.reader = reader;
this.shellPromptAccessor = shellPromptAccessor;
userInterfaceThreadName = Thread.currentThread().getName();
ansiSupported = reader.getTerminal().isANSISupported() && AnsiEscapeCode.isAnsiEnabled();
setFormatter(new Formatter() {
@Override
public String format(final LogRecord record) {
final StringBuilder sb = new StringBuilder();
String message = record.getMessage();
if (message != null) {
final Object[] parameters = record.getParameters();
if (!ArrayUtils.isEmpty(parameters)) {
final Pattern pattern = Pattern.compile("\\{.*?\\}");
final Matcher matcher = pattern.matcher(message);
int i = 0;
while (matcher.find()) {
message = StringUtils.replace(message, matcher.group(0), parameters[i].toString());
i++;
}
}
sb.append(message).append(LINE_SEPARATOR);
}
if (record.getThrown() != null) {
PrintWriter pw = null;
try {
final StringWriter sw = new StringWriter();
pw = new PrintWriter(sw);
record.getThrown().printStackTrace(pw);
sb.append(sw.toString());
} catch (final Exception ex) {
} finally {
IOUtils.closeQuietly(pw);
}
}
return sb.toString();
}
});
}
@Override
public void close() throws SecurityException {}
@Override
public void flush() {}
@Override
public void publish(final LogRecord record) {
try {
// Avoid repeating the same message that displayed immediately
// before the current message (ROO-30, ROO-1873)
final String toDisplay = toDisplay(record);
if (toDisplay.equals(lastMessage) && suppressDuplicateMessages) {
return;
}
lastMessage = toDisplay;
final StringBuffer buffer = reader.getCursorBuffer().getBuffer();
final int cursor = reader.getCursorBuffer().cursor;
if (reader.getCursorBuffer().length() > 0) {
// The user has semi-typed something, so put a new line in so
// the debug message is separated
reader.printNewline();
// We need to cancel whatever they typed (it's reset later on),
// so the line appears empty
reader.getCursorBuffer().setBuffer(new StringBuffer());
reader.getCursorBuffer().cursor = 0;
}
// This ensures nothing is ever displayed when redrawing the line
reader.setDefaultPrompt("");
reader.redrawLine();
// Now restore the line formatting settings back to their original
reader.setDefaultPrompt(shellPromptAccessor.getShellPrompt());
reader.getCursorBuffer().setBuffer(buffer);
reader.getCursorBuffer().cursor = cursor;
reader.printString(toDisplay);
final Boolean prohibitingRedraw = redrawProhibit.get();
if (prohibitingRedraw == null) {
reader.redrawLine();
}
reader.flushConsole();
} catch (final Exception e) {
reportError("Could not publish log message", e, Level.SEVERE.intValue());
}
}
private String toDisplay(final LogRecord event) {
final StringBuilder sb = new StringBuilder();
String threadName;
String eventString;
if (includeThreadName && !userInterfaceThreadName.equals(Thread.currentThread().getName())
&& !"".equals(Thread.currentThread().getName())) {
threadName = "[" + Thread.currentThread().getName() + "]";
// Build an event string that will indent nicely given the left hand
// side now contains a thread name
final StringBuilder lineSeparatorAndIndentingString = new StringBuilder();
for (int i = 0; i <= threadName.length(); i++) {
lineSeparatorAndIndentingString.append(" ");
}
eventString =
" "
+ getFormatter().format(event).replace(LINE_SEPARATOR,
LINE_SEPARATOR + lineSeparatorAndIndentingString.toString());
if (eventString.endsWith(lineSeparatorAndIndentingString.toString())) {
eventString =
eventString.substring(0,
eventString.length() - lineSeparatorAndIndentingString.length());
}
} else {
threadName = "";
eventString = getFormatter().format(event);
}
if (ansiSupported) {
if (event.getLevel().intValue() >= Level.SEVERE.intValue()) {
sb.append(getANSIBuffer().reverse(threadName).red(eventString));
} else if (event.getLevel().intValue() >= Level.WARNING.intValue()) {
sb.append(getANSIBuffer().reverse(threadName).magenta(eventString));
} else if (event.getLevel().intValue() >= Level.INFO.intValue()) {
sb.append(getANSIBuffer().reverse(threadName).green(eventString));
} else {
sb.append(getANSIBuffer().reverse(threadName).append(eventString));
}
} else {
sb.append(threadName).append(eventString);
}
return sb.toString();
}
}