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.
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.getComponentCount());
144        tabbedPane.add(debugger.tabbedPane, tabbedPane.getComponentCount() - 1);
145        tabbedPane.setIconAt(tabbedPane.indexOfComponent(debugger.tabbedPane), connectionCreatedIcon);
146        frame.setTitle(
147                "Smack Debug Window -- Total connections: " + (tabbedPane.getComponentCount() - 1));
148        // Keep the added debugger for later access
149        debuggers.add(debugger);
150    }
151
152    /**
153     * Notification that a user has logged in to the server. A new title will be set
154     * to the tab of the given debugger.
155     *
156     * @param debugger the debugger whose connection logged in to the server
157     * @param user     the user@host/resource that has just logged in
158     */
159    static synchronized void userHasLogged(EnhancedDebugger debugger, String user) {
160        int index = getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane);
161        getInstance().tabbedPane.setTitleAt(
162                index,
163                user);
164        getInstance().tabbedPane.setIconAt(
165                index,
166                connectionActiveIcon);
167    }
168
169    /**
170     * Notification that the connection was properly closed.
171     *
172     * @param debugger the debugger whose connection was properly closed.
173     */
174    static synchronized void connectionClosed(EnhancedDebugger debugger) {
175        getInstance().tabbedPane.setIconAt(
176                getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane),
177                connectionClosedIcon);
178    }
179
180    /**
181     * Notification that the connection was closed due to an exception.
182     *
183     * @param debugger the debugger whose connection was closed due to an exception.
184     * @param e        the exception.
185     */
186    static synchronized void connectionClosedOnError(EnhancedDebugger debugger, Exception e) {
187        int index = getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane);
188        getInstance().tabbedPane.setToolTipTextAt(
189                index,
190                "XMPPConnection closed due to the exception: " + e.getMessage());
191        getInstance().tabbedPane.setIconAt(
192                index,
193                connectionClosedOnErrorIcon);
194    }
195
196    static synchronized void connectionEstablished(EnhancedDebugger debugger) {
197        getInstance().tabbedPane.setIconAt(
198                getInstance().tabbedPane.indexOfComponent(debugger.tabbedPane),
199                connectionActiveIcon);
200    }
201
202    /**
203     * Creates the main debug window that provides information about Smack and also shows
204     * a tab panel for each connection that is being debugged.
205     */
206    @SuppressWarnings({ "rawtypes", "unchecked" })
207    private void createDebug() {
208
209        frame = new JFrame("Smack Debug Window");
210
211        if (!PERSISTED_DEBUGGER) {
212            // Add listener for window closing event
213            frame.addWindowListener(new WindowAdapter() {
214                @Override
215                public void windowClosing(WindowEvent evt) {
216                    rootWindowClosing(evt);
217                }
218            });
219        }
220
221        // We'll arrange the UI into tabs. The last tab contains Smack's information.
222        // All the connection debugger tabs will be shown before the Smack info tab.
223        tabbedPane = new JTabbedPane();
224
225        // Create the Smack info panel
226        JPanel informationPanel = new JPanel();
227        informationPanel.setLayout(new BoxLayout(informationPanel, BoxLayout.Y_AXIS));
228
229        // Add the Smack version label
230        JPanel versionPanel = new JPanel();
231        versionPanel.setLayout(new BoxLayout(versionPanel, BoxLayout.X_AXIS));
232        versionPanel.setMaximumSize(new Dimension(2000, 31));
233        versionPanel.add(new JLabel(" Smack version: "));
234        JFormattedTextField field = new JFormattedTextField(SmackConfiguration.getVersion());
235        field.setEditable(false);
236        field.setBorder(null);
237        versionPanel.add(field);
238        informationPanel.add(versionPanel);
239
240        // Add the list of installed IQ Providers
241        JPanel iqProvidersPanel = new JPanel();
242        iqProvidersPanel.setLayout(new GridLayout(1, 1));
243        iqProvidersPanel.setBorder(BorderFactory.createTitledBorder("Installed IQ Providers"));
244        Vector<String> providers = new Vector<>();
245        for (Object provider : ProviderManager.getIQProviders()) {
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        JList list = new JList(providers);
256        iqProvidersPanel.add(new JScrollPane(list));
257        informationPanel.add(iqProvidersPanel);
258
259        // Add the list of installed Extension Providers
260        JPanel extensionProvidersPanel = new JPanel();
261        extensionProvidersPanel.setLayout(new GridLayout(1, 1));
262        extensionProvidersPanel.setBorder(BorderFactory.createTitledBorder("Installed Extension Providers"));
263        providers = new Vector<>();
264        for (Object provider : ProviderManager.getExtensionProviders()) {
265            if (provider.getClass() == Class.class) {
266                providers.add(((Class<?>) provider).getName());
267            }
268            else {
269                providers.add(provider.getClass().getName());
270            }
271        }
272        // Sort the collection of providers
273        Collections.sort(providers);
274        list = new JList(providers);
275        extensionProvidersPanel.add(new JScrollPane(list));
276        informationPanel.add(extensionProvidersPanel);
277
278        tabbedPane.add("Smack Info", informationPanel);
279
280        // Add pop-up menu.
281        JPopupMenu menu = new JPopupMenu();
282        // Add a menu item that allows to close the current selected tab
283        JMenuItem menuItem = new JMenuItem("Close");
284        menuItem.addActionListener(new ActionListener() {
285            @Override
286            public void actionPerformed(ActionEvent e) {
287                // Remove the selected tab pane if it's not the Smack info pane
288                if (tabbedPane.getSelectedIndex() < tabbedPane.getComponentCount() - 1) {
289                    int index = tabbedPane.getSelectedIndex();
290                    // Notify to the debugger to stop debugging
291                    EnhancedDebugger debugger = debuggers.get(index);
292                    debugger.cancel();
293                    // Remove the debugger from the root window
294                    tabbedPane.remove(debugger.tabbedPane);
295                    debuggers.remove(debugger);
296                    // Update the root window title
297                    frame.setTitle(
298                            "Smack Debug Window -- Total connections: "
299                                    + (tabbedPane.getComponentCount() - 1));
300                }
301            }
302        });
303        menu.add(menuItem);
304        // Add a menu item that allows to close all the tabs that have their connections closed
305        menuItem = new JMenuItem("Close All Not Active");
306        menuItem.addActionListener(new ActionListener() {
307            @Override
308            public void actionPerformed(ActionEvent e) {
309                ArrayList<EnhancedDebugger> debuggersToRemove = new ArrayList<>();
310                // Remove all the debuggers of which their connections are no longer valid
311                for (int index = 0; index < tabbedPane.getComponentCount() - 1; index++) {
312                    EnhancedDebugger debugger = debuggers.get(index);
313                    if (!debugger.isConnectionActive()) {
314                        // Notify to the debugger to stop debugging
315                        debugger.cancel();
316                        debuggersToRemove.add(debugger);
317                    }
318                }
319                for (EnhancedDebugger debugger : debuggersToRemove) {
320                    // Remove the debugger from the root window
321                    tabbedPane.remove(debugger.tabbedPane);
322                    debuggers.remove(debugger);
323                }
324                // Update the root window title
325                frame.setTitle(
326                        "Smack Debug Window -- Total connections: "
327                                + (tabbedPane.getComponentCount() - 1));
328            }
329        });
330        menu.add(menuItem);
331        // Add listener to the text area so the popup menu can come up.
332        tabbedPane.addMouseListener(new PopupListener(menu));
333
334        frame.getContentPane().add(tabbedPane);
335
336        frame.setSize(650, 400);
337
338        if (!PERSISTED_DEBUGGER) {
339            frame.setVisible(true);
340        }
341    }
342
343    /**
344     * Notification that the root window is closing. Stop listening for received and
345     * transmitted packets in all the debugged connections.
346     *
347     * @param evt the event that indicates that the root window is closing
348     */
349    private synchronized void rootWindowClosing(WindowEvent evt) {
350        // Notify to all the debuggers to stop debugging
351        for (EnhancedDebugger debugger : debuggers) {
352            debugger.cancel();
353        }
354        // Release any reference to the debuggers
355        debuggers.clear();
356        // Release the default instance
357        instance = null;
358        frame = null;
359        notifyAll();
360    }
361
362    /**
363     * Listens for debug window popup dialog events.
364     */
365    private static class PopupListener extends MouseAdapter {
366
367        JPopupMenu popup;
368
369        PopupListener(JPopupMenu popupMenu) {
370            popup = popupMenu;
371        }
372
373        @Override
374        public void mousePressed(MouseEvent e) {
375            maybeShowPopup(e);
376        }
377
378        @Override
379        public void mouseReleased(MouseEvent e) {
380            maybeShowPopup(e);
381        }
382
383        private void maybeShowPopup(MouseEvent e) {
384            if (e.isPopupTrigger()) {
385                popup.show(e.getComponent(), e.getX(), e.getY());
386            }
387        }
388    }
389
390    public void setVisible(boolean visible) {
391        if (frame != null) {
392            frame.setVisible(visible);
393        }
394    }
395
396    public boolean isVisible() {
397        return frame != null && frame.isVisible();
398    }
399
400    public synchronized void waitUntilClosed() throws InterruptedException {
401        if (frame == null) {
402            return;
403        }
404
405        while (frame != null) {
406            wait();
407        }
408    }
409}