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}