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}