/*
* Copyright 2010 JBoss, a divison Red Hat, Inc
*
* 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.jboss.errai.tools.monitoring;
import org.jboss.errai.bus.client.api.Message;
import org.jboss.errai.bus.server.util.ServerBusUtils;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableColumnModel;
import java.awt.*;
import java.awt.event.*;
import java.sql.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static java.lang.System.currentTimeMillis;
import static javax.swing.SwingUtilities.invokeLater;
public class ServiceActivityMonitor extends JFrame implements Attachable {
private JTable activityTable;
private JTable detailsTable;
private ActivityMonitorTableModel tableModel;
private MessageDetailsTableModel detailsModel;
protected String busId;
protected String service;
protected ServerMonitorPanel serverMonitor;
private boolean lockable;
private boolean scrollLock = true;
private int lastScrollAmount;
protected ActivityProcessor.Handle handle;
private ObjectExplorer explorer;
protected WindowListener defaultWindowListener;
public ServiceActivityMonitor(final ServerMonitorPanel serverMonitor, final String busId, final String service) {
this.serverMonitor = serverMonitor;
this.busId = busId;
this.service = service;
updateTitle(null);
tableModel = new ActivityMonitorTableModel();
activityTable = new JTable(tableModel);
activityTable.setDefaultRenderer(Message.class, new MessageCellRenderer());
activityTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
detailsModel = new MessageDetailsTableModel();
detailsTable = new JTable(detailsModel);
activityTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
activityTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
detailsModel.clear();
Message m = UiHelper.uglyReEncode((String) tableModel.getValueAt(activityTable.getSelectedRow(), 1));
if (m == null) return;
for (Map.Entry<String, Object> entry : m.getParts().entrySet()) {
detailsModel.addPart(entry.getKey(), entry.getValue());
}
detailsModel.fireTableRowsUpdated(0, m.getParts().size() - 1);
detailsModel.fireTableDataChanged();
}
});
detailsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
invokeLater(new Runnable() {
public void run() {
if (detailsTable.getSelectedRow() == -1 &&
detailsTable.getSelectedRow() >= detailsModel.getRowCount()) return;
Object v = detailsModel.getValueAt(detailsTable.getSelectedRow(), 1);
explorer.setRoot(v);
explorer.buildTree();
}
});
}
});
final JScrollPane activityScroll;
final JSplitPane bottomSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
new JScrollPane(detailsTable), new JScrollPane(explorer = new ObjectExplorer()));
bottomSplit.setDividerLocation(300);
final JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
activityScroll = new JScrollPane(activityTable),
bottomSplit);
activityTable.addKeyListener(new KeyListener() {
public void keyTyped(KeyEvent e) {
if (!Character.isWhitespace(e.getKeyChar()) || e.getKeyChar() == '\n') {
searchDialog.setAlwaysOnTop(true);
searchDialog.setLocationRelativeTo(ServiceActivityMonitor.this);
if (e.getKeyChar() != '\n') searchDialog.keyTyped(e);
searchDialog.setVisible(true);
}
}
public void keyPressed(KeyEvent e) {
}
public void keyReleased(KeyEvent e) {
}
});
KeyStroke escKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
getLayeredPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(escKeyStroke, "esc-pressed");
getLayeredPane().getActionMap().put("esc-pressed", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
if (tableModel.isFiltered()) {
invokeLater(new Runnable() {
public void run() {
activityTable.clearSelection();
tableModel.setFilterTerm(null);
activityTable.setModel(tableModel);
tableModel.fireTableDataChanged();
updateTitle(null);
}
});
}
}
});
splitPane.setDividerLocation(150);
final JScrollBar vertScroll = activityScroll.getVerticalScrollBar();
vertScroll.addMouseListener(new MouseListener() {
public void mouseClicked(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
lockable = true;
}
public void mouseReleased(MouseEvent e) {
lockable = false;
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
});
vertScroll.addMouseWheelListener(new MouseWheelListener() {
public void mouseWheelMoved(MouseWheelEvent e) {
lastScrollAmount = e.getScrollAmount();
}
});
vertScroll.addAdjustmentListener(new AdjustmentListener() {
public void adjustmentValueChanged(AdjustmentEvent e) {
if (lockable || lastScrollAmount != 0) {
scrollLock = (e.getValue() == e.getAdjustable().getMaximum() - e.getAdjustable().getVisibleAmount());
return;
}
lastScrollAmount = 0;
if (scrollLock) e.getAdjustable().setValue(e.getAdjustable().getMaximum());
}
});
getContentPane().add(splitPane);
Point point = serverMonitor.getMainMonitorGUI().getLocation();
setLocation(point.x + 20, point.y + 20);
setSize(500, 300);
DefaultTableColumnModel defaultColumn = (DefaultTableColumnModel) activityTable.getColumnModel();
defaultColumn.getColumn(0).setResizable(false);
defaultColumn.getColumn(0).setPreferredWidth(120);
defaultColumn.getColumn(0).setMaxWidth(120);
defaultColumn = (DefaultTableColumnModel) detailsTable.getColumnModel();
defaultColumn.getColumn(0).setPreferredWidth(120);
defaultColumn.getColumn(0).setMaxWidth(250);
setVisible(true);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
addWindowListener(defaultWindowListener = new WindowListener() {
public void windowOpened(WindowEvent e) {
}
public void windowClosing(WindowEvent e) {
}
public void windowClosed(WindowEvent e) {
handle.dispose();
serverMonitor.stopMonitor(service);
}
public void windowIconified(WindowEvent e) {
}
public void windowDeiconified(WindowEvent e) {
}
public void windowActivated(WindowEvent e) {
}
public void windowDeactivated(WindowEvent e) {
}
});
}
public void updateTitle(String s) {
if (s == null) setTitle(service + "@" + busId);
else setTitle(service + "@" + busId + ": " + s);
}
public class ActivityLogEntry {
private long time;
private String message;
public ActivityLogEntry(long time, String message) {
this.time = time;
this.message = message;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
public class ActivityMonitorTableModel extends AbstractTableModel {
private ArrayList<ActivityLogEntry> messages = new ArrayList<ActivityLogEntry>();
private ArrayList<ActivityLogEntry> filteredMessages = new ArrayList<ActivityLogEntry>();
private volatile Pattern filter = null;
private final String[] COLS
= {"Time", "Message Contents"};
private final Class[] TYPES
= {String.class, String.class};
private DateFormat formatter = new SimpleDateFormat("hh:mm:ss.SSS");
@Override
public String getColumnName(int column) {
return COLS[column];
}
public int getRowCount() {
return filter == null ? messages.size() : filteredMessages.size();
}
public int getColumnCount() {
return COLS.length;
}
public Object getValueAt(int rowIndex, int columnIndex) {
if (rowIndex == -1) return null;
if (filter == null) {
switch (columnIndex) {
case 0:
return formatter.format(new Date(messages.get(rowIndex).getTime()));
case 1:
return messages.get(rowIndex).getMessage();
}
}
else {
switch (columnIndex) {
case 0:
return formatter.format(new Date(filteredMessages.get(rowIndex).getTime()));
case 1:
return filteredMessages.get(rowIndex).getMessage();
}
}
return null;
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return TYPES[columnIndex];
}
public void addMessage(long time, String message) {
ActivityLogEntry entry = new ActivityLogEntry(time, message);
messages.add(entry);
if (filter != null) {
if (filter.matcher(message).find()) {
filteredMessages.add(entry);
fireTableRowsInserted(filteredMessages.size() - 1, filteredMessages.size() - 1);
}
}
else {
fireTableRowsInserted(messages.size() - 1, messages.size() - 1);
}
}
public void setFilterTerm(String filterTerm) {
if (filterTerm != null && filterTerm.length() != 0) {
Pattern filter = Pattern.compile(filterTerm);
this.filteredMessages.clear();
for (ActivityLogEntry entry : messages) {
if (filter.matcher(entry.getMessage()).find()) {
filteredMessages.add(entry);
}
}
this.filter = filter;
}
else {
this.filter = null;
this.filteredMessages.clear();
}
}
public boolean isFiltered() {
return this.filter != null;
}
}
public class MessageDetailsTableModel extends AbstractTableModel {
private ArrayList<String> fields = new ArrayList<String>();
private ArrayList<Object> values = new ArrayList<Object>();
private final String[] COLS = {"Message Part", "Value"};
private final Class[] TYPES = {String.class, Object.class};
@Override
public String getColumnName(int column) {
return COLS[column];
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return TYPES[columnIndex];
}
public int getRowCount() {
return fields.size();
}
public int getColumnCount() {
return COLS.length;
}
public Object getValueAt(int rowIndex, int columnIndex) {
if (rowIndex != -1) {
switch (columnIndex) {
case 0:
if (fields.size() > rowIndex)
return fields.get(rowIndex);
case 1:
if (values.size() > rowIndex)
return values.get(rowIndex);
}
}
return null;
}
public void addPart(String field, Object value) {
fields.add(field);
values.add(value);
}
public void clear() {
fields.clear();
values.clear();
}
}
class SearchDialog extends JFrame {
JTextField searchField;
KeyEvent preEvent;
public SearchDialog() {
setUndecorated(true);
setSize(300, 30);
searchField = new JTextField();
//searchField.setSize(300, 30);
getContentPane().add(searchField);
KeyStroke escKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
getLayeredPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(escKeyStroke, "esc-pressed");
getLayeredPane().getActionMap().put("esc-pressed", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
searchField.setText("");
preEvent = null;
setVisible(false);
}
});
searchField.addKeyListener(new KeyListener() {
public void keyTyped(KeyEvent e) {
if (e.getKeyChar() == '\n') {
final String val = searchField.getText();
searchField.setText("");
invokeLater(new Runnable() {
public void run() {
setVisible(false);
activityTable.clearSelection();
try {
tableModel.setFilterTerm(val);
}
catch (PatternSyntaxException e) {
JOptionPane.showMessageDialog(new JFrame(), e.getMessage(), "Dialog",
JOptionPane.ERROR_MESSAGE);
return;
}
activityTable.setModel(tableModel);
tableModel.fireTableDataChanged();
updateTitle("Filtering[\"" + val + "\"]");
}
});
}
}
public void keyPressed(KeyEvent e) {
}
public void keyReleased(KeyEvent e) {
}
});
searchField.addFocusListener(new FocusListener() {
public void focusGained(FocusEvent e) {
if (preEvent != null) {
searchField.setText(new String(new char[]{preEvent.getKeyChar()}));
searchField.setCaretPosition(1);
preEvent = null;
}
}
public void focusLost(FocusEvent e) {
searchField.setText("");
preEvent = null;
setVisible(false);
}
});
}
public void keyTyped(KeyEvent s) {
preEvent = s;
}
}
private SearchDialog searchDialog = new SearchDialog();
public void notifyMessage(long time, Message message) {
/*
* This is a huge hack to get the display of the messages consistent with what the payload does when
* encoded. And no it's not particularly efficient. But since inbus messages are not encoded by JSON and
* it would be wacky to make the monitoring API such that we had to scan for one or the other, this
* is much more consistent from an API point-of-view.
*/
tableModel.addMessage(time, ServerBusUtils.encodeJSON(message.getParts()));
}
public void attach(ActivityProcessor proc) {
handle = proc.registerEvent(EventType.MESSAGE, new MessageMonitor() {
public void monitorEvent(MessageEvent event) {
if (event.getToBus().equals(busId) && service.equals(event.getSubject())) {
if (service.endsWith("TagCloud")) {
try {
throw new Throwable();
}
catch (Throwable t) {
t.printStackTrace();
}
}
notifyMessage(event.getTime(), (Message) event.getContents());
}
}
});
/**
* When this monitor is attached send a request to replay the messages for this subject@bus.
*/
proc.notifyEvent(currentTimeMillis(), EventType.REPLAY_MESSAGES, SubEventType.NONE, busId, busId, service, null, null, false);
}
}