From 29073a307aed530f813a547364ec63369a6a09b2 Mon Sep 17 00:00:00 2001 From: Gaston Dombiak Date: Thu, 14 Sep 2006 19:21:38 +0000 Subject: [PATCH] Added reconnection support. SMACK-172 git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@5368 b35dd754-fafc-0310-a699-88a17e54d16e --- .../jivesoftware/smack/XMPPConnection.java | 371 ++++++++++++------ 1 file changed, 247 insertions(+), 124 deletions(-) diff --git a/source/org/jivesoftware/smack/XMPPConnection.java b/source/org/jivesoftware/smack/XMPPConnection.java index fdbd705f1..f5dbfe2fc 100644 --- a/source/org/jivesoftware/smack/XMPPConnection.java +++ b/source/org/jivesoftware/smack/XMPPConnection.java @@ -51,13 +51,25 @@ import java.util.concurrent.ConcurrentHashMap; *
  * // Create a connection to the jivesoftware.com XMPP server.
  * XMPPConnection con = new XMPPConnection("jivesoftware.com");
+ * // Connect to the server
+ * con.connect();
  * // Most servers require you to login before performing other tasks.
  * con.login("jsmith", "mypass");
  * // Start a new conversation with John Doe and send him a message.
  * Chat chat = con.createChat("jdoe@jabber.org");
  * chat.sendMessage("Hey, how's it going?");
+ * // Disconnect from the server
+ * con.disconnect();
  * 
* + * XMPPConnections can be reused between connections. This means that an XMPPConnection + * may be connected, disconnected and then connected again. Listeners of the XMPPConnection + * will remain accross connections.

