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