Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrsuen2007-04-19 02:05:20 +0000
committerrsuen2007-04-19 02:05:20 +0000
commit6430110271c1e26a54f61f560a0e5df426c091c3 (patch)
tree4eb7b8401070ab377a2280bc616d06c4dffe93c1
parent24d403c53095e4f3774c1082d168b845b7f53af8 (diff)
downloadorg.eclipse.ecf-6430110271c1e26a54f61f560a0e5df426c091c3.tar.gz
org.eclipse.ecf-6430110271c1e26a54f61f560a0e5df426c091c3.tar.xz
org.eclipse.ecf-6430110271c1e26a54f61f560a0e5df426c091c3.zip
Initial commit.
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/.classpath6
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/.cvsignore1
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/.project28
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/.settings/org.eclipse.jdt.core.prefs12
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/META-INF/MANIFEST.MF14
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/about.html29
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/build.properties17
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/plugin.properties13
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ChatSession.java396
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Contact.java311
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ContactList.java284
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/DispatchSession.java110
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Group.java84
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/MsnClient.java339
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/NotificationSession.java499
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Session.java299
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Status.java78
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IChatSessionListener.java73
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListListener.java75
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListener.java55
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/ISessionListener.java41
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/package.html5
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Base64.java289
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Challenge.java198
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Encryption.java65
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/ResponseCommand.java84
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/StringUtils.java224
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/package.html5
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/ClientTicketRequest.java135
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/package.html5
-rw-r--r--protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/package.html5
31 files changed, 3779 insertions, 0 deletions
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/.classpath b/protocols/bundles/org.eclipse.ecf.protocol.msn/.classpath
new file mode 100644
index 000000000..199fd6385
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/.classpath
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry path="src" kind="src"/>
+ <classpathentry path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/CDC-1.0%Foundation-1.0" kind="con"/>
+ <classpathentry path="bin" kind="output"/>
+</classpath>
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/.cvsignore b/protocols/bundles/org.eclipse.ecf.protocol.msn/.cvsignore
new file mode 100644
index 000000000..ba077a403
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/.cvsignore
@@ -0,0 +1 @@
+bin
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/.project b/protocols/bundles/org.eclipse.ecf.protocol.msn/.project
new file mode 100644
index 000000000..418534756
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/.project
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.eclipse.ecf.protocol.msn</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ </natures>
+</projectDescription>
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/.settings/org.eclipse.jdt.core.prefs b/protocols/bundles/org.eclipse.ecf.protocol.msn/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 000000000..d18917230
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,12 @@
+#Thu Jan 11 22:04:55 GMT 2007
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.1
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.3
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=warning
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=warning
+org.eclipse.jdt.core.compiler.source=1.3
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/META-INF/MANIFEST.MF b/protocols/bundles/org.eclipse.ecf.protocol.msn/META-INF/MANIFEST.MF
new file mode 100644
index 000000000..a150005b8
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/META-INF/MANIFEST.MF
@@ -0,0 +1,14 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: %pluginName
+Bundle-SymbolicName: org.eclipse.ecf.protocol.msn
+Bundle-Version: 0.3.1.qualifier
+Bundle-Localization: plugin
+Bundle-RequiredExecutionEnvironment: CDC-1.0/Foundation-1.0,
+ J2SE-1.3
+Export-Package: org.eclipse.ecf.protocol.msn,
+ org.eclipse.ecf.protocol.msn.events,
+ org.eclipse.ecf.protocol.msn.internal.encode;x-internal:=true,
+ org.eclipse.ecf.protocol.msn.internal.net;x-internal:=true
+Bundle-Vendor: %providerName
+Eclipse-LazyStart: false
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/about.html b/protocols/bundles/org.eclipse.ecf.protocol.msn/about.html
new file mode 100644
index 000000000..8f7767892
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/about.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"/>
+<title>About</title>
+</head>
+<body lang="EN-US">
+<h2>About This Content</h2>
+
+<p>June 2, 2006</p>
+<h3>License</h3>
+
+<p>The Eclipse Foundation makes available all content in this plug-in (&quot;Content&quot;). Unless otherwise
+indicated below, the Content is provided to you under the terms and conditions of the
+Eclipse Public License Version 1.0 (&quot;EPL&quot;). A copy of the EPL is available
+at <a href="http://www.eclipse.org/legal/epl-v10.html">http://www.eclipse.org/legal/epl-v10.html</a>.
+For purposes of the EPL, &quot;Program&quot; will mean the Content.</p>
+
+<p>If you did not receive this Content directly from the Eclipse Foundation, the Content is
+being redistributed by another party (&quot;Redistributor&quot;) and different terms and conditions may
+apply to your use of any object code in the Content. Check the Redistributor's license that was
+provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise
+indicated below, the terms and conditions of the EPL still apply to any source code in the Content
+and such source code may be obtained at <a href="http://www.eclipse.org">http://www.eclipse.org</a>.</p>
+
+</body>
+</html>
+
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/build.properties b/protocols/bundles/org.eclipse.ecf.protocol.msn/build.properties
new file mode 100644
index 000000000..d97bbb1cd
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/build.properties
@@ -0,0 +1,17 @@
+################################################################################
+# Copyright (c) 2007 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
+################################################################################
+source.. = src/
+output.. = bin/
+bin.includes = META-INF/,\
+ .,\
+ plugin.properties,\
+ about.html
+src.includes = about.html
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/plugin.properties b/protocols/bundles/org.eclipse.ecf.protocol.msn/plugin.properties
new file mode 100644
index 000000000..f7b7264f4
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/plugin.properties
@@ -0,0 +1,13 @@
+################################################################################
+# Copyright (c) 2005, 2007 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
+################################################################################
+
+pluginName = MSN Protocol Implementation
+providerName = Eclipse.org
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ChatSession.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ChatSession.java
new file mode 100644
index 000000000..b60c25bd0
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ChatSession.java
@@ -0,0 +1,396 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+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();
+
+ private boolean joined = false;
+
+ /**
+ * 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$
+ 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.");
+ }
+ idle();
+ }
+
+ public void close() {
+ try {
+ write("OUT"); //$NON-NLS-1$
+ } catch (Exception e) {
+ // ignored
+ }
+ super.close();
+ }
+
+ /**
+ * Invites the user with the specified email to this chat session.
+ *
+ * @param email
+ * 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 email) throws IOException {
+ synchronized (contacts) {
+ for (int i = 0; i < contacts.size(); i++) {
+ if (((Contact) contacts.get(i)).getEmail().equals(email)) {
+ return;
+ }
+ }
+ }
+ write("CAL", email); //$NON-NLS-1$
+ while (!joined)
+ ;
+ joined = false;
+ }
+
+ /**
+ * 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 ChatSessionListeners} 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 email
+ * 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 email) throws IllegalArgumentException {
+ for (int i = 0; i < contacts.size(); i++) {
+ Contact contact = (Contact) contacts.get(i);
+ if (contact.getEmail().equals(email)) {
+ return contact;
+ }
+ }
+ throw new IllegalArgumentException("A contact with the email " + email
+ + " could not be found in this ChatSession.");
+ }
+
+ /**
+ * 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 {
+ String input = super.read();
+ if (input == null) {
+ return null;
+ }
+
+ 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$
+ 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$
+ 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);
+ joined = true;
+ fireContactJoinedEvent(contact);
+ } else if (lines[i].startsWith("BYE")) { //$NON-NLS-1$
+ String[] split = StringUtils.splitOnSpace(lines[i]);
+ if (split.length == 2) {
+ 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$
+ String trim = input.substring(input.indexOf("MSG")); //$NON-NLS-1$
+ 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$
+ int index = input.indexOf("ANS") == -1 ? 2 : 3; //$NON-NLS-1$
+ String[] contents = StringUtils.split(input, "\r\n", index); //$NON-NLS-1$
+ String[] split = StringUtils
+ .splitOnSpace(contents[index - 2]);
+ Contact contact = findContact(split[1]);
+
+ int count = Integer.parseInt(split[3]);
+ split = StringUtils.split(contents[index - 1], "\r\n\r\n"); //$NON-NLS-1$
+
+ int text = count - (split[0].length() + 4);
+ fireMessageReceivedEvent(contact, split[1].substring(0,
+ text));
+ }
+ }
+ }
+
+ 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>
+ */
+ 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.length() + "\r\n" + message, false); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ /**
+ * 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 {
+ 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);
+ }
+ }
+ }
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Contact.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Contact.java
new file mode 100644
index 000000000..6403129f6
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Contact.java
@@ -0,0 +1,311 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn;
+
+import java.net.URLDecoder;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.eclipse.ecf.protocol.msn.events.IContactListener;
+import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils;
+
+/**
+ * <p>
+ * This class represents a contact that a user has on his or her MSN list.
+ * </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 Contact {
+
+ /**
+ * The list of listeners that is attached to this.
+ */
+ private final ArrayList listeners;
+
+ /**
+ * The list of groups that this contact is in.
+ */
+ private final ArrayList groups;
+
+ /**
+ * The email address that is associated with this contact.
+ */
+ private final String email;
+
+ /**
+ * The guid of this contact.
+ */
+ private final String guid;
+
+ /**
+ * The displayed name of this contact, this is typically different from
+ * their {@link #email}.
+ */
+ private String name;
+
+ /**
+ * The personal message that the contact is currently displaying.
+ */
+ private String personalMessage;
+
+ /**
+ * The current status of this user.
+ *
+ * @see Status#ONLINE Status.ONLINE and others
+ */
+ private Status status;
+
+ /**
+ * Creates a new Contact with the given name and email address.
+ * @param email
+ * the user's email address
+ * @param name
+ * the contact's MSN nickname in raw format, the name will be URL
+ * decoded accordingly
+ */
+ Contact(String email, String name) {
+ this(email, name, null);
+ }
+
+ Contact(String email, String name, String guid) {
+ this.name = URLDecoder.decode(name);
+ this.email = email;
+ this.guid = guid;
+ this.status = Status.OFFLINE;
+ listeners = new ArrayList();
+ groups = new ArrayList();
+ }
+
+ /**
+ * Invokes the {@link IContactListener#statusChanged(Status)} method on
+ * every listener within {@link #listeners}. This method is automatically
+ * invoked when {@link #setStatus(int)} is called.
+ *
+ * @param status
+ * the status that this contact has now switched to
+ */
+ private void fireStatusChanged(Status status) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((IContactListener) listeners.get(i)).statusChanged(status);
+ }
+ }
+ }
+
+ /**
+ * Invokes the {@link IContactListener#nameChanged(Status)} method on every
+ * listener within {@link #listeners}. This method is automatically invoked
+ * when {@link #setDisplayName(String)} is called.
+ *
+ * @param name
+ * the new name of this contact
+ */
+ private void fireNameChanged(String name) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((IContactListener) listeners.get(i)).nameChanged(name);
+ }
+ }
+ }
+
+ private void firePersonalMessageChanged(String message) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((IContactListener) listeners.get(i))
+ .personalMessageChanged(message);
+ }
+ }
+ }
+
+ void add(Group group) {
+ groups.add(group);
+ }
+
+ void remove() {
+ for (int i = 0; i < groups.size(); i++) {
+ ((Group) groups.get(i)).remove(this);
+ }
+ groups.clear();
+ }
+
+ /**
+ * Retrieves the groups that this contact is a part of.
+ *
+ * @return a collection of the groups that this contact is a member of
+ */
+ public Collection getGroups() {
+ return Collections.unmodifiableCollection(groups);
+ }
+
+ /**
+ * Sets this contact's status to the given status. Developers are highly
+ * discouraged from calling this method since if the user's status actually
+ * did change, the {@link IContactListener#nameChanged(Status)} method will
+ * be invoked on all the listeners attached to this contact.
+ *
+ * @param status
+ * the status that this contact is now in
+ */
+ void setStatus(Status status) {
+ if (this.status != status) {
+ this.status = status;
+ fireStatusChanged(status);
+ }
+ }
+
+ /**
+ * Retrieves the current status of this contact.
+ *
+ * @return the status that this contact currently is in
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ /**
+ * Sets the user name of this contact with the given name. Developers are
+ * highly discouraged from calling this method since if the value of
+ * <code>newName</code> differs from the current name, the
+ * {@link IContactListener#nameChanged(Status)} method will be invoked on
+ * all the listeners attached to this contact.
+ *
+ * @param newName
+ * the new user name of this Contact
+ */
+ void setDisplayName(String newName) {
+ newName = URLDecoder.decode(newName);
+ if (!newName.equals(name)) {
+ this.name = newName;
+ fireNameChanged(newName);
+ }
+ }
+
+ /**
+ * Gets the displayed name of this contact.
+ *
+ * @return the name that this contact uses
+ */
+ public String getDisplayName() {
+ return name;
+ }
+
+ /**
+ * Changes the contact's personal message to the provided message if the two
+ * messages differ.
+ *
+ * @param message
+ * the message the contact may have set to
+ */
+ void setPersonalMessage(String message) {
+ message = StringUtils.xmlDecode(message);
+ if (!message.equals(personalMessage)) {
+ personalMessage = message;
+ firePersonalMessageChanged(message);
+ }
+ }
+
+ /**
+ * Returns the personal message that this contact is currently using.
+ *
+ * @return the personal message in use
+ */
+ public String getPersonalMessage() {
+ return personalMessage;
+ }
+
+ /**
+ * Returns the email address of the user.
+ *
+ * @return the user's email address
+ */
+ public String getEmail() {
+ return email;
+ }
+
+ String getGuid() {
+ return guid;
+ }
+
+ /**
+ * Adds a IContactListener to this contact.
+ *
+ * @param listener
+ * the listener to be added
+ */
+ public void addContactListener(IContactListener listener) {
+ if (listener != null) {
+ synchronized (listeners) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a IContactListener from this contact.
+ *
+ * @param listener
+ * the listener to be removed
+ */
+ public void removeContactListener(IContactListener listener) {
+ if (listener != null) {
+ synchronized (listeners) {
+ listeners.remove(listener);
+ }
+ }
+ }
+
+ /**
+ * Returns this contact's email address that's being used for identification
+ * purposes in MSN.
+ *
+ * @return the contact's email address
+ */
+ public String toString() {
+ return email;
+ }
+
+ /**
+ * Returns whether the specified object is equal to this. An object is equal
+ * to this if it is also a <tt>Contact</tt> and its email addresses is
+ * equal to this contact's email address.
+ *
+ * @return <code>true</code> if the argument is a <tt>Contact</tt> and
+ * also has the same email address as this
+ */
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (obj instanceof Contact) {
+ return email.equals(((Contact) obj).email);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns a unique integer hash code for this contact.
+ *
+ * @return a integer hash code that represents this contact
+ */
+ public int hashCode() {
+ return 31 * -1 + email.hashCode();
+ }
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ContactList.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ContactList.java
new file mode 100644
index 000000000..bc73fcd43
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/ContactList.java
@@ -0,0 +1,284 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.eclipse.ecf.protocol.msn.events.IContactListListener;
+import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils;
+
+/**
+ * <p>
+ * A ContactList stores a list of {@link Contact}s.
+ * </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 ContactList {
+
+ private final Map groups;
+
+ private final ArrayList contacts;
+
+ /**
+ * The list of listeners that is associated with this.
+ */
+ private final ArrayList listeners;
+
+ private final MsnClient client;
+
+ /**
+ * Creates a new ContactList to store Contacts.
+ *
+ * @param client
+ * the client that this list is for
+ */
+ ContactList(MsnClient client) {
+ this.client = client;
+ groups = new HashMap();
+ contacts = new ArrayList();
+ listeners = new ArrayList();
+ }
+
+ /**
+ * Notifies all listeners attached to this contact list that the given
+ * contact has been added.
+ *
+ * @param contact
+ * the contact that has been added
+ */
+ private void fireContactAdded(Contact contact) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((IContactListListener) listeners.get(i)).contactAdded(contact);
+ }
+ }
+ }
+
+ void fireContactRemoved(String guid) {
+ Contact contact = findContactByGuid(guid);
+ if (!contact.getGroups().isEmpty()) {
+ contact.remove();
+ }
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((IContactListListener) listeners.get(i))
+ .contactRemoved(contact);
+ }
+ }
+ }
+
+ void fireContactAddedUser(String email) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((IContactListListener) listeners.get(i))
+ .contactAddedUser(email);
+ }
+ }
+ }
+
+ void fireContactRemovedUser(String email) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((IContactListListener) listeners.get(i))
+ .contactRemovedUser(email);
+ }
+ }
+ }
+
+ private void fireGroupAdded(Group group) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((IContactListListener) listeners.get(i)).groupAdded(group);
+ }
+ }
+ }
+
+ void internalAddContact(String email, String contactName) {
+ addContact(email, contactName, null);
+ }
+
+ void addContact(String email, String contactName, String guid) {
+ Contact contact;
+ if (guid == null) {
+ contact = new Contact(email, contactName);
+ contacts.add(contact);
+ } else {
+ contact = findContactByGuid(guid);
+ if (contact == null) {
+ contact = new Contact(email, contactName, guid);
+ }
+ contacts.add(contact);
+ }
+ fireContactAdded(contact);
+ }
+
+ void addContact(String contactName, String email, String guid,
+ String groupGUID) {
+ Contact contact = new Contact(email, contactName, guid);
+ contacts.add(contact);
+
+ String[] split = StringUtils.split(groupGUID, ',');
+ for (int i = 0; i < split.length; i++) {
+ ((Group) groups.get(split[i])).add(contact);
+ }
+
+ fireContactAdded(contact);
+ }
+
+ void addGroup(String guid, Group group) {
+ groups.put(guid, group);
+ fireGroupAdded(group);
+ }
+
+ /**
+ * Adds the contact with the specified email to this list.
+ *
+ * @param email
+ * the contact's email address
+ * @param userName
+ * the name to be assigned to this contact, or <tt>null</tt> if
+ * one does not need to be assigned
+ * @throws IOException
+ * If an I/O error occurs while attempting to send the request
+ * to the server
+ */
+ public void addContact(String email, String userName) throws IOException {
+ if (userName == null || userName.equals("")) { //$NON-NLS-1$
+ client.add(email, email);
+ } else {
+ client.add(email, userName);
+ }
+ }
+
+ /**
+ * Removes the specified contact from the user's list.
+ *
+ * @param contact
+ * the contact to remove
+ * @throws IOException
+ * If an I/O error occurs while attempting to send the request
+ * to the server
+ */
+ public void removeContact(Contact contact) throws IOException {
+ client.remove(contact);
+ }
+
+ /**
+ * Removes the specified group from the user's list.
+ *
+ * @param group
+ * the group to remove
+ * @throws IOException
+ * If an I/O error occurs while attempting to send the request
+ * to the server
+ */
+ public void removeGroup(Group group) throws IOException {
+ String guid = getGuid(group);
+ if (guid != null) {
+ client.remove(guid);
+ }
+ }
+
+ /**
+ * Returns the contact that uses the specified email address. The search
+ * performed is case-sensitive.
+ *
+ * @param email
+ * the email address of the desired contact
+ * @return the contact that is associated with the given email address, or
+ * <code>null</code> if none could be found
+ */
+ public Contact getContact(String email) {
+ for (int i = 0; i < contacts.size(); i++) {
+ Contact contact = (Contact) contacts.get(i);
+ if (contact.getEmail().equals(email)) {
+ return contact;
+ }
+ }
+ return null;
+ }
+
+ private Contact findContactByGuid(String guid) {
+ for (int i = 0; i < contacts.size(); i++) {
+ Contact contact = (Contact) contacts.get(i);
+ if (guid.equals(contact.getGuid())) {
+ return contact;
+ }
+ }
+ return null;
+ }
+
+ public Collection getContacts() {
+ return Collections.unmodifiableCollection(contacts);
+ }
+
+ public Collection getGroups() {
+ return Collections.unmodifiableCollection(groups.values());
+ }
+
+ String getGuid(Group group) {
+ for (Iterator it = groups.entrySet().iterator(); it.hasNext();) {
+ Map.Entry entry = (Map.Entry) it.next();
+ if (entry.getValue() == group) {
+ return (String) entry.getKey();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds a IContactListener to this.
+ *
+ * @param listener
+ * the listener to be added
+ */
+ public void addContactListListener(IContactListListener listener) {
+ if (listener != null) {
+ synchronized (listeners) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes a IContactListener from this.
+ *
+ * @param listener
+ * the listener to be removed
+ */
+ public void removeContactsListListener(IContactListListener listener) {
+ if (listener != null) {
+ synchronized (listeners) {
+ listeners.remove(listener);
+ }
+ }
+ }
+
+ public String toString() {
+ return contacts.toString();
+ }
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/DispatchSession.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/DispatchSession.java
new file mode 100644
index 000000000..e533f67e5
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/DispatchSession.java
@@ -0,0 +1,110 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn;
+
+import java.io.IOException;
+import java.net.ConnectException;
+
+import org.eclipse.ecf.protocol.msn.internal.encode.ResponseCommand;
+
+/**
+ * <p>
+ * The DispatchSession class connects to the dispatch server and retrieves the
+ * address of the notification server for the {@link NotificationSession} class
+ * to connect to. It currently does not serve any other purpose.
+ * </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>
+ */
+class DispatchSession extends Session {
+
+ DispatchSession(MsnClient client) {
+ super(client);
+ }
+
+ /**
+ * Creates a new DispatchSocket to connect to the given hostname and port.
+ *
+ * @param hostname
+ * the host to be connected to
+ * @param port
+ * the corresponding port number
+ * @param client
+ * the client that that invoked this dispatch session
+ * @throws IOException
+ * If an I/O error occurs while attempting to open the
+ * SocketChannel
+ */
+ DispatchSession(String hostname, int port) throws IOException {
+ super(hostname, port, null);
+ }
+
+ /**
+ * Connects to the server specified during this DispatchSession's
+ * construction and attempts to retrieve a viable notification server
+ * address.
+ *
+ * @param username
+ * the name to use for authentication
+ * @return a ResponseCommand which holds the information received from the
+ * dispatch server
+ * @throws ConnectException
+ * If the MSN servers did not respond as expected.
+ * @throws IOException
+ * If an I/O error occurs during the read or write operations
+ */
+ ResponseCommand connect(String username) throws ConnectException,
+ IOException {
+ write("VER", "MSNP11 CVR0"); //$NON-NLS-1$ //$NON-NLS-2$
+ String input = super.read();
+ if (!input.startsWith("VER")) { //$NON-NLS-1$
+ // TODO: throw a more descriptive exception
+ throw new ConnectException("The server did not respond properly.");
+ }
+
+ write("CVR", "0x040c winnt 5.1 i386 MSNMSGR 7.0.0813 msmsgs " //$NON-NLS-1$ //$NON-NLS-2$
+ + username);
+ input = super.read();
+ if (!input.startsWith("CVR")) { //$NON-NLS-1$
+ // TODO: throw a more descriptive exception
+ throw new ConnectException("The server did not respond properly.");
+ }
+
+ write("USR", "TWN I " + username); //$NON-NLS-1$ //$NON-NLS-2$
+ return new ResponseCommand(super.read());
+ }
+
+ /**
+ * Attempts to authenticate the given username with the MSN dispatch server.
+ *
+ * @param username
+ * the username to be authenticated with
+ * @return the hostname of the notification server
+ * @throws ConnectException
+ * If the MSN servers did not respond as expected.
+ * @throws IOException
+ * If an I/O error occurs during the read or write operations
+ */
+ String authenticate(String username) throws ConnectException, IOException {
+ ResponseCommand received = connect(username);
+ if (!received.getCommand().equals("XFR")) { //$NON-NLS-1$
+ throw new ConnectException("The server did not respond properly.");
+ }
+ return received.getParam(2);
+ }
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Group.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Group.java
new file mode 100644
index 000000000..e8debb1c1
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Group.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * <p>
+ * A Group is a collection of {@link Contact}s within a {@link ContactList}. A
+ * <tt>Contact</tt> can be in zero or more <tt>Group</tt>s.
+ * </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 Group {
+
+ private final List contacts;
+
+ /**
+ * The name of this group.
+ */
+ private final String name;
+
+ /**
+ * Create a new group with the specified name.
+ *
+ * @param name
+ * the name of the group
+ */
+ Group(String name) {
+ this.name = name;
+ contacts = new ArrayList();
+ }
+
+ void add(Contact contact) {
+ contacts.add(contact);
+ contact.add(this);
+ }
+
+ void remove(Contact contact) {
+ contacts.remove(contact);
+ }
+
+ /**
+ * Returns whether the specified contact is in this group.
+ *
+ * @return <tt>true</tt> if the contact is in this group, <tt>false</tt>
+ * otherwise
+ */
+ public boolean contains(Contact contact) {
+ return contacts.contains(contact);
+ }
+
+ public Collection getContacts() {
+ return Collections.unmodifiableCollection(contacts);
+ }
+
+ /**
+ * Returns the name of this group.
+ *
+ * @return this group's name
+ */
+ public String getName() {
+ return name;
+ }
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/MsnClient.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/MsnClient.java
new file mode 100644
index 000000000..08e0dd546
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/MsnClient.java
@@ -0,0 +1,339 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.Iterator;
+
+import org.eclipse.ecf.protocol.msn.events.ISessionListener;
+import org.eclipse.ecf.protocol.msn.internal.encode.ResponseCommand;
+import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils;
+
+/**
+ * <p>
+ * The MsnClient class allows a developer to easily create a client that will
+ * authenticate the user and establish a connection with the MSN servers.
+ * </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 MsnClient {
+
+ /**
+ * The default hostname that will be used to connect to the MSN servers -
+ * messenger.hotmail.com
+ */
+ private static final String HOSTNAME = "messenger.hotmail.com"; //$NON-NLS-1$
+
+ /**
+ * The default port that will be used to connect to the MSN servers - 1863
+ */
+ private static final int PORT = 1863;
+
+ /**
+ * The NotificationSession that will be connect to the notification server
+ * to handle most non-messaging related incoming and outgoing requests.
+ */
+ private NotificationSession notification;
+
+ /**
+ * The list of contacts that are on this user's list.
+ */
+ private final ContactList list;
+
+ /**
+ * The user's email address.
+ */
+ private String username;
+
+ /**
+ * The name the user displays to other contacts.
+ */
+ private String displayName;
+
+ /**
+ * The hostname to use to connect to the dispatch server.
+ */
+ private String hostname;
+
+ /**
+ * The user's personal message.
+ */
+ private String personalMessage = ""; //$NON-NLS-1$
+
+ /**
+ * The media that the user is currently playing.
+ */
+ private String currentMedia = "";//$NON-NLS-1$
+
+ /**
+ * The port to use to connect to the dispatch server.
+ */
+ private int port;
+
+ /**
+ * The current status of the user.
+ */
+ private Status status;
+
+ /**
+ * Instantiate a new MsnClient that defaults to setting the user as being
+ * online and available when signing in.
+ *
+ * @throws IOException
+ * If an I/O error occurred during instantiation of the
+ * DispatchSession.
+ */
+ public MsnClient() {
+ this(Status.ONLINE);
+ }
+
+ /**
+ * Instantiate a new MsnClient that set the user to the specified status
+ * when signing in.
+ *
+ * @param initialStatus
+ * the status that a user would like to sign on to the servers
+ * as, refer to {@link Status#ONLINE} and other static variables
+ * for the available options.
+ */
+ public MsnClient(Status initialStatus) {
+ status = initialStatus;
+ hostname = HOSTNAME;
+ port = PORT;
+ list = new ContactList(this);
+ notification = new NotificationSession(this);
+ }
+
+ /**
+ * Connects the client to the MSN servers.
+ *
+ * @param username
+ * the user's email address that is associated with an MSN
+ * account
+ * @param password
+ * the email's corresponding password
+ * @throws IOException
+ * If an I/O error occurred while connecting to the dispatch or
+ * notification servers.
+ */
+ public void connect(String username, String password) throws IOException {
+ this.username = username;
+ DispatchSession dispatch = new DispatchSession(hostname, port);
+ // get the address of the notification server by first authenticating
+ // ourselves
+ String address = dispatch.authenticate(username);
+ // close the session
+ dispatch.close();
+ // connect the notification session to the received address
+ notification.openSocket(address);
+ try {
+ // keep looping until we've connected successfully
+ while (!notification.login(username, password)) {
+ notification.reset();
+ }
+ } catch (RuntimeException e) {
+ if (!notification.isClosed()) {
+ throw e;
+ }
+ } catch (IOException e) {
+ if (!notification.isClosed()) {
+ throw e;
+ }
+ }
+ }
+
+ /**
+ * Disconnects the user from the MSN servers. Please note that any
+ * {@link ChatSession}s that may have been created since the client was
+ * instantiated are not disconnected automatically in this method.
+ */
+ public void disconnect() {
+ if (notification != null) {
+ try {
+ notification.write("OUT"); //$NON-NLS-1$
+ } catch (Exception e) {
+ // ignored since we're disconnecting anyway
+ }
+ notification.close();
+ }
+ notification = null;
+ }
+
+ /**
+ * Changes the user's status to the provided status flag.
+ *
+ * @param status
+ * the status that the user wishes to change to
+ */
+ public void setStatus(Status status) throws IOException {
+ if (this.status != status) {
+ if (status == Status.OFFLINE) {
+ disconnect();
+ } else {
+ notification.write("CHG", status.getLiteral() + " 268435488"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+ this.status = status;
+ }
+ }
+
+ /**
+ * Returns the status that the user is currently in.
+ *
+ * @return the user's current status
+ */
+ public Status getStatus() {
+ return status;
+ }
+
+ void add(String email, String userName) throws IOException {
+ notification.write("ADC", "FL N=" + email + " F=" + userName); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ }
+
+ void remove(Contact contact) throws IOException {
+ String guid = contact.getGuid();
+ for (Iterator it = contact.getGroups().iterator(); it.hasNext();) {
+ notification.write("REM", "FL " + guid + " " //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ + list.getGuid((Group) it.next()));
+ }
+ notification.write("REM", "FL " + guid); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ void remove(String guid) throws IOException {
+ notification.write("RMG", "FL " + guid); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ /**
+ * Returns the contact list that is associated with this client.
+ *
+ * @return this client's contact list
+ */
+ public ContactList getContactList() {
+ return list;
+ }
+
+ /**
+ * Creates a {@link ChatSession} to connect to the specified contact.
+ *
+ * @param email
+ * the contact to connect to
+ * @return the created ChatSession
+ * @throws IOException
+ * If an I/O error occurred
+ */
+ public ChatSession createChatSession(String email) throws IOException {
+ ResponseCommand cmd = notification.getChatSession();
+ ChatSession cs = new ChatSession(cmd.getParam(2), this, username, cmd
+ .getParam(4));
+ // reset the ResponseCommand so that the next XFR request won't conflict
+ cmd.process(null);
+ cs.invite(email);
+ return cs;
+ }
+
+ void internalSetDisplayName(String newName) {
+ displayName = newName;
+ }
+
+ /**
+ * Sets the display name of this user.
+ *
+ * @param newName
+ * the new name of this user
+ */
+ public void setDisplayName(String newName) throws IOException {
+ notification.write("PRP", "MFN " + URLEncoder.encode(newName)); //$NON-NLS-1$ //$NON-NLS-2$
+ displayName = newName;
+ }
+
+ /**
+ * Returns the displayed name of this user.
+ *
+ * @return the name that this user is using
+ */
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ /**
+ * Returns the user's account's email address.
+ *
+ * @return the email address the user is using for MSN login
+ */
+ public String getUserEmail() {
+ return username;
+ }
+
+ private void sendStatusData() throws IOException {
+ String message = "<Data><PSM>" + personalMessage //$NON-NLS-1$
+ + "</PSM><CurrentMedia>" + currentMedia //$NON-NLS-1$
+ + "</CurrentMedia></Data>"; //$NON-NLS-1$
+ notification.write("UUX", message.length() + "\r\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + message, false);
+ }
+
+ /**
+ * Sets the user's personal message to the specified string.
+ *
+ * @param personalMessage
+ * the new message to use as the user's personal message
+ * @throws IOException
+ * If an I/O error occurred while sending the data to the
+ * notification server
+ */
+ public void setPersonalMessage(String personalMessage) throws IOException {
+ if (personalMessage == null) {
+ personalMessage = ""; //$NON-NLS-1$
+ } else {
+ personalMessage = StringUtils.xmlEncode(personalMessage);
+ }
+ if (!this.personalMessage.equals(personalMessage)) {
+ this.personalMessage = personalMessage;
+ sendStatusData();
+ }
+ }
+
+ /**
+ * Retrieves the user's current personal message.
+ *
+ * @return the personal message that the user is currently using
+ */
+ public String getPersonalMessage() {
+ return StringUtils.xmlDecode(personalMessage);
+ }
+
+ /**
+ * Add an ISessionListener to this client.
+ *
+ * @param listener
+ * the listener to be added
+ */
+ public void addSessionListener(ISessionListener listener) {
+ notification.addSessionListener(listener);
+ }
+
+ /**
+ * Removes an ISessionListener from this client.
+ *
+ * @param listener
+ * the listener to be removed
+ */
+ public void removeSessionListener(ISessionListener listener) {
+ notification.removeSessionListener(listener);
+ }
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/NotificationSession.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/NotificationSession.java
new file mode 100644
index 000000000..2749d6698
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/NotificationSession.java
@@ -0,0 +1,499 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.ConnectException;
+import java.util.ArrayList;
+
+import org.eclipse.ecf.protocol.msn.events.ISessionListener;
+import org.eclipse.ecf.protocol.msn.internal.encode.Challenge;
+import org.eclipse.ecf.protocol.msn.internal.encode.ResponseCommand;
+import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils;
+import org.eclipse.ecf.protocol.msn.internal.net.ClientTicketRequest;
+
+/**
+ * <p>
+ * The NotificationSession manages all incoming and outgoing packets that
+ * concerns status changes in contacts, client pings, challenge strings, and
+ * others.
+ * </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>
+ */
+final class NotificationSession extends DispatchSession {
+
+ private final ContactList list;
+
+ /**
+ * The ClientTicketRequest that this NotificationSession will use to obtain
+ * the client ticket associated with the user's MSN email address username
+ * and password.
+ */
+ private ClientTicketRequest request;
+
+ /**
+ * The ResponseCommand used to process responses from the server.
+ */
+ private ResponseCommand response;
+
+ private Thread pingingThread;
+
+ /**
+ * The address of the alternate server that has been read in that the client
+ * should try redirecting its notification session to connect to.
+ */
+ private String alternateServer;
+
+ /**
+ * The user's MSN email address.
+ */
+ private String username;
+
+ /**
+ * Creates a new NotificationSession that will connect to the given host.
+ *
+ * @param client
+ * the MsnClient to hook onto
+ */
+ NotificationSession(MsnClient client) {
+ super(client);
+ list = client.getContactList();
+ listeners = new ArrayList();
+ request = new ClientTicketRequest();
+ }
+
+ /**
+ * Returns whether the user has connected with the notification server
+ * successfully or not. If the connecting process failed, {@link #reset()}
+ * should be called so that a connection attempt can be made to the server
+ * that the user has been redirected to.
+ *
+ * @param username
+ * the user's MSN email address login
+ * @param password
+ * the user's password
+ * @return <tt>true</tt> if the login completed successfully,
+ * <tt>false</tt> otherwise
+ * @throws IOException
+ * If an I/O error occurs while attempting to authenticate with
+ * the servers
+ */
+ boolean login(String username, String password) throws IOException {
+ response = connect(username);
+ if (response.getCommand().equals("USR")) { //$NON-NLS-1$
+ String ticket = request.getTicket(username, password, response
+ .getParam(3));
+ password = null;
+
+ write("USR", "TWN S " + ticket); //$NON-NLS-1$ //$NON-NLS-2$
+ ticket = null;
+ String input = super.read();
+ if (!input.startsWith("USR")) { //$NON-NLS-1$
+ throw new ConnectException(
+ "An error occurred while attempting to authenticate "
+ + "with the Tweener server.");
+ }
+
+ retrieveBuddyList();
+ this.username = username;
+ return true;
+ } else if (!response.getCommand().equals("XFR")) { //$NON-NLS-1$
+ throw new ConnectException("Unable to connect to the MSN server.");
+ } else {
+ alternateServer = response.getParam(2);
+ return false;
+ }
+ }
+
+ private void retrieveBuddyList() throws IOException {
+ write("SYN", "0 0"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ getInputStream()));
+ String input = reader.readLine();
+ while (input == null || !input.startsWith("SYN")) { //$NON-NLS-1$
+ input = reader.readLine();
+ }
+
+ String[] split = StringUtils.splitOnSpace(input);
+ int contacts = Integer.parseInt(split[4]);
+
+ while (!input.startsWith("LST")) { //$NON-NLS-1$
+ if (input.startsWith("PRP MFN")) { //$NON-NLS-1$
+ client.internalSetDisplayName(StringUtils.splitSubstring(input,
+ " ", 2)); //$NON-NLS-1$
+ } else if (input.startsWith("LSG")) { //$NON-NLS-1$
+ split = StringUtils.splitOnSpace(input);
+ list.addGroup(split[2], new Group(split[1]));
+ }
+ input = reader.readLine();
+ }
+
+ int count = 0;
+ while (true) {
+ if (input.startsWith("LST")) { //$NON-NLS-1$
+ count++;
+ String[] contact = StringUtils.splitOnSpace(input);
+ String email = contact[1].substring(2);
+ switch (contact.length) {
+ case 3:
+ list.internalAddContact(email, email);
+ break;
+ case 5:
+ list.addContact(email, email, contact[3].substring(2));
+ break;
+ default:
+ list.addContact(contact[2].substring(2), email, contact[3]
+ .substring(2), contact[5]);
+ break;
+ }
+
+ if (count == contacts) {
+ break;
+ }
+ }
+
+ input = reader.readLine();
+ }
+
+ write("CHG", client.getStatus().getLiteral() + " 268435488"); //$NON-NLS-1$ //$NON-NLS-2$
+ idle();
+ ping();
+ }
+
+ /**
+ * This method is invoked with another user has invited the client to a
+ * switchboard session. The created session will answer back and then invoke
+ * the {@link Session#idle()} method to begin processing incoming and
+ * outgoing requests.
+ *
+ * @param data
+ * a String array containing the request that the other user has
+ * sent to the client
+ * @throws IOException
+ * If an I/O error occurs while the ChatSession is created to
+ * handle this request.
+ */
+ private void processSwitchboardRequest(String[] data) throws IOException {
+ ChatSession ss = new ChatSession(data[2], client);
+ ss.write("ANS", username + ' ' + data[4] + ' ' + data[1]); //$NON-NLS-1$
+ ss.read();
+ fireSwitchboardConnectedEvent(ss);
+ // ss.read();
+ ss.idle();
+ }
+
+ /**
+ * Sends a request to the notification server for a switchboard server's
+ * information.
+ *
+ * @return the ResponseCommand that represents the notification server's
+ * output
+ * @throws IOException
+ * If an I/O error occurred while reading or writing data.
+ */
+ ResponseCommand getChatSession() throws IOException {
+ if (client.getStatus() == Status.APPEAR_OFFLINE
+ || client.getStatus() == Status.OFFLINE) {
+ throw new ConnectException("Switchboards cannot be created when "
+ + "the user is hidden or offline.");
+ }
+ write("XFR", "SB"); //$NON-NLS-1$ //$NON-NLS-2$
+ String command = response.getCommand();
+ while (command == null || !command.equals("XFR")) { //$NON-NLS-1$
+ command = response.getCommand();
+ }
+ return response;
+ }
+
+ /**
+ * Closes the connection to the original host and then open up a new
+ * connection to the redirected host. A new call to
+ * {@link #login(String, String)} should be made after this method has
+ * returned.
+ */
+ void reset() throws IOException {
+ close();
+ openSocket(alternateServer);
+ request.setCancelled(false);
+ }
+
+ /**
+ * Read the contents of the packet being sent from the server and handle any
+ * events accordingly.
+ */
+ String read() throws IOException {
+ String input = super.read();
+ if (input == null) {
+ return null;
+ }
+
+ if (input.indexOf("ILN") != -1) { //$NON-NLS-1$
+ String[] events = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
+ for (int i = 0; i < events.length; i++) {
+ if (!events[i].trim().equals("") //$NON-NLS-1$
+ && events[i].substring(1, 3).equals("LN")) { //$NON-NLS-1$
+ String[] sub = StringUtils.split(events[i], " ", 3); //$NON-NLS-1$
+ String[] split = StringUtils.splitOnSpace(sub[2]);
+ changeContactInfo(split);
+ }
+ }
+ } else if (input.indexOf("FLN") != -1) { //$NON-NLS-1$
+ String[] events = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
+ for (int i = 0; i < events.length; i++) {
+ if (events[i].startsWith("FLN")) { //$NON-NLS-1$
+ setContactToOffline(events[i]);
+ }
+ }
+ } else if (input.indexOf("LN") != -1) { //$NON-NLS-1$
+ String[] events = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
+ for (int i = 0; i < events.length; i++) {
+ if (events[i].substring(0, 3).equals("NLN")) { //$NON-NLS-1$
+ String[] sub = StringUtils.split(events[i], " ", 3); //$NON-NLS-1$
+ String[] split = StringUtils.splitOnSpace(sub[2]
+ .substring(4));
+ changeContactInfo(split);
+ } else if (events[i].substring(1, 3).equals("LN")) { //$NON-NLS-1$
+ String[] sub = StringUtils.split(events[i], " ", 3); //$NON-NLS-1$
+ String[] split = StringUtils.splitOnSpace(sub[2]);
+ changeContactInfo(split);
+ }
+ }
+ } else if (input.indexOf("CHL") != -1) { //$NON-NLS-1$
+ // the read input is a challenge string
+ String query = Challenge.createQuery(StringUtils.splitSubstring(
+ input, " ", 2)); //$NON-NLS-1$
+ write("QRY", Challenge.PRODUCT_ID + ' ' + query.length() + "\r\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + query, false);
+ } else if (input.indexOf("RNG") != -1) { //$NON-NLS-1$
+ processSwitchboardRequest(StringUtils.splitOnSpace(input));
+ }
+
+ if (input.indexOf("XFR") != -1) { //$NON-NLS-1$
+ String[] split = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
+ for (int i = 0; i < split.length; i++) {
+ if (split[i].startsWith("XFR")) { //$NON-NLS-1$
+ response.process(split[i]);
+ }
+ }
+ }
+
+ if (input.indexOf("UBX") != -1) { //$NON-NLS-1$
+ String[] split = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
+ for (int i = 0; i < split.length; i++) {
+ if (split[i].startsWith("UBX")) { //$NON-NLS-1$
+ processContactData(split, i);
+ }
+ }
+ }
+
+ if (input.indexOf("ADC") != -1) { //$NON-NLS-1$
+ String[] split = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
+ for (int i = 0; i < split.length; i++) {
+ if (split[i].startsWith("ADC")) { //$NON-NLS-1$
+ String[] subSplit = StringUtils.splitOnSpace(split[i]);
+ if (subSplit[2].equals("FL")) { //$NON-NLS-1$
+ processContactAdded(subSplit[3].substring(2),
+ subSplit[4].substring(2), subSplit[5]
+ .substring(2));
+ } else if (subSplit[2].equals("RL")) { //$NON-NLS-1$
+ processContactAddedUser(subSplit[3].substring(2));
+ }
+ }
+ }
+ }
+
+ if (input.indexOf("REM") != -1) { //$NON-NLS-1$
+ String[] split = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
+ for (int i = 0; i < split.length; i++) {
+ if (split[i].startsWith("REM")) { //$NON-NLS-1$
+ String[] subSplit = StringUtils.splitOnSpace(split[i]);
+ if (subSplit[2].equals("FL")) { //$NON-NLS-1$
+ processContactRemoved(subSplit[3]);
+ } else if (subSplit[2].equals("RL")) { //$NON-NLS-1$
+ processContactRemovedUser(subSplit[3]);
+ }
+ }
+ }
+ }
+
+ if (input.indexOf("OUT OTH") != -1) { //$NON-NLS-1$
+ String[] split = StringUtils.split(input, "\r\n"); //$NON-NLS-1$
+ for (int i = 0; i < split.length; i++) {
+ if (split[i].startsWith("OUT OTH")) { //$NON-NLS-1$
+ close();
+ break;
+ }
+ }
+ }
+
+ return input;
+ }
+
+ void close() {
+ request.setCancelled(true);
+ if (pingingThread != null) {
+ pingingThread.interrupt();
+ }
+ super.close();
+ }
+
+ /**
+ * Create a new thread that will ping the host every sixty seconds to keep
+ * this connection alive.
+ */
+ private void ping() {
+ pingingThread = new Thread() {
+ public void run() {
+ try {
+ while (true) {
+ sleep(60000);
+ write("PNG"); //$NON-NLS-1$
+ }
+ } catch (IOException e) {
+ // ignored
+ } catch (InterruptedException e) {
+ // ignored
+ }
+ }
+ };
+ pingingThread.start();
+ }
+
+ private void processContactAdded(String email, String contactName,
+ String guid) {
+ list.addContact(email, contactName, guid);
+ }
+
+ private void processContactRemoved(String guid) {
+ list.fireContactRemoved(guid);
+ }
+
+ private void processContactAddedUser(String email) {
+ list.fireContactAddedUser(email);
+ }
+
+ private void processContactRemovedUser(String email) {
+ list.fireContactRemovedUser(email);
+ }
+
+ /**
+ * Checks whether a contact has changed his or her personal message or
+ * current media.
+ *
+ * @param eventString
+ * a String array containing the notification server's
+ * information
+ * @param index
+ * the index of the String array that processing should begin
+ */
+ private void processContactData(String[] eventString, int index) {
+ if (eventString.length == index + 1
+ || StringUtils.splitSubstring(eventString[index], " ", 2) //$NON-NLS-1$
+ .equals("0")) { //$NON-NLS-1$
+ eventString = StringUtils.splitOnSpace(eventString[index]);
+ Contact contact = list.getContact(eventString[1]);
+ if (contact != null) {
+ contact.setPersonalMessage(""); //$NON-NLS-1$
+ }
+ return;
+ }
+
+ String data = eventString[index + 1];
+ eventString = StringUtils.splitOnSpace(eventString[index]);
+ Contact contact = list.getContact(eventString[1]);
+ contact.setPersonalMessage(data.substring(data.indexOf("<PSM>") + 5, //$NON-NLS-1$
+ data.indexOf("</PSM>"))); //$NON-NLS-1$
+ // TODO: set media
+ }
+
+ /**
+ * Changes the contact's status based on the string array that has been
+ * received from the notification server.
+ *
+ * @param eventString
+ * a formatted String literal obtained from an incoming message
+ */
+ private void changeContactInfo(String[] eventString) {
+ // we are not interested in our own changes
+ if (!eventString[1].equals(client.getUserEmail())) {
+ Contact contact = list.getContact(eventString[1]);
+ contact.setStatus(Status.getStatus(eventString[0]));
+ contact.setDisplayName(eventString[2]);
+ }
+ }
+
+ /**
+ * Changes the contact specified by the notification server to be an offline
+ * status.
+ *
+ * @param eventString
+ * a formatted String literal obtained from an incoming message
+ */
+ private void setContactToOffline(String eventString) {
+ String email = StringUtils.splitSubstring(eventString, " ", 1); //$NON-NLS-1$
+ list.getContact(email).setStatus(Status.OFFLINE);
+ }
+
+ /**
+ * Fires an event to all attached notification listeners to indicate that
+ * the specified chat session has been connected to.
+ *
+ * @param session
+ * the chat session that has been connected to
+ */
+ private void fireSwitchboardConnectedEvent(ChatSession session) {
+ synchronized (listeners) {
+ for (int i = 0; i < listeners.size(); i++) {
+ ((ISessionListener) listeners.get(i)).sessionConnected(session);
+ }
+ }
+ }
+
+ /**
+ * Adds the provided ISessionListener to this notification session.
+ *
+ * @param listener
+ * the listener to add
+ */
+ public void addSessionListener(ISessionListener listener) {
+ if (listener != null) {
+ synchronized (listeners) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes the specified ISessionListener from this notification session.
+ *
+ * @param listener
+ * the listener to remove
+ */
+ public void removeSessionListener(ISessionListener listener) {
+ if (listener != null) {
+ synchronized (listeners) {
+ listeners.remove(listener);
+ }
+ }
+ }
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Session.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Session.java
new file mode 100644
index 000000000..db74ced2f
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Session.java
@@ -0,0 +1,299 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.ArrayList;
+
+/**
+ * <p>
+ * An abstract base class that all other sessions should extend. This class
+ * provides common methods that a session will need to use such as reading and
+ * writing information from and to a socket.
+ * </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>
+ */
+abstract class Session {
+
+ /**
+ * The client that this session is attached to.
+ */
+ final MsnClient client;
+
+ /**
+ * The list of listeners that have been connected to this session.
+ */
+ ArrayList listeners;
+
+ private final byte[] buffer = new byte[1024];
+
+ private Socket socket;
+
+ private InputStream is;
+
+ private OutputStream os;
+
+ /**
+ * The number of transactions that has been transferred through this session
+ * thus far. This value will automatically increment when
+ * {@link #write(String, String)} or {@link #write(String, String, boolean)}
+ * has been invoked.
+ */
+ private long transactionID = 0;
+
+ /**
+ * A thread used to wait indefinitely for incoming messages from the server.
+ */
+ private IdleThread idleThread;
+
+ private boolean closed = false;
+
+ Session(MsnClient client) {
+ this.client = client;
+ }
+
+ /**
+ * Creates a session instance that will be connected to the given host. The
+ * string must be of the form '12.345.678.9:1234'.
+ *
+ * @param host
+ * the host to connect to
+ * @param client
+ * the client to hook onto
+ * @throws IOException
+ * If an I/O error occurred while attempting to connect to the
+ * given host.
+ */
+ Session(String host, MsnClient client) throws IOException {
+ this.client = client;
+ openSocket(host);
+ }
+
+ /**
+ * Creates a session instance that will be connected to the given host and
+ * port.
+ *
+ * @param ip
+ * the host to connect to
+ * @param port
+ * the port to connect to
+ * @param client
+ * the client to hook onto
+ * @throws IOException
+ * If an I/O error occurred while attempting to connect to the
+ * port at the given host.
+ */
+ Session(String ip, int port, MsnClient client) throws IOException {
+ this.client = client;
+ openSocket(ip, port);
+ }
+
+ /**
+ * Opens a connection to the specified host.
+ *
+ * @param host
+ * the host to connect to
+ * @throws IOException
+ * If an error occurs while attempting to connect to the host
+ */
+ final void openSocket(String host) throws IOException {
+ closed = false;
+ int index = host.indexOf(':');
+ openSocket(host.substring(0, index), Integer.parseInt(host
+ .substring(index + 1)));
+ }
+
+ final void openSocket(String ip, int port) throws IOException {
+ closed = false;
+ socket = new Socket(ip, port);
+ is = socket.getInputStream();
+ os = socket.getOutputStream();
+ }
+
+ final InputStream getInputStream() {
+ return is;
+ }
+
+ /**
+ * Reads data from the channel and returns it as a String.
+ *
+ * @return the contents that have been read, or <code>null</code> if
+ * nothing is currently available
+ * @throws IOException
+ * If an I/O error occurred while reading from the channel.
+ */
+ String read() throws IOException {
+ int read = is.read(buffer);
+ if (read < 1) {
+ return null;
+ } else {
+ return new String(buffer, 0, read).trim();
+ }
+ }
+
+ /**
+ * This method writes the given string input onto the channel. The carriage
+ * return and newline characters may be appended depending on the value of
+ * <code>newline</code>.
+ *
+ * @param input
+ * the String to be written
+ * @param newline
+ * <code>true</code> if a '\r\n' should be appended at the end
+ * of the write
+ * @throws IOException
+ * If an I/O error occurs while attempting to write to the
+ * channel.
+ */
+ private final void write(String input, boolean newline) throws IOException {
+ byte[] bytes = newline ? (input + "\r\n").getBytes() : input.getBytes(); //$NON-NLS-1$
+ os.write(bytes);
+ os.flush();
+ }
+
+ /**
+ * This method is synonymous with {@link #write(String, boolean)} with the
+ * exception that this method will always insert the carriage return and
+ * newline characters.
+ *
+ * @param input
+ * the String to be written
+ * @throws IOException
+ * If an I/O error occurs while attempting to write to the
+ * channel.
+ */
+ final void write(String input) throws IOException {
+ write(input, true);
+ }
+
+ /**
+ * Writes the given command with the specified parameters to the channel. A
+ * transaction identification number will also be inserted between the
+ * command and its parameters. A carriage return and a newline character
+ * will be inserted if <tt>newline</tt> is true.
+ *
+ * @param command
+ * the command to be inserted
+ * @param parameters
+ * additional parameters that are associated with the command
+ * @param newline
+ * <tt>true</tt> if a <tt>"\r\n"</tt> should be appended at
+ * the end of the write
+ * @throws IOException
+ * If an I/O error occurs while attempting to write to the
+ * channel.
+ */
+ final void write(String command, String parameters, boolean newline)
+ throws IOException {
+ transactionID++;
+ write(command + ' ' + transactionID + ' ' + parameters, newline);
+ }
+
+ /**
+ * This method is synonymous with {@link #write(String, String, boolean)}
+ * with the exception that this method will always insert the carriage
+ * return and newline characters.
+ *
+ * @param command
+ * the command to be inserted
+ * @param parameters
+ * additional parameters that are associated with the command
+ * @throws IOException
+ * If an I/O error occurs while attempting to write to the
+ * channel.
+ */
+ final void write(String command, String parameters) throws IOException {
+ write(command, parameters, true);
+ }
+
+ void close() {
+ closed = false;
+ if (idleThread != null) {
+ idleThread.interrupt();
+ idleThread = null;
+ }
+
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (Exception e) {
+ // ignored
+ }
+ socket = null;
+ }
+
+ if (is != null) {
+ try {
+ is.close();
+ } catch (Exception e) {
+ // ignored
+ }
+ is = null;
+ }
+
+ if (os != null) {
+ try {
+ os.close();
+ } catch (Exception e) {
+ // ignored
+ }
+ os = null;
+ }
+ }
+
+ protected void finalize() throws Throwable {
+ close();
+ super.finalize();
+ }
+
+ /**
+ * Wait in an infinite loop for information to be read in.
+ */
+ final void idle() {
+ if (idleThread == null || !idleThread.isAlive()) {
+ idleThread = new IdleThread();
+ idleThread.start();
+ }
+ }
+
+ final boolean isClosed() {
+ return closed;
+ }
+
+ /**
+ * IdleThread waits for an indefinite amount of time for incoming messages.
+ */
+ private class IdleThread extends Thread {
+
+ /**
+ * Begin waiting for incoming messages indefinitely.
+ */
+ public void run() {
+ while (!isInterrupted()) {
+ try {
+ read();
+ } catch (IOException e) {
+ return;
+ }
+ }
+ }
+ }
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Status.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Status.java
new file mode 100644
index 000000000..b435f5e27
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/Status.java
@@ -0,0 +1,78 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn;
+
+/**
+ * <p>
+ * The Status class represents the different states that a user can be in.
+ * </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 Status {
+
+ public static final Status ONLINE = new Status("NLN"); //$NON-NLS-1$
+
+ public static final Status BUSY = new Status("BSY"); //$NON-NLS-1$
+
+ public static final Status BE_RIGHT_BACK = new Status("BRB"); //$NON-NLS-1$
+
+ public static final Status AWAY = new Status("AWY"); //$NON-NLS-1$
+
+ public static final Status IDLE = new Status("IDL"); //$NON-NLS-1$
+
+ public static final Status ON_THE_PHONE = new Status("PHN"); //$NON-NLS-1$
+
+ public static final Status OUT_TO_LUNCH = new Status("LUN"); //$NON-NLS-1$
+
+ public static final Status APPEAR_OFFLINE = new Status("HDN"); //$NON-NLS-1$
+
+ public static final Status OFFLINE = new Status(null);
+
+ private String literal;
+
+ static Status getStatus(String literal) {
+ if (literal.equals("NLN")) { //$NON-NLS-1$
+ return ONLINE;
+ } else if (literal.equals("AWY")) { //$NON-NLS-1$
+ return AWAY;
+ } else if (literal.equals("IDL")) { //$NON-NLS-1$
+ return IDLE;
+ } else if (literal.equals("BSY")) { //$NON-NLS-1$
+ return BUSY;
+ } else if (literal.equals("BRB")) { //$NON-NLS-1$
+ return BE_RIGHT_BACK;
+ } else if (literal.equals("PHN")) { //$NON-NLS-1$
+ return ON_THE_PHONE;
+ } else if (literal.equals("LUN")) { //$NON-NLS-1$
+ return OUT_TO_LUNCH;
+ } else if (literal.equals("HDN")) { //$NON-NLS-1$
+ return APPEAR_OFFLINE;
+ } else {
+ throw new IllegalArgumentException("Unknown literal: " + literal);
+ }
+ }
+
+ private Status(String literal) {
+ this.literal = literal;
+ }
+
+ String getLiteral() {
+ return literal;
+ }
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IChatSessionListener.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IChatSessionListener.java
new file mode 100644
index 000000000..448250b95
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IChatSessionListener.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn.events;
+
+import java.util.EventListener;
+
+import org.eclipse.ecf.protocol.msn.ChatSession;
+import org.eclipse.ecf.protocol.msn.Contact;
+
+/**
+ * <p>
+ * The IChatSessionListener monitors the events that are occurring within a
+ * {@link ChatSession}.
+ * </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 interface IChatSessionListener extends EventListener {
+
+ /**
+ * This method is called when a contact joins the session.
+ *
+ * @param contact
+ * the contact that has joined
+ */
+ public void contactJoined(Contact contact);
+
+ /**
+ * This method is called when a contact leaves the session.
+ *
+ * @param contact
+ * the contact that has left
+ */
+ public void contactLeft(Contact contact);
+
+ /**
+ * This method is called when a contact begins typing.
+ *
+ * @param contact
+ * the contact that is currently typing
+ */
+ public void contactIsTyping(Contact contact);
+
+ /**
+ * This method is called when a message has been received from a contact.
+ *
+ * @param contact
+ * the contact that has sent out a message
+ * @param message
+ * the message that has been sent
+ */
+ public void messageReceived(Contact contact, String message);
+
+ /**
+ * This method is called when the session has timed out.
+ */
+ public void sessionTimedOut();
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListListener.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListListener.java
new file mode 100644
index 000000000..d52fdc0fd
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListListener.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn.events;
+
+import org.eclipse.ecf.protocol.msn.Contact;
+import org.eclipse.ecf.protocol.msn.Group;
+
+/**
+ * <p>
+ * The IContactListListener monitors events pertaining to the addition and
+ * removal of contacts on the user's client.
+ * </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 interface IContactListListener {
+
+ /**
+ * This method is invoked when a contact has been added to the user's list.
+ *
+ * @param contact
+ * the contact that has been added
+ */
+ public void contactAdded(Contact contact);
+
+ /**
+ * This method is invoked when a contact has been removed from the user's
+ * list.
+ *
+ * @param contact
+ * the contact that has been added
+ */
+ public void contactRemoved(Contact contact);
+
+ /**
+ * This method is invoked when a contact has added the user to his or her
+ * contact list.
+ *
+ * @param email
+ * the email of the contact
+ */
+ public void contactAddedUser(String email);
+
+ /**
+ * This method is invoked when a contact has removed the user from his or
+ * her contact list.
+ *
+ * @param email
+ * the email of the contact
+ */
+ public void contactRemovedUser(String email);
+
+ /**
+ * This method is invoked when a group has been added to the contact list.
+ *
+ * @param group
+ * the group that has been added
+ */
+ public void groupAdded(Group group);
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListener.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListener.java
new file mode 100644
index 000000000..29763c61a
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/IContactListener.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn.events;
+
+import org.eclipse.ecf.protocol.msn.Status;
+
+/**
+ * <p>
+ * The IContactListener interface defines methods that developers can listen for
+ * which pertains to the {@link org.eclipse.ecf.protocol.msn.Contact} class.
+ * </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 interface IContactListener {
+
+ /**
+ * This method is called when contact has changed his or her user name.
+ *
+ * @param name
+ * the new name that the contact is using
+ */
+ public void nameChanged(String name);
+
+ /**
+ * This method is called when the user changes his or her personal message.
+ *
+ * @param personalMessage
+ * the new message that the contact is displaying
+ */
+ public void personalMessageChanged(String personalMessage);
+
+ /**
+ * This method is called when the contact has changed his or her status.
+ *
+ * @param status
+ * the status that the contact has now switched to
+ */
+ public void statusChanged(Status status);
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/ISessionListener.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/ISessionListener.java
new file mode 100644
index 000000000..a154fc8fb
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/ISessionListener.java
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn.events;
+
+import org.eclipse.ecf.protocol.msn.ChatSession;
+
+/**
+ * <p>
+ * A listener that can be used to monitor the creation of {@link ChatSession}s.
+ * This is used for listening for incoming instant messages from a party that
+ * the current user has not already established a chat session with.
+ * </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 interface ISessionListener {
+
+ /**
+ * A method that is called when a chat session has been created because of a
+ * request from an external host.
+ *
+ * @param chatSession
+ * the created chat session
+ */
+ public void sessionConnected(ChatSession chatSession);
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/package.html b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/package.html
new file mode 100644
index 000000000..fb4fade75
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/events/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+Provides listener interfaces for handling events received from the MSN network.
+</body>
+</html>
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Base64.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Base64.java
new file mode 100644
index 000000000..441cd6021
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Base64.java
@@ -0,0 +1,289 @@
+/*******************************************************************************
+ * Copyright (c) Robert Harder
+ * 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:
+ * Robert Harder <rob@iharder.net> - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn.internal.encode;
+
+/**
+ * <p>This file is distributed under the
+ * <a href="http://www.opensource.org/licenses/eclipse-1.0.php">Eclipse Public License</a>.
+ * A warm "Thank You" goes out to the open source community for all their contributions
+ * to the common good.</p>
+ *
+ * <p>Encodes and decodes to and from Base64 notation.</p>
+ * <p>Homepage: <a href="http://iharder.net/base64">http://iharder.net/base64</a>.</p>
+ *
+ * <p>The <tt>options</tt> parameter, which appears in a few places, is used to pass
+ * several pieces of information to the encoder. In the "higher level" methods such as
+ * encodeBytes( bytes, options ) the options parameter can be used to indicate such
+ * things as first gzipping the bytes before encoding them, not inserting linefeeds
+ * (though that breaks strict Base64 compatibility), and encoding using the URL-safe
+ * and Ordered dialects.</p>
+ *
+ * <p>The constants defined in Base64 can be OR-ed together to combine options, so you
+ * might make a call like this:</p>
+ *
+ * <code>String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DONT_BREAK_LINES );</code>
+ *
+ * <p>to compress the data before encoding it and then making the output have no newline characters.</p>
+ *
+ *
+ * <p>
+ * Change Log:
+ * </p>
+ * <ul>
+ * <li>v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug
+ * when using very small files (~< 40 bytes).</li>
+ * <li>v2.2 - Added some helper methods for encoding/decoding directly from
+ * one file to the next. Also added a main() method to support command line
+ * encoding/decoding from one file to the next. Also added these Base64 dialects:
+ * <ol>
+ * <li>The default is RFC3548 format.</li>
+ * <li>Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates
+ * URL and file name friendly format as described in Section 4 of RFC3548.
+ * http://www.faqs.org/rfcs/rfc3548.html</li>
+ * <li>Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates
+ * URL and file name friendly format that preserves lexical ordering as described
+ * in http://www.faqs.org/qa/rfcc-1940.html</li>
+ * </ol>
+ * Special thanks to Jim Kellerman at <a href="http://www.powerset.com/">http://www.powerset.com/</a>
+ * for contributing the new Base64 dialects.
+ * </li>
+ *
+ * <li>v2.1 - Cleaned up javadoc comments and unused variables and methods. Added
+ * some convenience methods for reading and writing to and from files.</li>
+ * <li>v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems
+ * with other encodings (like EBCDIC).</li>
+ * <li>v2.0.1 - Fixed an error when decoding a single byte, that is, when the
+ * encoded data was a single byte.</li>
+ * <li>v2.0 - I got rid of methods that used booleans to set options.
+ * Now everything is more consolidated and cleaner. The code now detects
+ * when data that's being decoded is gzip-compressed and will decompress it
+ * automatically. Generally things are cleaner. You'll probably have to
+ * change some method calls that you were making to support the new
+ * options format (<tt>int</tt>s that you "OR" together).</li>
+ * <li>v1.5.1 - Fixed bug when decompressing and decoding to a
+ * byte[] using <tt>decode( String s, boolean gzipCompressed )</tt>.
+ * Added the ability to "suspend" encoding in the Output Stream so
+ * you can turn on and off the encoding if you need to embed base64
+ * data in an otherwise "normal" stream (like an XML file).</li>
+ * <li>v1.5 - Output stream pases on flush() command but doesn't do anything itself.
+ * This helps when using GZIP streams.
+ * Added the ability to GZip-compress objects before encoding them.</li>
+ * <li>v1.4 - Added helper methods to read/write files.</li>
+ * <li>v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.</li>
+ * <li>v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream
+ * where last buffer being read, if not completely full, was not returned.</li>
+ * <li>v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.</li>
+ * <li>v1.3.3 - Fixed I/O streams which were totally messed up.</li>
+ * </ul>
+ *
+ * <p>
+ * Please visit <a href="http://iharder.net/base64">http://iharder.net/base64</a>
+ * periodically to check for updates or to contribute improvements.
+ * </p>
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 2.2.1
+ */
+public class Base64
+{
+
+
+/* ******** P R I V A T E F I E L D S ******** */
+
+
+ /** Maximum line length (76) of Base64 output. */
+ private final static int MAX_LINE_LENGTH = 76;
+
+
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte)'=';
+
+
+ /** The new line character (\n) as a byte. */
+ private final static byte NEW_LINE = (byte)'\n';
+
+
+/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
+
+ /** The 64 valid Base64 values. */
+ //private final static byte[] ALPHABET;
+ /* Host platform me be something funny like EBCDIC, so we hardcode these values. */
+ private final static byte[] _STANDARD_ALPHABET =
+ {
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
+ (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/'
+ };
+
+
+
+ /** Defeats instantiation. */
+ private Base64(){}
+
+
+/* ******** E N C O D I N G M E T H O D S ******** */
+
+
+ /**
+ * <p>Encodes up to three bytes of the array <var>source</var>
+ * and writes the resulting four Base64 bytes to <var>destination</var>.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * <var>srcOffset</var> and <var>destOffset</var>.
+ * This method does not check to make sure your arrays
+ * are large enough to accomodate <var>srcOffset</var> + 3 for
+ * the <var>source</var> array or <var>destOffset</var> + 4 for
+ * the <var>destination</var> array.
+ * The actual number of significant bytes in your array is
+ * given by <var>numSigBytes</var>.</p>
+ * <p>This is the lowest level of the encoding methods with
+ * all possible parameters.</p>
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @return the <var>destination</var> array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(
+ byte[] source, int srcOffset, int numSigBytes,
+ byte[] destination, int destOffset )
+ {
+ byte[] ALPHABET = _STANDARD_ALPHABET;
+
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index ALPHABET
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an int.
+ int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 )
+ | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 )
+ | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 );
+
+ switch( numSigBytes )
+ {
+ case 3:
+ destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
+ destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+ destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ];
+ destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ];
+ return destination;
+
+ case 2:
+ destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
+ destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+ destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ];
+ destination[ destOffset + 3 ] = EQUALS_SIGN;
+ return destination;
+
+ case 1:
+ destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
+ destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+ destination[ destOffset + 2 ] = EQUALS_SIGN;
+ destination[ destOffset + 3 ] = EQUALS_SIGN;
+ return destination;
+
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Does not GZip-compress data.
+ *
+ * @param source The data to convert
+ * @since 1.4
+ */
+ public static String encodeBytes( byte[] source )
+ {
+ return encodeBytes( source, 0, source.length );
+ } // end encodeBytes
+
+
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * <p>
+ * Valid options:<pre>
+ * GZIP: gzip-compresses object before encoding it.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * <i>Note: Technically, this makes your encoding non-compliant.</i>
+ * </pre>
+ * <p>
+ * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or
+ * <p>
+ * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
+ *
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @param options Specified options
+ * @param options alphabet type is pulled from this (standard, url-safe, ordered)
+ * @see Base64#GZIP
+ * @see Base64#DONT_BREAK_LINES
+ * @since 2.0
+ */
+ public static String encodeBytes( byte[] source, int off, int len )
+ {
+
+ int len43 = len * 4 / 3;
+ byte[] outBuff = new byte[ ( len43 ) // Main 4:3
+ + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding
+ ]; // New lines
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for( ; d < len2; d+=3, e+=4 )
+ {
+ encode3to4( source, d+off, 3, outBuff, e );
+
+ lineLength += 4;
+ if(lineLength == MAX_LINE_LENGTH )
+ {
+ outBuff[e+4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // en dfor: each piece of array
+
+ if( d < len )
+ {
+ encode3to4( source, d+off, len - d, outBuff, e );
+ e += 4;
+ } // end if: some padding needed
+
+ return new String( outBuff, 0, e );
+
+ } // end encodeBytes
+
+
+} // end class Base64
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Challenge.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Challenge.java
new file mode 100644
index 000000000..2a7b2a24a
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Challenge.java
@@ -0,0 +1,198 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn.internal.encode;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public final class Challenge {
+
+ public static final String PRODUCT_ID = "PROD0090YUAUV{2B"; //$NON-NLS-1$
+
+ private static final String PRODUCT_KEY = "YMM8C_H7KCQ2S_KL"; //$NON-NLS-1$
+
+ private static MessageDigest instance;
+
+ static {
+ try {
+ instance = MessageDigest.getInstance("MD5"); //$NON-NLS-1$
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("No MD5 digest found");
+ }
+ }
+
+ public static String createQuery(String challenge) {
+ String[] s = computeMD5DigestAsStringArray((challenge + PRODUCT_KEY)
+ .getBytes());
+ String md5Hash = computeMD5Digest((challenge + PRODUCT_KEY).getBytes());
+ int[] md5 = new int[4];
+ for (int i = 0; i < 4; i++) {
+ md5[i] = Integer.parseInt(s[i], 16);
+ }
+
+ String chl = challenge + PRODUCT_ID;
+ while (chl.length() % 8 != 0) {
+ chl += '0';
+ }
+
+ char[] array = chl.toCharArray();
+ String[] values = new String[chl.length() / 4];
+ for (int i = 0; i < array.length; i += 4) {
+ int j = array[i + 3];
+ String value = Integer.toHexString(j);
+ j = array[i + 2];
+ value += Integer.toHexString(j);
+ j = array[i + 1];
+ value += Integer.toHexString(j);
+ j = array[i];
+ value += Integer.toHexString(j);
+ values[i / 4] = value;
+ }
+
+ int[] ints = new int[values.length];
+ for (int i = 0; i < values.length; i++) {
+ ints[i] = Integer.parseInt(values[i], 16);
+ }
+
+ long high = 0;
+ long low = 0;
+ for (int i = 0; i < ints.length; i += 2) {
+ long temp = ints[i];
+ temp = (temp * 0xe79a9c1L) % 0x7fffffff;
+ temp += high;
+ temp = md5[0] * temp + md5[1];
+ temp = temp % 0x7fffffff;
+
+ high = ints[i + 1];
+ high = (high + temp) % 0x7fffffff;
+ high = md5[2] * high + md5[3];
+ high = high % 0x7fffffff;
+
+ low = low + high + temp;
+ }
+
+ high = (high + md5[1]) % 0x7fffffff;
+ low = (low + md5[3]) % 0x7fffffff;
+
+ String highString = Long.toHexString(high);
+ String lowString = Long.toHexString(low);
+
+ while (highString.length() < 8) {
+ highString = '0' + highString;
+ }
+
+ while (lowString.length() < 8) {
+ lowString = '0' + lowString;
+ }
+
+ highString = highString.substring(6, 8) + highString.substring(4, 6)
+ + highString.substring(2, 4) + highString.substring(0, 2);
+ lowString = lowString.substring(6, 8) + lowString.substring(4, 6)
+ + lowString.substring(2, 4) + lowString.substring(0, 2);
+
+ high = Long.parseLong(highString, 16);
+ low = Long.parseLong(lowString, 16);
+
+ String first = Long.toHexString((Long.parseLong(
+ md5Hash.substring(0, 8), 16) ^ high));
+ String second = Long.toHexString((Long.parseLong(md5Hash.substring(8,
+ 16), 16) ^ low));
+ String third = Long.toHexString((Long.parseLong(md5Hash.substring(16,
+ 24), 16) ^ high));
+ String fourth = Long.toHexString((Long.parseLong(md5Hash.substring(24,
+ 32), 16) ^ low));
+
+ while (first.length() < 8) {
+ first = '0' + first;
+ }
+
+ while (second.length() < 8) {
+ second = '0' + second;
+ }
+
+ while (third.length() < 8) {
+ third = '0' + third;
+ }
+
+ while (fourth.length() < 8) {
+ fourth = '0' + fourth;
+ }
+
+ return first + second + third + fourth;
+ }
+
+ /**
+ * Computes the MD5 digest of a string given its bytes.
+ *
+ * @param bytes
+ * the bytes of the string to be digested
+ * @return the MD5 digest of the original string
+ */
+ private static final String computeMD5Digest(byte[] bytes) {
+ byte[] hash = instance.digest(bytes);
+ StringBuffer buffer = new StringBuffer();
+ synchronized (buffer) {
+ for (int i = 0; i < hash.length; i++) {
+ int value = 0xff & hash[i];
+ if (value < 16) {
+ buffer.append('0').append(Integer.toHexString(value));
+ } else {
+ buffer.append(Integer.toHexString(value));
+ }
+ }
+ return buffer.toString();
+ }
+ }
+
+ private static final String[] computeMD5DigestAsStringArray(byte[] bytes) {
+ byte[] hash = instance.digest(bytes);
+ StringBuffer buffer = new StringBuffer();
+ synchronized (buffer) {
+ for (int i = 0; i < hash.length; i++) {
+ int value = 0xff & hash[i];
+ if (value < 16) {
+ buffer.append('0').append(Integer.toHexString(value));
+ } else {
+ buffer.append(Integer.toHexString(value));
+ }
+ }
+ }
+
+ String result = buffer.toString();
+ String[] results = new String[4];
+ results[0] = result.substring(0, 8);
+ results[1] = result.substring(8, 16);
+ results[2] = result.substring(16, 24);
+ results[3] = result.substring(24, 32);
+
+ for (int i = 0; i < 4; i++) {
+ char[] array = results[i].toCharArray();
+ char[] swapped = new char[8];
+ for (int j = 0; j < 8; j += 2) {
+ swapped[7 - j] = array[j + 1];
+ swapped[6 - j] = array[j];
+ }
+ results[i] = new String(swapped);
+ }
+
+ for (int i = 0; i < 4; i++) {
+ long l = Long.parseLong(results[i], 16);
+ l = l & 0x7fffffff;
+ if (l < 0x10000000) {
+ results[i] = '0' + Long.toHexString(l);
+ } else {
+ results[i] = Long.toHexString(l);
+ }
+ }
+
+ return results;
+ }
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Encryption.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Encryption.java
new file mode 100644
index 000000000..3634156b7
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/Encryption.java
@@ -0,0 +1,65 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn.internal.encode;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * This class provides static methods to compute the SHA digest of strings.
+ */
+public class Encryption {
+
+ private static MessageDigest shaDigest;
+
+ static {
+ try {
+ shaDigest = MessageDigest.getInstance("SHA-1"); //$NON-NLS-1$
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException();
+ }
+ }
+
+ /**
+ * Computes the SHA hash of the bytes provided and then truncate it down to
+ * a length of twenty. A base 64 encoding of the twenty character string
+ * literal is then computed and returned.
+ *
+ * @param bytes
+ * the bytes to be computed from
+ * @return a base 64 encoding of a twenty character SHA hash of the bytes
+ * provided
+ */
+ public static String computeSHA(byte[] bytes) {
+ synchronized (shaDigest) {
+ bytes = shaDigest.digest(bytes);
+ }
+
+ StringBuffer buffer = new StringBuffer();
+ synchronized (buffer) {
+ for (int i = 0; i < 20; i++) {
+ if (0 < bytes[i] && bytes[i] < 16) {
+ buffer.append('0');
+ }
+ buffer.append(Integer.toHexString((0xff & bytes[i])));
+ }
+
+ bytes = new byte[20];
+ for (int i = 0; i < 20; i++) {
+ bytes[i] = (byte) Integer.parseInt(buffer.substring(i * 2,
+ i * 2 + 2), 16);
+ }
+ }
+
+ return Base64.encodeBytes(bytes);
+ }
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/ResponseCommand.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/ResponseCommand.java
new file mode 100644
index 000000000..e6d6244ab
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/ResponseCommand.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn.internal.encode;
+
+/**
+ * The ResponseCommand class processes one line of simple output from the MSN
+ * servers. It provides methods to handle the output easier by separating the
+ * command and its parameters.
+ */
+public class ResponseCommand {
+
+ /**
+ * The command of the output, there is a large variety of commands in use
+ * currently and are all three characters long in uppercase.
+ */
+ private String cmd;
+
+ /**
+ * The parameters that was sent with the command.
+ */
+ private String[] params;
+
+ /**
+ * Creates a new ResponseCommand that will process the given line. The
+ * constructor does not do anything outside of calling
+ *
+ * @link #process(String) on the given line.
+ *
+ * @param line
+ * the line that this should represent
+ */
+ public ResponseCommand(String line) {
+ process(line);
+ }
+
+ /**
+ * Process the given line. It will store the first three characters as the
+ * command and the rest of the line will be split by a single space and
+ * stored as a String array. If <code>line</code> is null, both the
+ * command and the String array will store null pointers.
+ *
+ * @param line
+ * the line to be processed
+ */
+ public void process(String line) {
+ if (line == null) {
+ cmd = null;
+ params = null;
+ } else {
+ cmd = line.substring(0, 3);
+ params = StringUtils.splitOnSpace(line.substring(4));
+ }
+ }
+
+ /**
+ * Returns the command of this line.
+ *
+ * @return the three character command
+ */
+ public String getCommand() {
+ return cmd;
+ }
+
+ /**
+ * Returns the string literal stored at the given index in params. If the
+ * first parameter is desired, a 0 should be passed.
+ *
+ * @param index
+ * the parameter at the given index
+ * @return the desired parameter that is at the given index
+ */
+ public String getParam(int index) {
+ return params[index];
+ }
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/StringUtils.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/StringUtils.java
new file mode 100644
index 000000000..1cd3b970e
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/StringUtils.java
@@ -0,0 +1,224 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn.internal.encode;
+
+import java.util.ArrayList;
+
+/**
+ * <p>
+ * The StringUtils class provides static methods that helps make string
+ * manipulation easy. The primary functionality it is meant to provide is the
+ * ability to split a string into a string array based on a given delimiter.
+ * This functionality is meant to take the place of the split(String) and
+ * split(String, int) method that was introduced in J2SE-1.4. Please note,
+ * however, that the splitting performed by this class simply splits the string
+ * based on the delimiter and does not perform any regular expression matching
+ * like the split methods provided in J2SE-1.4.
+ * </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 StringUtils {
+
+ public static final String[] splitOnSpace(String string) {
+ int index = string.indexOf(' ');
+ if (index == -1) {
+ return new String[] { string };
+ }
+
+ ArrayList split = new ArrayList();
+ while (index != -1) {
+ split.add(string.substring(0, index));
+ string = string.substring(index + 1);
+ index = string.indexOf(' ');
+ }
+
+ if (!string.equals("")) { //$NON-NLS-1$
+ split.add(string);
+ }
+
+ return (String[]) split.toArray(new String[split.size()]);
+ }
+
+ public static final String[] split(String string, char character) {
+ int index = string.indexOf(character);
+ if (index == -1) {
+ return new String[] { string };
+ }
+
+ ArrayList split = new ArrayList();
+ while (index != -1) {
+ split.add(string.substring(0, index));
+ string = string.substring(index + 1);
+ index = string.indexOf(character);
+ }
+
+ if (!string.equals("")) { //$NON-NLS-1$
+ split.add(string);
+ }
+
+ return (String[]) split.toArray(new String[split.size()]);
+ }
+
+ public static final String[] split(String string, String delimiter) {
+ int index = string.indexOf(delimiter);
+ if (index == -1) {
+ return new String[] { string };
+ }
+
+ int length = delimiter.length();
+ ArrayList split = new ArrayList();
+ while (index != -1) {
+ split.add(string.substring(0, index));
+ string = string.substring(index + length);
+ index = string.indexOf(delimiter);
+ }
+
+ if (!string.equals("")) { //$NON-NLS-1$
+ split.add(string);
+ }
+
+ return (String[]) split.toArray(new String[split.size()]);
+ }
+
+ public static final String[] split(String string, String delimiter,
+ int limit) {
+ int index = string.indexOf(delimiter);
+ if (index == -1) {
+ return new String[] { string };
+ }
+
+ int count = 0;
+ int length = delimiter.length();
+ ArrayList split = new ArrayList(limit);
+ while (index != -1 && count < limit - 1) {
+ split.add(string.substring(0, index));
+ string = string.substring(index + length);
+ index = string.indexOf(delimiter);
+ count++;
+ }
+
+ if (!string.equals("")) { //$NON-NLS-1$
+ split.add(string);
+ }
+
+ return (String[]) split.toArray(new String[split.size()]);
+ }
+
+ public static final String splitSubstring(String string, String delimiter,
+ int pos) {
+ int index = string.indexOf(delimiter);
+ if (index == -1) {
+ return string;
+ }
+
+ int count = 0;
+ int length = delimiter.length();
+ while (count < pos) {
+ string = string.substring(index + length);
+ index = string.indexOf(delimiter);
+ count++;
+ }
+
+ return index == -1 ? string : string.substring(0, index);
+ }
+
+ public static final String xmlDecode(String string) {
+ if (string.equals("")) { //$NON-NLS-1$
+ return string;
+ }
+
+ int index = string.indexOf("&amp;"); //$NON-NLS-1$
+ while (index != -1) {
+ string = string.substring(0, index) + '&'
+ + string.substring(index + 5);
+ index = string.indexOf("&amp;", index + 1); //$NON-NLS-1$
+ }
+
+ index = string.indexOf("&quot;"); //$NON-NLS-1$
+ while (index != -1) {
+ string = string.substring(0, index) + '"'
+ + string.substring(index + 6);
+ index = string.indexOf("&quot;", index + 1); //$NON-NLS-1$
+ }
+
+ index = string.indexOf("&apos;"); //$NON-NLS-1$
+ while (index != -1) {
+ string = string.substring(0, index) + '\''
+ + string.substring(index + 6);
+ index = string.indexOf("&apos;", index + 1); //$NON-NLS-1$
+ }
+
+ index = string.indexOf("&lt;"); //$NON-NLS-1$
+ while (index != -1) {
+ string = string.substring(0, index) + '<'
+ + string.substring(index + 4);
+ index = string.indexOf("&lt;", index + 1); //$NON-NLS-1$
+ }
+
+ index = string.indexOf("&gt;"); //$NON-NLS-1$
+ while (index != -1) {
+ string = string.substring(0, index) + '>'
+ + string.substring(index + 4);
+ index = string.indexOf("&gt;", index + 1); //$NON-NLS-1$
+ }
+ return string;
+ }
+
+ public static final String xmlEncode(String string) {
+ if (string.equals("")) { //$NON-NLS-1$
+ return string;
+ }
+
+ int index = string.indexOf('&');
+ while (index != -1) {
+ string = string.substring(0, index) + "&amp;" //$NON-NLS-1$
+ + string.substring(index + 1);
+ index = string.indexOf('&', index + 1);
+ }
+
+ index = string.indexOf('"');
+ while (index != -1) {
+ string = string.substring(0, index) + "&quot;" //$NON-NLS-1$
+ + string.substring(index + 1);
+ index = string.indexOf('"', index + 1);
+ }
+
+ index = string.indexOf('\'');
+ while (index != -1) {
+ string = string.substring(0, index) + "&apos;" //$NON-NLS-1$
+ + string.substring(index + 1);
+ index = string.indexOf('\'', index + 1);
+ }
+
+ index = string.indexOf('<');
+ while (index != -1) {
+ string = string.substring(0, index) + "&lt;" //$NON-NLS-1$
+ + string.substring(index + 1);
+ index = string.indexOf('<', index + 1);
+ }
+
+ index = string.indexOf('>');
+ while (index != -1) {
+ string = string.substring(0, index) + "&gt;" //$NON-NLS-1$
+ + string.substring(index + 1);
+ index = string.indexOf('>', index + 1);
+ }
+ return string;
+ }
+
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/package.html b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/package.html
new file mode 100644
index 000000000..13014e71e
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/encode/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+Package containing classes that are used internally by the protocol implementation. Client developers should not need to use these classes at all.
+</body>
+</html>
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/ClientTicketRequest.java b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/ClientTicketRequest.java
new file mode 100644
index 000000000..d1701b9e9
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/ClientTicketRequest.java
@@ -0,0 +1,135 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2007 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
+ ******************************************************************************/
+package org.eclipse.ecf.protocol.msn.internal.net;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+
+import org.eclipse.ecf.protocol.msn.internal.encode.StringUtils;
+
+/**
+ * The ClientTicketRequest class authenticates the user through Passport. This
+ * is a necessary procedure during the NotificationSession authentication
+ * process.
+ */
+public final class ClientTicketRequest {
+
+ /**
+ * This String value holds the URL of the Passport Nexus page -
+ * https://nexus.passport.com/rdr/pprdr.asp
+ */
+ private static final String PASSPORT_NEXUS = "https://nexus.passport.com/rdr/pprdr.asp"; //$NON-NLS-1$
+
+ /**
+ * The connection that will be used to perform all http requests.
+ */
+ private HttpURLConnection request;
+
+ /**
+ * TODO: documentation
+ */
+ private String daLoginURL;
+
+ private boolean cancelled = false;
+
+ /**
+ * Creates a new ClientTicketRequest object with http redirects set to true.
+ */
+ public ClientTicketRequest() {
+ HttpURLConnection.setFollowRedirects(true);
+ }
+
+ public void setCancelled(boolean cancelled) {
+ this.cancelled = cancelled;
+ }
+
+ /**
+ * Retrieves information from {@link #PASSPORT_NEXUS} and stores it in
+ * {@link #passportInfo}.
+ *
+ * @return <code>true</code> if the retrieval process completed
+ * successfully
+ * @throws IOException
+ * If an I/O error occurs while attempting to connect to the
+ * Passport Nexus page
+ */
+ private boolean getLoginServerAddress() throws IOException {
+ request = (HttpURLConnection) new URL(PASSPORT_NEXUS).openConnection();
+ if (request.getResponseCode() == HttpURLConnection.HTTP_OK) {
+ daLoginURL = StringUtils.splitSubstring(request
+ .getHeaderField("PassportURLs"), ",", 1); //$NON-NLS-1$ //$NON-NLS-2$
+ daLoginURL = "https://" //$NON-NLS-1$
+ + daLoginURL.substring(daLoginURL.indexOf('=') + 1);
+ request.disconnect();
+ return true;
+ }
+ request.disconnect();
+ return false;
+ }
+
+ /**
+ * Retrieves the client ticket that is associated with the given username,
+ * password, and challenge string.
+ *
+ * @param username
+ * the user's email address
+ * @param password
+ * the user's password
+ * @param challengeString
+ * the challenge string received from the notification session
+ * @return the client ticket
+ * @throws IOException
+ * If an I/O error occurs while connecting to the Passport Nexus
+ * page or when getting the response codes from the connection
+ */
+ public synchronized String getTicket(String username, String password,
+ String challengeString) throws IOException {
+ if (getLoginServerAddress()) {
+ username = URLEncoder.encode(username);
+ password = URLEncoder.encode(password);
+ try {
+ while (!cancelled) {
+ request = (HttpURLConnection) new URL(daLoginURL)
+ .openConnection();
+ request.setRequestProperty("Authorization", //$NON-NLS-1$
+ "Passport1.4 OrgVerb=GET,OrgURL=http%3A%2F%2Fmessenger%2Emsn%2Ecom,sign-in=" //$NON-NLS-1$
+ + username + ",pwd=" + password + ',' //$NON-NLS-1$
+ + challengeString);
+ if (request.getResponseCode() == HttpURLConnection.HTTP_OK) {
+ password = null;
+ String authenticationInfo = request
+ .getHeaderField("Authentication-Info"); //$NON-NLS-1$
+ int start = authenticationInfo.indexOf('\'');
+ int end = authenticationInfo.lastIndexOf('\'');
+ request.disconnect();
+ return authenticationInfo.substring(start + 1, end);
+ } else if (request.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) {
+ daLoginURL = request.getHeaderField("Location"); //$NON-NLS-1$
+ // truncate the uri as the received string is of the
+ // form [http://www.msn.com/]
+ daLoginURL = daLoginURL.substring(1, daLoginURL
+ .length() - 1);
+ }
+ }
+ } catch (Exception e) {
+ if (request.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ return "401"; //$NON-NLS-1$
+ }
+ e.printStackTrace();
+ } finally {
+ request.disconnect();
+ }
+ }
+ return "0"; //$NON-NLS-1$
+ }
+}
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/package.html b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/package.html
new file mode 100644
index 000000000..a77a57fab
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/internal/net/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+Provides classes that deals with connecting to websites or servers that are not directly related to MSN.
+</body>
+</html>
diff --git a/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/package.html b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/package.html
new file mode 100644
index 000000000..22bd8df44
--- /dev/null
+++ b/protocols/bundles/org.eclipse.ecf.protocol.msn/src/org/eclipse/ecf/protocol/msn/package.html
@@ -0,0 +1,5 @@
+<html>
+<body>
+Provides support for connecting to the MSN servers used by Windows Live Messenger.
+</body>
+</html>

Back to the top