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