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 final 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                @Override
214                public void windowClosing(WindowEvent evt) {
215                    rootWindowClosing(evt);
216                }
217            });
218        }
219
220        // We'll arrange the UI into tabs. The last tab contains Smack's information.
221        // All the connection debugger tabs will be shown before the Smack info tab. 
222        tabbedPane = new JTabbedPane();
223
224        // Create the Smack info panel 
225        JPanel informationPanel = new JPanel();
226        informationPanel.setLayout(new BoxLayout(informationPanel, BoxLayout.Y_AXIS));
227
228        // Add the Smack version label
229        JPanel versionPanel = new JPanel();
230        versionPanel.setLayout(new BoxLayout(versionPanel, BoxLayout.X_AXIS));
231        versionPanel.setMaximumSize(new Dimension(2000, 31));
232        versionPanel.add(new JLabel(" Smack version: "));
233        JFormattedTextField field = new JFormattedTextField(SmackConfiguration.getVersion());
234        field.setEditable(false);
235        field.setBorder(null);
236        versionPanel.add(field);
237        informationPanel.add(versionPanel);
238
239        // Add the list of installed IQ Providers
240        JPanel iqProvidersPanel = new JPanel();
241        iqProvidersPanel.setLayout(new GridLayout(1, 1));
242        iqProvidersPanel.setBorder(BorderFactory.createTitledBorder("Installed IQ Providers"));
243        Vector<String> providers = new Vector<String>();
244        for (Object provider : ProviderManager.getIQProviders()) {
245            if (provider.getClass() == Class.class) {
246                providers.add(((Class<?>) provider).getName());
247            }
248            else {
249                providers.add(provider.getClass().getName());
250            }
251        }
252        // Sort the collection of providers
253        Collections.sort(providers);
254        JList list = new JList(providers);
255        iqProvidersPanel.add(new JScrollPane(list));
256        informationPanel.add(iqProvidersPanel);
257
258        // Add the list of installed Extension Providers
259        JPanel extensionProvidersPanel = new JPanel();
260        extensionProvidersPanel.setLayout(new GridLayout(1, 1));
261        extensionProvidersPanel.setBorder(BorderFactory.createTitledBorder("Installed Extension Providers"));
262        providers = new Vector<String>();
263        for (Object provider : ProviderManager.getExtensionProviders()) {
264            if (provider.getClass() == Class.class) {
265                providers.add(((Class<?>) provider).getName());
266            }
267            else {
268                providers.add(provider.getClass().getName());
269            }
270        }
271        // Sort the collection of providers
272        Collections.sort(providers);
273        list = new JList(providers);
274        extensionProvidersPanel.add(new JScrollPane(list));
275        informationPanel.add(extensionProvidersPanel);
276
277        tabbedPane.add("Smack Info", informationPanel);
278
279        // Add pop-up menu.
280        JPopupMenu menu = new JPopupMenu();
281        // Add a menu item that allows to close the current selected tab
282        JMenuItem menuItem = new JMenuItem("Close");
283        menuItem.addActionListener(new ActionListener() {
284            @Override
285            public void actionPerformed(ActionEvent e) {
286                // Remove the selected tab pane if it's not the Smack info pane
287                if (tabbedPane.getSelectedIndex() < tabbedPane.getComponentCount() - 1) {
288                    int index = tabbedPane.getSelectedIndex();
289                    // Notify to the debugger to stop debugging
290                    EnhancedDebugger debugger = debuggers.get(index);
291                    debugger.cancel();
292                    // Remove the debugger from the root window
293                    tabbedPane.remove(debugger.tabbedPane);
294                    debuggers.remove(debugger);
295                    // Update the root window title
296                    frame.setTitle(
297                            "Smack Debug Window -- Total connections: "
298                                    + (tabbedPane.getComponentCount() - 1));
299                }
300            }
301        });
302        menu.add(menuItem);
303        // Add a menu item that allows to close all the tabs that have their connections closed
304        menuItem = new JMenuItem("Close All Not Active");
305        menuItem.addActionListener(new ActionListener() {
306            @Override
307            public void actionPerformed(ActionEvent e) {
308                ArrayList<EnhancedDebugger> debuggersToRemove = new ArrayList<EnhancedDebugger>();
309                // Remove all the debuggers of which their connections are no longer valid
310                for (int index = 0; index < tabbedPane.getComponentCount() - 1; index++) {
311                    EnhancedDebugger debugger = debuggers.get(index);
312                    if (!debugger.isConnectionActive()) {
313                        // Notify to the debugger to stop debugging
314                        debugger.cancel();
315                        debuggersToRemove.add(debugger);
316                    }
317                }
318                for (EnhancedDebugger debugger : debuggersToRemove) {
319                    // Remove the debugger from the root window
320                    tabbedPane.remove(debugger.tabbedPane);
321                    debuggers.remove(debugger);
322                }
323                // Update the root window title
324                frame.setTitle(
325                        "Smack Debug Window -- Total connections: "
326                                + (tabbedPane.getComponentCount() - 1));
327            }
328        });
329        menu.add(menuItem);
330        // Add listener to the text area so the popup menu can come up.
331        tabbedPane.addMouseListener(new PopupListener(menu));
332
333        frame.getContentPane().add(tabbedPane);
334
335        frame.setSize(650, 400);
336
337        if (!PERSISTED_DEBUGGER) {
338            frame.setVisible(true);
339        }
340    }
341
342    /**
343     * Notification that the root window is closing. Stop listening for received and
344     * transmitted packets in all the debugged connections.
345     *
346     * @param evt the event that indicates that the root window is closing
347     */
348    public void rootWindowClosing(WindowEvent evt) {
349        // Notify to all the debuggers to stop debugging
350        for (EnhancedDebugger debugger : debuggers) {
351            debugger.cancel();
352        }
353        // Release any reference to the debuggers
354        debuggers.clear();
355        // Release the default instance
356        instance = null;
357    }
358
359    /**
360     * Listens for debug window popup dialog events.
361     */
362    private static class PopupListener extends MouseAdapter {
363
364        JPopupMenu popup;
365
366        PopupListener(JPopupMenu popupMenu) {
367            popup = popupMenu;
368        }
369
370        @Override
371        public void mousePressed(MouseEvent e) {
372            maybeShowPopup(e);
373        }
374
375        @Override
376        public void mouseReleased(MouseEvent e) {
377            maybeShowPopup(e);
378        }
379
380        private void maybeShowPopup(MouseEvent e) {
381            if (e.isPopupTrigger()) {
382                popup.show(e.getComponent(), e.getX(), e.getY());
383            }
384        }
385    }
386
387    public void setVisible(boolean visible) {
388        if (frame != null) {
389            frame.setVisible(visible);
390        }
391    }
392
393    public boolean isVisible() {
394        return frame != null && frame.isVisible();
395    }
396}