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