/* * Copyright 2011-2012 the original author or authors. * * Licensed 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.springframework.shell.core; import static org.fusesource.jansi.Ansi.ansi; import static org.fusesource.jansi.Ansi.Color.GREEN; import static org.fusesource.jansi.Ansi.Color.MAGENTA; import static org.fusesource.jansi.Ansi.Color.RED; import static org.springframework.shell.support.util.OsUtils.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 jline.console.ConsoleReader; import org.fusesource.jansi.Ansi; import org.fusesource.jansi.Ansi.Attribute; import org.springframework.shell.support.util.IOUtils; import org.springframework.util.Assert; /** * JDK logging {@link Handler} that emits log messages to a JLine {@link ConsoleReader}. * * @author Ben Alex * @since 1.0 */ public class JLineLogHandler extends Handler { // Fields private ConsoleReader reader; private ShellPromptAccessor shellPromptAccessor; private static ThreadLocal<Boolean> redrawProhibit = new ThreadLocal<Boolean>(); private static String lastMessage; private static boolean includeThreadName = false; private boolean ansiSupported; private String userInterfaceThreadName; private static boolean suppressDuplicateMessages = true; public JLineLogHandler(final ConsoleReader reader, final ShellPromptAccessor shellPromptAccessor) { Assert.notNull(reader, "Console reader required"); Assert.notNull(shellPromptAccessor, "Shell prompt accessor required"); this.reader = reader; this.shellPromptAccessor = shellPromptAccessor; this.userInterfaceThreadName = Thread.currentThread().getName(); this.ansiSupported = reader.getTerminal().isAnsiSupported(); setFormatter(new Formatter() { @Override public String format(final LogRecord record) { StringBuffer sb = new StringBuffer(); if (record.getMessage() != null) { sb.append(record.getMessage()).append(LINE_SEPARATOR); } if (record.getThrown() != null) { PrintWriter pw = null; try { StringWriter sw = new StringWriter(); pw = new PrintWriter(sw); record.getThrown().printStackTrace(pw); sb.append(sw.toString()); } catch (Exception ex) { } finally { IOUtils.closeQuietly(pw); } } return sb.toString(); } }); } @Override public void flush() { } @Override public void close() throws SecurityException { } public static void prohibitRedraw() { redrawProhibit.set(true); } public static void cancelRedrawProhibition() { redrawProhibit.remove(); } public static void setIncludeThreadName(final boolean include) { includeThreadName = include; } public static void resetMessageTracking() { lastMessage = null; // see ROO-251 } public static boolean isSuppressDuplicateMessages() { return suppressDuplicateMessages; } public static void setSuppressDuplicateMessages(final boolean suppressDuplicateMessages) { JLineLogHandler.suppressDuplicateMessages = suppressDuplicateMessages; } @Override public void publish(final LogRecord record) { try { // Avoid repeating the same message that displayed immediately before the current message (ROO-30, ROO-1873) String toDisplay = toDisplay(record); if (toDisplay.equals(lastMessage) && suppressDuplicateMessages) { return; } lastMessage = toDisplay; StringBuilder buffer = reader.getCursorBuffer().copy().buffer; 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.println(); // We need to cancel whatever they typed (it's reset later on), so the line appears empty reader.getCursorBuffer().clear(); } // This ensures nothing is ever displayed when redrawing the line reader.setPrompt(""); reader.redrawLine(); // Now restore the line formatting settings back to their original reader.setPrompt(shellPromptAccessor.getShellPrompt()); reader.getCursorBuffer().write(buffer.toString()); reader.getCursorBuffer().cursor = cursor; reader.print(toDisplay); Boolean prohibitingRedraw = redrawProhibit.get(); if (prohibitingRedraw == null) { reader.redrawLine(); } reader.flush(); } catch (Exception e) { reportError("Could not publish log message", e, Level.SEVERE.intValue()); } } private String toDisplay(final LogRecord event) { 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 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) { Ansi ansi = ansi(sb); if (event.getLevel().intValue() >= Level.SEVERE.intValue()) { ansi.a(Attribute.NEGATIVE_ON).a(threadName).a(Attribute.NEGATIVE_OFF).fg(RED).a(eventString).reset(); } else if (event.getLevel().intValue() >= Level.WARNING.intValue()) { ansi.a(Attribute.NEGATIVE_ON).a(threadName).a(Attribute.NEGATIVE_OFF).fg(MAGENTA).a(eventString) .reset(); } else if (event.getLevel().intValue() >= Level.INFO.intValue()) { ansi.a(Attribute.NEGATIVE_ON).a(threadName).a(Attribute.NEGATIVE_OFF).fg(GREEN).a(eventString).reset(); } else { ansi.a(Attribute.NEGATIVE_ON).a(threadName).a(Attribute.NEGATIVE_OFF).a(eventString); } } else { sb.append(threadName).append(eventString); } return sb.toString(); } }