1 /*
2  * Copyright (c) 2004, 2012, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package sun.tools.jconsole.inspector;
27 
28 import javax.swing.*;
29 import javax.swing.table.*;
30 import javax.swing.tree.*;
31 import java.awt.Font;
32 
33 import java.text.SimpleDateFormat;
34 
35 import java.awt.Component;
36 import java.awt.EventQueue;
37 import java.awt.event.*;
38 import java.awt.Dimension;
39 import java.util.*;
40 import java.io.*;
41 import java.lang.reflect.Array;
42 
43 import javax.management.*;
44 import javax.management.openmbean.CompositeData;
45 import javax.management.openmbean.TabularData;
46 
47 import sun.tools.jconsole.JConsole;
48 import sun.tools.jconsole.Messages;
49 
50 @SuppressWarnings("serial")
51 public class XMBeanNotifications extends JTable implements NotificationListener {
52 
53     private final static String[] columnNames = {
54         Messages.TIME_STAMP,
55         Messages.TYPE,
56         Messages.USER_DATA,
57         Messages.SEQ_NUM,
58         Messages.MESSAGE,
59         Messages.EVENT,
60         Messages.SOURCE
61     };
62     private HashMap<ObjectName, XMBeanNotificationsListener> listeners =
63             new HashMap<ObjectName, XMBeanNotificationsListener>();
64     private volatile boolean subscribed;
65     private XMBeanNotificationsListener currentListener;
66     public final static String NOTIFICATION_RECEIVED_EVENT =
67             "jconsole.xnotification.received";
68     private List<NotificationListener> notificationListenersList;
69     private volatile boolean enabled;
70     private Font normalFont,  boldFont;
71     private int rowMinHeight = -1;
72     private TableCellEditor userDataEditor = new UserDataCellEditor();
73     private NotifMouseListener mouseListener = new NotifMouseListener();
74     private SimpleDateFormat timeFormater = new SimpleDateFormat("HH:mm:ss:SSS");
75     private static TableCellEditor editor =
76             new Utils.ReadOnlyTableCellEditor(new JTextField());
77 
XMBeanNotifications()78     public XMBeanNotifications() {
79         super(new TableSorter(columnNames, 0));
80         setColumnSelectionAllowed(false);
81         setRowSelectionAllowed(false);
82         getTableHeader().setReorderingAllowed(false);
83         ArrayList<NotificationListener> l =
84                 new ArrayList<NotificationListener>(1);
85         notificationListenersList = Collections.synchronizedList(l);
86 
87         addMouseListener(mouseListener);
88 
89         TableColumnModel colModel = getColumnModel();
90         colModel.getColumn(0).setPreferredWidth(45);
91         colModel.getColumn(1).setPreferredWidth(50);
92         colModel.getColumn(2).setPreferredWidth(50);
93         colModel.getColumn(3).setPreferredWidth(40);
94         colModel.getColumn(4).setPreferredWidth(50);
95         colModel.getColumn(5).setPreferredWidth(50);
96         setColumnEditors();
97         addKeyListener(new Utils.CopyKeyAdapter());
98     }
99 
100     // Call on EDT
cancelCellEditing()101     public void cancelCellEditing() {
102         TableCellEditor tce = getCellEditor();
103         if (tce != null) {
104             tce.cancelCellEditing();
105         }
106     }
107 
108     // Call on EDT
stopCellEditing()109     public void stopCellEditing() {
110         TableCellEditor tce = getCellEditor();
111         if (tce != null) {
112             tce.stopCellEditing();
113         }
114     }
115 
116     // Call on EDT
117     @Override
isCellEditable(int row, int col)118     public boolean isCellEditable(int row, int col) {
119         UserDataCell cell = getUserDataCell(row, col);
120         if (cell != null) {
121             return cell.isMaximized();
122         }
123         return true;
124     }
125 
126     // Call on EDT
127     @Override
setValueAt(Object value, int row, int column)128     public void setValueAt(Object value, int row, int column) {
129     }
130 
131     // Call on EDT
132     @Override
prepareRenderer( TableCellRenderer renderer, int row, int column)133     public synchronized Component prepareRenderer(
134             TableCellRenderer renderer, int row, int column) {
135         //In case we have a repaint thread that is in the process of
136         //repainting an obsolete table, just ignore the call.
137         //It can happen when MBean selection is switched at a very quick rate
138         if (row >= getRowCount()) {
139             return null;
140         }
141 
142         Component comp = super.prepareRenderer(renderer, row, column);
143 
144         if (normalFont == null) {
145             normalFont = comp.getFont();
146             boldFont = normalFont.deriveFont(Font.BOLD);
147         }
148         UserDataCell cell = getUserDataCell(row, 2);
149         if (column == 2 && cell != null) {
150             comp.setFont(boldFont);
151             int size = cell.getHeight();
152             if (size > 0) {
153                 if (getRowHeight(row) != size) {
154                     setRowHeight(row, size);
155                 }
156             }
157         } else {
158             comp.setFont(normalFont);
159         }
160 
161         return comp;
162     }
163 
164     // Call on EDT
165     @Override
getCellRenderer(int row, int column)166     public synchronized TableCellRenderer getCellRenderer(int row, int column) {
167         //In case we have a repaint thread that is in the process of
168         //repainting an obsolete table, just ignore the call.
169         //It can happen when MBean selection is switched at a very quick rate
170         if (row >= getRowCount()) {
171             return null;
172         }
173 
174         DefaultTableCellRenderer renderer;
175         String toolTip = null;
176         UserDataCell cell = getUserDataCell(row, column);
177         if (cell != null && cell.isInited()) {
178             renderer = (DefaultTableCellRenderer) cell.getRenderer();
179         } else {
180             renderer =
181                     (DefaultTableCellRenderer) super.getCellRenderer(row, column);
182         }
183 
184         if (cell != null) {
185             toolTip = Messages.DOUBLE_CLICK_TO_EXPAND_FORWARD_SLASH_COLLAPSE+
186                     ". " + cell.toString();
187         } else {
188             Object val =
189                     ((DefaultTableModel) getModel()).getValueAt(row, column);
190             if (val != null) {
191                 toolTip = val.toString();
192             }
193         }
194 
195         renderer.setToolTipText(toolTip);
196 
197         return renderer;
198     }
199 
200     // Call on EDT
getUserDataCell(int row, int column)201     private UserDataCell getUserDataCell(int row, int column) {
202         Object obj = ((DefaultTableModel) getModel()).getValueAt(row, column);
203         if (obj instanceof UserDataCell) {
204             return (UserDataCell) obj;
205         }
206         return null;
207     }
208 
dispose()209     synchronized void dispose() {
210         listeners.clear();
211     }
212 
getReceivedNotifications(XMBean mbean)213     public long getReceivedNotifications(XMBean mbean) {
214         XMBeanNotificationsListener listener =
215                 listeners.get(mbean.getObjectName());
216         if (listener == null) {
217             return 0;
218         } else {
219             return listener.getReceivedNotifications();
220         }
221     }
222 
clearCurrentNotifications()223     public synchronized boolean clearCurrentNotifications() {
224         emptyTable();
225         if (currentListener != null) {
226             currentListener.clear();
227             return true;
228         } else {
229             return false;
230         }
231     }
232 
unregisterListener(DefaultMutableTreeNode node)233     public synchronized boolean unregisterListener(DefaultMutableTreeNode node) {
234         XMBean mbean = (XMBean) ((XNodeInfo) node.getUserObject()).getData();
235         return unregister(mbean.getObjectName());
236     }
237 
registerListener(DefaultMutableTreeNode node)238     public synchronized void registerListener(DefaultMutableTreeNode node)
239             throws InstanceNotFoundException, IOException {
240         XMBean mbean = (XMBean) ((XNodeInfo) node.getUserObject()).getData();
241         if (!subscribed) {
242             try {
243                 mbean.getMBeanServerConnection().addNotificationListener(
244                         MBeanServerDelegate.DELEGATE_NAME, this, null, null);
245                 subscribed = true;
246             } catch (Exception e) {
247                 if (JConsole.isDebug()) {
248                     System.err.println("Error adding listener for delegate:");
249                     e.printStackTrace();
250                 }
251             }
252         }
253         XMBeanNotificationsListener listener =
254                 listeners.get(mbean.getObjectName());
255         if (listener == null) {
256             listener = new XMBeanNotificationsListener(
257                     this, mbean, node, columnNames);
258             listeners.put(mbean.getObjectName(), listener);
259         } else {
260             if (!listener.isRegistered()) {
261                 emptyTable();
262                 listener.register(node);
263             }
264         }
265         enabled = true;
266         currentListener = listener;
267     }
268 
handleNotification( Notification notif, Object handback)269     public synchronized void handleNotification(
270             Notification notif, Object handback) {
271         try {
272             if (notif instanceof MBeanServerNotification) {
273                 ObjectName mbean =
274                         ((MBeanServerNotification) notif).getMBeanName();
275                 if (notif.getType().indexOf("JMX.mbean.unregistered") >= 0) {
276                     unregister(mbean);
277                 }
278             }
279         } catch (Exception e) {
280             if (JConsole.isDebug()) {
281                 System.err.println("Error unregistering notification:");
282                 e.printStackTrace();
283             }
284         }
285     }
286 
disableNotifications()287     public synchronized void disableNotifications() {
288         emptyTable();
289         currentListener = null;
290         enabled = false;
291     }
292 
unregister(ObjectName mbean)293     private synchronized boolean unregister(ObjectName mbean) {
294         XMBeanNotificationsListener listener = listeners.get(mbean);
295         if (listener != null && listener.isRegistered()) {
296             listener.unregister();
297             return true;
298         } else {
299             return false;
300         }
301     }
302 
addNotificationsListener(NotificationListener nl)303     public void addNotificationsListener(NotificationListener nl) {
304         notificationListenersList.add(nl);
305     }
306 
removeNotificationsListener(NotificationListener nl)307     public void removeNotificationsListener(NotificationListener nl) {
308         notificationListenersList.remove(nl);
309     }
310 
311     // Call on EDT
fireNotificationReceived( XMBeanNotificationsListener listener, XMBean mbean, DefaultMutableTreeNode node, Object[] rowData, long received)312     void fireNotificationReceived(
313             XMBeanNotificationsListener listener, XMBean mbean,
314             DefaultMutableTreeNode node, Object[] rowData, long received) {
315         if (enabled) {
316             DefaultTableModel tableModel = (DefaultTableModel) getModel();
317             if (listener == currentListener) {
318                 tableModel.insertRow(0, rowData);
319                 repaint();
320             }
321         }
322         Notification notif =
323                 new Notification(NOTIFICATION_RECEIVED_EVENT, this, 0);
324         notif.setUserData(received);
325         for (NotificationListener nl : notificationListenersList) {
326             nl.handleNotification(notif, node);
327         }
328     }
329 
330     // Call on EDT
updateModel(List<Object[]> data)331     private void updateModel(List<Object[]> data) {
332         emptyTable();
333         DefaultTableModel tableModel = (DefaultTableModel) getModel();
334         for (Object[] rowData : data) {
335             tableModel.addRow(rowData);
336         }
337     }
338 
isListenerRegistered(XMBean mbean)339     public synchronized boolean isListenerRegistered(XMBean mbean) {
340         XMBeanNotificationsListener listener =
341                 listeners.get(mbean.getObjectName());
342         if (listener == null) {
343             return false;
344         }
345         return listener.isRegistered();
346     }
347 
348     // Call on EDT
loadNotifications(XMBean mbean)349     public synchronized void loadNotifications(XMBean mbean) {
350         XMBeanNotificationsListener listener =
351                 listeners.get(mbean.getObjectName());
352         emptyTable();
353         if (listener != null) {
354             enabled = true;
355             List<Object[]> data = listener.getData();
356             updateModel(data);
357             currentListener = listener;
358             validate();
359             repaint();
360         } else {
361             enabled = false;
362         }
363     }
364 
365     // Call on EDT
setColumnEditors()366     private void setColumnEditors() {
367         TableColumnModel tcm = getColumnModel();
368         for (int i = 0; i < columnNames.length; i++) {
369             TableColumn tc = tcm.getColumn(i);
370             if (i == 2) {
371                 tc.setCellEditor(userDataEditor);
372             } else {
373                 tc.setCellEditor(editor);
374             }
375         }
376     }
377 
378     // Call on EDT
isTableEditable()379     public boolean isTableEditable() {
380         return true;
381     }
382 
383     // Call on EDT
emptyTable()384     public synchronized void emptyTable() {
385         DefaultTableModel model = (DefaultTableModel) getModel();
386         //invalidate();
387         while (model.getRowCount() > 0) {
388             model.removeRow(0);
389         }
390         validate();
391     }
392 
393     // Call on EDT
updateUserDataCell(int row, int col)394     synchronized void updateUserDataCell(int row, int col) {
395         Object obj = getModel().getValueAt(row, 2);
396         if (obj instanceof UserDataCell) {
397             UserDataCell cell = (UserDataCell) obj;
398             if (!cell.isInited()) {
399                 if (rowMinHeight == -1) {
400                     rowMinHeight = getRowHeight(row);
401                 }
402                 cell.init(super.getCellRenderer(row, col), rowMinHeight);
403             }
404 
405             cell.switchState();
406             setRowHeight(row, cell.getHeight());
407 
408             if (!cell.isMaximized()) {
409                 cancelCellEditing();
410                 //Back to simple editor.
411                 editCellAt(row, 2);
412             }
413 
414             invalidate();
415             repaint();
416         }
417     }
418 
419     class UserDataCellRenderer extends DefaultTableCellRenderer {
420 
421         Component comp;
422 
UserDataCellRenderer(Component comp)423         UserDataCellRenderer(Component comp) {
424             this.comp = comp;
425             Dimension d = comp.getPreferredSize();
426             if (d.getHeight() > 200) {
427                 comp.setPreferredSize(new Dimension((int) d.getWidth(), 200));
428             }
429         }
430 
431         @Override
getTableCellRendererComponent( JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)432         public Component getTableCellRendererComponent(
433                 JTable table,
434                 Object value,
435                 boolean isSelected,
436                 boolean hasFocus,
437                 int row,
438                 int column) {
439             return comp;
440         }
441 
getComponent()442         public Component getComponent() {
443             return comp;
444         }
445     }
446 
447     class UserDataCell {
448 
449         TableCellRenderer minRenderer;
450         UserDataCellRenderer maxRenderer;
451         int minHeight;
452         boolean minimized = true;
453         boolean init = false;
454         Object userData;
455 
UserDataCell(Object userData, Component max)456         UserDataCell(Object userData, Component max) {
457             this.userData = userData;
458             this.maxRenderer = new UserDataCellRenderer(max);
459 
460         }
461 
462         @Override
toString()463         public String toString() {
464             if (userData == null) {
465                 return null;
466             }
467             if (userData.getClass().isArray()) {
468                 String name =
469                         Utils.getArrayClassName(userData.getClass().getName());
470                 int length = Array.getLength(userData);
471                 return name + "[" + length + "]";
472             }
473 
474             if (userData instanceof CompositeData ||
475                     userData instanceof TabularData) {
476                 return userData.getClass().getName();
477             }
478 
479             return userData.toString();
480         }
481 
isInited()482         boolean isInited() {
483             return init;
484         }
485 
init(TableCellRenderer minRenderer, int minHeight)486         void init(TableCellRenderer minRenderer, int minHeight) {
487             this.minRenderer = minRenderer;
488             this.minHeight = minHeight;
489             init = true;
490         }
491 
switchState()492         void switchState() {
493             minimized = !minimized;
494         }
495 
isMaximized()496         boolean isMaximized() {
497             return !minimized;
498         }
499 
minimize()500         void minimize() {
501             minimized = true;
502         }
503 
maximize()504         void maximize() {
505             minimized = false;
506         }
507 
getHeight()508         int getHeight() {
509             if (minimized) {
510                 return minHeight;
511             } else {
512                 return (int) maxRenderer.getComponent().
513                         getPreferredSize().getHeight();
514             }
515         }
516 
getRenderer()517         TableCellRenderer getRenderer() {
518             if (minimized) {
519                 return minRenderer;
520             } else {
521                 return maxRenderer;
522             }
523         }
524     }
525 
526     class NotifMouseListener extends MouseAdapter {
527 
528         @Override
mousePressed(MouseEvent e)529         public void mousePressed(MouseEvent e) {
530             if (e.getButton() == MouseEvent.BUTTON1) {
531                 if (e.getClickCount() >= 2) {
532                     int row = XMBeanNotifications.this.getSelectedRow();
533                     int col = XMBeanNotifications.this.getSelectedColumn();
534                     if (col != 2) {
535                         return;
536                     }
537                     if (col == -1 || row == -1) {
538                         return;
539                     }
540 
541                     XMBeanNotifications.this.updateUserDataCell(row, col);
542                 }
543             }
544         }
545     }
546 
547     class UserDataCellEditor extends XTextFieldEditor {
548         // implements javax.swing.table.TableCellEditor
549         @Override
getTableCellEditorComponent( JTable table, Object value, boolean isSelected, int row, int column)550         public Component getTableCellEditorComponent(
551                 JTable table,
552                 Object value,
553                 boolean isSelected,
554                 int row,
555                 int column) {
556             Object val = value;
557             if (column == 2) {
558                 Object obj = getModel().getValueAt(row, column);
559                 if (obj instanceof UserDataCell) {
560                     UserDataCell cell = (UserDataCell) obj;
561                     if (cell.getRenderer() instanceof UserDataCellRenderer) {
562                         UserDataCellRenderer zr =
563                                 (UserDataCellRenderer) cell.getRenderer();
564                         return zr.getComponent();
565                     }
566                 } else {
567                     Component comp = super.getTableCellEditorComponent(
568                             table, val, isSelected, row, column);
569                     textField.setEditable(false);
570                     return comp;
571                 }
572             }
573             return super.getTableCellEditorComponent(
574                     table,
575                     val,
576                     isSelected,
577                     row,
578                     column);
579         }
580 
581         @Override
stopCellEditing()582         public boolean stopCellEditing() {
583             int editingRow = getEditingRow();
584             int editingColumn = getEditingColumn();
585             if (editingColumn == 2) {
586                 Object obj = getModel().getValueAt(editingRow, editingColumn);
587                 if (obj instanceof UserDataCell) {
588                     UserDataCell cell = (UserDataCell) obj;
589                     if (cell.isMaximized()) {
590                         cancelCellEditing();
591                         return true;
592                     }
593                 }
594             }
595             return super.stopCellEditing();
596         }
597     }
598 
599     class XMBeanNotificationsListener implements NotificationListener {
600 
601         private XMBean xmbean;
602         private DefaultMutableTreeNode node;
603         private volatile long received;
604         private XMBeanNotifications notifications;
605         private volatile boolean unregistered;
606         private ArrayList<Object[]> data = new ArrayList<Object[]>();
607 
XMBeanNotificationsListener( XMBeanNotifications notifications, XMBean xmbean, DefaultMutableTreeNode node, String[] columnNames)608         public XMBeanNotificationsListener(
609                 XMBeanNotifications notifications,
610                 XMBean xmbean,
611                 DefaultMutableTreeNode node,
612                 String[] columnNames) {
613             this.notifications = notifications;
614             this.xmbean = xmbean;
615             this.node = node;
616             register(node);
617         }
618 
getData()619         public synchronized List<Object[]> getData() {
620             return data;
621         }
622 
clear()623         public synchronized void clear() {
624             data.clear();
625             received = 0;
626         }
627 
isRegistered()628         public synchronized boolean isRegistered() {
629             return !unregistered;
630         }
631 
unregister()632         public synchronized void unregister() {
633             try {
634                 xmbean.getMBeanServerConnection().removeNotificationListener(
635                         xmbean.getObjectName(), this, null, null);
636             } catch (Exception e) {
637                 if (JConsole.isDebug()) {
638                     System.err.println("Error removing listener:");
639                     e.printStackTrace();
640                 }
641             }
642             unregistered = true;
643         }
644 
getReceivedNotifications()645         public synchronized long getReceivedNotifications() {
646             return received;
647         }
648 
register(DefaultMutableTreeNode node)649         public synchronized void register(DefaultMutableTreeNode node) {
650             clear();
651             this.node = node;
652             try {
653                 xmbean.getMBeanServerConnection().addNotificationListener(
654                         xmbean.getObjectName(), this, null, null);
655                 unregistered = false;
656             } catch (Exception e) {
657                 if (JConsole.isDebug()) {
658                     System.err.println("Error adding listener:");
659                     e.printStackTrace();
660                 }
661             }
662         }
663 
handleNotification( final Notification n, Object hb)664         public synchronized void handleNotification(
665                 final Notification n, Object hb) {
666             EventQueue.invokeLater(new Runnable() {
667 
668                 public void run() {
669                     synchronized (XMBeanNotificationsListener.this) {
670                         try {
671                             if (unregistered) {
672                                 return;
673                             }
674                             Date receivedDate = new Date(n.getTimeStamp());
675                             String time = timeFormater.format(receivedDate);
676 
677                             Object userData = n.getUserData();
678                             Component comp = null;
679                             UserDataCell cell = null;
680                             if ((comp = XDataViewer.createNotificationViewer(userData)) != null) {
681                                 XDataViewer.registerForMouseEvent(comp, mouseListener);
682                                 cell = new UserDataCell(userData, comp);
683                             }
684 
685                             Object[] rowData = {
686                                 time,
687                                 n.getType(),
688                                 (cell == null ? userData : cell),
689                                 n.getSequenceNumber(),
690                                 n.getMessage(),
691                                 n,
692                                 n.getSource()
693                             };
694                             received++;
695                             data.add(0, rowData);
696 
697                             notifications.fireNotificationReceived(
698                                     XMBeanNotificationsListener.this,
699                                     xmbean, node, rowData, received);
700                         } catch (Exception e) {
701                             if (JConsole.isDebug()) {
702                                 System.err.println("Error handling notification:");
703                                 e.printStackTrace();
704                             }
705                         }
706                     }
707                 }
708             });
709         }
710     }
711 }
712