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