package org.limewire.xmpp.client.impl; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import net.jcip.annotations.GuardedBy; import org.jivesoftware.smack.Chat; import org.jivesoftware.smack.ChatManagerListener; import org.jivesoftware.smack.RosterEntry; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smackx.ChatStateListener; import org.jivesoftware.smackx.ChatStateManager; import org.limewire.concurrent.ThreadExecutor; import org.limewire.friend.api.ChatState; import org.limewire.friend.api.FriendException; import org.limewire.friend.api.FriendPresence; import org.limewire.friend.api.IncomingChatListener; import org.limewire.friend.api.MessageReader; import org.limewire.friend.api.MessageWriter; import org.limewire.friend.api.Network; import org.limewire.friend.api.PresenceEvent; import org.limewire.friend.api.feature.Feature; import org.limewire.friend.api.feature.FeatureRegistry; import org.limewire.friend.impl.AbstractFriend; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.util.DebugRunnable; import org.limewire.util.StringUtils; public class XMPPFriendImpl extends AbstractFriend { private static final Log LOG = LogFactory.getLog(XMPPFriendImpl.class); private final String id; private final FeatureRegistry featureRegistry; private final String idNoService; private AtomicReference<RosterEntry> rosterEntry; private final org.jivesoftware.smack.XMPPConnection connection; private final Network network; // ----------------------------------------------------------------- // presences map (presences of this user who are signed in) // and the active presence JID (the presence lw is chatting with) // represent the Presence State private final Object presenceLock; @GuardedBy("presenceLock") private final Map<String, FriendPresence> presences; @GuardedBy("presenceLock") private String activePresenceJid; // ----------------------------------------------------------------- // ----------------------------------------------------------------- private final Object chatListenerLock; @GuardedBy("chatListenerLock") private volatile IncomingChatListenerAdapter listenerAdapter; // ----------------------------------------------------------------- XMPPFriendImpl(String id, RosterEntry rosterEntry, Network network, org.jivesoftware.smack.XMPPConnection connection, FeatureRegistry featureRegistry) { this.id = id; this.featureRegistry = featureRegistry; this.idNoService = stripService(id, network.getNetworkName()); this.network = network; this.rosterEntry = new AtomicReference<RosterEntry>(rosterEntry); this.presences = new HashMap<String, FriendPresence>(); this.activePresenceJid = null; this.connection = connection; this.presenceLock = new Object(); this.chatListenerLock = new Object(); } private static String stripService(String id, String service) { int idx = id.lastIndexOf("@" + service); if(idx == -1) { return id; } else { return id.substring(0, idx); } } @Override public boolean isAnonymous() { return false; } @Override public String getId() { return id; } @Override public String getName() { String name = rosterEntry.get().getName(); if(name != null) { String service = network.getNetworkName(); int idx = name.lastIndexOf("@" + service); if(idx == -1) { return name; } else { return name.substring(0, idx); } } else { return null; } } @Override public String getRenderName() { String visualName = getName(); if(visualName == null) { return idNoService; } else { return visualName; } } @Override public String getFirstName() { String visualName = getName(); if(visualName == null) { return idNoService; } else { String[] subStrings = visualName.split(" "); return subStrings[0]; } } void setRosterEntry(RosterEntry rosterEntry) { this.rosterEntry.set(rosterEntry); } @Override public void setName(final String name) { Thread t = ThreadExecutor.newManagedThread(new DebugRunnable(new Runnable() { public void run() { try { XMPPFriendImpl.this.rosterEntry.get().setName(name); } catch (org.jivesoftware.smack.XMPPException e) { LOG.debugf("set name failed", e); } } }), "set-name-thread-" + toString()); t.start(); } @Override public Map<String, FriendPresence> getPresences() { synchronized (presenceLock) { return Collections.unmodifiableMap(new HashMap<String, FriendPresence>(presences)); } } @Override public boolean isSubscribed() { switch(rosterEntry.get().getType()) { case both: case to: return true; default: return false; } } void addPresense(FriendPresence presence) { if(LOG.isDebugEnabled()) { LOG.debugf("adding presence {0}", presence.getPresenceId()); } synchronized (presenceLock) { presences.put(presence.getPresenceId(), presence); } firePresenceEvent(new PresenceEvent(presence, PresenceEvent.Type.PRESENCE_NEW)); } void removePresense(FriendPresence presence) { if(LOG.isDebugEnabled()) { LOG.debugf("removing presence {0}", presence.getPresenceId()); } Collection<Feature> features = presence.getFeatures(); for(Feature feature : features) { LOG.debugf("removing feature {0} for {1}", feature, presence); featureRegistry.get(feature.getID()).removeFeature(presence); } synchronized (presenceLock) { presences.remove(presence.getPresenceId()); // if the presence being removed is the same presence as the active presence, set the // active presence to null so the next outgoing message goes to all of this user's presences if (presence.getPresenceId().equals(activePresenceJid)) { activePresenceJid = null; } if (!isSignedIn()) { removeChatListener(); } } firePresenceEvent(new PresenceEvent(presence, PresenceEvent.Type.PRESENCE_UPDATE)); } @Override public String toString() { return StringUtils.toString(this, id, getName()); } void updatePresence(FriendPresence updatedPresence) { if(LOG.isDebugEnabled()) { LOG.debugf("updating presence {0}", updatedPresence.getPresenceId()); } synchronized (presenceLock) { presences.put(updatedPresence.getPresenceId(), updatedPresence); } firePresenceEvent(new PresenceEvent(updatedPresence, PresenceEvent.Type.PRESENCE_UPDATE)); } FriendPresence getPresence(String jid) { synchronized (presenceLock) { return presences.get(jid); } } @Override public Network getNetwork() { return network; } private String getChatParticipantId() { synchronized (presenceLock) { return (activePresenceJid == null) ? id : activePresenceJid; } } private void setActivePresence(String presenceId) { synchronized (presenceLock) { activePresenceJid = presenceId; } } @Override public FriendPresence getActivePresence() { synchronized (presenceLock) { return presences.get(activePresenceJid); } } @Override public boolean hasActivePresence() { synchronized (presenceLock) { return (activePresenceJid != null); } } @Override public boolean isSignedIn() { synchronized (presenceLock) { return !(presences.isEmpty()); } } @Override public MessageWriter createChat(final MessageReader reader) { if(LOG.isInfoEnabled()) { LOG.info("new chat with " + getChatParticipantId()); } final Chat chat = connection.getChatManager().createChat(getChatParticipantId(), new DefaultChatStateListener(reader)); return new DefaultMessageWriter(chat); } @Override public void setChatListenerIfNecessary(final IncomingChatListener listener) { synchronized (chatListenerLock) { if (listenerAdapter == null) { listenerAdapter = new IncomingChatListenerAdapter(listener); connection.getChatManager().addChatListener(listenerAdapter); } } } @Override public void removeChatListener() { synchronized (chatListenerLock) { if(listenerAdapter != null) { connection.getChatManager().removeChatListener(listenerAdapter); listenerAdapter = null; } } } private class IncomingChatListenerAdapter implements ChatManagerListener { private final IncomingChatListener listener; public IncomingChatListenerAdapter(IncomingChatListener listener) { this.listener = listener; } @Override public void chatCreated(final Chat chat, boolean createdLocally) { String chatParticipant = chat.getParticipant(); if (!createdLocally && isForThisUser(chatParticipant)) { if (!chatParticipant.equals(getChatParticipantId())) { setActivePresence(chatParticipant); } if (LOG.isInfoEnabled()) { LOG.info("new incoming chat with " + getChatParticipantId()); } DefaultMessageWriter writer = new DefaultMessageWriter(chat); MessageReader reader = listener.incomingChat(writer); chat.addMessageListener(new DefaultChatStateListener(reader)); } } private boolean isForThisUser(String incomingMsgJid) { return incomingMsgJid.startsWith(id); } } /** * This class encapsulates the actual writing of messages in the smack API. * * The message writer must take into account which presence is the current * active presence and set the chat participant appropriately * (set to jid identifying the presence, or the user id if no active presence) * */ private class DefaultMessageWriter implements MessageWriter { private Chat chat; DefaultMessageWriter(Chat chat) { this.chat = chat; } @Override public void writeMessage(String message) throws FriendException { try { chat.setParticipant(getChatParticipantId()); chat.sendMessage(message); } catch (org.jivesoftware.smack.XMPPException e) { throw new FriendException(e); } } @Override public void setChatState(ChatState chatState) throws FriendException { try { ChatStateManager.getInstance(connection).setCurrentState(org.jivesoftware.smackx.ChatState.valueOf(chatState.toString()), chat); } catch (org.jivesoftware.smack.XMPPException e) { throw new FriendException(e); } } } /** * Acts as an adapter between the smack message/chat state listener and our * {@link MessageReader} class. * * Note: If a message is received which comes from a different presence * than the presence LW is currently chatting with, processMessage will * set the active presence to the presence from which it received the message. * */ private class DefaultChatStateListener implements ChatStateListener { private final MessageReader reader; DefaultChatStateListener(MessageReader reader) { this.reader = reader; } @Override public void processMessage(Chat chat, Message message) { String msgFromJid = message.getFrom(); if (!(getChatParticipantId().equals(msgFromJid))) { setActivePresence(msgFromJid); } if (message.getType() == Message.Type.error) { String errorMsg = parseError(message); if (errorMsg != null) { reader.error(parseError(message)); } } else { reader.readMessage(message.getBody()); } } @Override public void stateChanged(Chat chat, org.jivesoftware.smackx.ChatState state) { if (isSignedIn()) { reader.newChatState(ChatState.valueOf(state.toString())); } } private String parseError(Message errorMessage) { XMPPError error = errorMessage.getError(); if (error != null) { String body = errorMessage.getBody(); if (body != null) { return "Error sending message: '" + body + "' : " + error.toString(); } } return null; } } }