View Javadoc

1   /*
2    *  XNap - A P2P framework and client.
3    *
4    *  See the file AUTHORS for copyright information.
5    *
6    *  This program is free software; you can redistribute it and/or modify
7    *  it under the terms of the GNU General Public License as published by
8    *  the Free Software Foundation.
9    *
10   *  This program is distributed in the hope that it will be useful,
11   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   *  GNU General Public License for more details.
14   *
15   *  You should have received a copy of the GNU General Public License
16   *  along with this program; if not, write to the Free Software
17   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18   */
19  
20  package org.xnap.plugin.joscar;
21  
22  import java.awt.event.ActionEvent;
23  import java.beans.PropertyChangeEvent;
24  import java.beans.PropertyChangeListener;
25  import java.io.File;
26  import java.io.IOException;
27  import java.util.Collections;
28  import java.util.Hashtable;
29  import java.util.Iterator;
30  import java.util.LinkedList;
31  import java.util.List;
32  import java.util.Map;
33  
34  import javax.swing.AbstractAction;
35  import javax.swing.Action;
36  import javax.swing.Icon;
37  import javax.swing.JMenu;
38  import javax.swing.JOptionPane;
39  
40  import org.apache.log4j.Logger;
41  import org.xnap.XNap;
42  import org.xnap.XNapFacade;
43  import org.xnap.action.*;
44  import org.xnap.chat.ChatManager;
45  import org.xnap.event.StateEvent;
46  import org.xnap.event.StateListener;
47  import org.xnap.gui.AbstractPreferencesDialog;
48  import org.xnap.gui.StatusBar;
49  import org.xnap.gui.XNapFrame;
50  import org.xnap.gui.component.XNapMenu;
51  import org.xnap.gui.component.XNapMenuItem;
52  import org.xnap.gui.event.AbstractPreferencesDialogListener;
53  import org.xnap.gui.util.IconHelper;
54  import org.xnap.peer.HotlistManager;
55  import org.xnap.plugin.AbstractPlugin;
56  import org.xnap.util.FileHelper;
57  import org.xnap.util.IllegalOperationException;
58  import org.xnap.util.State;
59  
60  import JOscarLib.Integration.Event.IncomingMessageEvent;
61  import JOscarLib.Integration.Event.IncomingUrlEvent;
62  import JOscarLib.Integration.Event.IncomingUserEvent;
63  import JOscarLib.Integration.Event.MessagingListener;
64  import JOscarLib.Integration.Event.OffgoingUserEvent;
65  import JOscarLib.Integration.Event.OfflineMessageEvent;
66  import JOscarLib.Integration.Event.StatusListener;
67  
68  /***
69   * Provides a simple chat plugin for ICQ.
70   *
71   * This class creates the connection to the oscar server and is responsible of
72   * managing the buddies and open chat channels, it also updates the online
73   * status of each buddy.
74   */
75  public class JOscarPlugin extends AbstractPlugin 
76  	implements StateListener, MessagingListener, StatusListener,
77  			   PropertyChangeListener
78  {
79  
80      //--- Constant(s) ---
81  	
82  	public static String ICON_FILENAME = "licq.png";
83  	
84  	public static Icon ICON_16 = IconHelper.getIcon(ICON_FILENAME, 16, false);
85      
86  	public static final String BUDDY_FILE = 
87  		FileHelper.getHomeDir() + "joscar_buddies";
88  
89      //--- Data Field(s) ---
90  
91      private static JOscarPlugin instance;
92  
93  	private PreferencesDialogListener listener;
94  
95  	/***
96  	 * The wrapped connection to the icq network.
97  	 */
98  	private JOscarConnection connection;
99  	/***
100 	 * The local JOscarPeer.
101 	 */
102 	private JOscarPeer peer;
103 	/***
104 	 * The list of active channels hashed by chat partner.
105 	 */
106 	private Map channelsByPeer;
107 	/***
108 	 * The buddy list, hashed by their uins.
109 	 */
110 	private Map buddies;
111 	/***
112 	 * Menu plugged into XNap's plugins menu.
113 	 */
114 	private JMenu jmOscar;
115 	/***
116 	 * Displays if we're online or offline for now.
117 	 */
118 	private JOscarStatusPanel spOscar;
119 	
120 	private JOscarPreferences prefs;
121 		
122     private static Logger logger = Logger.getLogger(JOscarPlugin.class);
123 
124     //--- Constructor(s) ---
125 
126     public JOscarPlugin()
127     {
128     }
129 
130     //--- Method(s) ---
131 
132     public static JOscarPlugin getInstance() 
133     {
134 		return instance;
135     }
136     
137     /***
138      * Returns the name of the plugin.
139      */
140     public String getName()
141     {
142 		return getInfo().getName();
143     }
144 
145     /***
146      * Starts the plugin.
147      */
148     public void start() 
149     {
150 		instance = this;
151 		prefs = JOscarPreferences.getInstance();
152 		prefs.addPropertyChangeListener("username", this);
153 
154 		connection = new JOscarConnection();
155 		connection.addStateListener(this);
156 		
157 		peer = new JOscarPeer(prefs.getUsername());
158 
159 		channelsByPeer = Collections.synchronizedMap(new Hashtable());
160 		buddies = Collections.synchronizedMap(new Hashtable());
161 
162 		readBuddiesList();
163 
164 		if (prefs.getAutoconnect()) {
165 			connection.connect();
166 		}
167     }
168 
169     /***
170      * Starts the GUI of the plugin.
171      */
172     public void startGUI()
173     {
174 		initializeMenu();
175 		XNapFrame.getInstance().getMainMenuBar().addPluginMenu(jmOscar);
176 		listener = new PreferencesDialogListener();
177 		XNapFacade.addPluginPreferencesDialogListener(listener);
178 
179 		// add status panel to GUI
180 		spOscar = new JOscarStatusPanel();
181 		connection.addStateListener(spOscar);
182 		StatusBar.getInstance().addComponent(spOscar);
183     }
184 
185 	private void initializeMenu()
186 	{
187 		jmOscar = new XNapMenu(getName());
188 		jmOscar.setIcon(IconHelper.getMenuIcon(ICON_FILENAME));
189 		jmOscar.add(new XNapMenuItem(new AddBuddyAction()));
190 		jmOscar.addSeparator();
191 		jmOscar.add(new XNapMenuItem(new ConnectAction()));
192 		jmOscar.add(new XNapMenuItem(new DisconnectAction()));
193 	}
194 
195     /***
196      * Stops the plugin. Disposes all singletons.
197      */
198     public void stop()
199     {
200 		removeChannels();
201 		writeBuddiesList();
202 		removeBuddies();
203 
204 		try {
205 			connection.disconnect();
206 		}
207 		catch (IllegalOperationException ie) {
208 		}
209 		
210 		connection = null;
211 		peer = null;
212 		buddies = null;
213 		channelsByPeer = null;
214 		prefs.removePropertyChangeListener(this);
215 		JOscarPreferences.disposeInstance();
216 		prefs = null;
217 		instance = null;
218     }
219 
220 	public synchronized void propertyChange(PropertyChangeEvent e)
221 	{
222 		peer = new JOscarPeer(prefs.getUsername());
223 		if (connection.getState() == State.CONNECTED) {
224 			connection.disconnect();
225 			connection.connect();
226 		}
227 	}
228 
229 	/***
230 	 * 
231 	 */
232 	public void stateChanged(StateEvent event)
233 	{
234 		if (connection.getState() == State.CONNECTED) {
235 			connection.addMessagingListener(this);
236 			connection.addStatusListener(this);
237 
238 			synchronized(buddies) {
239 				for (Iterator i = buddies.keySet().iterator(); i.hasNext();) {
240 					connection.addUser((String)i.next());
241 				}
242 			}
243 		}
244 		else {
245 		}
246 	}
247 
248     /***
249      * Stops the GUI of the plugin.
250      */
251     public void stopGUI()
252     {
253 		listener.dispose();
254 		XNapFacade.removePluginPreferencesDialogListener(listener);
255 		listener = null;
256 
257 		XNapFrame.getInstance().getMainMenuBar().removePluginMenu(jmOscar);
258 		jmOscar = null;
259 
260 		StatusBar.getInstance().removeComponent(spOscar);
261 		connection.removeStateListener(spOscar);
262 		spOscar = null;
263     }
264 
265 	/***
266 	 * Tells {@link ChatManager} to close all joscar channels.
267 	 */
268 	private void removeChannels()
269 	{
270 		synchronized (channelsByPeer) {
271 			for (Iterator i = channelsByPeer.values().iterator();
272 				 i.hasNext();) {
273 				ChatManager.getInstance().remove((JOscarChannel)i.next());
274 			}
275 		}
276 	}
277 
278 	public void sendMessage(String uin, String message)
279 	{
280 		connection.sendMessage(uin, message);
281 	}
282 
283 	public void sendSMS(String uin, String message)
284 	{
285 		connection.sendSMS(uin, message);
286 	}
287 
288 	/***
289 	 * Creates a new {@link JOscarPeer} if it's not already in the
290 	 * <code>buddies</code> hashtable.
291 	 */
292 	public JOscarPeer addBuddy(String uin)
293 	{
294 		return addBuddy(new JOscarPeer(uin));
295 	}
296 
297 	/***
298 	 * Creates a new {@link JOscarPeer} if it's not already in the
299 	 * <code>buddies</code> hashtable.
300 	 *
301 	 */
302 	public JOscarPeer addBuddy(JOscarPeer buddy)
303 	{
304 		if (!buddies.containsKey(buddy.getName())) {
305 			connection.addStateListener(buddy);
306 			buddies.put(buddy.getName(), buddy);
307 			connection.addUser(buddy.getName());
308 			HotlistManager.getInstance().add(buddy);
309 			return buddy;
310 		}
311 		return (JOscarPeer)buddies.get(buddy.getName());
312 	}
313 
314 	/***
315 	 * Returns the local peer.
316 	 */
317 	public synchronized JOscarPeer getLocalPeer()
318 	{
319 		return peer;
320 	}
321 	
322 	/***
323 	 * Returns the oscar connection for listeners to subscribe to.
324 	 */
325 	public JOscarConnection getConnection()
326 	{
327 		return connection;
328 	}
329 
330 	/***
331 	 * Removes a buddy.
332 	 */
333 	public void removeBuddy(JOscarPeer buddy)
334 	{
335 		buddies.remove(buddy.getName());
336 		connection.removeStateListener(buddy);
337 		HotlistManager.getInstance().remove(buddy);
338 	}
339 
340 	private void removeBuddies()
341 	{
342 		JOscarPeer[] peers = 
343 			(JOscarPeer[])buddies.values().toArray(new JOscarPeer[0]);
344 		
345 		for (int i = 0; i < peers.length; i++) {
346 			removeBuddy(peers[i]);
347 		}
348 	}
349 
350 	public void addChannel(JOscarChannel channel)
351 	{
352 		if (!channelsByPeer.containsKey(channel.getJOscarPeer())) {
353 			channelsByPeer.put(channel.getJOscarPeer(), channel);
354 			ChatManager.getInstance().add(channel);
355 		}
356 	}
357 
358 	public void removeChannel(JOscarChannel channel)
359 	{
360 		logger.debug("channel remove called for " 
361 					 + channel.getJOscarPeer().getName());
362 		channelsByPeer.remove(channel.getJOscarPeer());
363 	}
364 
365 	private void readBuddiesList()
366 	{
367 		LinkedList list = new LinkedList();
368 		
369 		try {
370 			FileHelper.readBinary(new File(BUDDY_FILE), list);
371 		}
372 		catch (IOException ie) {
373 			logger.debug("error reading buddies file", ie);
374 		}
375 
376 		for (Iterator i = list.iterator(); i.hasNext();) {
377 			addBuddy((String)i.next());
378 		}
379 	}
380 
381 	private void writeBuddiesList()
382 	{
383 		try {
384 			FileHelper.writeBinary(new File(BUDDY_FILE), buddies.keySet());
385 		}
386 		catch (IOException ie) {
387 			logger.debug("error writing buddies file", ie);
388 		}
389 	}
390 
391 	/***
392 	 * Implements the {@link OscarListener} interface.
393 	 */
394 	public void onIncomingMessage(IncomingMessageEvent event)
395 	{
396 		JOscarPeer peer = addBuddy(event.getSenderID());
397 		if (!channelsByPeer.containsKey(peer)) {
398 			JOscarChannel channel = new JOscarChannel(peer);
399 			addChannel(channel);
400 			// show the first message, even if it's shown twice
401 			channel.messageReceived(peer, event.getMessage());
402 		}
403 	}
404 	
405 	/***
406 	 * Implements the {@link OscarListener} interface.
407 	 */
408 	public void onIncomingUrl(IncomingUrlEvent e)
409 	{
410 		
411 	}
412             
413 	/***
414 	 * Implements the {@link OscarListener} interface.
415 	 */
416 	public void onIncomingUser(IncomingUserEvent e) 
417 	{
418 		JOscarPeer peer = (JOscarPeer)buddies.get(e.getIncomingUserId());
419 		if (peer != null) {
420 			logger.debug("buddy " + peer.getName() 
421 						 + " seems to have gone online");
422 			peer.setStatus(e.getStatusMode().toString());
423 		}
424 	}
425             
426 	/***
427 	 * Implements the {@link OscarListener} interface.
428 	 */
429 	public void onOffgoingUser(OffgoingUserEvent e) 
430 	{
431 		JOscarPeer peer = (JOscarPeer)buddies.get(e.getOffgoingUserId());
432 		logger.debug("buddy " + peer.getName() + " seems to have gone offline");
433 		if (peer != null) {
434 			peer.setStatus(XNap.tr("Offline"));
435 		}
436 	}
437             
438 	/***
439 	 * Implements the {@link OscarListener} interface.
440 	 */
441 	public void onOfflineMessage(OfflineMessageEvent e) 
442 	{
443 		logger.debug("buddy " + e.getSenderUin() + " is going offline, " +
444 					 e.getMessage());
445 	}
446 
447 	// --- Inner Class(es) ---
448 
449 	private class AddBuddyAction extends AbstractAction
450 	{
451         public AddBuddyAction() 
452 		{
453             putValue(Action.NAME, XNap.tr("Add Buddy"));
454             putValue(Action.SHORT_DESCRIPTION,
455 					 XNap.tr("Asks for a UIN and adds it to the Hotlist."));
456 			putValue(IconHelper.XNAP_ICON, "edit_add.png");
457         }
458 
459         public void actionPerformed(ActionEvent event) 
460 		{
461 			Object[] message = new Object[] {
462 				XNap.tr("Buddy UIN")
463 			};
464 			
465 			String uin = JOptionPane.showInputDialog
466 				(XNapFrame.getInstance().getRootPane(), message,
467 				 XNap.tr("Add Buddy"), JOptionPane.PLAIN_MESSAGE);
468 				
469 			if (uin != null) {
470 				JOscarPlugin.getInstance().addBuddy(uin.trim());
471 			}
472         }
473 	}
474 
475 	private class ConnectAction extends AbstractXNapAction
476 		implements StateListener
477 	{
478 		public ConnectAction()
479 		{
480 			putValue(Action.NAME, XNap.tr("Connect"));
481 			putValue(IconHelper.XNAP_ICON, "connect_creating.png");
482 			connection.addStateListener(this);
483 			stateChanged(null);
484 		}
485 
486 		public void actionPerformed(ActionEvent event)
487 		{
488 			connection.connect();
489 		}
490 
491 		public void stateChanged(StateEvent e)
492 		{
493 			setEnabledLater(connection.getState() == State.DISCONNECTED);
494 		}
495 	}
496 
497 	private class DisconnectAction extends AbstractXNapAction
498 		implements StateListener
499 	{
500 		public DisconnectAction()
501 		{
502 			putValue(Action.NAME, XNap.tr("Disconnect"));
503 			putValue(IconHelper.XNAP_ICON, "connect_no.png");
504 			connection.addStateListener(this);
505 			stateChanged(null);
506 		}
507 
508 		public void actionPerformed(ActionEvent event)
509 		{
510 			connection.disconnect();
511 		}
512 
513 		public void stateChanged(StateEvent e)
514 		{
515 			setEnabledLater(connection.getState() == State.CONNECTED);
516 		}
517 	}
518 
519 	private class PreferencesDialogListener 
520 		extends AbstractPreferencesDialogListener
521 	{
522 		public void addPanels(AbstractPreferencesDialog dialog, List panels)
523 		{
524 			JOscarPreferencesPanel jpp = new JOscarPreferencesPanel();
525 			panels.add(jpp);
526 			panels.add(dialog.addPanel(jpp, ICON_FILENAME));
527 		}
528 	}
529 }