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