blob: b002ffca97e26ba83d8e5fb2393820ec9270ae5d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2008 Remy Suen
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Remy Suen <remy.suen@gmail.com> - initial API and implementation
* Stoyan Boshev <s.boshev@prosyst.com> - [MSN] Session and subclasses needs to handle whitespace and exceptions better
******************************************************************************/
package org.eclipse.ecf.protocol.msn;
import java.io.IOException;
import java.net.ConnectException;
import java.util.ArrayList;
import org.eclipse.ecf.protocol.msn.events.IChatSessionListener;
import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils;
/**
* <p>
* A ChatSession is a conversation that's held between two or more participants.
* </p>
*
* <p>
* As specified by {@link MsnClient}'s
* {@link MsnClient#disconnect() disconnect()} method, ChatSessions are not
* automatically disconnected when the client itself disconnects. However,
* clean-up will be performed automatically when a
* {@link IChatSessionListener#sessionTimedOut() sessionTimedOut()} event occurs
* or when the last user has left (as checked by monitoring the
* {@link IChatSessionListener#contactLeft(Contact) contactLeft(Contact)}
* event).
* </p>
*
* <p>
* <b>Note:</b> This class/interface is part of an interim API that is still
* under development and expected to change significantly before reaching
* stability. It is being made available at this early stage to solicit feedback
* from pioneering adopters on the understanding that any code that uses this
* API will almost certainly be broken (repeatedly) as the API evolves.
* </p>
*/
public final class ChatSession extends Session {
/**
* The list of contacts that are currently a part of this session. Note that
* this does not include the client user.
*/
private final ArrayList contacts;
private final ContactList contactList;
private final String email = client.getUserEmail();
/**
* Create a new ChatSession that connects to the given host.
*
* @param host
* the host to be connected to
* @param client
* the MsnClient to hook onto
* @throws IOException
* If an I/O error occurs while attempting to connect to the
* host
*/
ChatSession(String host, MsnClient client) throws IOException {
super(host, client);
listeners = new ArrayList();
contacts = new ArrayList();
contactList = client.getContactList();
}
/**
* Create a new ChatSession that will connect to the given server response
* using the specified username.
*
* @param host
* the host to be connected to
* @param client
* the MsnClient to hook onto
* @param username
* the username to authenticate with
* @param info
* the authentication info
* @throws IOException
* If an I/O error occurs while attempting to connect to the
* specified host
*/
ChatSession(String host, MsnClient client, String username, String info) throws IOException {
this(host, client);
authenticate(username, info);
}
/**
* This method attempts to authenticate the user with the switchboard server
* that it was instantiated to.
*
* @param username
* the user's MSN email address
* @param info
* the authentication information
* @throws IOException
*/
private void authenticate(String username, String info) throws IOException {
write("USR", username + ' ' + info); //$NON-NLS-1$
final String input = super.read();
// FIXME: check if this indexOf(String) call can be replaced with
// startsWith(String)
if (input == null || input.indexOf("USR") == -1) { //$NON-NLS-1$
throw new ConnectException("Authentication has failed with the switchboard server."); //$NON-NLS-1$
}
idle();
}
public void close() {
try {
write("OUT"); //$NON-NLS-1$
} catch (final Exception e) {
// ignored
}
super.close();
}
/**
* Invites the user with the specified email to this chat session.
*
* @param userEmail
* the user's email address
* @throws IOException
* If an I/O error occurs while attempting to send the
* invitation to the user
*/
public void invite(String userEmail) throws IOException {
synchronized (contacts) {
for (int i = 0; i < contacts.size(); i++) {
if (((Contact) contacts.get(i)).getEmail().equals(userEmail)) {
return;
}
}
}
write("CAL", userEmail); //$NON-NLS-1$
}
/**
* Sends a notifying event to the listeners connected to this that the
* specified contact has joined this switchboard.
*
* @param contact
* the contact that has joined this session
*/
private void fireContactJoinedEvent(Contact contact) {
synchronized (listeners) {
for (int i = 0; i < listeners.size(); i++) {
((IChatSessionListener) listeners.get(i)).contactJoined(contact);
}
}
}
/**
* Informs the {@link IChatSessionListener}s attached to this that the
* given contact has left this session.
*
* @param contact
* the contact that left this session
*/
private void fireContactLeftEvent(Contact contact) {
synchronized (listeners) {
for (int i = 0; i < listeners.size(); i++) {
((IChatSessionListener) listeners.get(i)).contactLeft(contact);
}
}
}
/**
* This event is fired when the specified contact has started typing.
*
* @param contact
* the contact who is typing
*/
private void fireContactIsTypingEvent(Contact contact) {
synchronized (listeners) {
for (int i = 0; i < listeners.size(); i++) {
((IChatSessionListener) listeners.get(i)).contactIsTyping(contact);
}
}
}
/**
* This event is fired when a message has been received from the given user.
*
* @param contact
* the user that sent the message
* @param message
* the message that has been received
*/
private void fireMessageReceivedEvent(Contact contact, String message) {
synchronized (listeners) {
for (int i = 0; i < listeners.size(); i++) {
((IChatSessionListener) listeners.get(i)).messageReceived(contact, message);
}
}
}
/**
* Notifies attached {@link IChatSessionListener}s that this session has now
* timed out.
*/
private void fireSessionTimedOutEvent() {
synchronized (listeners) {
for (int i = 0; i < listeners.size(); i++) {
((IChatSessionListener) listeners.get(i)).sessionTimedOut();
}
}
}
/**
* Look for a contact that is connected to this switchboard connected to the
* given email. Comparison is done with the String class's equals(String)
* method, so case sensitivity is an issue.
*
* @param userEmail
* the email of the contact being sought after
* @return the contact that uses the specified email
* @throws IllegalArgumentException
* If the contact could not be found
*/
private Contact findContact(String userEmail) throws IllegalArgumentException {
for (int i = 0; i < contacts.size(); i++) {
final Contact contact = (Contact) contacts.get(i);
if (contact.getEmail().equals(userEmail)) {
return contact;
}
}
throw new IllegalArgumentException("A contact with the email " + userEmail + " could not be found in this ChatSession."); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Read the contents of the packet being sent from the server and handle any
* events accordingly.
*
* @return the String returned from {@link Session#read()}
*/
String read() throws IOException {
final String input = super.read();
if (input == null) {
return null;
}
final String[] lines = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
for (int i = 0; i < lines.length; i++) {
if (lines[i].startsWith("IRO")) { //$NON-NLS-1$
final String[] split = StringUtils.splitOnSpace(lines[i]);
Contact contact = contactList.getContact(split[4]);
if (contact == null) {
contact = new Contact(split[4], split[5]);
}
contacts.add(contact);
fireContactJoinedEvent(contact);
} else if (lines[i].startsWith("JOI")) { //$NON-NLS-1$
final String[] split = StringUtils.splitOnSpace(lines[i]);
Contact contact = contactList.getContact(split[2]);
if (contact == null) {
contact = new Contact(split[1], split[2]);
}
contacts.add(contact);
fireContactJoinedEvent(contact);
} else if (lines[i].startsWith("BYE")) { //$NON-NLS-1$
final String[] split = StringUtils.splitOnSpace(lines[i]);
if (split.length == 2) {
final Contact contact = findContact(split[1]);
contacts.remove(contact);
fireContactLeftEvent(contact);
if (contacts.isEmpty()) {
close();
}
} else {
fireSessionTimedOutEvent();
close();
}
} else if (lines[i].startsWith("MSG")) { //$NON-NLS-1$
if (input.indexOf("TypingUser:") != -1) { //$NON-NLS-1$
final String trim = input.substring(input.indexOf("MSG")); //$NON-NLS-1$
final String content = StringUtils.splitSubstring(trim, "\r\n", 3); //$NON-NLS-1$
fireContactIsTypingEvent(findContact(StringUtils.splitOnSpace(content)[1]));
} else if (input.indexOf("text/plain") != -1) { //$NON-NLS-1$
final int index = input.indexOf("ANS") == -1 ? 2 : 3; //$NON-NLS-1$
final String[] contents = StringUtils.split(input, "\r\n", index); //$NON-NLS-1$
String[] split = StringUtils.splitOnSpace(contents[index - 2]);
final Contact contact = findContact(split[1]);
final int count = Integer.parseInt(split[3]);
split = StringUtils.split(contents[index - 1], "\r\n\r\n", 2); //$NON-NLS-1$
final int text = count - (split[0].getBytes("UTF-8").length + 4); //$NON-NLS-1$
fireMessageReceivedEvent(contact, new String(split[1].getBytes("UTF-8"), 0, text, "UTF-8")); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
return input;
}
/**
* <p>
* <b>Note:</b> This method will likely be modified in the future (renamed,
* change in return type, <tt>Contact[]</tt> <-> <tt>java.util.List</tt>,
* inclusion/exclusion of the current user, complete removal, etc.). Please
* use it at your own risk.
* </p>
*
* <p>
* This method returns the Contacts that are currently participating in this
* ChatSession. Note that this does not include the current user.
* </p>
* @return array of contacts that are the participants.
*/
public Contact[] getParticipants() {
return (Contact[]) contacts.toArray(new Contact[contacts.size()]);
}
/**
* Sends a message to the users connected to this chat session.
*
* @param message
* the message to be sent
* @throws IOException
* If an I/O occurs when sending the message to the server
*/
public void sendMessage(String message) throws IOException {
message = "MIME-Version: 1.0\r\n" //$NON-NLS-1$
+ "Content-Type: text/plain; charset=UTF-8\r\n" //$NON-NLS-1$
+ "X-MMS-IM-Format: FN=MS%20Sans%20Serif; EF=; CO=0; PF=0\r\n\r\n" //$NON-NLS-1$
+ message;
write("MSG", "N " + message.getBytes("UTF-8").length + "\r\n" + message, false); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
}
/**
* Notifies the participants of this chat session that the current user is
* typing a message.
*
* @throws IOException
* If an I/O occurs when sending the message to the server
*/
public void sendTypingNotification() throws IOException {
final String message = "MIME-Version: 1.0\r\n" //$NON-NLS-1$
+ "Content-Type: text/x-msmsgscontrol\r\nTypingUser: " + email //$NON-NLS-1$
+ "\r\n\r\n\r\n"; //$NON-NLS-1$
write("MSG", "U " + message.length() + "\r\n" + message, false); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
/**
* Adds a IChatSessionListener to this session.
*
* @param listener
* the listener to be added
*/
public void addChatSessionListener(IChatSessionListener listener) {
if (listener != null) {
synchronized (listeners) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
}
}
}
/**
* Removes a IChatSessionListener from this session.
*
* @param listener
* the listener to be removed
*/
public void removeChatSessionListener(IChatSessionListener listener) {
if (listener != null) {
synchronized (listeners) {
listeners.remove(listener);
}
}
}
}