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(); } }