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}