001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smackx.debugger;
019
020import org.jivesoftware.smack.SmackConfiguration;
021import org.jivesoftware.smack.provider.ProviderManager;
022
023import javax.swing.*;
024
025import java.awt.*;
026import java.awt.event.*;
027import java.net.URL;
028import java.util.ArrayList;
029import java.util.Collections;
030import java.util.Vector;
031
032/**
033 * The EnhancedDebuggerWindow is the main debug window that will show all the EnhancedDebuggers.
034 * For each connection to debug there will be an EnhancedDebugger that will be shown in the
035 * EnhancedDebuggerWindow.<p>
036 * <p/>
037 * This class also provides information about Smack like for example the Smack version and the
038 * installed providers.
039 *
040 * @author Gaston Dombiak
041 */
042public class EnhancedDebuggerWindow {
043
044    private static EnhancedDebuggerWindow instance;
045
046    private static ImageIcon connectionCreatedIcon;
047    private static ImageIcon connectionActiveIcon;
048    private static ImageIcon connectionClosedIcon;
049    private static ImageIcon connectionClosedOnErrorIcon;
050
051    public static boolean PERSISTED_DEBUGGER = false;
052    /**
053     * Keeps the max number of rows to keep in the tables. A value less than 0 means that packets
054     * will never be removed. If you are planning to use this debugger in a
055     * production environment then you should set a lower value (e.g. 50) to prevent the debugger
056     * from consuming all the JVM memory.
057     */
058    public static int MAX_TABLE_ROWS = 150;
059
060    {
061        URL url;
062
063        url =
064                Thread.currentThread().getContextClassLoader().getResource(
065                        "images/trafficlight_off.png");
066        if (url != null) {
067            connectionCreatedIcon = new ImageIcon(url);
068        }
069        url =
070                Thread.currentThread().getContextClassLoader().getResource(
071                        "images/trafficlight_green.png");
072        if (url != null) {
073            connectionActiveIcon = new ImageIcon(url);
074        }
075        url =
076                Thread.currentThread().getContextClassLoader().getResource(
077                        "images/trafficlight_red.png");
078        if (url != null) {
079            connectionClosedIcon = new ImageIcon(url);
080        }
081        url = Thread.currentThread().getContextClassLoader().getResource("images/warning.png");
082        if (url != null) {
083            connectionClosedOnErrorIcon = new ImageIcon(url);
084        }
085
086    }
087
088    private JFrame frame = null;
089    private JTabbedPane tabbedPane = null;
090    private java.util.List<EnhancedDebugger> debuggers = new ArrayList<EnhancedDebugger>();
091
092    private EnhancedDebuggerWindow() {
093    }
094
095    /**
096     * Returns the unique EnhancedDebuggerWindow instance available in the system.
097     *
098     * @return the unique EnhancedDebuggerWindow instance
099     */
100    public static EnhancedDebuggerWindow getInstance() {
101        if (instance == null) {
102            instance = new EnhancedDebuggerWindow();
103        }
104        return instance;
105    }
106
107    /**
108     * Adds the new specified debugger to the list of debuggers to show in the main window.
109     *
110     * @param debugger the new debugger to show in the debug window
111     */
112    synchronized static void addDebugger(EnhancedDebugger debugger) {
113        getInstance().showNewDebugger(debugger);
114    }
115
116    /**
117     * Shows the new debugger in the debug window.
118     *
119     * @param debugger the new debugger to show
120     */
121    private void showNewDebugger(EnhancedDebugger debugger) {
122        if (frame == null) {
123            createDebug();
124        }
125        debugger.tabbedPane.setName("XMPPConnection_" + tabbedPane.getComponentCount());
126        tabbedPane.add(debugger.tabbedPane, tabbedPane.getComponentCount() - 1);
127        tabbedPane.setIconAt(tabbedPane.indexOfComponent(debugger.tabbedPane), connectionCreatedIcon);
128        frame.setTitle(
129                "Smack Debug Window -- Total connections: " + (tabbedPane.getComponentCount() - 1));
130        // Keep the added debugger for later access
131        debuggers.add(debugger);
132    }
133
134    /**
135     * Notification that a user has logged in to the server. A new title will be set
136     * to the tab of the given debugger.
137     *
138     * @param debugger the debugger whose connection logged in to the server
139     * @param user     the user@host/resource that has just logged in
140     */
141    synchronized static void userHasLogged(EnhancedDebugger debugger, String user) {
142        int index = getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane);
143        getInstance().tabbedPane.setTitleAt(
144                index,
145                user);
146        getInstance().tabbedPane.setIconAt(
147                index,
148                connectionActiveIcon);
149    }
150
151    /**
152     * Notification that the connection was properly closed.
153     *
154     * @param debugger the debugger whose connection was properly closed.
155     */
156    synchronized static void connectionClosed(EnhancedDebugger debugger) {
157        getInstance().tabbedPane.setIconAt(
158                getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane),
159                connectionClosedIcon);
160    }
161
162    /**
163     * Notification that the connection was closed due to an exception.
164     *
165     * @param debugger the debugger whose connection was closed due to an exception.
166     * @param e        the exception.
167     */
168    synchronized static void connectionClosedOnError(EnhancedDebugger debugger, Exception e) {
169        int index = getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane);
170        getInstance().tabbedPane.setToolTipTextAt(
171                index,
172                "XMPPConnection closed due to the exception: " + e.getMessage());
173        getInstance().tabbedPane.setIconAt(
174                index,
175                connectionClosedOnErrorIcon);
176    }
177
178    synchronized static void connectionEstablished(EnhancedDebugger debugger) {
179        getInstance().tabbedPane.setIconAt(
180                getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane),
181                connectionActiveIcon);
182    }
183    
184    /**
185     * Creates the main debug window that provides information about Smack and also shows
186     * a tab panel for each connection that is being debugged.
187     */
188    @SuppressWarnings({ "rawtypes", "unchecked" })
189        private void createDebug() {
190
191        frame = new JFrame("Smack Debug Window");
192
193        if (!PERSISTED_DEBUGGER) {
194            // Add listener for window closing event
195            frame.addWindowListener(new WindowAdapter() {
196                public void windowClosing(WindowEvent evt) {
197                    rootWindowClosing(evt);
198                }
199            });
200        }
201
202        // We'll arrange the UI into tabs. The last tab contains Smack's information.
203        // All the connection debugger tabs will be shown before the Smack info tab. 
204        tabbedPane = new JTabbedPane();
205
206        // Create the Smack info panel 
207        JPanel informationPanel = new JPanel();
208        informationPanel.setLayout(new BoxLayout(informationPanel, BoxLayout.Y_AXIS));
209
210        // Add the Smack version label
211        JPanel versionPanel = new JPanel();
212        versionPanel.setLayout(new BoxLayout(versionPanel, BoxLayout.X_AXIS));
213        versionPanel.setMaximumSize(new Dimension(2000, 31));
214        versionPanel.add(new JLabel(" Smack version: "));
215        JFormattedTextField field = new JFormattedTextField(SmackConfiguration.getVersion());
216        field.setEditable(false);
217        field.setBorder(null);
218        versionPanel.add(field);
219        informationPanel.add(versionPanel);
220
221        // Add the list of installed IQ Providers
222        JPanel iqProvidersPanel = new JPanel();
223        iqProvidersPanel.setLayout(new GridLayout(1, 1));
224        iqProvidersPanel.setBorder(BorderFactory.createTitledBorder("Installed IQ Providers"));
225        Vector<String> providers = new Vector<String>();
226        for (Object provider : ProviderManager.getIQProviders()) {
227            if (provider.getClass() == Class.class) {
228                providers.add(((Class<?>) provider).getName());
229            }
230            else {
231                providers.add(provider.getClass().getName());
232            }
233        }
234        // Sort the collection of providers
235        Collections.sort(providers);
236        JList list = new JList(providers);
237        iqProvidersPanel.add(new JScrollPane(list));
238        informationPanel.add(iqProvidersPanel);
239
240        // Add the list of installed Extension Providers
241        JPanel extensionProvidersPanel = new JPanel();
242        extensionProvidersPanel.setLayout(new GridLayout(1, 1));
243        extensionProvidersPanel.setBorder(BorderFactory.createTitledBorder("Installed Extension Providers"));
244        providers = new Vector<String>();
245        for (Object provider : ProviderManager.getExtensionProviders()) {
246            if (provider.getClass() == Class.class) {
247                providers.add(((Class<?>) provider).getName());
248            }
249            else {
250                providers.add(provider.getClass().getName());
251            }
252        }
253        // Sort the collection of providers
254        Collections.sort(providers);
255        list = new JList(providers);
256        extensionProvidersPanel.add(new JScrollPane(list));
257        informationPanel.add(extensionProvidersPanel);
258
259        tabbedPane.add("Smack Info", informationPanel);
260
261        // Add pop-up menu.
262        JPopupMenu menu = new JPopupMenu();
263        // Add a menu item that allows to close the current selected tab
264        JMenuItem menuItem = new JMenuItem("Close");
265        menuItem.addActionListener(new ActionListener() {
266            public void actionPerformed(ActionEvent e) {
267                // Remove the selected tab pane if it's not the Smack info pane
268                if (tabbedPane.getSelectedIndex() < tabbedPane.getComponentCount() - 1) {
269                    int index = tabbedPane.getSelectedIndex();
270                    // Notify to the debugger to stop debugging
271                    EnhancedDebugger debugger = debuggers.get(index);
272                    debugger.cancel();
273                    // Remove the debugger from the root window
274                    tabbedPane.remove(debugger.tabbedPane);
275                    debuggers.remove(debugger);
276                    // Update the root window title
277                    frame.setTitle(
278                            "Smack Debug Window -- Total connections: "
279                                    + (tabbedPane.getComponentCount() - 1));
280                }
281            }
282        });
283        menu.add(menuItem);
284        // Add a menu item that allows to close all the tabs that have their connections closed
285        menuItem = new JMenuItem("Close All Not Active");
286        menuItem.addActionListener(new ActionListener() {
287            public void actionPerformed(ActionEvent e) {
288                ArrayList<EnhancedDebugger> debuggersToRemove = new ArrayList<EnhancedDebugger>();
289                // Remove all the debuggers of which their connections are no longer valid
290                for (int index = 0; index < tabbedPane.getComponentCount() - 1; index++) {
291                    EnhancedDebugger debugger = debuggers.get(index);
292                    if (!debugger.isConnectionActive()) {
293                        // Notify to the debugger to stop debugging
294                        debugger.cancel();
295                        debuggersToRemove.add(debugger);
296                    }
297                }
298                for (EnhancedDebugger debugger : debuggersToRemove) {
299                    // Remove the debugger from the root window
300                    tabbedPane.remove(debugger.tabbedPane);
301                    debuggers.remove(debugger);
302                }
303                // Update the root window title
304                frame.setTitle(
305                        "Smack Debug Window -- Total connections: "
306                                + (tabbedPane.getComponentCount() - 1));
307            }
308        });
309        menu.add(menuItem);
310        // Add listener to the text area so the popup menu can come up.
311        tabbedPane.addMouseListener(new PopupListener(menu));
312
313        frame.getContentPane().add(tabbedPane);
314
315        frame.setSize(650, 400);
316
317        if (!PERSISTED_DEBUGGER) {
318            frame.setVisible(true);
319        }
320    }
321
322    /**
323     * Notification that the root window is closing. Stop listening for received and
324     * transmitted packets in all the debugged connections.
325     *
326     * @param evt the event that indicates that the root window is closing
327     */
328    public void rootWindowClosing(WindowEvent evt) {
329        // Notify to all the debuggers to stop debugging
330        for (EnhancedDebugger debugger : debuggers) {
331            debugger.cancel();
332        }
333        // Release any reference to the debuggers
334        debuggers.removeAll(debuggers);
335        // Release the default instance
336        instance = null;
337    }
338
339    /**
340     * Listens for debug window popup dialog events.
341     */
342    private class PopupListener extends MouseAdapter {
343
344        JPopupMenu popup;
345
346        PopupListener(JPopupMenu popupMenu) {
347            popup = popupMenu;
348        }
349
350        public void mousePressed(MouseEvent e) {
351            maybeShowPopup(e);
352        }
353
354        public void mouseReleased(MouseEvent e) {
355            maybeShowPopup(e);
356        }
357
358        private void maybeShowPopup(MouseEvent e) {
359            if (e.isPopupTrigger()) {
360                popup.show(e.getComponent(), e.getX(), e.getY());
361            }
362        }
363    }
364
365    public void setVisible(boolean visible) {
366        if (frame != null) {
367            frame.setVisible(visible);
368        }
369    }
370
371    public boolean isVisible() {
372        return frame != null && frame.isVisible();
373    }
374}