1/*
2 * Copyright (c) 2004, 2013, 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
26package sun.tools.jconsole;
27
28import java.awt.*;
29import java.awt.event.*;
30import java.beans.*;
31import java.lang.reflect.*;
32import java.util.*;
33import java.util.List;
34import java.util.Timer;
35import javax.swing.*;
36import javax.swing.plaf.*;
37
38
39import com.sun.tools.jconsole.JConsolePlugin;
40import com.sun.tools.jconsole.JConsoleContext;
41
42import static sun.tools.jconsole.ProxyClient.*;
43
44@SuppressWarnings("serial")
45public class VMPanel extends JTabbedPane implements PropertyChangeListener {
46
47    private ProxyClient proxyClient;
48    private Timer timer;
49    private int updateInterval;
50    private String hostName;
51    private int port;
52    private String userName;
53    private String password;
54    private String url;
55    private VMInternalFrame vmIF = null;
56    private static ArrayList<TabInfo> tabInfos = new ArrayList<TabInfo>();
57    private boolean wasConnected = false;
58    private boolean userDisconnected = false;
59    private boolean shouldUseSSL = true;
60
61    // The everConnected flag keeps track of whether the window can be
62    // closed if the user clicks Cancel after a failed connection attempt.
63    //
64    private boolean everConnected = false;
65
66    // The initialUpdate flag is used to enable/disable tabs each time
67    // a connect or reconnect takes place. This flag avoids having to
68    // enable/disable tabs on each update call.
69    //
70    private boolean initialUpdate = true;
71
72    // Each VMPanel has its own instance of the JConsolePlugin
73    // A map of JConsolePlugin to the previous SwingWorker
74    private Map<ExceptionSafePlugin, SwingWorker<?, ?>> plugins = null;
75    private boolean pluginTabsAdded = false;
76
77    // Update these only on the EDT
78    private JOptionPane optionPane;
79    private JProgressBar progressBar;
80    private long time0;
81
82    static {
83        tabInfos.add(new TabInfo(OverviewTab.class, OverviewTab.getTabName(), true));
84        tabInfos.add(new TabInfo(MemoryTab.class, MemoryTab.getTabName(), true));
85        tabInfos.add(new TabInfo(ThreadTab.class, ThreadTab.getTabName(), true));
86        tabInfos.add(new TabInfo(ClassTab.class, ClassTab.getTabName(), true));
87        tabInfos.add(new TabInfo(SummaryTab.class, SummaryTab.getTabName(), true));
88        tabInfos.add(new TabInfo(MBeansTab.class, MBeansTab.getTabName(), true));
89    }
90
91    public static TabInfo[] getTabInfos() {
92        return tabInfos.toArray(new TabInfo[tabInfos.size()]);
93    }
94
95    VMPanel(ProxyClient proxyClient, int updateInterval) {
96        this.proxyClient = proxyClient;
97        this.updateInterval = updateInterval;
98        this.hostName = proxyClient.getHostName();
99        this.port = proxyClient.getPort();
100        this.userName = proxyClient.getUserName();
101        this.password = proxyClient.getPassword();
102        this.url = proxyClient.getUrl();
103
104        for (TabInfo tabInfo : tabInfos) {
105            if (tabInfo.tabVisible) {
106                addTab(tabInfo);
107            }
108        }
109
110        plugins = new LinkedHashMap<ExceptionSafePlugin, SwingWorker<?, ?>>();
111        for (JConsolePlugin p : JConsole.getPlugins()) {
112            p.setContext(proxyClient);
113            plugins.put(new ExceptionSafePlugin(p), null);
114        }
115
116        Utilities.updateTransparency(this);
117
118        ToolTipManager.sharedInstance().registerComponent(this);
119
120        // Start listening to connection state events
121        //
122        proxyClient.addPropertyChangeListener(this);
123
124        addMouseListener(new MouseAdapter() {
125
126            public void mouseClicked(MouseEvent e) {
127                if (connectedIconBounds != null
128                        && (e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) != 0
129                        && connectedIconBounds.contains(e.getPoint())) {
130
131                    if (isConnected()) {
132                        userDisconnected = true;
133                        disconnect();
134                        wasConnected = false;
135                    } else {
136                        connect();
137                    }
138                    repaint();
139                }
140            }
141        });
142
143    }
144    private static Icon connectedIcon16 =
145            new ImageIcon(VMPanel.class.getResource("resources/connected16.png"));
146    private static Icon connectedIcon24 =
147            new ImageIcon(VMPanel.class.getResource("resources/connected24.png"));
148    private static Icon disconnectedIcon16 =
149            new ImageIcon(VMPanel.class.getResource("resources/disconnected16.png"));
150    private static Icon disconnectedIcon24 =
151            new ImageIcon(VMPanel.class.getResource("resources/disconnected24.png"));
152    private Rectangle connectedIconBounds;
153
154    // Override to increase right inset for tab area,
155    // in order to reserve space for the connect toggle.
156    public void setUI(TabbedPaneUI ui) {
157        Insets insets = (Insets) UIManager.getLookAndFeelDefaults().get("TabbedPane.tabAreaInsets");
158        if (insets != null) {
159            insets = (Insets) insets.clone();
160            insets.right += connectedIcon24.getIconWidth() + 8;
161            UIManager.put("TabbedPane.tabAreaInsets", insets);
162        }
163        super.setUI(ui);
164    }
165
166    // Override to paint the connect toggle
167    protected void paintComponent(Graphics g) {
168        super.paintComponent(g);
169
170        Icon icon;
171        Component c0 = getComponent(0);
172        if (c0 != null && c0.getY() > 24) {
173            icon = isConnected() ? connectedIcon24 : disconnectedIcon24;
174        } else {
175            icon = isConnected() ? connectedIcon16 : disconnectedIcon16;
176        }
177        Insets insets = getInsets();
178        int x = getWidth() - insets.right - icon.getIconWidth() - 4;
179        int y = insets.top;
180        if (c0 != null) {
181            y = (c0.getY() - icon.getIconHeight()) / 2;
182        }
183        icon.paintIcon(this, g, x, y);
184        connectedIconBounds = new Rectangle(x, y, icon.getIconWidth(), icon.getIconHeight());
185    }
186
187    public String getToolTipText(MouseEvent event) {
188        if (connectedIconBounds.contains(event.getPoint())) {
189            if (isConnected()) {
190                return Messages.CONNECTED_PUNCTUATION_CLICK_TO_DISCONNECT_;
191            } else {
192                return Messages.DISCONNECTED_PUNCTUATION_CLICK_TO_CONNECT_;
193            }
194        } else {
195            return super.getToolTipText(event);
196        }
197    }
198
199    private synchronized void addTab(TabInfo tabInfo) {
200        Tab tab = instantiate(tabInfo);
201        if (tab != null) {
202            addTab(tabInfo.name, tab);
203        } else {
204            tabInfo.tabVisible = false;
205        }
206    }
207
208    private synchronized void insertTab(TabInfo tabInfo, int index) {
209        Tab tab = instantiate(tabInfo);
210        if (tab != null) {
211            insertTab(tabInfo.name, null, tab, null, index);
212        } else {
213            tabInfo.tabVisible = false;
214        }
215    }
216
217    public synchronized void removeTabAt(int index) {
218        super.removeTabAt(index);
219    }
220
221    private Tab instantiate(TabInfo tabInfo) {
222        try {
223            Constructor<?> con = tabInfo.tabClass.getConstructor(VMPanel.class);
224            return (Tab) con.newInstance(this);
225        } catch (Exception ex) {
226            System.err.println(ex);
227            return null;
228        }
229    }
230
231    boolean isConnected() {
232        return proxyClient.isConnected();
233    }
234
235    public int getUpdateInterval() {
236        return updateInterval;
237    }
238
239    /**
240     * WARNING NEVER CALL THIS METHOD TO MAKE JMX REQUEST
241     * IF  assertThread == false.
242     * DISPATCHER THREAD IS NOT ASSERTED.
243     * IT IS USED TO MAKE SOME LOCAL MANIPULATIONS.
244     */
245    ProxyClient getProxyClient(boolean assertThread) {
246        if (assertThread) {
247            return getProxyClient();
248        } else {
249            return proxyClient;
250        }
251    }
252
253    public ProxyClient getProxyClient() {
254        String threadClass = Thread.currentThread().getClass().getName();
255        if (threadClass.equals("java.awt.EventDispatchThread")) {
256            String msg = "Calling VMPanel.getProxyClient() from the Event Dispatch Thread!";
257            new RuntimeException(msg).printStackTrace();
258            System.exit(1);
259        }
260        return proxyClient;
261    }
262
263    public void cleanUp() {
264        //proxyClient.disconnect();
265        for (Tab tab : getTabs()) {
266            tab.dispose();
267        }
268        for (JConsolePlugin p : plugins.keySet()) {
269            p.dispose();
270        }
271        // Cancel pending update tasks
272        //
273        if (timer != null) {
274            timer.cancel();
275        }
276        // Stop listening to connection state events
277        //
278        proxyClient.removePropertyChangeListener(this);
279    }
280
281    // Call on EDT
282    public void connect() {
283        if (isConnected()) {
284            // create plugin tabs if not done
285            createPluginTabs();
286            // Notify tabs
287            fireConnectedChange(true);
288            // Enable/disable tabs on initial update
289            initialUpdate = true;
290            // Start/Restart update timer on connect/reconnect
291            startUpdateTimer();
292        } else {
293            new Thread("VMPanel.connect") {
294
295                public void run() {
296                    proxyClient.connect(shouldUseSSL);
297                }
298            }.start();
299        }
300    }
301
302    // Call on EDT
303    public void disconnect() {
304        proxyClient.disconnect();
305        updateFrameTitle();
306    }
307
308    // Called on EDT
309    public void propertyChange(PropertyChangeEvent ev) {
310        String prop = ev.getPropertyName();
311
312        if (prop == CONNECTION_STATE_PROPERTY) {
313            ConnectionState oldState = (ConnectionState) ev.getOldValue();
314            ConnectionState newState = (ConnectionState) ev.getNewValue();
315            switch (newState) {
316                case CONNECTING:
317                    onConnecting();
318                    break;
319
320                case CONNECTED:
321                    if (progressBar != null) {
322                        progressBar.setIndeterminate(false);
323                        progressBar.setValue(100);
324                    }
325                    closeOptionPane();
326                    updateFrameTitle();
327                    // create tabs if not done
328                    createPluginTabs();
329                    repaint();
330                    // Notify tabs
331                    fireConnectedChange(true);
332                    // Enable/disable tabs on initial update
333                    initialUpdate = true;
334                    // Start/Restart update timer on connect/reconnect
335                    startUpdateTimer();
336                    break;
337
338                case DISCONNECTED:
339                    if (progressBar != null) {
340                        progressBar.setIndeterminate(false);
341                        progressBar.setValue(0);
342                        closeOptionPane();
343                    }
344                    vmPanelDied();
345                    if (oldState == ConnectionState.CONNECTED) {
346                        // Notify tabs
347                        fireConnectedChange(false);
348                    }
349                    break;
350            }
351        }
352    }
353
354    // Called on EDT
355    private void onConnecting() {
356        time0 = System.currentTimeMillis();
357
358        SwingUtilities.getWindowAncestor(this);
359
360        String connectionName = getConnectionName();
361        progressBar = new JProgressBar();
362        progressBar.setIndeterminate(true);
363        JPanel progressPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
364        progressPanel.add(progressBar);
365
366        Object[] message = {
367            "<html><h3>" + Resources.format(Messages.CONNECTING_TO1, connectionName) + "</h3></html>",
368            progressPanel,
369            "<html><b>" + Resources.format(Messages.CONNECTING_TO2, connectionName) + "</b></html>"
370        };
371
372        optionPane =
373                SheetDialog.showOptionDialog(this,
374                message,
375                JOptionPane.DEFAULT_OPTION,
376                JOptionPane.INFORMATION_MESSAGE, null,
377                new String[]{Messages.CANCEL},
378                0);
379
380
381    }
382
383    // Called on EDT
384    private void closeOptionPane() {
385        if (optionPane != null) {
386            new Thread("VMPanel.sleeper") {
387                public void run() {
388                    long elapsed = System.currentTimeMillis() - time0;
389                    if (elapsed < 2000) {
390                        try {
391                            sleep(2000 - elapsed);
392                        } catch (InterruptedException ex) {
393                        // Ignore
394                        }
395                    }
396                    SwingUtilities.invokeLater(new Runnable() {
397
398                        public void run() {
399                            optionPane.setVisible(false);
400                            progressBar = null;
401                        }
402                    });
403                }
404            }.start();
405        }
406    }
407
408    void updateFrameTitle() {
409        VMInternalFrame vmIF = getFrame();
410        if (vmIF != null) {
411            String displayName = getDisplayName();
412            if (!proxyClient.isConnected()) {
413                displayName = Resources.format(Messages.CONNECTION_NAME__DISCONNECTED_, displayName);
414            }
415            vmIF.setTitle(displayName);
416        }
417    }
418
419    private VMInternalFrame getFrame() {
420        if (vmIF == null) {
421            vmIF = (VMInternalFrame) SwingUtilities.getAncestorOfClass(VMInternalFrame.class,
422                    this);
423        }
424        return vmIF;
425    }
426
427    // TODO: this method is not needed when all JConsole tabs
428    // are migrated to use the new JConsolePlugin API.
429    //
430    // A thread safe clone of all JConsole tabs
431    synchronized List<Tab> getTabs() {
432        ArrayList<Tab> list = new ArrayList<Tab>();
433        int n = getTabCount();
434        for (int i = 0; i < n; i++) {
435            Component c = getComponentAt(i);
436            if (c instanceof Tab) {
437                list.add((Tab) c);
438            }
439        }
440        return list;
441    }
442
443    private void startUpdateTimer() {
444        if (timer != null) {
445            timer.cancel();
446        }
447        TimerTask timerTask = new TimerTask() {
448
449            public void run() {
450                update();
451            }
452        };
453        String timerName = "Timer-" + getConnectionName();
454        timer = new Timer(timerName, true);
455        timer.schedule(timerTask, 0, updateInterval);
456    }
457
458    // Call on EDT
459    private void vmPanelDied() {
460        disconnect();
461
462        if (userDisconnected) {
463            userDisconnected = false;
464            return;
465        }
466
467        JOptionPane optionPane;
468        String msgTitle, msgExplanation, buttonStr;
469
470        if (wasConnected) {
471            wasConnected = false;
472            msgTitle = Messages.CONNECTION_LOST1;
473            msgExplanation = Resources.format(Messages.CONNECTING_TO2, getConnectionName());
474            buttonStr = Messages.RECONNECT;
475        } else if (shouldUseSSL) {
476            msgTitle = Messages.CONNECTION_FAILED_SSL1;
477            msgExplanation = Resources.format(Messages.CONNECTION_FAILED_SSL2, getConnectionName());
478            buttonStr = Messages.INSECURE;
479        } else {
480            msgTitle = Messages.CONNECTION_FAILED1;
481            msgExplanation = Resources.format(Messages.CONNECTION_FAILED2, getConnectionName());
482            buttonStr = Messages.CONNECT;
483        }
484
485        optionPane =
486                SheetDialog.showOptionDialog(this,
487                "<html><h3>" + msgTitle + "</h3>" +
488                "<b>" + msgExplanation + "</b>",
489                JOptionPane.DEFAULT_OPTION,
490                JOptionPane.WARNING_MESSAGE, null,
491                new String[]{buttonStr, Messages.CANCEL},
492                0);
493
494        optionPane.addPropertyChangeListener(new PropertyChangeListener() {
495
496            public void propertyChange(PropertyChangeEvent event) {
497                if (event.getPropertyName().equals(JOptionPane.VALUE_PROPERTY)) {
498                    Object value = event.getNewValue();
499
500                    if (value == Messages.RECONNECT || value == Messages.CONNECT) {
501                        connect();
502                    } else if (value == Messages.INSECURE) {
503                        shouldUseSSL = false;
504                        connect();
505                    } else if (!everConnected) {
506                        try {
507                            getFrame().setClosed(true);
508                        } catch (PropertyVetoException ex) {
509                        // Should not happen, but can be ignored.
510                        }
511                    }
512                }
513            }
514        });
515    }
516
517    // Note: This method is called on a TimerTask thread. Any GUI manipulation
518    // must be performed with invokeLater() or invokeAndWait().
519    private Object lockObject = new Object();
520
521    private void update() {
522        synchronized (lockObject) {
523            if (!isConnected()) {
524                if (wasConnected) {
525                    EventQueue.invokeLater(new Runnable() {
526
527                        public void run() {
528                            vmPanelDied();
529                        }
530                    });
531                }
532                wasConnected = false;
533                return;
534            } else {
535                wasConnected = true;
536                everConnected = true;
537            }
538            proxyClient.flush();
539            List<Tab> tabs = getTabs();
540            final int n = tabs.size();
541            for (int i = 0; i < n; i++) {
542                final int index = i;
543                try {
544                    if (!proxyClient.isDead()) {
545                        // Update tab
546                        //
547                        tabs.get(index).update();
548                        // Enable tab on initial update
549                        //
550                        if (initialUpdate) {
551                            EventQueue.invokeLater(new Runnable() {
552
553                                public void run() {
554                                    setEnabledAt(index, true);
555                                }
556                            });
557                        }
558                    }
559                } catch (Exception e) {
560                    // Disable tab on initial update
561                    //
562                    if (initialUpdate) {
563                        EventQueue.invokeLater(new Runnable() {
564                            public void run() {
565                                setEnabledAt(index, false);
566                            }
567                        });
568                    }
569                }
570            }
571
572            // plugin GUI update
573            for (ExceptionSafePlugin p : plugins.keySet()) {
574                SwingWorker<?, ?> sw = p.newSwingWorker();
575                SwingWorker<?, ?> prevSW = plugins.get(p);
576                // schedule SwingWorker to run only if the previous
577                // SwingWorker has finished its task and it hasn't started.
578                if (prevSW == null || prevSW.isDone()) {
579                    if (sw == null || sw.getState() == SwingWorker.StateValue.PENDING) {
580                        plugins.put(p, sw);
581                        if (sw != null) {
582                            p.executeSwingWorker(sw);
583                        }
584                    }
585                }
586            }
587
588            // Set the first enabled tab in the tab's list
589            // as the selected tab on initial update
590            //
591            if (initialUpdate) {
592                EventQueue.invokeLater(new Runnable() {
593                    public void run() {
594                        // Select first enabled tab if current tab isn't.
595                        int index = getSelectedIndex();
596                        if (index < 0 || !isEnabledAt(index)) {
597                            for (int i = 0; i < n; i++) {
598                                if (isEnabledAt(i)) {
599                                    setSelectedIndex(i);
600                                    break;
601                                }
602                            }
603                        }
604                    }
605                });
606                initialUpdate = false;
607            }
608        }
609    }
610
611    public String getHostName() {
612        return hostName;
613    }
614
615    public int getPort() {
616        return port;
617    }
618
619    public String getUserName() {
620        return userName;
621    }
622
623    public String getUrl() {
624        return url;
625    }
626
627    public String getPassword() {
628        return password;
629    }
630
631    public String getConnectionName() {
632        return proxyClient.connectionName();
633    }
634
635    public String getDisplayName() {
636        return proxyClient.getDisplayName();
637    }
638
639    static class TabInfo {
640
641        Class<? extends Tab> tabClass;
642        String name;
643        boolean tabVisible;
644
645        TabInfo(Class<? extends Tab> tabClass, String name, boolean tabVisible) {
646            this.tabClass = tabClass;
647            this.name = name;
648            this.tabVisible = tabVisible;
649        }
650    }
651
652    private void createPluginTabs() {
653        // add plugin tabs if not done
654        if (!pluginTabsAdded) {
655            for (JConsolePlugin p : plugins.keySet()) {
656                Map<String, JPanel> tabs = p.getTabs();
657                for (Map.Entry<String, JPanel> e : tabs.entrySet()) {
658                    addTab(e.getKey(), e.getValue());
659                }
660            }
661            pluginTabsAdded = true;
662        }
663    }
664
665    private void fireConnectedChange(boolean connected) {
666        for (Tab tab : getTabs()) {
667            tab.firePropertyChange(JConsoleContext.CONNECTION_STATE_PROPERTY, !connected, connected);
668        }
669    }
670}
671