1 /*
   2  * Copyright (c) 2004, 2008, 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.resources.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 
  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
 101     public void cancelCellEditing() {
 102         TableCellEditor tce = getCellEditor();
 103         if (tce != null) {
 104             tce.cancelCellEditing();
 105         }
 106     }
 107 
 108     // Call on EDT
 109     public void stopCellEditing() {
 110         TableCellEditor tce = getCellEditor();
 111         if (tce != null) {
 112             tce.stopCellEditing();
 113         }
 114     }
 115 
 116     // Call on EDT
 117     @Override
 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
 128     public void setValueAt(Object value, int row, int column) {
 129     }
 130 
 131     // Call on EDT
 132     @Override
 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
 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
 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 
 209     synchronized void dispose() {
 210         listeners.clear();
 211     }
 212 
 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 
 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 
 233     public synchronized boolean unregisterListener(DefaultMutableTreeNode node) {
 234         XMBean mbean = (XMBean) ((XNodeInfo) node.getUserObject()).getData();
 235         return unregister(mbean.getObjectName());
 236     }
 237 
 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 
 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 
 287     public synchronized void disableNotifications() {
 288         emptyTable();
 289         currentListener = null;
 290         enabled = false;
 291     }
 292 
 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 
 303     public void addNotificationsListener(NotificationListener nl) {
 304         notificationListenersList.add(nl);
 305     }
 306 
 307     public void removeNotificationsListener(NotificationListener nl) {
 308         notificationListenersList.remove(nl);
 309     }
 310 
 311     // Call on EDT
 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
 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 
 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
 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
 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
 379     public boolean isTableEditable() {
 380         return true;
 381     }
 382 
 383     // Call on EDT
 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
 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 
 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
 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 
 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 
 456         UserDataCell(Object userData, Component max) {
 457             this.userData = userData;
 458             this.maxRenderer = new UserDataCellRenderer(max);
 459 
 460         }
 461 
 462         @Override
 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 
 482         boolean isInited() {
 483             return init;
 484         }
 485 
 486         void init(TableCellRenderer minRenderer, int minHeight) {
 487             this.minRenderer = minRenderer;
 488             this.minHeight = minHeight;
 489             init = true;
 490         }
 491 
 492         void switchState() {
 493             minimized = !minimized;
 494         }
 495 
 496         boolean isMaximized() {
 497             return !minimized;
 498         }
 499 
 500         void minimize() {
 501             minimized = true;
 502         }
 503 
 504         void maximize() {
 505             minimized = false;
 506         }
 507 
 508         int getHeight() {
 509             if (minimized) {
 510                 return minHeight;
 511             } else {
 512                 return (int) maxRenderer.getComponent().
 513                         getPreferredSize().getHeight();
 514             }
 515         }
 516 
 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
 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
 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
 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 
 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 
 619         public synchronized List<Object[]> getData() {
 620             return data;
 621         }
 622 
 623         public synchronized void clear() {
 624             data.clear();
 625             received = 0;
 626         }
 627 
 628         public synchronized boolean isRegistered() {
 629             return !unregistered;
 630         }
 631 
 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 
 645         public synchronized long getReceivedNotifications() {
 646             return received;
 647         }
 648 
 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 
 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 }