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