/*
* SoapUI, Copyright (C) 2004-2016 SmartBear Software
*
* Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent
* versions of the EUPL (the "Licence");
* You may not use this work except in compliance with the Licence.
* You may obtain a copy of the Licence at:
*
* http://ec.europa.eu/idabc/eupl
*
* Unless required by applicable law or agreed to in writing, software distributed under the Licence is
* distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the Licence for the specific language governing permissions and limitations
* under the Licence.
*/
package com.eviware.soapui.support.log;
import com.eviware.soapui.SoapUI;
import com.eviware.soapui.support.UISupport;
import org.apache.commons.collections.list.TreeList;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import javax.swing.AbstractAction;
import javax.swing.AbstractListModel;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* Component for displaying log entries
*
* @author Ole.Matzura
*/
public class JLogList extends JPanel {
private long maxRows = 1000;
private JList logList;
private final LogListModel model;
private List<Logger> loggers = new ArrayList<Logger>();
private InternalLogAppender internalLogAppender = new InternalLogAppender();
private boolean tailing = true;
private BlockingQueue<Object> linesToAdd = new LinkedBlockingQueue<Object>();
private JCheckBoxMenuItem enableMenuItem;
private final String title;
public JLogList(String title) {
super(new BorderLayout());
this.title = title;
model = new LogListModel();
logList = new JList(model);
logList.setToolTipText(title);
logList.setCellRenderer(new LogAreaCellRenderer());
logList.setPrototypeCellValue("Testing 123");
logList.setFixedCellWidth(-1);
JPopupMenu listPopup = new JPopupMenu();
listPopup.add(new ClearAction());
EnableAction enableAction = new EnableAction();
enableMenuItem = new JCheckBoxMenuItem(enableAction);
enableMenuItem.setSelected(true);
listPopup.add(enableMenuItem);
listPopup.addSeparator();
listPopup.add(new CopyAction());
listPopup.add(new SetMaxRowsAction());
listPopup.addSeparator();
listPopup.add(new ExportToFileAction());
logList.setComponentPopupMenu(listPopup);
setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
JScrollPane scrollPane = new JScrollPane(logList);
UISupport.addPreviewCorner(scrollPane, true);
add(scrollPane, BorderLayout.CENTER);
SimpleAttributeSet requestAttributes = new SimpleAttributeSet();
StyleConstants.setForeground(requestAttributes, Color.BLUE);
SimpleAttributeSet responseAttributes = new SimpleAttributeSet();
StyleConstants.setForeground(responseAttributes, Color.GREEN);
try {
maxRows = Long.parseLong(SoapUI.getSettings().getString("JLogList#" + title, "1000"));
} catch (NumberFormatException ignore) {
}
}
public void clear() {
model.clear();
}
public JList getLogList() {
return logList;
}
public long getMaxRows() {
return maxRows;
}
public void setMaxRows(long maxRows) {
this.maxRows = maxRows;
}
public void addLine(Object line) {
if (!isEnabled()) {
return;
}
if (line instanceof LoggingEvent) {
LoggingEvent ev = (LoggingEvent) line;
linesToAdd.add(new LoggingEventWrapper(ev));
if (ev.getThrowableInformation() != null) {
Throwable t = ev.getThrowableInformation().getThrowable();
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
t.printStackTrace(pw);
StringTokenizer st = new StringTokenizer(sw.toString(), "\r\n");
while (st.hasMoreElements()) {
linesToAdd.add(" " + st.nextElement());
}
}
} else {
linesToAdd.add(line);
}
model.ensureUpdateIsStarted();
}
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
logList.setEnabled(enabled);
enableMenuItem.setSelected(enabled);
}
private static class LogAreaCellRenderer extends DefaultListCellRenderer {
private Map<Level, Color> levelColors = new HashMap<Level, Color>();
private LogAreaCellRenderer() {
levelColors.put(Level.ERROR, new Color(192, 0, 0));
levelColors.put(Level.INFO, new Color(0, 92, 0));
levelColors.put(Level.WARN, Color.ORANGE.darker().darker());
levelColors.put(Level.DEBUG, new Color(0, 0, 128));
}
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
boolean cellHasFocus) {
JLabel component = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
if (value instanceof LoggingEventWrapper) {
LoggingEventWrapper eventWrapper = (LoggingEventWrapper) value;
if (!isSelected && levelColors.containsKey(eventWrapper.getLevel())) {
component.setForeground(levelColors.get(eventWrapper.getLevel()));
}
}
// Limit the length of the tool tip, to prevent long delays.
String toolTip = component.getText();
if (toolTip != null && toolTip.length() > 1000) {
toolTip = toolTip.substring(0, 1000);
}
component.setToolTipText(toolTip);
return component;
}
}
private final static class LoggingEventWrapper {
private final LoggingEvent loggingEvent;
private String str;
public LoggingEventWrapper(LoggingEvent loggingEvent) {
this.loggingEvent = loggingEvent;
}
public Level getLevel() {
return loggingEvent.getLevel();
}
public String toString() {
if (str == null) {
StringBuilder builder = new StringBuilder();
builder.append(new Date(loggingEvent.timeStamp));
builder.append(':').append(loggingEvent.getLevel()).append(':').append(loggingEvent.getMessage());
str = builder.toString();
}
return str;
}
}
public void addLogger(String loggerName, boolean addAppender) {
Logger logger = Logger.getLogger(loggerName);
if (addAppender) {
logger.addAppender(internalLogAppender);
}
loggers.add(logger);
}
public Logger[] getLoggers() {
return loggers.toArray(new Logger[loggers.size()]);
}
public void setLevel(Level level) {
for (Logger logger : loggers) {
logger.setLevel(level);
}
}
public Logger getLogger(String loggerName) {
for (Logger logger : loggers) {
if (logger.getName().equals(loggerName)) {
return logger;
}
}
return null;
}
private class InternalLogAppender extends AppenderSkeleton {
protected void append(LoggingEvent event) {
addLine(event);
}
public void close() {
}
public boolean requiresLayout() {
return false;
}
}
public boolean monitors(String loggerName) {
for (Logger logger : loggers) {
if (loggerName.startsWith(logger.getName())) {
return true;
}
}
return false;
}
public void removeLogger(String loggerName) {
for (Logger logger : loggers) {
if (loggerName.equals(logger.getName())) {
logger.removeAppender(internalLogAppender);
}
}
}
public void saveToFile(File file) {
try {
PrintWriter writer = new PrintWriter(file);
for (int c = 0; c < model.getSize(); c++) {
writer.println(model.getElementAt(c));
}
writer.close();
} catch (Exception e) {
UISupport.showErrorMessage(e);
}
}
public boolean isTailing() {
return tailing;
}
public void setTailing(boolean tail) {
this.tailing = tail;
}
/*
Helper classes.
*/
private class ClearAction extends AbstractAction {
public ClearAction() {
super("Clear");
}
public void actionPerformed(ActionEvent e) {
model.clear();
}
}
private class SetMaxRowsAction extends AbstractAction {
public SetMaxRowsAction() {
super("Set Max Rows");
}
public void actionPerformed(ActionEvent e) {
String val = UISupport.prompt("Set maximum number of log rows to keep", "Set Max Rows",
String.valueOf(maxRows));
if (val != null) {
try {
maxRows = Long.parseLong(val);
SoapUI.getSettings().setString("JLogList#" + title, val);
} catch (NumberFormatException e1) {
UISupport.beep();
}
}
}
}
private class ExportToFileAction extends AbstractAction {
public ExportToFileAction() {
super("Export to File");
}
public void actionPerformed(ActionEvent e) {
if (model.getSize() == 0) {
UISupport.showErrorMessage("Log is empty; nothing to export");
return;
}
File file = UISupport.getFileDialogs().saveAs(JLogList.this, "Save Log [] to File", "*.log", "*.log", null);
if (file != null) {
saveToFile(file);
}
}
}
private class CopyAction extends AbstractAction {
public CopyAction() {
super("Copy to clipboard");
}
public void actionPerformed(ActionEvent e) {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
StringBuilder buf = new StringBuilder();
int[] selectedIndices = logList.getSelectedIndices();
if (selectedIndices.length == 0) {
for (int c = 0; c < logList.getModel().getSize(); c++) {
buf.append(logList.getModel().getElementAt(c).toString());
buf.append("\r\n");
}
} else {
for (int selectedIndex : selectedIndices) {
buf.append(logList.getModel().getElementAt(selectedIndex).toString());
buf.append("\r\n");
}
}
StringSelection selection = new StringSelection(buf.toString());
clipboard.setContents(selection, selection);
}
}
private class EnableAction extends AbstractAction {
public EnableAction() {
super("Enable");
}
public void actionPerformed(ActionEvent e) {
JLogList.this.setEnabled(enableMenuItem.isSelected());
}
}
/**
* Internal list model that for optimized storage and notifications
*
* @author Ole.Matzura
*/
@SuppressWarnings("unchecked")
private final class LogListModel extends AbstractListModel {
private final List<Object> lines = Collections.synchronizedList(new TreeList());
private ListUpdater updater = new ListUpdater();
public int getSize() {
return lines.size();
}
public Object getElementAt(int index) {
return lines.get(index);
}
public void clear() {
final int size = lines.size();
if (size == 0) {
return;
}
lines.clear();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
fireIntervalRemoved(LogListModel.this, 0, size - 1);
}
});
}
public void ensureUpdateIsStarted() {
updater.ensureUpdateIsStarted();
}
private class ListUpdater implements Runnable {
private volatile boolean updating;
public void run() {
String originalThreadName = Thread.currentThread().getName();
Thread.currentThread().setName("LogList Updater for " + title);
setUpdating(true);
try {
Object line;
while ((line = getNextLine()) != null) {
try {
List<Object> linesToAddNow = new ArrayList<Object>();
linesToAddNow.add(line);
while ((line = linesToAdd.poll()) != null) {
linesToAddNow.add(line);
}
int oldSize = lines.size();
lines.addAll(linesToAddNow);
updateJList(oldSize);
} catch (Exception e) {
SoapUI.logError(e);
}
}
} finally {
synchronized (this) {
updating = false;
if (!linesToAdd.isEmpty()) {
ensureUpdateIsStarted();
}
}
Thread.currentThread().setName(originalThreadName);
}
}
public synchronized void ensureUpdateIsStarted() {
if (!updating) {
setUpdating(true);
SoapUI.getThreadPool().submit(this);
}
}
private Object getNextLine() {
try {
return linesToAdd.poll(500, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
//shouldn't really happen
return null;
}
}
private void updateJList(final int oldSize) {
try {
SwingUtilities.invokeAndWait(new Runnable() {
public void run() {
fireIntervalAdded(LogListModel.this, oldSize, lines.size() - 1);
int linesToRemove = lines.size() - ((int) maxRows);
if (linesToRemove > 0) {
for (int i = 0; i < linesToRemove; i++) {
lines.remove(0);
}
fireIntervalRemoved(LogListModel.this, 0, linesToRemove);
}
if (tailing) {
logList.ensureIndexIsVisible(lines.size() - 1);
}
}
});
} catch (InterruptedException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
private synchronized void setUpdating(boolean updating) {
this.updating = updating;
}
}
}
}