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.util.Date;
038import java.util.logging.Level;
039import java.util.logging.Logger;
040
041import javax.swing.AbstractAction;
042import javax.swing.BorderFactory;
043import javax.swing.Icon;
044import javax.swing.ImageIcon;
045import javax.swing.JButton;
046import javax.swing.JFormattedTextField;
047import javax.swing.JLabel;
048import javax.swing.JMenuItem;
049import javax.swing.JPanel;
050import javax.swing.JPopupMenu;
051import javax.swing.JScrollPane;
052import javax.swing.JSplitPane;
053import javax.swing.JTabbedPane;
054import javax.swing.JTable;
055import javax.swing.JTextArea;
056import javax.swing.ListSelectionModel;
057import javax.swing.SwingUtilities;
058import javax.swing.event.ListSelectionEvent;
059import javax.swing.event.ListSelectionListener;
060import javax.swing.table.DefaultTableModel;
061import javax.swing.text.BadLocationException;
062
063import org.jivesoftware.smack.AbstractXMPPConnection;
064import org.jivesoftware.smack.ConnectionListener;
065import org.jivesoftware.smack.ReconnectionListener;
066import org.jivesoftware.smack.ReconnectionManager;
067import org.jivesoftware.smack.SmackException.NotConnectedException;
068import org.jivesoftware.smack.XMPPConnection;
069import org.jivesoftware.smack.debugger.SmackDebugger;
070import org.jivesoftware.smack.debugger.SmackDebuggerFactory;
071import org.jivesoftware.smack.packet.IQ;
072import org.jivesoftware.smack.packet.Message;
073import org.jivesoftware.smack.packet.Presence;
074import org.jivesoftware.smack.packet.Stanza;
075import org.jivesoftware.smack.packet.TopLevelStreamElement;
076import org.jivesoftware.smack.packet.XmlEnvironment;
077import org.jivesoftware.smack.util.ObservableReader;
078import org.jivesoftware.smack.util.ObservableWriter;
079import org.jivesoftware.smack.util.ReaderListener;
080import org.jivesoftware.smack.util.StringUtils;
081import org.jivesoftware.smack.util.WriterListener;
082import org.jivesoftware.smack.util.XmlUtil;
083
084import org.jxmpp.jid.EntityFullJid;
085import org.jxmpp.jid.Jid;
086
087/**
088 * The EnhancedDebugger is a debugger that allows to debug sent, received and interpreted messages
089 * but also provides the ability to send ad-hoc messages composed by the user.
090 * <p>
091 * A new EnhancedDebugger will be created for each connection to debug. All the EnhancedDebuggers
092 * will be shown in the same debug window provided by the class EnhancedDebuggerWindow.
093 * </p>
094 *
095 * @author Gaston Dombiak
096 */
097public class EnhancedDebugger extends SmackDebugger {
098
099    private static final Logger LOGGER = Logger.getLogger(EnhancedDebugger.class.getName());
100
101    private static final String NEWLINE = "\n";
102
103    private static ImageIcon packetReceivedIcon;
104    private static ImageIcon packetSentIcon;
105    private static ImageIcon presencePacketIcon;
106    private static ImageIcon iqPacketIcon;
107    private static ImageIcon messagePacketIcon;
108    private static ImageIcon unknownPacketTypeIcon;
109
110    {
111        URL url;
112        // Load the image icons
113        url =
114                Thread.currentThread().getContextClassLoader().getResource("images/nav_left_blue.png");
115        if (url != null) {
116            packetReceivedIcon = new ImageIcon(url);
117        }
118        url =
119                Thread.currentThread().getContextClassLoader().getResource("images/nav_right_red.png");
120        if (url != null) {
121            packetSentIcon = new ImageIcon(url);
122        }
123        url =
124                Thread.currentThread().getContextClassLoader().getResource("images/photo_portrait.png");
125        if (url != null) {
126            presencePacketIcon = new ImageIcon(url);
127        }
128        url =
129                Thread.currentThread().getContextClassLoader().getResource(
130                        "images/question_and_answer.png");
131        if (url != null) {
132            iqPacketIcon = new ImageIcon(url);
133        }
134        url = Thread.currentThread().getContextClassLoader().getResource("images/message.png");
135        if (url != null) {
136            messagePacketIcon = new ImageIcon(url);
137        }
138        url = Thread.currentThread().getContextClassLoader().getResource("images/unknown.png");
139        if (url != null) {
140            unknownPacketTypeIcon = new ImageIcon(url);
141        }
142    }
143
144    private DefaultTableModel messagesTable = null;
145    private JTextArea messageTextArea = null;
146    private JFormattedTextField userField = null;
147    private JFormattedTextField statusField = null;
148
149    private ConnectionListener connListener = null;
150    private final ReconnectionListener reconnectionListener;
151
152    private Writer writer;
153    private Reader reader;
154    private ReaderListener readerListener;
155    private WriterListener writerListener;
156
157    private Date creationTime = new Date();
158
159    // Statistics variables
160    private DefaultTableModel statisticsTable = null;
161    private int sentPackets = 0;
162    private int receivedPackets = 0;
163    private int sentIQPackets = 0;
164    private int receivedIQPackets = 0;
165    private int sentMessagePackets = 0;
166    private int receivedMessagePackets = 0;
167    private int sentPresencePackets = 0;
168    private int receivedPresencePackets = 0;
169    private int sentOtherPackets = 0;
170    private int receivedOtherPackets = 0;
171
172    JTabbedPane tabbedPane;
173
174    public EnhancedDebugger(XMPPConnection connection) {
175        super(connection);
176
177        reconnectionListener = new ReconnectionListener() {
178            @Override
179            public void reconnectingIn(final int seconds) {
180                SwingUtilities.invokeLater(new Runnable() {
181                    @Override
182                    public void run() {
183                        statusField.setValue("Attempt to reconnect in " + seconds + " seconds");
184                    }
185                });
186            }
187
188            @Override
189            public void reconnectionFailed(Exception e) {
190                SwingUtilities.invokeLater(new Runnable() {
191                    @Override
192                    public void run() {
193                        statusField.setValue("Reconnection failed");
194                    }
195                });
196            }
197        };
198
199        if (connection instanceof AbstractXMPPConnection) {
200            AbstractXMPPConnection abstractXmppConnection = (AbstractXMPPConnection) connection;
201            ReconnectionManager.getInstanceFor(abstractXmppConnection).addReconnectionListener(reconnectionListener);
202        } else {
203            LOGGER.info("The connection instance " + connection
204                            + " is not an instance of AbstractXMPPConnection, thus we can not install the ReconnectionListener");
205        }
206
207        // We'll arrange the UI into six tabs. The first tab contains all data, the second
208        // client generated XML, the third server generated XML, the fourth allows to send
209        // ad-hoc messages and the fifth contains connection information.
210        tabbedPane = new JTabbedPane();
211
212        // Add the All Packets, Sent, Received and Interpreted panels
213        addBasicPanels();
214
215        // Add the panel to send ad-hoc messages
216        addAdhocPacketPanel();
217
218        // Add the connection information panel
219        addInformationPanel();
220
221        // Create a thread that will listen for any connection closed event
222        connListener = new ConnectionListener() {
223            @Override
224            public void connectionClosed() {
225                SwingUtilities.invokeLater(new Runnable() {
226                    @Override
227                    public void run() {
228                        statusField.setValue("Closed");
229                        EnhancedDebuggerWindow.connectionClosed(EnhancedDebugger.this);
230                    }
231                });
232
233            }
234
235            @Override
236            public void connectionClosedOnError(final Exception e) {
237                SwingUtilities.invokeLater(new Runnable() {
238                    @Override
239                    public void run() {
240                        statusField.setValue("Closed due to an exception");
241                        EnhancedDebuggerWindow.connectionClosedOnError(EnhancedDebugger.this, e);
242                    }
243                });
244
245            }
246        };
247
248        EnhancedDebuggerWindow.addDebugger(this);
249    }
250
251    private void addBasicPanels() {
252        JSplitPane allPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
253        allPane.setOneTouchExpandable(true);
254
255        messagesTable =
256                new DefaultTableModel(
257                        new Object[] {"Hide", "Timestamp", "", "", "Message", "Id", "Type", "To", "From"},
258                        0) {
259                    private static final long serialVersionUID = 8136121224474217264L;
260                    @Override
261                    public boolean isCellEditable(int rowIndex, int mColIndex) {
262                        return false;
263                    }
264
265                    @Override
266                    public Class<?> getColumnClass(int columnIndex) {
267                        if (columnIndex == 2 || columnIndex == 3) {
268                            return Icon.class;
269                        }
270                        return super.getColumnClass(columnIndex);
271                    }
272
273                };
274        JTable table = new JTable(messagesTable);
275        // Allow only single a selection
276        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
277        // Hide the first column
278        table.getColumnModel().getColumn(0).setMaxWidth(0);
279        table.getColumnModel().getColumn(0).setMinWidth(0);
280        table.getTableHeader().getColumnModel().getColumn(0).setMaxWidth(0);
281        table.getTableHeader().getColumnModel().getColumn(0).setMinWidth(0);
282        // Set the column "timestamp" size
283        table.getColumnModel().getColumn(1).setMaxWidth(300);
284        table.getColumnModel().getColumn(1).setPreferredWidth(90);
285        // Set the column "direction" icon size
286        table.getColumnModel().getColumn(2).setMaxWidth(50);
287        table.getColumnModel().getColumn(2).setPreferredWidth(30);
288        // Set the column "packet type" icon size
289        table.getColumnModel().getColumn(3).setMaxWidth(50);
290        table.getColumnModel().getColumn(3).setPreferredWidth(30);
291        // Set the column "Id" size
292        table.getColumnModel().getColumn(5).setMaxWidth(100);
293        table.getColumnModel().getColumn(5).setPreferredWidth(55);
294        // Set the column "type" size
295        table.getColumnModel().getColumn(6).setMaxWidth(200);
296        table.getColumnModel().getColumn(6).setPreferredWidth(50);
297        // Set the column "to" size
298        table.getColumnModel().getColumn(7).setMaxWidth(300);
299        table.getColumnModel().getColumn(7).setPreferredWidth(90);
300        // Set the column "from" size
301        table.getColumnModel().getColumn(8).setMaxWidth(300);
302        table.getColumnModel().getColumn(8).setPreferredWidth(90);
303        // Create a table listener that listen for row selection events
304        SelectionListener selectionListener = new SelectionListener(table);
305        table.getSelectionModel().addListSelectionListener(selectionListener);
306        table.getColumnModel().getSelectionModel().addListSelectionListener(selectionListener);
307        allPane.setTopComponent(new JScrollPane(table));
308        messageTextArea = new JTextArea();
309        messageTextArea.setEditable(false);
310        // Add pop-up menu.
311        JPopupMenu menu = new JPopupMenu();
312        JMenuItem menuItem1 = new JMenuItem("Copy");
313        menuItem1.addActionListener(new ActionListener() {
314            @Override
315            public void actionPerformed(ActionEvent e) {
316                // Get the clipboard
317                Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
318                // Set the sent text as the new content of the clipboard
319                clipboard.setContents(new StringSelection(messageTextArea.getText()), null);
320            }
321        });
322        menu.add(menuItem1);
323        // Add listener to the text area so the popup menu can come up.
324        messageTextArea.addMouseListener(new PopupListener(menu));
325        JPanel sublayout = new JPanel(new BorderLayout());
326        sublayout.add(new JScrollPane(messageTextArea), BorderLayout.CENTER);
327
328        JButton clearb = new JButton("Clear All Packets");
329
330        clearb.addActionListener(new AbstractAction() {
331            private static final long serialVersionUID = -8576045822764763613L;
332
333            @Override
334            public void actionPerformed(ActionEvent e) {
335                messagesTable.setRowCount(0);
336            }
337        });
338
339        sublayout.add(clearb, BorderLayout.NORTH);
340        allPane.setBottomComponent(sublayout);
341
342        allPane.setDividerLocation(150);
343
344        tabbedPane.add("All Packets", allPane);
345        tabbedPane.setToolTipTextAt(0, "Sent and received packets processed by Smack");
346
347        // Create UI elements for client generated XML traffic.
348        final JTextArea sentText = new JTextArea();
349        sentText.setWrapStyleWord(true);
350        sentText.setLineWrap(true);
351        sentText.setEditable(false);
352        sentText.setForeground(new Color(112, 3, 3));
353        tabbedPane.add("Raw Sent Packets", new JScrollPane(sentText));
354        tabbedPane.setToolTipTextAt(1, "Raw text of the sent packets");
355
356        // Add pop-up menu.
357        menu = new JPopupMenu();
358        menuItem1 = new JMenuItem("Copy");
359        menuItem1.addActionListener(new ActionListener() {
360            @Override
361            public void actionPerformed(ActionEvent e) {
362                // Get the clipboard
363                Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
364                // Set the sent text as the new content of the clipboard
365                clipboard.setContents(new StringSelection(sentText.getText()), null);
366            }
367        });
368
369        JMenuItem menuItem2 = new JMenuItem("Clear");
370        menuItem2.addActionListener(new ActionListener() {
371            @Override
372            public void actionPerformed(ActionEvent e) {
373                sentText.setText("");
374            }
375        });
376
377        // Add listener to the text area so the popup menu can come up.
378        sentText.addMouseListener(new PopupListener(menu));
379        menu.add(menuItem1);
380        menu.add(menuItem2);
381
382        // Create UI elements for server generated XML traffic.
383        final JTextArea receivedText = new JTextArea();
384        receivedText.setWrapStyleWord(true);
385        receivedText.setLineWrap(true);
386        receivedText.setEditable(false);
387        receivedText.setForeground(new Color(6, 76, 133));
388        tabbedPane.add("Raw Received Packets", new JScrollPane(receivedText));
389        tabbedPane.setToolTipTextAt(
390                2,
391                "Raw text of the received packets before Smack process them");
392
393        // Add pop-up menu.
394        menu = new JPopupMenu();
395        menuItem1 = new JMenuItem("Copy");
396        menuItem1.addActionListener(new ActionListener() {
397            @Override
398            public void actionPerformed(ActionEvent e) {
399                // Get the clipboard
400                Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
401                // Set the sent text as the new content of the clipboard
402                clipboard.setContents(new StringSelection(receivedText.getText()), null);
403            }
404        });
405
406        menuItem2 = new JMenuItem("Clear");
407        menuItem2.addActionListener(new ActionListener() {
408            @Override
409            public void actionPerformed(ActionEvent e) {
410                receivedText.setText("");
411            }
412        });
413
414        // Add listener to the text area so the popup menu can come up.
415        receivedText.addMouseListener(new PopupListener(menu));
416        menu.add(menuItem1);
417        menu.add(menuItem2);
418
419        // Create a special Reader that wraps the main Reader and logs data to the GUI.
420        ObservableReader debugReader = new ObservableReader(reader);
421        readerListener = new ReaderListener() {
422            @Override
423            public void read(final String str) {
424                SwingUtilities.invokeLater(new Runnable() {
425                    @Override
426                    public void run() {
427                        if (EnhancedDebuggerWindow.PERSISTED_DEBUGGER &&
428                                !EnhancedDebuggerWindow.getInstance().isVisible()) {
429                            // Do not add content if the parent is not visible
430                            return;
431                        }
432
433                        int index = str.lastIndexOf(">");
434                        if (index != -1) {
435                            if (receivedText.getLineCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) {
436                                try {
437                                    receivedText.replaceRange("", 0, receivedText.getLineEndOffset(0));
438                                }
439                                catch (BadLocationException e) {
440                                    LOGGER.log(Level.SEVERE, "Error with line offset, MAX_TABLE_ROWS is set too low: " + EnhancedDebuggerWindow.MAX_TABLE_ROWS, e);
441                                }
442                            }
443                            receivedText.append(str.substring(0, index + 1));
444                            receivedText.append(NEWLINE);
445                            if (str.length() > index) {
446                                receivedText.append(str.substring(index + 1));
447                            }
448                        }
449                        else {
450                            receivedText.append(str);
451                        }
452                    }
453                });
454            }
455        };
456        debugReader.addReaderListener(readerListener);
457
458        // Create a special Writer that wraps the main Writer and logs data to the GUI.
459        ObservableWriter debugWriter = new ObservableWriter(writer);
460        writerListener = new WriterListener() {
461            @Override
462            public void write(final String str) {
463                SwingUtilities.invokeLater(new Runnable() {
464                    @Override
465                    public void run() {
466                        if (EnhancedDebuggerWindow.PERSISTED_DEBUGGER &&
467                                !EnhancedDebuggerWindow.getInstance().isVisible()) {
468                            // Do not add content if the parent is not visible
469                            return;
470                        }
471
472                        if (sentText.getLineCount() >= EnhancedDebuggerWindow.MAX_TABLE_ROWS) {
473                            try {
474                                sentText.replaceRange("", 0, sentText.getLineEndOffset(0));
475                            }
476                            catch (BadLocationException e) {
477                                LOGGER.log(Level.SEVERE, "Error with line offset, MAX_TABLE_ROWS is set too low: " + EnhancedDebuggerWindow.MAX_TABLE_ROWS, e);
478                            }
479                        }
480
481                        sentText.append(str);
482                        if (str.endsWith(">")) {
483                            sentText.append(NEWLINE);
484                        }
485                    }
486                });
487
488
489            }
490        };
491        debugWriter.addWriterListener(writerListener);
492
493        // Assign the reader/writer objects to use the debug versions. The packet reader
494        // and writer will use the debug versions when they are created.
495        reader = debugReader;
496        writer = debugWriter;
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}