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.Color;
021import java.awt.GridLayout;
022import java.awt.Toolkit;
023import java.awt.datatransfer.Clipboard;
024import java.awt.datatransfer.StringSelection;
025import java.awt.event.ActionEvent;
026import java.awt.event.ActionListener;
027import java.awt.event.MouseAdapter;
028import java.awt.event.MouseEvent;
029import java.awt.event.MouseListener;
030import java.awt.event.WindowAdapter;
031import java.awt.event.WindowEvent;
032import java.io.Reader;
033import java.io.Writer;
034
035import javax.swing.JFrame;
036import javax.swing.JMenuItem;
037import javax.swing.JPanel;
038import javax.swing.JPopupMenu;
039import javax.swing.JScrollPane;
040import javax.swing.JTabbedPane;
041import javax.swing.JTextArea;
042
043import org.jivesoftware.smack.StanzaListener;
044import org.jivesoftware.smack.XMPPConnection;
045import org.jivesoftware.smack.debugger.SmackDebugger;
046import org.jivesoftware.smack.packet.Stanza;
047import org.jivesoftware.smack.util.ObservableReader;
048import org.jivesoftware.smack.util.ObservableWriter;
049import org.jivesoftware.smack.util.ReaderListener;
050import org.jivesoftware.smack.util.WriterListener;
051
052import org.jxmpp.jid.EntityFullJid;
053
054/**
055 * The LiteDebugger is a very simple debugger that allows to debug sent, received and 
056 * interpreted messages.
057 * 
058 * @author Gaston Dombiak
059 */
060public class LiteDebugger implements SmackDebugger {
061
062    private static final String NEWLINE = "\n";
063
064    private JFrame frame = null;
065    private XMPPConnection connection = null;
066
067    private StanzaListener listener = null;
068
069    private Writer writer;
070    private Reader reader;
071    private ReaderListener readerListener;
072    private WriterListener writerListener;
073
074    public LiteDebugger(XMPPConnection connection, Writer writer, Reader reader) {
075        this.connection = connection;
076        this.writer = writer;
077        this.reader = reader;
078        createDebug();
079    }
080
081    /**
082     * Creates the debug process, which is a GUI window that displays XML traffic.
083     */
084    private void createDebug() {
085        frame = new JFrame("Smack Debug Window -- " + connection.getXMPPServiceDomain() + ":" +
086                connection.getPort());
087
088        // Add listener for window closing event 
089        frame.addWindowListener(new WindowAdapter() {
090            @Override
091            public void windowClosing(WindowEvent evt) {
092                rootWindowClosing(evt);
093            }
094        });
095
096        // We'll arrange the UI into four tabs. The first tab contains all data, the second
097        // client generated XML, the third server generated XML, and the fourth is packet
098        // data from the server as seen by Smack.
099        JTabbedPane tabbedPane = new JTabbedPane();
100
101        JPanel allPane = new JPanel();
102        allPane.setLayout(new GridLayout(3, 1));
103        tabbedPane.add("All", allPane);
104
105        // Create UI elements for client generated XML traffic.
106        final JTextArea sentText1 = new JTextArea();
107        final JTextArea sentText2 = new JTextArea();
108        sentText1.setEditable(false);
109        sentText2.setEditable(false);
110        sentText1.setForeground(new Color(112, 3, 3));
111        sentText2.setForeground(new Color(112, 3, 3));
112        allPane.add(new JScrollPane(sentText1));
113        tabbedPane.add("Sent", new JScrollPane(sentText2));
114
115        // Add pop-up menu.
116        JPopupMenu menu = new JPopupMenu();
117        JMenuItem menuItem1 = new JMenuItem("Copy");
118        menuItem1.addActionListener(new ActionListener() {
119            @Override
120            public void actionPerformed(ActionEvent e) {
121                // Get the clipboard
122                Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
123                // Set the sent text as the new content of the clipboard
124                clipboard.setContents(new StringSelection(sentText1.getText()), null);
125            }
126        });
127
128        JMenuItem menuItem2 = new JMenuItem("Clear");
129        menuItem2.addActionListener(new ActionListener() {
130            @Override
131            public void actionPerformed(ActionEvent e) {
132                sentText1.setText("");
133                sentText2.setText("");
134            }
135        });
136
137        // Add listener to the text area so the popup menu can come up.
138        MouseListener popupListener = new PopupListener(menu);
139        sentText1.addMouseListener(popupListener);
140        sentText2.addMouseListener(popupListener);
141        menu.add(menuItem1);
142        menu.add(menuItem2);
143
144        // Create UI elements for server generated XML traffic.
145        final JTextArea receivedText1 = new JTextArea();
146        final JTextArea receivedText2 = new JTextArea();
147        receivedText1.setEditable(false);
148        receivedText2.setEditable(false);
149        receivedText1.setForeground(new Color(6, 76, 133));
150        receivedText2.setForeground(new Color(6, 76, 133));
151        allPane.add(new JScrollPane(receivedText1));
152        tabbedPane.add("Received", new JScrollPane(receivedText2));
153
154        // Add pop-up menu.
155        menu = new JPopupMenu();
156        menuItem1 = new JMenuItem("Copy");
157        menuItem1.addActionListener(new ActionListener() {
158            @Override
159            public void actionPerformed(ActionEvent e) {
160                // Get the clipboard
161                Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
162                // Set the sent text as the new content of the clipboard
163                clipboard.setContents(new StringSelection(receivedText1.getText()), null);
164            }
165        });
166
167        menuItem2 = new JMenuItem("Clear");
168        menuItem2.addActionListener(new ActionListener() {
169            @Override
170            public void actionPerformed(ActionEvent e) {
171                receivedText1.setText("");
172                receivedText2.setText("");
173            }
174        });
175
176        // Add listener to the text area so the popup menu can come up.
177        popupListener = new PopupListener(menu);
178        receivedText1.addMouseListener(popupListener);
179        receivedText2.addMouseListener(popupListener);
180        menu.add(menuItem1);
181        menu.add(menuItem2);
182
183        // Create UI elements for interpreted XML traffic.
184        final JTextArea interpretedText1 = new JTextArea();
185        final JTextArea interpretedText2 = new JTextArea();
186        interpretedText1.setEditable(false);
187        interpretedText2.setEditable(false);
188        interpretedText1.setForeground(new Color(1, 94, 35));
189        interpretedText2.setForeground(new Color(1, 94, 35));
190        allPane.add(new JScrollPane(interpretedText1));
191        tabbedPane.add("Interpreted", new JScrollPane(interpretedText2));
192
193        // Add pop-up menu.
194        menu = new JPopupMenu();
195        menuItem1 = new JMenuItem("Copy");
196        menuItem1.addActionListener(new ActionListener() {
197            @Override
198            public void actionPerformed(ActionEvent e) {
199                // Get the clipboard
200                Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
201                // Set the sent text as the new content of the clipboard
202                clipboard.setContents(new StringSelection(interpretedText1.getText()), null);
203            }
204        });
205
206        menuItem2 = new JMenuItem("Clear");
207        menuItem2.addActionListener(new ActionListener() {
208            @Override
209            public void actionPerformed(ActionEvent e) {
210                interpretedText1.setText("");
211                interpretedText2.setText("");
212            }
213        });
214
215        // Add listener to the text area so the popup menu can come up.
216        popupListener = new PopupListener(menu);
217        interpretedText1.addMouseListener(popupListener);
218        interpretedText2.addMouseListener(popupListener);
219        menu.add(menuItem1);
220        menu.add(menuItem2);
221
222        frame.getContentPane().add(tabbedPane);
223
224        frame.setSize(550, 400);
225        frame.setVisible(true);
226
227        // Create a special Reader that wraps the main Reader and logs data to the GUI.
228        ObservableReader debugReader = new ObservableReader(reader);
229        readerListener = new ReaderListener() {
230                    @Override
231                    public void read(String str) {
232                        int index = str.lastIndexOf(">");
233                        if (index != -1) {
234                            receivedText1.append(str.substring(0, index + 1));
235                            receivedText2.append(str.substring(0, index + 1));
236                            receivedText1.append(NEWLINE);
237                            receivedText2.append(NEWLINE);
238                            if (str.length() > index) {
239                                receivedText1.append(str.substring(index + 1));
240                                receivedText2.append(str.substring(index + 1));
241                            }
242                        }
243                        else {
244                            receivedText1.append(str);
245                            receivedText2.append(str);
246                        }
247                    }
248                };
249        debugReader.addReaderListener(readerListener);
250
251        // Create a special Writer that wraps the main Writer and logs data to the GUI.
252        ObservableWriter debugWriter = new ObservableWriter(writer);
253        writerListener = new WriterListener() {
254                    @Override
255                    public void write(String str) {
256                        sentText1.append(str);
257                        sentText2.append(str);
258                        if (str.endsWith(">")) {
259                            sentText1.append(NEWLINE);
260                            sentText2.append(NEWLINE);
261                        }
262                    }
263                };
264        debugWriter.addWriterListener(writerListener);
265
266        // Assign the reader/writer objects to use the debug versions. The packet reader
267        // and writer will use the debug versions when they are created.
268        reader = debugReader;
269        writer = debugWriter;
270
271        // Create a thread that will listen for all incoming packets and write them to
272        // the GUI. This is what we call "interpreted" packet data, since it's the packet
273        // data as Smack sees it and not as it's coming in as raw XML.
274        listener = new StanzaListener() {
275            @Override
276            public void processStanza(Stanza packet) {
277                interpretedText1.append(packet.toXML().toString());
278                interpretedText2.append(packet.toXML().toString());
279                interpretedText1.append(NEWLINE);
280                interpretedText2.append(NEWLINE);
281            }
282        };
283    }
284
285    /**
286     * Notification that the root window is closing. Stop listening for received and 
287     * transmitted packets.
288     * 
289     * @param evt the event that indicates that the root window is closing 
290     */
291    public void rootWindowClosing(WindowEvent evt) {
292        connection.removeAsyncStanzaListener(listener);
293        ((ObservableReader) reader).removeReaderListener(readerListener);
294        ((ObservableWriter) writer).removeWriterListener(writerListener);
295    }
296
297    /**
298     * Listens for debug window popup dialog events.
299     */
300    private static class PopupListener extends MouseAdapter {
301        JPopupMenu popup;
302
303        PopupListener(JPopupMenu popupMenu) {
304            popup = popupMenu;
305        }
306
307        @Override
308        public void mousePressed(MouseEvent e) {
309            maybeShowPopup(e);
310        }
311
312        @Override
313        public void mouseReleased(MouseEvent e) {
314            maybeShowPopup(e);
315        }
316
317        private void maybeShowPopup(MouseEvent e) {
318            if (e.isPopupTrigger()) {
319                popup.show(e.getComponent(), e.getX(), e.getY());
320            }
321        }
322    }
323
324    @Override
325    public Reader newConnectionReader(Reader newReader) {
326        ((ObservableReader) reader).removeReaderListener(readerListener);
327        ObservableReader debugReader = new ObservableReader(newReader);
328        debugReader.addReaderListener(readerListener);
329        reader = debugReader;
330        return reader;
331    }
332
333    @Override
334    public Writer newConnectionWriter(Writer newWriter) {
335        ((ObservableWriter) writer).removeWriterListener(writerListener);
336        ObservableWriter debugWriter = new ObservableWriter(newWriter);
337        debugWriter.addWriterListener(writerListener);
338        writer = debugWriter;
339        return writer;
340    }
341
342    @Override
343    public void userHasLogged(EntityFullJid user) {
344        String title =
345            "Smack Debug Window -- "
346                + user;
347        frame.setTitle(title);
348    }
349
350    @Override
351    public Reader getReader() {
352        return reader;
353    }
354
355    @Override
356    public Writer getWriter() {
357        return writer;
358    }
359
360    @Override
361    public StanzaListener getReaderListener() {
362        return listener;
363    }
364
365    @Override
366    public StanzaListener getWriterListener() {
367        return null;
368    }
369}