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}