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