/*
 * Copyright (c) 2004, 2007, Oracle and/or its affiliates. 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.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.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.MBeansTab;
import sun.tools.jconsole.Plotter;
import sun.tools.jconsole.JConsole;
import sun.tools.jconsole.Messages;
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 =
    {Messages.NAME,
     Messages.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(Messages.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 = Messages.DOUBLE_CLICK_TO_EXPAND_FORWARD_SLASH_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.
/** * 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) { 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.getAttribute(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.getAttribute(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); XDataViewer.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() { MBeanServerConnection mbsc = mbeansTab.getMBeanServerConnection(); if (mbsc instanceof SnapshotMBeanServerConnection) { ((SnapshotMBeanServerConnection) 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()); XDataViewer.registerForMouseEvent(comp, mouseListener); } } else { cell = new ZoomedCell(value); viewers.put(attribute, cell); } viewersCache.put(mbean, viewers); return cell; } } //will be called in a synchronized 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] = Messages.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", Messages.PROBLEM_SETTING_ATTRIBUTE, JOptionPane.ERROR_MESSAGE)); } refreshAttributes(); } }); } } } }