6711106: REGRESSION: Bad usage of SnapshotMBeanServerConnection in MBeans tab and JConsole plugins.
Reviewed-by: jfdenise
/*
* Copyright 2004-2007 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Sun designates this
* particular file as subject to the "Classpath" exception as provided
* by Sun in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
* CA 95054 USA or visit www.sun.com if you need additional information or
* have any questions.
*/
package sun.tools.jconsole.inspector;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
import javax.swing.tree.*;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridLayout;
import java.awt.FlowLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.*;
import java.awt.Insets;
import java.awt.Dimension;
import java.util.*;
import java.io.*;
import java.lang.reflect.Array;
import javax.management.*;
import javax.management.openmbean.CompositeData;
import javax.management.openmbean.TabularData;
import sun.tools.jconsole.Resources;
import sun.tools.jconsole.MBeansTab;
import sun.tools.jconsole.Plotter;
import sun.tools.jconsole.JConsole;
import sun.tools.jconsole.ProxyClient.SnapshotMBeanServerConnection;
/*IMPORTANT :
There is a deadlock issue there if we don't synchronize well loadAttributes,
refresh attributes and empty table methods since a UI thread can call
loadAttributes and at the same time a JMX notification can raise an
emptyTable. Since there are synchronization in the JMX world it's
COMPULSORY to not call the JMX world in synchronized blocks */
@SuppressWarnings("serial")
public class XMBeanAttributes extends XTable {
private final static String[] columnNames =
{Resources.getText("Name"),
Resources.getText("Value")};
private boolean editable = true;
private XMBean mbean;
private MBeanInfo mbeanInfo;
private MBeanAttributeInfo[] attributesInfo;
private HashMap<String, Object> attributes;
private HashMap<String, Object> unavailableAttributes;
private HashMap<String, Object> viewableAttributes;
private WeakHashMap<XMBean, HashMap<String, ZoomedCell>> viewersCache =
new WeakHashMap<XMBean, HashMap<String, ZoomedCell>>();
private TableModelListener attributesListener;
private MBeansTab mbeansTab;
private XTable table;
private TableCellEditor valueCellEditor = new ValueCellEditor();
private int rowMinHeight = -1;
private AttributesMouseListener mouseListener = new AttributesMouseListener();
private static TableCellEditor editor =
new Utils.ReadOnlyTableCellEditor(new JTextField());
public XMBeanAttributes(MBeansTab mbeansTab) {
super();
this.mbeansTab = mbeansTab;
((DefaultTableModel)getModel()).setColumnIdentifiers(columnNames);
getModel().addTableModelListener(attributesListener =
new AttributesListener(this));
getColumnModel().getColumn(NAME_COLUMN).setPreferredWidth(40);
addMouseListener(mouseListener);
getTableHeader().setReorderingAllowed(false);
setColumnEditors();
addKeyListener(new Utils.CopyKeyAdapter());
}
public synchronized Component prepareRenderer(TableCellRenderer renderer,
int row, int column) {
//In case we have a repaint thread that is in the process of
//repainting an obsolete table, just ignore the call.
//It can happen when MBean selection is switched at a very quick rate
if(row >= getRowCount())
return null;
else
return super.prepareRenderer(renderer, row, column);
}
void updateRowHeight(Object obj, int row) {
ZoomedCell cell = null;
if(obj instanceof ZoomedCell) {
cell = (ZoomedCell) obj;
if(cell.isInited())
setRowHeight(row, cell.getHeight());
else
if(rowMinHeight != - 1)
setRowHeight(row, rowMinHeight);
} else
if(rowMinHeight != - 1)
setRowHeight(row, rowMinHeight);
}
public synchronized TableCellRenderer getCellRenderer(int row,
int column) {
//In case we have a repaint thread that is in the process of
//repainting an obsolete table, just ignore the call.
//It can happen when MBean selection is switched at a very quick rate
if (row >= getRowCount()) {
return null;
} else {
if (column == VALUE_COLUMN) {
Object obj = getModel().getValueAt(row, column);
if (obj instanceof ZoomedCell) {
ZoomedCell cell = (ZoomedCell) obj;
if (cell.isInited()) {
DefaultTableCellRenderer renderer =
(DefaultTableCellRenderer) cell.getRenderer();
renderer.setToolTipText(getToolTip(row,column));
return renderer;
}
}
}
DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)
super.getCellRenderer(row, column);
if (!isCellError(row, column)) {
if (!(isColumnEditable(column) && isWritable(row) &&
Utils.isEditableType(getClassName(row)))) {
renderer.setForeground(getDefaultColor());
}
}
return renderer;
}
}
private void setColumnEditors() {
TableColumnModel tcm = getColumnModel();
for (int i = 0; i < columnNames.length; i++) {
TableColumn tc = tcm.getColumn(i);
if (isColumnEditable(i)) {
tc.setCellEditor(valueCellEditor);
} else {
tc.setCellEditor(editor);
}
}
}
public void cancelCellEditing() {
TableCellEditor editor = getCellEditor();
if (editor != null) {
editor.cancelCellEditing();
}
}
public void stopCellEditing() {
TableCellEditor editor = getCellEditor();
if (editor != null) {
editor.stopCellEditing();
}
}
public final boolean editCellAt(int row, int column, EventObject e) {
boolean retVal = super.editCellAt(row, column, e);
if (retVal) {
TableCellEditor editor =
getColumnModel().getColumn(column).getCellEditor();
if (editor == valueCellEditor) {
((JComponent) editor).requestFocus();
}
}
return retVal;
}
@Override
public boolean isCellEditable(int row, int col) {
// All the cells in non-editable columns are editable
if (!isColumnEditable(col)) {
return true;
}
// Maximized zoomed cells are editable
Object obj = getModel().getValueAt(row, col);
if (obj instanceof ZoomedCell) {
ZoomedCell cell = (ZoomedCell) obj;
return cell.isMaximized();
}
return true;
}
@Override
public void setValueAt(Object value, int row, int column) {
if (!isCellError(row, column) && isColumnEditable(column) &&
isWritable(row) && Utils.isEditableType(getClassName(row))) {
super.setValueAt(value, row, column);
}
}
//Table methods
public boolean isTableEditable() {
return true;
}
public void setTableValue(Object value, int row) {
}
public boolean isColumnEditable(int column) {
if (column < getColumnCount()) {
return getColumnName(column).equals(Resources.getText("Value"));
}
else {
return false;
}
}
public String getClassName(int row) {
int index = convertRowToIndex(row);
if (index != -1) {
return attributesInfo[index].getType();
}
else {
return null;
}
}
public String getValueName(int row) {
int index = convertRowToIndex(row);
if (index != -1) {
return attributesInfo[index].getName();
}
else {
return null;
}
}
public Object getValue(int row) {
return ((DefaultTableModel) getModel()).getValueAt(row, VALUE_COLUMN);
}
//tool tip only for editable column
public String getToolTip(int row, int column) {
if (isCellError(row, column)) {
return (String) unavailableAttributes.get(getValueName(row));
}
if (isColumnEditable(column)) {
Object value = getValue(row);
String tip = null;
if (value != null) {
tip = value.toString();
if(isAttributeViewable(row, VALUE_COLUMN))
tip = Resources.getText("Double click to expand/collapse")+
". " + tip;
}
return tip;
}
if(column == NAME_COLUMN) {
int index = convertRowToIndex(row);
if (index != -1) {
return attributesInfo[index].getDescription();
}
}
return null;
}
public synchronized boolean isWritable(int row) {
int index = convertRowToIndex(row);
if (index != -1) {
return (attributesInfo[index].isWritable());
}
else {
return false;
}
}
/**
* Override JTable method in order to make any call to this method
* atomic with TableModel elements.
*/
public synchronized int getRowCount() {
return super.getRowCount();
}
public synchronized boolean isReadable(int row) {
int index = convertRowToIndex(row);
if (index != -1) {
return (attributesInfo[index].isReadable());
}
else {
return false;
}
}
public synchronized boolean isCellError(int row, int col) {
return (isColumnEditable(col) &&
(unavailableAttributes.containsKey(getValueName(row))));
}
public synchronized boolean isAttributeViewable(int row, int col) {
boolean isViewable = false;
if(col == VALUE_COLUMN) {
Object obj = getModel().getValueAt(row, col);
if(obj instanceof ZoomedCell)
isViewable = true;
}
return isViewable;
}
public void loadAttributes(final XMBean mbean, MBeanInfo mbeanInfo) {
// To avoid deadlock with events coming from the JMX side,
// we retrieve all JMX stuff in a non synchronized block.
if(mbean == null) return;
final MBeanAttributeInfo[] attributesInfo = mbeanInfo.getAttributes();
final HashMap<String, Object> attributes =
new HashMap<String, Object>(attributesInfo.length);
final HashMap<String, Object> unavailableAttributes =
new HashMap<String, Object>(attributesInfo.length);
final HashMap<String, Object> viewableAttributes =
new HashMap<String, Object>(attributesInfo.length);
AttributeList list = null;
try {
list = mbean.getAttributes(attributesInfo);
} catch (Exception e) {
if (JConsole.isDebug()) {
System.err.println("Error calling getAttributes() on MBean \"" +
mbean.getObjectName() + "\". JConsole will " +
"try to get them individually calling " +
"getAttribute() instead. Exception:");
e.printStackTrace(System.err);
}
list = new AttributeList();
//Can't load all attributes, do it one after each other.
for(int i = 0; i < attributesInfo.length; i++) {
String name = null;
try {
name = attributesInfo[i].getName();
Object value =
mbean.getMBeanServerConnection().getAttribute(mbean.getObjectName(), name);
list.add(new Attribute(name, value));
}catch(Exception ex) {
if(attributesInfo[i].isReadable()) {
unavailableAttributes.put(name,
Utils.getActualException(ex).
toString());
}
}
}
}
try {
int att_length = list.size();
for (int i=0;i<att_length;i++) {
Attribute attribute = (Attribute) list.get(i);
if(isViewable(attribute)) {
viewableAttributes.put(attribute.getName(),
attribute.getValue());
}
else
attributes.put(attribute.getName(),attribute.getValue());
}
// if not all attributes are accessible,
// check them one after the other.
if (att_length < attributesInfo.length) {
for (int i=0;i<attributesInfo.length;i++) {
MBeanAttributeInfo attributeInfo = attributesInfo[i];
if (!attributes.containsKey(attributeInfo.getName()) &&
!viewableAttributes.containsKey(attributeInfo.
getName()) &&
!unavailableAttributes.containsKey(attributeInfo.
getName())) {
if (attributeInfo.isReadable()) {
// getAttributes didn't help resolving the
// exception.
// We must call it again to understand what
// went wrong.
try {
Object v =
mbean.getMBeanServerConnection().getAttribute(
mbean.getObjectName(), attributeInfo.getName());
//What happens if now it is ok?
// Be pragmatic, add it to readable...
attributes.put(attributeInfo.getName(),
v);
}catch(Exception e) {
//Put the exception that will be displayed
// in tooltip
unavailableAttributes.put(attributeInfo.
getName(),
Utils.
getActualException(e)
.toString());
}
}
}
}
}
}
catch(Exception e) {
//sets all attributes unavailable except the writable ones
for (int i=0;i<attributesInfo.length;i++) {
MBeanAttributeInfo attributeInfo = attributesInfo[i];
if (attributeInfo.isReadable()) {
unavailableAttributes.put(attributeInfo.getName(),
Utils.getActualException(e).
toString());
}
}
}
//end of retrieval
//one update at a time
synchronized(this) {
this.mbean = mbean;
this.mbeanInfo = mbeanInfo;
this.attributesInfo = attributesInfo;
this.attributes = attributes;
this.unavailableAttributes = unavailableAttributes;
this.viewableAttributes = viewableAttributes;
EventQueue.invokeLater(new Runnable() {
public void run() {
DefaultTableModel tableModel =
(DefaultTableModel) getModel();
// don't listen to these events
tableModel.removeTableModelListener(attributesListener);
// add attribute information
emptyTable();
addTableData(tableModel,
mbean,
attributesInfo,
attributes,
unavailableAttributes,
viewableAttributes);
// update the model with the new data
tableModel.newDataAvailable(new TableModelEvent(tableModel));
// re-register for change events
tableModel.addTableModelListener(attributesListener);
}
});
}
}
void collapse(String attributeName, final Component c) {
final int row = getSelectedRow();
Object obj = getModel().getValueAt(row, VALUE_COLUMN);
if(obj instanceof ZoomedCell) {
cancelCellEditing();
ZoomedCell cell = (ZoomedCell) obj;
cell.reset();
setRowHeight(row,
cell.getHeight());
editCellAt(row,
VALUE_COLUMN);
invalidate();
repaint();
}
}
ZoomedCell updateZoomedCell(int row,
int col) {
Object obj = getModel().getValueAt(row, VALUE_COLUMN);
ZoomedCell cell = null;
if(obj instanceof ZoomedCell) {
cell = (ZoomedCell) obj;
if(!cell.isInited()) {
Object elem = cell.getValue();
String attributeName =
(String) getModel().getValueAt(row,
NAME_COLUMN);
Component comp = mbeansTab.getDataViewer().
createAttributeViewer(elem, mbean, attributeName, this);
if(comp != null){
if(rowMinHeight == -1)
rowMinHeight = getRowHeight(row);
cell.init(super.getCellRenderer(row, col),
comp,
rowMinHeight);
mbeansTab.getDataViewer().registerForMouseEvent(
comp, mouseListener);
} else
return cell;
}
cell.switchState();
setRowHeight(row,
cell.getHeight());
if(!cell.isMaximized()) {
cancelCellEditing();
//Back to simple editor.
editCellAt(row,
VALUE_COLUMN);
}
invalidate();
repaint();
}
return cell;
}
public void refreshAttributes() {
SnapshotMBeanServerConnection mbsc = mbeansTab.getSnapshotMBeanServerConnection();
mbsc.flush();
stopCellEditing();
loadAttributes(mbean, mbeanInfo);
}
public void emptyTable() {
synchronized(this) {
((DefaultTableModel) getModel()).
removeTableModelListener(attributesListener);
super.emptyTable();
}
}
private boolean isViewable(Attribute attribute) {
Object data = attribute.getValue();
return XDataViewer.isViewableValue(data);
}
synchronized void removeAttributes() {
if (attributes != null) {
attributes.clear();
}
if (unavailableAttributes != null) {
unavailableAttributes.clear();
}
if (viewableAttributes != null) {
viewableAttributes.clear();
}
mbean = null;
}
private ZoomedCell getZoomedCell(XMBean mbean, String attribute, Object value) {
synchronized (viewersCache) {
HashMap<String, ZoomedCell> viewers;
if (viewersCache.containsKey(mbean)) {
viewers = viewersCache.get(mbean);
} else {
viewers = new HashMap<String, ZoomedCell>();
}
ZoomedCell cell;
if (viewers.containsKey(attribute)) {
cell = viewers.get(attribute);
cell.setValue(value);
if (cell.isMaximized() && cell.getType() != XDataViewer.NUMERIC) {
// Plotters are the only viewers with auto update capabilities.
// Other viewers need to be updated manually.
Component comp =
mbeansTab.getDataViewer().createAttributeViewer(
value, mbean, attribute, XMBeanAttributes.this);
cell.init(cell.getMinRenderer(), comp, cell.getMinHeight());
mbeansTab.getDataViewer().registerForMouseEvent(comp, mouseListener);
}
} else {
cell = new ZoomedCell(value);
viewers.put(attribute, cell);
}
viewersCache.put(mbean, viewers);
return cell;
}
}
//will be called in a synchronzed block
protected void addTableData(DefaultTableModel tableModel,
XMBean mbean,
MBeanAttributeInfo[] attributesInfo,
HashMap<String, Object> attributes,
HashMap<String, Object> unavailableAttributes,
HashMap<String, Object> viewableAttributes) {
Object rowData[] = new Object[2];
int col1Width = 0;
int col2Width = 0;
for (int i = 0; i < attributesInfo.length; i++) {
rowData[0] = (attributesInfo[i].getName());
if (unavailableAttributes.containsKey(rowData[0])) {
rowData[1] = Resources.getText("Unavailable");
} else if (viewableAttributes.containsKey(rowData[0])) {
rowData[1] = viewableAttributes.get(rowData[0]);
if (!attributesInfo[i].isWritable() ||
!Utils.isEditableType(attributesInfo[i].getType())) {
rowData[1] = getZoomedCell(mbean, (String) rowData[0], rowData[1]);
}
} else {
rowData[1] = attributes.get(rowData[0]);
}
tableModel.addRow(rowData);
//Update column width
//
String str = null;
if(rowData[0] != null) {
str = rowData[0].toString();
if(str.length() > col1Width)
col1Width = str.length();
}
if(rowData[1] != null) {
str = rowData[1].toString();
if(str.length() > col2Width)
col2Width = str.length();
}
}
updateColumnWidth(col1Width, col2Width);
}
private void updateColumnWidth(int col1Width, int col2Width) {
TableColumnModel colModel = getColumnModel();
//Get the column at index pColumn, and set its preferred width.
col1Width = col1Width * 7;
col2Width = col2Width * 7;
if(col1Width + col2Width <
(int) getPreferredScrollableViewportSize().getWidth())
col2Width = (int) getPreferredScrollableViewportSize().getWidth()
- col1Width;
colModel.getColumn(NAME_COLUMN).setPreferredWidth(50);
}
class AttributesMouseListener extends MouseAdapter {
public void mousePressed(MouseEvent e) {
if(e.getButton() == MouseEvent.BUTTON1) {
if(e.getClickCount() >= 2) {
int row = XMBeanAttributes.this.getSelectedRow();
int col = XMBeanAttributes.this.getSelectedColumn();
if(col != VALUE_COLUMN) return;
if(col == -1 || row == -1) return;
XMBeanAttributes.this.updateZoomedCell(row, col);
}
}
}
}
class ValueCellEditor extends XTextFieldEditor {
// implements javax.swing.table.TableCellEditor
public Component getTableCellEditorComponent(JTable table,
Object value,
boolean isSelected,
int row,
int column) {
Object val = value;
if(column == VALUE_COLUMN) {
Object obj = getModel().getValueAt(row,
column);
if(obj instanceof ZoomedCell) {
ZoomedCell cell = (ZoomedCell) obj;
if(cell.getRenderer() instanceof MaximizedCellRenderer) {
MaximizedCellRenderer zr =
(MaximizedCellRenderer) cell.getRenderer();
return zr.getComponent();
}
} else {
Component comp = super.getTableCellEditorComponent(
table, val, isSelected, row, column);
if (isCellError(row, column) ||
!isWritable(row) ||
!Utils.isEditableType(getClassName(row))) {
textField.setEditable(false);
}
return comp;
}
}
return super.getTableCellEditorComponent(table,
val,
isSelected,
row,
column);
}
@Override
public boolean stopCellEditing() {
int editingRow = getEditingRow();
int editingColumn = getEditingColumn();
if (editingColumn == VALUE_COLUMN) {
Object obj = getModel().getValueAt(editingRow, editingColumn);
if (obj instanceof ZoomedCell) {
ZoomedCell cell = (ZoomedCell) obj;
if (cell.isMaximized()) {
this.cancelCellEditing();
return true;
}
}
}
return super.stopCellEditing();
}
}
class MaximizedCellRenderer extends DefaultTableCellRenderer {
Component comp;
MaximizedCellRenderer(Component comp) {
this.comp = comp;
Dimension d = comp.getPreferredSize();
if (d.getHeight() > 200) {
comp.setPreferredSize(new Dimension((int) d.getWidth(), 200));
}
}
public Component getTableCellRendererComponent(JTable table,
Object value,
boolean isSelected,
boolean hasFocus,
int row,
int column) {
return comp;
}
public Component getComponent() {
return comp;
}
}
class ZoomedCell {
TableCellRenderer minRenderer;
MaximizedCellRenderer maxRenderer;
int minHeight;
boolean minimized = true;
boolean init = false;
int type;
Object value;
ZoomedCell(Object value) {
type = XDataViewer.getViewerType(value);
this.value = value;
}
boolean isInited() {
return init;
}
Object getValue() {
return value;
}
void setValue(Object value) {
this.value = value;
}
void init(TableCellRenderer minRenderer,
Component maxComponent,
int minHeight) {
this.minRenderer = minRenderer;
this.maxRenderer = new MaximizedCellRenderer(maxComponent);
this.minHeight = minHeight;
init = true;
}
int getType() {
return type;
}
void reset() {
init = false;
minimized = true;
}
void switchState() {
minimized = !minimized;
}
boolean isMaximized() {
return !minimized;
}
void minimize() {
minimized = true;
}
void maximize() {
minimized = false;
}
int getHeight() {
if(minimized) return minHeight;
else
return (int) maxRenderer.getComponent().
getPreferredSize().getHeight() ;
}
int getMinHeight() {
return minHeight;
}
public String toString() {
if(value == null) return null;
if(value.getClass().isArray()) {
String name =
Utils.getArrayClassName(value.getClass().getName());
int length = Array.getLength(value);
return name + "[" + length +"]";
}
if(value instanceof CompositeData ||
value instanceof TabularData)
return value.getClass().getName();
return value.toString();
}
TableCellRenderer getRenderer() {
if(minimized) return minRenderer;
else return maxRenderer;
}
TableCellRenderer getMinRenderer() {
return minRenderer;
}
}
class AttributesListener implements TableModelListener {
private Component component;
public AttributesListener(Component component) {
this.component = component;
}
public void tableChanged(final TableModelEvent e) {
final TableModel model = (TableModel)e.getSource();
// only post changes to the draggable column
if (isColumnEditable(e.getColumn())) {
mbeansTab.workerAdd(new Runnable() {
public void run() {
try {
Object tableValue =
model.getValueAt(e.getFirstRow(),
e.getColumn());
// if it's a String, try construct new value
// using the defined type.
if (tableValue instanceof String) {
tableValue =
Utils.createObjectFromString(getClassName(e.getFirstRow()), // type
(String)tableValue);// value
}
String attributeName =
getValueName(e.getFirstRow());
Attribute attribute =
new Attribute(attributeName,tableValue);
mbean.setAttribute(attribute);
}
catch (Throwable ex) {
if (JConsole.isDebug()) {
ex.printStackTrace();
}
ex = Utils.getActualException(ex);
String message = (ex.getMessage() != null) ? ex.getMessage() : ex.toString();
EventQueue.invokeLater(new ThreadDialog(component,
message+"\n",
Resources.getText("Problem setting attribute"),
JOptionPane.ERROR_MESSAGE));
}
refreshAttributes();
}
});
}
}
}
}