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