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.BorderLayout; 021import java.awt.Color; 022import java.awt.GridBagConstraints; 023import java.awt.GridBagLayout; 024import java.awt.GridLayout; 025import java.awt.Insets; 026import java.awt.Toolkit; 027import java.awt.datatransfer.Clipboard; 028import java.awt.datatransfer.StringSelection; 029import java.awt.event.ActionEvent; 030import java.awt.event.ActionListener; 031import java.awt.event.MouseAdapter; 032import java.awt.event.MouseEvent; 033import java.io.Reader; 034import java.io.Writer; 035import java.net.URL; 036import java.text.SimpleDateFormat; 037import java.time.Duration; 038import java.time.Instant; 039import java.util.ArrayList; 040import java.util.Date; 041import java.util.List; 042import java.util.concurrent.PriorityBlockingQueue; 043import java.util.concurrent.TimeUnit; 044import java.util.logging.Level; 045import java.util.logging.Logger; 046 047import javax.swing.AbstractAction; 048import javax.swing.BorderFactory; 049import javax.swing.Icon; 050import javax.swing.ImageIcon; 051import javax.swing.JButton; 052import javax.swing.JFormattedTextField; 053import javax.swing.JLabel; 054import javax.swing.JMenuItem; 055import javax.swing.JPanel; 056import javax.swing.JPopupMenu; 057import javax.swing.JScrollPane; 058import javax.swing.JSplitPane; 059import javax.swing.JTabbedPane; 060import javax.swing.JTable; 061import javax.swing.JTextArea; 062import javax.swing.ListSelectionModel; 063import javax.swing.SwingUtilities; 064import javax.swing.event.ListSelectionEvent; 065import javax.swing.event.ListSelectionListener; 066import javax.swing.table.DefaultTableModel; 067import javax.swing.text.BadLocationException; 068 069import org.jivesoftware.smack.AbstractXMPPConnection; 070import org.jivesoftware.smack.ConnectionListener; 071import org.jivesoftware.smack.ReconnectionListener; 072import org.jivesoftware.smack.ReconnectionManager; 073import org.jivesoftware.smack.SmackException.NotConnectedException; 074import org.jivesoftware.smack.XMPPConnection; 075import org.jivesoftware.smack.debugger.SmackDebugger; 076import org.jivesoftware.smack.debugger.SmackDebuggerFactory; 077import org.jivesoftware.smack.packet.IQ; 078import org.jivesoftware.smack.packet.Message; 079import org.jivesoftware.smack.packet.Presence; 080import org.jivesoftware.smack.packet.Stanza; 081import org.jivesoftware.smack.packet.TopLevelStreamElement; 082import org.jivesoftware.smack.packet.XmlEnvironment; 083import org.jivesoftware.smack.util.ObservableReader; 084import org.jivesoftware.smack.util.ObservableWriter; 085import org.jivesoftware.smack.util.ReaderListener; 086import org.jivesoftware.smack.util.StringUtils; 087import org.jivesoftware.smack.util.WriterListener; 088import org.jivesoftware.smack.util.XmlUtil; 089 090import org.jxmpp.jid.EntityFullJid; 091import org.jxmpp.jid.Jid; 092 093/** 094 * The EnhancedDebugger is a debugger that allows to debug sent, received and interpreted messages 095 * but also provides the ability to send ad-hoc messages composed by the user. 096 * <p> 097 * A new EnhancedDebugger will be created for each connection to debug. All the EnhancedDebuggers 098 * will be shown in the same debug window provided by the class EnhancedDebuggerWindow. 099 * </p> 100 * 101 * @author Gaston Dombiak 102 */ 103public class EnhancedDebugger extends SmackDebugger { 104 105 private static final Logger LOGGER = Logger.getLogger(EnhancedDebugger.class.getName()); 106 107 private static final String NEWLINE = "\n"; 108 109 private static ImageIcon packetReceivedIcon; 110 private static ImageIcon packetSentIcon; 111 private static ImageIcon presencePacketIcon; 112 private static ImageIcon iqPacketIcon; 113 private static ImageIcon messagePacketIcon; 114 private static ImageIcon unknownPacketTypeIcon; 115 116 { 117 URL url; 118 // Load the image icons 119 url = 120 Thread.currentThread().getContextClassLoader().getResource("images/nav_left_blue.png"); 121 if (url != null) { 122 packetReceivedIcon = new ImageIcon(url); 123 } 124 url = 125 Thread.currentThread().getContextClassLoader().getResource("images/nav_right_red.png"); 126 if (url != null) { 127 packetSentIcon = new ImageIcon(url); 128 } 129 url = 130 Thread.currentThread().getContextClassLoader().getResource("images/photo_portrait.png"); 131 if (url != null) { 132 presencePacketIcon = new ImageIcon(url); 133 } 134 url = 135 Thread.currentThread().getContextClassLoader().getResource( 136 "images/question_and_answer.png"); 137 if (url != null) { 138 iqPacketIcon = new ImageIcon(url); 139 } 140 url = Thread.currentThread().getContextClassLoader().getResource("images/message.png"); 141 if (url != null) { 142 messagePacketIcon = new ImageIcon(url); 143 } 144 url = Thread.currentThread().getContextClassLoader().getResource("images/unknown.png"); 145 if (url != null) { 146 unknownPacketTypeIcon = new ImageIcon(url); 147 } 148 } 149 150 private DefaultTableModel messagesTable = null; 151 private JTextArea messageTextArea = null; 152 private JFormattedTextField userField = null; 153 private JFormattedTextField statusField = null; 154 155 private ConnectionListener connListener = null; 156 private final ReconnectionListener reconnectionListener; 157 158 private Writer writer; 159 private Reader reader; 160 private ReaderListener readerListener; 161 private WriterListener writerListener; 162 163 private Date creationTime = new Date(); 164 165 // Statistics variables 166 private DefaultTableModel statisticsTable = null; 167 private int sentPackets = 0; 168 private int receivedPackets = 0; 169 private int sentIQPackets = 0; 170 private int receivedIQPackets = 0; 171 private int sentMessagePackets = 0; 172 private int receivedMessagePackets = 0; 173 private int sentPresencePackets = 0; 174 private int receivedPresencePackets = 0; 175 private int sentOtherPackets = 0; 176 private int receivedOtherPackets = 0; 177 178 JTabbedPane tabbedPane; 179 180 public EnhancedDebugger(XMPPConnection connection) { 181 super(connection); 182 183 reconnectionListener = new ReconnectionListener() { 184 @Override 185 public void reconnectingIn(final int seconds) { 186 SwingUtilities.invokeLater(new Runnable() { 187 @Override 188 public void run() { 189 statusField.setValue("Attempt to reconnect in " + seconds + " seconds"); 190 } 191 }); 192 } 193 194 @Override 195 public void reconnectionFailed(Exception e) { 196 SwingUtilities.invokeLater(new Runnable() { 197 @Override 198 public void run() { 199 statusField.setValue("Reconnection failed"); 200 } 201 }); 202 } 203 }; 204 205 if (connection instanceof AbstractXMPPConnection) { 206 AbstractXMPPConnection abstractXmppConnection = (AbstractXMPPConnection) connection; 207 ReconnectionManager.getInstanceFor(abstractXmppConnection).addReconnectionListener(reconnectionListener); 208 } else { 209 LOGGER.info("The connection instance " + connection 210 + " is not an instance of AbstractXMPPConnection, thus we can not install the ReconnectionListener"); 211 } 212 213 // We'll arrange the UI into six tabs. The first tab contains all data, the second 214 // client generated XML, the third server generated XML, the fourth allows to send 215 // ad-hoc messages and the fifth contains connection information. 216 tabbedPane = new JTabbedPane(); 217 218 // Add the All Packets, Sent, Received and Interpreted panels 219 addBasicPanels(); 220 221 // Add the panel to send ad-hoc messages 222 addAdhocPacketPanel(); 223 224 // Add the connection information panel 225 addInformationPanel(); 226 227 // Create a thread that will listen for any connection closed event 228 connListener = new ConnectionListener() { 229 @Override 230 public void connectionClosed() { 231 SwingUtilities.invokeLater(new Runnable() { 232 @Override 233 public void run() { 234 statusField.setValue("Closed"); 235 EnhancedDebuggerWindow.connectionClosed(EnhancedDebugger.this); 236 } 237 }); 238 239 } 240 241 @Override 242 public void connectionClosedOnError(final Exception e) { 243 SwingUtilities.invokeLater(new Runnable() { 244 @Override 245 public void run() { 246 statusField.setValue("Closed due to an exception"); 247 EnhancedDebuggerWindow.connectionClosedOnError(EnhancedDebugger.this, e); 248 } 249 }); 250 251 } 252 }; 253 254 EnhancedDebuggerWindow.addDebugger(this); 255 } 256 257 private void addBasicPanels() { 258 JSplitPane allPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); 259 allPane.setOneTouchExpandable(true); 260 261 messagesTable = 262 new DefaultTableModel( 263 new Object[] {"Hide", "Timestamp", "", "", "Message", "Id", "Type", "To", "From"}, 264 0) { 265 private static final long serialVersionUID = 8136121224474217264L; 266 @Override 267 public boolean isCellEditable(int rowIndex, int mColIndex) { 268 return false; 269 } 270 271 @Override 272 public Class<?> getColumnClass(int columnIndex) { 273 if (columnIndex == 2 || columnIndex == 3) { 274 return Icon.class; 275 } 276 return super.getColumnClass(columnIndex); 277 } 278 279 }; 280 JTable table = new JTable(messagesTable); 281 // Allow only single a selection 282 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 283 // Hide the first column 284 table.getColumnModel().getColumn(0).setMaxWidth(0); 285 table.getColumnModel().getColumn(0).setMinWidth(0); 286 table.getTableHeader().getColumnModel().getColumn(0).setMaxWidth(0); 287 table.getTableHeader().getColumnModel().getColumn(0).setMinWidth(0); 288 // Set the column "timestamp" size 289 table.getColumnModel().getColumn(1).setMaxWidth(300); 290 table.getColumnModel().getColumn(1).setPreferredWidth(90); 291 // Set the column "direction" icon size 292 table.getColumnModel().getColumn(2).setMaxWidth(50); 293 table.getColumnModel().getColumn(2).setPreferredWidth(30); 294 // Set the column "packet type" icon size 295 table.getColumnModel().getColumn(3).setMaxWidth(50); 296 table.getColumnModel().getColumn(3).setPreferredWidth(30); 297 // Set the column "Id" size 298 table.getColumnModel().getColumn(5).setMaxWidth(100); 299 table.getColumnModel().getColumn(5).setPreferredWidth(55); 300 // Set the column "type" size 301 table.getColumnModel().getColumn(6).setMaxWidth(200); 302 table.getColumnModel().getColumn(6).setPreferredWidth(50); 303 // Set the column "to" size 304 table.getColumnModel().getColumn(7).setMaxWidth(300); 305 table.getColumnModel().getColumn(7).setPreferredWidth(90); 306 // Set the column "from" size 307 table.getColumnModel().getColumn(8).setMaxWidth(300); 308 table.getColumnModel().getColumn(8).setPreferredWidth(90); 309 // Create a table listener that listen for row selection events 310 SelectionListener selectionListener = new SelectionListener(table); 311 table.getSelectionModel().addListSelectionListener(selectionListener); 312 table.getColumnModel().getSelectionModel().addListSelectionListener(selectionListener); 313 allPane.setTopComponent(new JScrollPane(table)); 314 messageTextArea = new JTextArea(); 315 messageTextArea.setEditable(false); 316 // Add pop-up menu. 317 JPopupMenu menu = new JPopupMenu(); 318 JMenuItem menuItem1 = new JMenuItem("Copy"); 319 menuItem1.addActionListener(new ActionListener() { 320 @Override 321 public void actionPerformed(ActionEvent e) { 322 // Get the clipboard 323 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 324 // Set the sent text as the new content of the clipboard 325 clipboard.setContents(new StringSelection(messageTextArea.getText()), null); 326 } 327 }); 328 menu.add(menuItem1); 329 // Add listener to the text area so the popup menu can come up. 330 messageTextArea.addMouseListener(new PopupListener(menu)); 331 JPanel sublayout = new JPanel(new BorderLayout()); 332 sublayout.add(new JScrollPane(messageTextArea), BorderLayout.CENTER); 333 334 JButton clearb = new JButton("Clear All Packets"); 335 336 clearb.addActionListener(new AbstractAction() { 337 private static final long serialVersionUID = -8576045822764763613L; 338 339 @Override 340 public void actionPerformed(ActionEvent e) { 341 messagesTable.setRowCount(0); 342 } 343 }); 344 345 sublayout.add(clearb, BorderLayout.NORTH); 346 allPane.setBottomComponent(sublayout); 347 348 allPane.setDividerLocation(150); 349 350 tabbedPane.add("All Packets", allPane); 351 tabbedPane.setToolTipTextAt(0, "Sent and received packets processed by Smack"); 352 353 // Create UI elements for client generated XML traffic. 354 final JTextArea sentText = new JTextArea(); 355 sentText.setWrapStyleWord(true); 356 sentText.setLineWrap(true); 357 sentText.setEditable(false); 358 sentText.setForeground(new Color(112, 3, 3)); 359 tabbedPane.add("Raw Sent Packets", new JScrollPane(sentText)); 360 tabbedPane.setToolTipTextAt(1, "Raw text of the sent packets"); 361 362 // Add pop-up menu. 363 menu = new JPopupMenu(); 364 menuItem1 = new JMenuItem("Copy"); 365 menuItem1.addActionListener(new ActionListener() { 366 @Override 367 public void actionPerformed(ActionEvent e) { 368 // Get the clipboard 369 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 370 // Set the sent text as the new content of the clipboard 371 clipboard.setContents(new StringSelection(sentText.getText()), null); 372 } 373 }); 374 375 JMenuItem menuItem2 = new JMenuItem("Clear"); 376 menuItem2.addActionListener(new ActionListener() { 377 @Override 378 public void actionPerformed(ActionEvent e) { 379 sentText.setText(""); 380 } 381 }); 382 383 // Add listener to the text area so the popup menu can come up. 384 sentText.addMouseListener(new PopupListener(menu)); 385 menu.add(menuItem1); 386 menu.add(menuItem2); 387 388 // Create UI elements for server generated XML traffic. 389 final JTextArea receivedText = new JTextArea(); 390 receivedText.setWrapStyleWord(true); 391 receivedText.setLineWrap(true); 392 receivedText.setEditable(false); 393 receivedText.setForeground(new Color(6, 76, 133)); 394 tabbedPane.add("Raw Received Packets", new JScrollPane(receivedText)); 395 tabbedPane.setToolTipTextAt( 396 2, 397 "Raw text of the received packets before Smack process them"); 398 399 // Add pop-up menu. 400 menu = new JPopupMenu(); 401 menuItem1 = new JMenuItem("Copy"); 402 menuItem1.addActionListener(new ActionListener() { 403 @Override 404 public void actionPerformed(ActionEvent e) { 405 // Get the clipboard 406 Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 407 // Set the sent text as the new content of the clipboard 408 clipboard.setContents(new StringSelection(receivedText.getText()), null); 409 } 410 }); 411 412 menuItem2 = new JMenuItem("Clear"); 413 menuItem2.addActionListener(new ActionListener() { 414 @Override 415 public void actionPerformed(ActionEvent e) { 416 receivedText.setText(""); 417 } 418 }); 419 420 // Add listener to the text area so the popup menu can come up. 421 receivedText.addMouseListener(new PopupListener(menu)); 422 menu.add(menuItem1); 423 menu.add(menuItem2); 424 425 // Create a special Reader that wraps the main Reader and logs data to the GUI. 426 ObservableReader debugReader = new ObservableReader(reader); 427 readerListener = new ReaderListener() { 428 private final PriorityBlockingQueue<String> buffer = new PriorityBlockingQueue<>(); 429 430 @Override 431 public void read(final String string) { 432 addBatched(string, buffer, receivedText); 433 } 434 }; 435 debugReader.addReaderListener(readerListener); 436 437 // Create a special Writer that wraps the main Writer and logs data to the GUI. 438 ObservableWriter debugWriter = new ObservableWriter(writer); 439 writerListener = new WriterListener() { 440 private final PriorityBlockingQueue<String> buffer = new PriorityBlockingQueue<>(); 441 442 @Override 443 public void write(final String string) { 444 addBatched(string, buffer, sentText); 445 } 446 }; 447 debugWriter.addWriterListener(writerListener); 448 449 // Assign the reader/writer objects to use the debug versions. The packet reader 450 // and writer will use the debug versions when they are created. 451 reader = debugReader; 452 writer = debugWriter; 453 454 } 455 456 private static void addBatched(String string, PriorityBlockingQueue<String> buffer, JTextArea jTextArea) { 457 buffer.add(string); 458 459 SwingUtilities.invokeLater(() -> { 460 List<String> linesToAdd = new ArrayList<>(); 461 String data; 462 Instant start = Instant.now(); 463 try { 464 // To reduce overhead/increase performance, try to process up to a certain amount of lines at the 465 // same time, when they arrive in rapid succession. 466 while (linesToAdd.size() < 50 467 && Duration.between(start, Instant.now()).compareTo(Duration.ofMillis(100)) < 0 468 && (data = buffer.poll(10, TimeUnit.MILLISECONDS)) != null) { 469 linesToAdd.add(data); 470 } 471 } catch (InterruptedException e) { 472 LOGGER.log(Level.FINER, "Interrupted wait-for-poll in addBatched(). Process all data now.", e); 473 } 474 475 if (linesToAdd.isEmpty()) { 476 return; 477 } 478 479 if (EnhancedDebuggerWindow.PERSISTED_DEBUGGER && !EnhancedDebuggerWindow.getInstance().isVisible()) { 480 // Do not add content if the parent is not visible 481 return; 482 } 483 484 // Delete lines from the top, if lines to be added will exceed the maximum. 485 int linesToDelete = jTextArea.getLineCount() + linesToAdd.size() - EnhancedDebuggerWindow.MAX_TABLE_ROWS; 486 if (linesToDelete > 0) { 487 try { 488 jTextArea.replaceRange("", 0, jTextArea.getLineEndOffset(linesToDelete - 1)); 489 } catch (BadLocationException e) { 490 LOGGER.log(Level.SEVERE, "Error with line offset, MAX_TABLE_ROWS is set too low: " 491 + EnhancedDebuggerWindow.MAX_TABLE_ROWS, e); 492 } 493 } 494 495 // Add the new content. 496 jTextArea.append(String.join(NEWLINE, linesToAdd)); 497 }); 498 } 499 500 private void addAdhocPacketPanel() { 501 // Create UI elements for sending ad-hoc messages. 502 final JTextArea adhocMessages = new JTextArea(); 503 adhocMessages.setEditable(true); 504 adhocMessages.setForeground(new Color(1, 94, 35)); 505 tabbedPane.add("Ad-hoc message", new JScrollPane(adhocMessages)); 506 tabbedPane.setToolTipTextAt(3, "Panel that allows you to send adhoc packets"); 507 508 // Add pop-up menu. 509 JPopupMenu menu = new JPopupMenu(); 510 JMenuItem menuItem = new JMenuItem("Message"); 511 menuItem.addActionListener(new ActionListener() { 512 @Override 513 public void actionPerformed(ActionEvent e) { 514 adhocMessages.setText( 515 "<message to=\"\" id=\"" 516 + StringUtils.randomString(5) 517 + "-X\"><body></body></message>"); 518 } 519 }); 520 menu.add(menuItem); 521 522 menuItem = new JMenuItem("IQ Get"); 523 menuItem.addActionListener(new ActionListener() { 524 @Override 525 public void actionPerformed(ActionEvent e) { 526 adhocMessages.setText( 527 "<iq type=\"get\" to=\"\" id=\"" 528 + StringUtils.randomString(5) 529 + "-X\"><query xmlns=\"\"></query></iq>"); 530 } 531 }); 532 menu.add(menuItem); 533 534 menuItem = new JMenuItem("IQ Set"); 535 menuItem.addActionListener(new ActionListener() { 536 @Override 537 public void actionPerformed(ActionEvent e) { 538 adhocMessages.setText( 539 "<iq type=\"set\" to=\"\" id=\"" 540 + StringUtils.randomString(5) 541 + "-X\"><query xmlns=\"\"></query></iq>"); 542 } 543 }); 544 menu.add(menuItem); 545 546 menuItem = new JMenuItem("Presence"); 547 menuItem.addActionListener(new ActionListener() { 548 @Override 549 public void actionPerformed(ActionEvent e) { 550 adhocMessages.setText( 551 "<presence to=\"\" id=\"" + StringUtils.randomString(5) + "-X\"/>"); 552 } 553 }); 554 menu.add(menuItem); 555 menu.addSeparator(); 556 557 menuItem = new JMenuItem("Send"); 558 menuItem.addActionListener(new ActionListener() { 559 @Override 560 public void actionPerformed(ActionEvent e) { 561 if (!"".equals(adhocMessages.getText())) { 562 AdHocPacket packetToSend = new AdHocPacket(adhocMessages.getText()); 563 try { 564 connection.sendStanza(packetToSend); 565 } 566 catch (InterruptedException | NotConnectedException e1) { 567 LOGGER.log(Level.WARNING, "exception", e); 568 } 569 } 570 } 571 }); 572 menu.add(menuItem); 573 574 menuItem = new JMenuItem("Clear"); 575 menuItem.addActionListener(new ActionListener() { 576 @Override 577 public void actionPerformed(ActionEvent e) { 578 adhocMessages.setText(null); 579 } 580 }); 581 menu.add(menuItem); 582 583 // Add listener to the text area so the popup menu can come up. 584 adhocMessages.addMouseListener(new PopupListener(menu)); 585 } 586 587 private void addInformationPanel() { 588 // Create UI elements for connection information. 589 JPanel informationPanel = new JPanel(); 590 informationPanel.setLayout(new BorderLayout()); 591 592 // Add the Host information 593 JPanel connPanel = new JPanel(); 594 connPanel.setLayout(new GridBagLayout()); 595 connPanel.setBorder(BorderFactory.createTitledBorder("XMPPConnection information")); 596 597 JLabel label = new JLabel("Host: "); 598 label.setMinimumSize(new java.awt.Dimension(150, 14)); 599 label.setMaximumSize(new java.awt.Dimension(150, 14)); 600 connPanel.add( 601 label, 602 new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); 603 JFormattedTextField field = new JFormattedTextField(connection.getXMPPServiceDomain()); 604 field.setMinimumSize(new java.awt.Dimension(150, 20)); 605 field.setMaximumSize(new java.awt.Dimension(150, 20)); 606 field.setEditable(false); 607 field.setBorder(null); 608 connPanel.add( 609 field, 610 new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); 611 612 // Add the Port information 613 label = new JLabel("Port: "); 614 label.setMinimumSize(new java.awt.Dimension(150, 14)); 615 label.setMaximumSize(new java.awt.Dimension(150, 14)); 616 connPanel.add( 617 label, 618 new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); 619 field = new JFormattedTextField(connection.getPort()); 620 field.setMinimumSize(new java.awt.Dimension(150, 20)); 621 field.setMaximumSize(new java.awt.Dimension(150, 20)); 622 field.setEditable(false); 623 field.setBorder(null); 624 connPanel.add( 625 field, 626 new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); 627 628 // Add the connection's User information 629 label = new JLabel("User: "); 630 label.setMinimumSize(new java.awt.Dimension(150, 14)); 631 label.setMaximumSize(new java.awt.Dimension(150, 14)); 632 connPanel.add( 633 label, 634 new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); 635 userField = new JFormattedTextField(); 636 userField.setMinimumSize(new java.awt.Dimension(150, 20)); 637 userField.setMaximumSize(new java.awt.Dimension(150, 20)); 638 userField.setEditable(false); 639 userField.setBorder(null); 640 connPanel.add( 641 userField, 642 new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); 643 644 // Add the connection's creationTime information 645 label = new JLabel("Creation time: "); 646 label.setMinimumSize(new java.awt.Dimension(150, 14)); 647 label.setMaximumSize(new java.awt.Dimension(150, 14)); 648 connPanel.add( 649 label, 650 new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); 651 field = new JFormattedTextField(new SimpleDateFormat("yyyy.MM.dd HH:mm:ss:SS")); 652 field.setMinimumSize(new java.awt.Dimension(150, 20)); 653 field.setMaximumSize(new java.awt.Dimension(150, 20)); 654 field.setValue(creationTime); 655 field.setEditable(false); 656 field.setBorder(null); 657 connPanel.add( 658 field, 659 new GridBagConstraints(1, 3, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); 660 661 // Add the connection's creationTime information 662 label = new JLabel("Status: "); 663 label.setMinimumSize(new java.awt.Dimension(150, 14)); 664 label.setMaximumSize(new java.awt.Dimension(150, 14)); 665 connPanel.add( 666 label, 667 new GridBagConstraints(0, 4, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); 668 statusField = new JFormattedTextField(); 669 statusField.setMinimumSize(new java.awt.Dimension(150, 20)); 670 statusField.setMaximumSize(new java.awt.Dimension(150, 20)); 671 statusField.setValue("Active"); 672 statusField.setEditable(false); 673 statusField.setBorder(null); 674 connPanel.add( 675 statusField, 676 new GridBagConstraints(1, 4, 1, 1, 0.0, 0.0, 10, 2, new Insets(0, 0, 0, 0), 0, 0)); 677 // Add the connection panel to the information panel 678 informationPanel.add(connPanel, BorderLayout.NORTH); 679 680 // Add the Number of sent packets information 681 JPanel packetsPanel = new JPanel(); 682 packetsPanel.setLayout(new GridLayout(1, 1)); 683 packetsPanel.setBorder(BorderFactory.createTitledBorder("Transmitted Packets")); 684 685 statisticsTable = 686 new DefaultTableModel(new Object[][] { {"IQ", 0, 0}, {"Message", 0, 0}, 687 {"Presence", 0, 0}, {"Other", 0, 0}, {"Total", 0, 0}}, 688 new Object[] {"Type", "Received", "Sent"}) { 689 private static final long serialVersionUID = -6793886085109589269L; 690 @Override 691 public boolean isCellEditable(int rowIndex, int mColIndex) { 692 return false; 693 } 694 }; 695 JTable table = new JTable(statisticsTable); 696 // Allow only single a selection 697 table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 698 packetsPanel.add(new JScrollPane(table)); 699 700 // Add the packets panel to the information panel 701 informationPanel.add(packetsPanel, BorderLayout.CENTER); 702 703 tabbedPane.add("Information", new JScrollPane(informationPanel)); 704 tabbedPane.setToolTipTextAt(4, "Information and statistics about the debugged connection"); 705 } 706 707 @Override 708 public final void outgoingStreamSink(CharSequence outgoingCharSequence) { 709 writerListener.write(outgoingCharSequence.toString()); 710 } 711 712 @Override 713 public final void incomingStreamSink(CharSequence incomingCharSequence) { 714 readerListener.read(incomingCharSequence.toString()); 715 } 716 717 @Override 718 public void userHasLogged(final EntityFullJid user) { 719 final EnhancedDebugger debugger = this; 720 SwingUtilities.invokeLater(new Runnable() { 721 @Override 722 public void run() { 723 userField.setText(user.toString()); 724 EnhancedDebuggerWindow.userHasLogged(debugger, user.toString()); 725 // Add the connection listener to the connection so that the debugger can be notified 726 // whenever the connection is closed. 727 connection.addConnectionListener(connListener); 728 } 729 }); 730 731 } 732 733 /** 734 * Updates the statistics table 735 */ 736 private void updateStatistics() { 737 statisticsTable.setValueAt(Integer.valueOf(receivedIQPackets), 0, 1); 738 statisticsTable.setValueAt(Integer.valueOf(sentIQPackets), 0, 2); 739 740 statisticsTable.setValueAt(Integer.valueOf(receivedMessagePackets), 1, 1); 741 statisticsTable.setValueAt(Integer.valueOf(sentMessagePackets), 1, 2); 742 743 statisticsTable.setValueAt(Integer.valueOf(receivedPresencePackets), 2, 1); 744 statisticsTable.setValueAt(Integer.valueOf(sentPresencePackets), 2, 2); 745 746 statisticsTable.setValueAt(Integer.valueOf(receivedOtherPackets), 3, 1); 747 statisticsTable.setValueAt(Integer.valueOf(sentOtherPackets), 3, 2); 748 749 statisticsTable.setValueAt(Integer.valueOf(receivedPackets), 4, 1); 750 statisticsTable.setValueAt(Integer.valueOf(sentPackets), 4, 2); 751 } 752 753 /** 754 * Adds the received stanza detail to the messages table. 755 * 756 * @param dateFormatter the SimpleDateFormat to use to format Dates 757 * @param packet the read stanza to add to the table 758 */ 759 private void addReadPacketToTable(final SimpleDateFormat dateFormatter, final TopLevelStreamElement packet) { 760 SwingUtilities.invokeLater(new Runnable() { 761 @Override 762 public void run() { 763 String messageType; 764 Jid from; 765 String stanzaId; 766 if (packet instanceof Stanza) { 767 Stanza stanza = (Stanza) packet; 768 from = stanza.getFrom(); 769 stanzaId = stanza.getStanzaId(); 770 } else { 771 from = null; 772 stanzaId = "(Nonza)"; 773 } 774 String type = ""; 775 Icon packetTypeIcon; 776 receivedPackets++; 777 if (packet instanceof IQ) { 778 packetTypeIcon = iqPacketIcon; 779 messageType = "IQ Received (class=" + packet.getClass().getName() + ")"; 780 type = ((IQ) packet).getType().toString(); 781 receivedIQPackets++; 782 } 783 else if (packet instanceof Message) { 784 packetTypeIcon = messagePacketIcon; 785 messageType = "Message Received"; 786 type = ((Message) packet).getType().toString(); 787 receivedMessagePackets++; 788 } 789 else if (packet instanceof Presence) { 790 packetTypeIcon = presencePacketIcon; 791 messageType = "Presence Received"; 792 type = ((Presence) packet).getType().toString(); 793 receivedPresencePackets++; 794 } 795 else { 796 packetTypeIcon = unknownPacketTypeIcon; 797 messageType = packet.getClass().getName() + " Received"; 798 receivedOtherPackets++; 799 } 800 801 // Check if we need to remove old rows from the table to keep memory consumption low 802 if (EnhancedDebuggerWindow.MAX_TABLE_ROWS > 0 && 803 messagesTable.getRowCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) { 804 messagesTable.removeRow(0); 805 } 806 807 messagesTable.addRow( 808 new Object[] { 809 XmlUtil.prettyFormatXml(packet.toXML().toString()), 810 dateFormatter.format(new Date()), 811 packetReceivedIcon, 812 packetTypeIcon, 813 messageType, 814 stanzaId, 815 type, 816 "", 817 from}); 818 // Update the statistics table 819 updateStatistics(); 820 } 821 }); 822 } 823 824 /** 825 * Adds the sent stanza detail to the messages table. 826 * 827 * @param dateFormatter the SimpleDateFormat to use to format Dates 828 * @param packet the sent stanza to add to the table 829 */ 830 private void addSentPacketToTable(final SimpleDateFormat dateFormatter, final TopLevelStreamElement packet) { 831 SwingUtilities.invokeLater(new Runnable() { 832 @Override 833 public void run() { 834 String messageType; 835 Jid to; 836 String stanzaId; 837 if (packet instanceof Stanza) { 838 Stanza stanza = (Stanza) packet; 839 to = stanza.getTo(); 840 stanzaId = stanza.getStanzaId(); 841 } else { 842 to = null; 843 stanzaId = "(Nonza)"; 844 } 845 String type = ""; 846 Icon packetTypeIcon; 847 sentPackets++; 848 if (packet instanceof IQ) { 849 packetTypeIcon = iqPacketIcon; 850 messageType = "IQ Sent (class=" + packet.getClass().getName() + ")"; 851 type = ((IQ) packet).getType().toString(); 852 sentIQPackets++; 853 } 854 else if (packet instanceof Message) { 855 packetTypeIcon = messagePacketIcon; 856 messageType = "Message Sent"; 857 type = ((Message) packet).getType().toString(); 858 sentMessagePackets++; 859 } 860 else if (packet instanceof Presence) { 861 packetTypeIcon = presencePacketIcon; 862 messageType = "Presence Sent"; 863 type = ((Presence) packet).getType().toString(); 864 sentPresencePackets++; 865 } 866 else { 867 packetTypeIcon = unknownPacketTypeIcon; 868 messageType = packet.getClass().getName() + " Sent"; 869 sentOtherPackets++; 870 } 871 872 // Check if we need to remove old rows from the table to keep memory consumption low 873 if (EnhancedDebuggerWindow.MAX_TABLE_ROWS > 0 && 874 messagesTable.getRowCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) { 875 messagesTable.removeRow(0); 876 } 877 878 messagesTable.addRow( 879 new Object[] { 880 XmlUtil.prettyFormatXml(packet.toXML().toString()), 881 dateFormatter.format(new Date()), 882 packetSentIcon, 883 packetTypeIcon, 884 messageType, 885 stanzaId, 886 type, 887 to, 888 ""}); 889 890 // Update the statistics table 891 updateStatistics(); 892 } 893 }); 894 } 895 896 /** 897 * Returns true if the debugger's connection with the server is up and running. 898 * 899 * @return true if the connection with the server is active. 900 */ 901 boolean isConnectionActive() { 902 return connection.isConnected(); 903 } 904 905 /** 906 * Stops debugging the connection. Removes any listener on the connection. 907 */ 908 void cancel() { 909 connection.removeConnectionListener(connListener); 910 ((ObservableReader) reader).removeReaderListener(readerListener); 911 ((ObservableWriter) writer).removeWriterListener(writerListener); 912 messagesTable = null; 913 } 914 915 /** 916 * An ad-hoc stanza is like any regular stanza but with the exception that it's intention is 917 * to be used only <b>to send packets</b>.<p> 918 * <p/> 919 * The whole text to send must be passed to the constructor. This implies that the client of 920 * this class is responsible for sending a valid text to the constructor. 921 */ 922 private static final class AdHocPacket extends Stanza { 923 924 private final String text; 925 926 /** 927 * Create a new AdHocPacket with the text to send. The passed text must be a valid text to 928 * send to the server, no validation will be done on the passed text. 929 * 930 * @param text the whole text of the stanza to send 931 */ 932 private AdHocPacket(String text) { 933 this.text = text; 934 } 935 936 @Override 937 public String toXML(XmlEnvironment enclosingNamespace) { 938 return text; 939 } 940 941 @Override 942 public String toString() { 943 return toXML((XmlEnvironment) null); 944 } 945 946 @Override 947 public String getElementName() { 948 return null; 949 } 950 } 951 952 /** 953 * Listens for debug window popup dialog events. 954 */ 955 private static class PopupListener extends MouseAdapter { 956 957 JPopupMenu popup; 958 959 PopupListener(JPopupMenu popupMenu) { 960 popup = popupMenu; 961 } 962 963 @Override 964 public void mousePressed(MouseEvent e) { 965 maybeShowPopup(e); 966 } 967 968 @Override 969 public void mouseReleased(MouseEvent e) { 970 maybeShowPopup(e); 971 } 972 973 private void maybeShowPopup(MouseEvent e) { 974 if (e.isPopupTrigger()) { 975 popup.show(e.getComponent(), e.getX(), e.getY()); 976 } 977 } 978 } 979 980 private class SelectionListener implements ListSelectionListener { 981 982 JTable table; 983 984 // It is necessary to keep the table since it is not possible 985 // to determine the table from the event's source 986 SelectionListener(JTable table) { 987 this.table = table; 988 } 989 990 @Override 991 public void valueChanged(ListSelectionEvent e) { 992 if (table.getSelectedRow() == -1) { 993 // Clear the messageTextArea since there is none packet selected 994 messageTextArea.setText(null); 995 } 996 else { 997 // Set the detail of the packet in the messageTextArea 998 messageTextArea.setText( 999 (String) table.getModel().getValueAt(table.getSelectedRow(), 0)); 1000 // Scroll up to the top 1001 messageTextArea.setCaretPosition(0); 1002 } 1003 } 1004 } 1005 1006 @Override 1007 public void onIncomingStreamElement(final TopLevelStreamElement streamElement) { 1008 final SimpleDateFormat dateFormatter = new SimpleDateFormat("HH:mm:ss:SS"); 1009 SwingUtilities.invokeLater(new Runnable() { 1010 @Override 1011 public void run() { 1012 addReadPacketToTable(dateFormatter, streamElement); 1013 } 1014 }); 1015 } 1016 1017 @Override 1018 public void onOutgoingStreamElement(final TopLevelStreamElement streamElement) { 1019 final SimpleDateFormat dateFormatter = new SimpleDateFormat("HH:mm:ss:SS"); 1020 SwingUtilities.invokeLater(new Runnable() { 1021 @Override 1022 public void run() { 1023 addSentPacketToTable(dateFormatter, streamElement); 1024 } 1025 }); 1026 } 1027 1028 public static final class Factory implements SmackDebuggerFactory { 1029 1030 public static final SmackDebuggerFactory INSTANCE = new Factory(); 1031 1032 private Factory() { 1033 } 1034 1035 @Override 1036 public SmackDebugger create(XMPPConnection connection) throws IllegalArgumentException { 1037 return new EnhancedDebugger(connection); 1038 } 1039 1040 } 1041}