+ * + * If a connected XMPPConnection gets disconnected abruptly then it will try to reconnect + * again. To stop the reconnection process just use {@link #disconnect()}. Once stopped + * you can use {@link #connect()} to manually connect to the server. + * * @author Matt Tucker */ public class XMPPConnection { @@ -113,7 +125,15 @@ public class XMPPConnection { String connectionID; private String user = null; private boolean connected = false; + /** + * Flag that indicates if the user is currently authenticated with the server. + */ private boolean authenticated = false; + /** + * Flag that indicates if the user was authenticated with the server when the connection + * to the server was closed (abruptly or not). + */ + private boolean wasAuthenticated = false; private boolean anonymous = false; private boolean usingTLS = false; @@ -122,7 +142,7 @@ public class XMPPConnection { Roster roster = null; private AccountManager accountManager = null; - private SASLAuthentication saslAuthentication = new SASLAuthentication(this); + protected SASLAuthentication saslAuthentication = new SASLAuthentication(this); Writer writer; Reader reader; @@ -147,22 +167,20 @@ public class XMPPConnection { * Holds the initial configuration used while creating the connection. */ private ConnectionConfiguration configuration; - + /** * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be * performed to try to determine the IP address and port corresponding to the * serviceName; if that lookup fails, it's assumed that server resides at serviceName * with the default port of 5222. This is the preferred constructor for connecting - * to an XMPP server. + * to an XMPP server.

+ * + * Note that XMPPConnection constructors do not establish the connection to the server, + * to make it effective use the connect method. {@link #connect()}. * * @param serviceName the name of the XMPP server to connect to; e.g. jivesoftware.com. - * @throws XMPPException if an error occurs while trying to establish the connection. - * Two possible errors can occur which will be wrapped by an XMPPException -- - * UnknownHostException (XMPP error code 504), and IOException (XMPP error code - * 502). The error codes and wrapped exceptions can be used to present more - * appropiate error messages to end-users. */ - public XMPPConnection(String serviceName) throws XMPPException { + public XMPPConnection(String serviceName) { // Perform DNS lookup to get host and port to use DNSUtil.HostAddress address = DNSUtil.resolveXMPPDomain(serviceName); // Create the configuration for this new connection @@ -172,53 +190,46 @@ public class XMPPConnection { config.setCompressionEnabled(false); config.setSASLAuthenticationEnabled(true); config.setDebuggerEnabled(DEBUG_ENABLED); - // Set the new connection configuration - connectUsingConfiguration(config, null); + init(config, null); } /** - * Creates a new connection to the XMPP server at the specifiec host and port. + * Creates a new connection to the XMPP server at the specifiec host and port.

+ * + * Note that XMPPConnection constructors do not establish the connection to the server, + * to make it effective use the connect method. {@link #connect()}. * * @param host the name of the XMPP server to connect to; e.g. jivesoftware.com. * @param port the port on the server that should be used; e.g. 5222. - * @throws XMPPException if an error occurs while trying to establish the connection. - * Two possible errors can occur which will be wrapped by an XMPPException -- - * UnknownHostException (XMPP error code 504), and IOException (XMPP error code - * 502). The error codes and wrapped exceptions can be used to present more - * appropiate error messages to end-users. */ - public XMPPConnection(String host, int port) throws XMPPException { + public XMPPConnection(String host, int port) { // Create the configuration for this new connection ConnectionConfiguration config = new ConnectionConfiguration(host, port); config.setTLSEnabled(true); config.setCompressionEnabled(false); config.setSASLAuthenticationEnabled(true); config.setDebuggerEnabled(DEBUG_ENABLED); - // Set the new connection configuration - connectUsingConfiguration(config, null); + init(config, null); } /** - * Creates a new connection to the specified XMPP server on the given host and port. + * Creates a new connection to the specified XMPP server on the given host and port.

+ * + * Note that XMPPConnection constructors do not establish the connection to the server, + * to make it effective use the connect method. {@link #connect()}. * * @param host the host name, or null for the loopback address. * @param port the port on the server that should be used; e.g. 5222. * @param serviceName the name of the XMPP server to connect to; e.g. jivesoftware.com. - * @throws XMPPException if an error occurs while trying to establish the connection. - * Two possible errors can occur which will be wrapped by an XMPPException -- - * UnknownHostException (XMPP error code 504), and IOException (XMPP error code - * 502). The error codes and wrapped exceptions can be used to present more - * appropiate error messages to end-users. */ - public XMPPConnection(String host, int port, String serviceName) throws XMPPException { + public XMPPConnection(String host, int port, String serviceName) { // Create the configuration for this new connection ConnectionConfiguration config = new ConnectionConfiguration(host, port, serviceName); config.setTLSEnabled(true); config.setCompressionEnabled(false); config.setSASLAuthenticationEnabled(true); config.setDebuggerEnabled(DEBUG_ENABLED); - // Set the new connection configuration - connectUsingConfiguration(config, null); + init(config, null); } /** @@ -227,53 +238,44 @@ public class XMPPConnection { * * A custom SocketFactory allows fine-grained control of the actual connection to the * XMPP server. A typical use for a custom SocketFactory is when connecting through a - * SOCKS proxy. + * SOCKS proxy.

+ * + * Note that XMPPConnection constructors do not establish the connection to the server, + * to make it effective use the connect method. {@link #connect()}. * * @param host the host name, or null for the loopback address. * @param port the port on the server that should be used; e.g. 5222. * @param serviceName the name of the XMPP server to connect to; e.g. jivesoftware.com. * @param socketFactory a SocketFactory that will be used to create the socket to the XMPP * server. - * @throws XMPPException if an error occurs while trying to establish the connection. - * Two possible errors can occur which will be wrapped by an XMPPException -- - * UnknownHostException (XMPP error code 504), and IOException (XMPP error code - * 502). The error codes and wrapped exceptions can be used to present more - * appropiate error messages to end-users. */ - public XMPPConnection(String host, int port, String serviceName, SocketFactory socketFactory) - throws XMPPException - { + public XMPPConnection(String host, int port, String serviceName, SocketFactory socketFactory) { // Create the configuration for this new connection ConnectionConfiguration config = new ConnectionConfiguration(host, port, serviceName); config.setTLSEnabled(true); config.setCompressionEnabled(false); config.setSASLAuthenticationEnabled(true); config.setDebuggerEnabled(DEBUG_ENABLED); - // Set the new connection configuration - connectUsingConfiguration(config, socketFactory); + init(config, socketFactory); } - public XMPPConnection(ConnectionConfiguration config) throws XMPPException { - // Set the new connection configuration - connectUsingConfiguration(config, null); + public XMPPConnection(ConnectionConfiguration config) { + init(config, null); } - public XMPPConnection(ConnectionConfiguration config, SocketFactory socketFactory) - throws XMPPException { - // Set the new connection configuration - connectUsingConfiguration(config, socketFactory); + public XMPPConnection(ConnectionConfiguration config, SocketFactory socketFactory) { + init(config, socketFactory); } - private void connectUsingConfiguration(ConnectionConfiguration config, - SocketFactory socketFactory) throws XMPPException { + private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException { this.host = config.getHost(); this.port = config.getPort(); try { - if (socketFactory == null) { + if (config.getSocketFactory() == null) { this.socket = new Socket(host, port); } else { - this.socket = socketFactory.createSocket(host, port); + this.socket = config.getSocketFactory().createSocket(host, port); } } catch (UnknownHostException uhe) { @@ -289,15 +291,7 @@ public class XMPPConnection { XMPPError.Condition.remote_server_error, errorMessage), ioe); } this.serviceName = config.getServiceName(); - try { - // Keep a copy to be sure that once the configuration has been passed to the - // constructor it cannot be modified - this.configuration = (ConnectionConfiguration) config.clone(); - } - catch (CloneNotSupportedException e) { - // Do nothing - } - init(); + initConnection(); } /** @@ -403,7 +397,11 @@ public class XMPPConnection { * presence may optionally be sent. If sendPresence * is false, a presence packet must be sent manually later. If more than five seconds * (default timeout) elapses in each step of the authentication process without a - * response from the server, or if an error occurs, a XMPPException will be thrown. + * response from the server, or if an error occurs, a XMPPException will be thrown.

+ * + * Before logging in (i.e. authenticate) to the server the connection must be connected. + * For compatibility and easiness of use the connection will automatically connect to the + * server if not already connected. * * @param username the username. * @param password the password. @@ -456,19 +454,24 @@ public class XMPPConnection { useCompression(); } - // Create the roster. - this.roster = new Roster(this); + // Create the roster if it is not a reconnection. + if (this.roster == null) { + this.roster = new Roster(this); + } roster.reload(); // Set presence to online. if (sendPresence) { packetWriter.sendPacket(new Presence(Presence.Type.available)); } - + // Indicate that we're now authenticated. authenticated = true; anonymous = false; + // Stores the autentication for future reconnection + this.getConfiguration().setUsernameAndPassword(username, password); + // If debugging is enabled, change the the debug window title to include the // name we are now logged-in as. // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger @@ -642,9 +645,12 @@ public class XMPPConnection { /** * Closes the connection by setting presence to unavailable then closing the stream to - * the XMPP server. Once a connection has been closed, it cannot be re-opened. + * the XMPP server. The shutdown logic will be used during a planned disconnection or when + * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's + * {@link PacketReader}, {@link PacketWriter} and {@link Roster} will not be removed thus + * connection's state is kept. */ - public void close() { + protected void shutdown() { // Set presence to offline. packetWriter.sendPacket(new Presence(Presence.Type.unavailable)); packetReader.shutdown(); @@ -675,8 +681,32 @@ public class XMPPConnection { catch (Exception e) { // Ignore. } + + this.setWasAuthenticated(authenticated); authenticated = false; connected = false; + + saslAuthentication.init(); + } + + /** + * Closes the connection by setting presence to unavailable then closing the stream to + * the XMPP server. The XMPPConnection can still be used for connecting to the server + * again. + * + * The difference between disconnect and shutdown is that disconnect makes a complete reset + * of the connection state whereas shutdown only cleans the connection and keeps alive + * packet reader listeners, previous login and roster presences. + */ + public void disconnect() { + this.shutdown(); + + this.roster = null; + + this.wasAuthenticated = false; + + packetWriter = null; + packetReader = null; } /** @@ -834,29 +864,53 @@ public class XMPPConnection { } } + /** + * Initializes the connection configuration. This method is only executed + * when the XMPPConnection is created. + */ + private void init(ConnectionConfiguration config, SocketFactory socketFactory) { + try { + // Keep a copy to be sure that once the configuration has been passed to the + // constructor it cannot be modified + this.configuration = (ConnectionConfiguration) config.clone(); + this.configuration.setSocketFactory(socketFactory); + } + catch (CloneNotSupportedException e) { + // Do nothing + } + } + /** * Initializes the connection by creating a packet reader and writer and opening a * XMPP stream to the server. * * @throws XMPPException if establishing a connection to the server fails. */ - private void init() throws XMPPException { + private void initConnection() throws XMPPException { // Set the reader and writer instance variables initReaderAndWriter(); - try - { - packetWriter = new PacketWriter(this); - packetReader = new PacketReader(this); + try { + boolean isFirstInitialization = packetReader == null || packetWriter == null; - // If debugging is enabled, we should start the thread that will listen for - // all packets and then log them. - if (configuration.isDebuggerEnabled()) { - packetReader.addPacketListener(debugger.getReaderListener(), null); - if (debugger.getWriterListener() != null) { - packetWriter.addPacketListener(debugger.getWriterListener(), null); + if (isFirstInitialization) { + packetWriter = new PacketWriter(this); + packetReader = new PacketReader(this); + + // If debugging is enabled, we should start the thread that will listen for + // all packets and then log them. + if (configuration.isDebuggerEnabled()) { + packetReader.addPacketListener(debugger.getReaderListener(), null); + if (debugger.getWriterListener() != null) { + packetWriter.addPacketListener(debugger.getWriterListener(), null); + } } } + else { + packetWriter.init(); + packetReader.init(); + } + // Start the packet writer. This will open a XMPP stream to the server packetWriter.startup(); // Start the packet reader. The startup() method will block until we @@ -869,99 +923,123 @@ public class XMPPConnection { // Start keep alive process (after TLS was negotiated - if available) packetWriter.startKeepAliveProcess(); - // Notify that a new connection has been established - connectionEstablished(this); - // Add a listener for all message packets so that we can deliver errant - // messages to the best Chat instance available. - addPacketListener(new PacketListener() { - public void processPacket(Packet packet) { - Message message = (Message)packet; - // Ignore any messages with a thread ID, as they will likely - // already be associated with a Chat. This will miss messages - // with new thread ID values, but we can only assume that a - // listener is registered to deal with this case. - if (message.getThread() == null && - message.getType() != Message.Type.GROUP_CHAT && - message.getType() != Message.Type.HEADLINE) { - WeakReference chatRef = - chats.get(StringUtils.parseBareAddress(message.getFrom())); - if (chatRef != null) { - // Do some extra clean-up if the reference was cleared. - Chat chat = chatRef.get(); - if (chat == null) { - chats.remove(message.getFrom()); - } - else { - chat.deliver(message); + if (isFirstInitialization) { + // Notify that a new connection has been established + connectionEstablished(this); + + // Add a listener for all message packets so that we can deliver errant + // messages to the best Chat instance available. + addPacketListener(new PacketListener() { + public void processPacket(Packet packet) { + Message message = (Message) packet; + // Ignore any messages with a thread ID, as they will likely + // already be associated with a Chat. This will miss messages + // with new thread ID values, but we can only assume that a + // listener is registered to deal with this case. + if (message.getThread() == null && + message.getType() != Message.Type.GROUP_CHAT && + message.getType() != Message.Type.HEADLINE) { + WeakReference chatRef = + chats.get(StringUtils.parseBareAddress(message.getFrom())); + if (chatRef != null) { + // Do some extra clean-up if the reference was cleared. + Chat chat = chatRef.get(); + if (chat == null) { + chats.remove(message.getFrom()); + } + else { + chat.deliver(message); + } } } } - } - }, new PacketTypeFilter(Message.class)); + }, new PacketTypeFilter(Message.class)); + } + else { + packetReader.notifyReconnection(); + } + } catch (XMPPException ex) { // An exception occurred in setting up the connection. Make sure we shut down the // readers and writers and close the socket. if (packetWriter != null) { - try { packetWriter.shutdown(); } catch (Throwable ignore) { /* ignore */ } + try { + packetWriter.shutdown(); + } + catch (Throwable ignore) { /* ignore */ } packetWriter = null; } if (packetReader != null) { - try { packetReader.shutdown(); } catch (Throwable ignore) { /* ignore */ } + try { + packetReader.shutdown(); + } + catch (Throwable ignore) { /* ignore */ } packetReader = null; } if (reader != null) { - try { reader.close(); } catch (Throwable ignore) { /* ignore */ } + try { + reader.close(); + } + catch (Throwable ignore) { /* ignore */ } reader = null; } if (writer != null) { - try { writer.close(); } catch (Throwable ignore) { /* ignore */} + try { + writer.close(); + } + catch (Throwable ignore) { /* ignore */} writer = null; } if (socket != null) { - try { socket.close(); } catch (Exception e) { /* ignore */ } + try { + socket.close(); + } + catch (Exception e) { /* ignore */ } socket = null; } + this.setWasAuthenticated(authenticated); authenticated = false; connected = false; - throw ex; // Everything stoppped. Now throw the exception. + throw ex; // Everything stoppped. Now throw the exception. } } private void initReaderAndWriter() throws XMPPException { try { if (!usingCompression) { - reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); - writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); + reader = + new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); + writer = new BufferedWriter( + new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); } else { try { Class zoClass = Class.forName("com.jcraft.jzlib.ZOutputStream"); - //ZOutputStream out = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_COMPRESSION); Constructor constructor = zoClass.getConstructor(OutputStream.class, Integer.TYPE); Object out = constructor.newInstance(socket.getOutputStream(), 9); - //out.setFlushMode(JZlib.Z_PARTIAL_FLUSH); Method method = zoClass.getMethod("setFlushMode", Integer.TYPE); method.invoke(out, 1); - writer = new BufferedWriter(new OutputStreamWriter((OutputStream) out, "UTF-8")); + writer = + new BufferedWriter(new OutputStreamWriter((OutputStream) out, "UTF-8")); Class ziClass = Class.forName("com.jcraft.jzlib.ZInputStream"); - //ZInputStream in = new ZInputStream(socket.getInputStream()); constructor = ziClass.getConstructor(InputStream.class); Object in = constructor.newInstance(socket.getInputStream()); - //in.setFlushMode(JZlib.Z_PARTIAL_FLUSH); method = ziClass.getMethod("setFlushMode", Integer.TYPE); method.invoke(in, 1); reader = new BufferedReader(new InputStreamReader((InputStream) in, "UTF-8")); } catch (Exception e) { e.printStackTrace(); - reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8")); - writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); + reader = new BufferedReader( + new InputStreamReader(socket.getInputStream(), "UTF-8")); + writer = new BufferedWriter( + new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); } } } @@ -1002,7 +1080,8 @@ public class XMPPConnection { } catch (Exception ex) { try { - debuggerClass = Class.forName("org.jivesoftware.smack.debugger.LiteDebugger"); + debuggerClass = + Class.forName("org.jivesoftware.smack.debugger.LiteDebugger"); } catch (Exception ex2) { ex2.printStackTrace(); @@ -1012,8 +1091,8 @@ public class XMPPConnection { // Create a new debugger instance. If an exception occurs then disable the debugging // option try { - Constructor constructor = - debuggerClass.getConstructor(XMPPConnection.class, Writer.class, Reader.class); + Constructor constructor = debuggerClass + .getConstructor(XMPPConnection.class, Writer.class, Reader.class); debugger = (SmackDebugger) constructor.newInstance(this, writer, reader); reader = debugger.getReader(); writer = debugger.getWriter(); @@ -1071,11 +1150,11 @@ public class XMPPConnection { } /** - * Returns the configuration used while connecting to the server. + * Returns the configuration used to connect to the server. * - * @return the configuration used while connecting to the server. + * @return the configuration used to connect to the server. */ - ConnectionConfiguration getConfiguration() { + protected ConnectionConfiguration getConfiguration() { return configuration; } @@ -1244,4 +1323,48 @@ public class XMPPConnection { this.notify(); } } + /** + * Establishes a connection to the XMPP server and performs an automatic login + * only if the previous connection state was logged (authenticated). It basically + * creates and maintains a socket connection to the server.

+ * + * Listeners will be preserved from a previous connection if the reconnection + * occurs after an abrupt termination. + * + * @throws XMPPException if an error occurs while trying to establish the connection. + * Two possible errors can occur which will be wrapped by an XMPPException -- + * UnknownHostException (XMPP error code 504), and IOException (XMPP error code + * 502). The error codes and wrapped exceptions can be used to present more + * appropiate error messages to end-users. + */ + public void connect() throws XMPPException { + // Stablishes the connection, readers and writers + connectUsingConfiguration(configuration); + // Automatically makes the login if the user was previouslly connected successfully + // to the server and the connection was terminated abruptly + if (connected && wasAuthenticated) { + // Make the login + try { + if (isAnonymous()) { + // Make the anonymous login + loginAnonymously(); + } else { + login(getConfiguration().getUsername(), getConfiguration().getPassword()); + } + } catch (XMPPException e) { + e.printStackTrace(); + } + } + } + + /** + * Sets if the connection has already logged in the server. + * + * @param wasAuthenticated + */ + private void setWasAuthenticated(boolean wasAuthenticated) { + if (!this.wasAuthenticated) { + this.wasAuthenticated = wasAuthenticated; + } + } } \ No newline at end of file