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}