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