1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-06-26 13:24:49 +02:00
Smack/source/org/jivesoftware/smack/XMPPConnection.java
Gaston Dombiak 53c97378d3 1) Fixed reconnection when connection was using compression.
2) Fixed conflict error while reconnecting (reconnection was not keeping track of resource and sendPresence.
3) Improved javadoc (and checking) that reconnectionlisteners should be added once connected.
4) Added new reconnection test case for reconnection and multiple resources.

git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/trunk@6050 b35dd754-fafc-0310-a699-88a17e54d16e
2006-11-10 19:06:33 +00:00

1371 lines
53 KiB
Java

/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright 2003-2004 Jive Software.
*
* All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack;
import org.jivesoftware.smack.debugger.SmackDebugger;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.util.DNSUtil;
import org.jivesoftware.smack.util.StringUtils;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import java.io.*;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Creates a connection to a XMPP server. A simple use of this API might
* look like the following:
* <pre>
* // 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();
* </pre>
*
* XMPPConnections can be reused between connections. This means that an XMPPConnection
* may be connected, disconnected and then connected again. Listeners of the XMPPConnection
* will be retained accross connections.<p>
*
* If a connected XMPPConnection gets disconnected abruptly then it will try to reconnect
* again. To stop the reconnection process, use {@link #disconnect()}. Once stopped
* you can use {@link #connect()} to manually connect to the server.
*
* @author Matt Tucker
*/
public class XMPPConnection {
/**
* Value that indicates whether debugging is enabled. When enabled, a debug
* window will apear for each new connection that will contain the following
* information:<ul>
* <li> Client Traffic -- raw XML traffic generated by Smack and sent to the server.
* <li> Server Traffic -- raw XML traffic sent by the server to the client.
* <li> Interpreted Packets -- shows XML packets from the server as parsed by Smack.
* </ul>
*
* Debugging can be enabled by setting this field to true, or by setting the Java system
* property <tt>smack.debugEnabled</tt> to true. The system property can be set on the
* command line such as "java SomeApp -Dsmack.debugEnabled=true".
*/
public static boolean DEBUG_ENABLED = false;
private final static Set<ConnectionCreationListener> connectionEstablishedListeners =
new CopyOnWriteArraySet<ConnectionCreationListener>();
static {
// Use try block since we may not have permission to get a system
// property (for example, when an applet).
try {
DEBUG_ENABLED = Boolean.getBoolean("smack.debugEnabled");
}
catch (Exception e) {
// Ignore.
}
// Ensure the SmackConfiguration class is loaded by calling a method in it.
SmackConfiguration.getVersion();
}
private SmackDebugger debugger = null;
/**
* IP address or host name of the server. This information is only used when
* creating new socket connections to the server. If this information is not
* configured then it will be assumed that the host name matches the service name.
*/
String host;
int port;
Socket socket;
/**
* Hostname of the XMPP server. Usually servers use the same service name as the name
* of the server. However, there are some servers like google where host would be
* talk.google.com and the serviceName would be gmail.com.
*/
String serviceName;
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;
PacketWriter packetWriter;
PacketReader packetReader;
Roster roster = null;
private AccountManager accountManager = null;
protected SASLAuthentication saslAuthentication = new SASLAuthentication(this);
Writer writer;
Reader reader;
/**
* A map between JIDs and the most recently created Chat object with that JID.
* Reference to the Chat is stored via a WeakReference so that the map
* does not interfere with garbage collection. The map of chats must be stored
* with each connection.
*/
Map<String, WeakReference<Chat>> chats = new ConcurrentHashMap<String, WeakReference<Chat>>();
/**
* Collection of available stream compression methods offered by the server.
*/
private Collection compressionMethods;
/**
* Flag that indicates if stream compression is actually in use.
*/
private boolean usingCompression;
/**
* 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.<p>
*
* 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. <tt>jivesoftware.com</tt>.
*/
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
ConnectionConfiguration config =
new ConnectionConfiguration(address.getHost(), address.getPort(), serviceName);
config.setTLSEnabled(true);
config.setCompressionEnabled(false);
config.setSASLAuthenticationEnabled(true);
config.setDebuggerEnabled(DEBUG_ENABLED);
init(config, null);
}
/**
* Creates a new connection to the XMPP server at the specifiec host and port.<p>
*
* 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. <tt>jivesoftware.com</tt>.
* @param port the port on the server that should be used; e.g. <tt>5222</tt>.
*/
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);
init(config, null);
}
/**
* Creates a new connection to the specified XMPP server on the given host and port.<p>
*
* 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. <tt>5222</tt>.
* @param serviceName the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>.
*/
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);
init(config, null);
}
/**
* Creates a new connection to the specified XMPP server on the given port using the
* specified SocketFactory.<p>
*
* 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.<p>
*
* 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. <tt>5222</tt>.
* @param serviceName the name of the XMPP server to connect to; e.g. <tt>jivesoftware.com</tt>.
* @param socketFactory a SocketFactory that will be used to create the socket to the XMPP
* server.
*/
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);
init(config, socketFactory);
}
public XMPPConnection(ConnectionConfiguration config) {
init(config, null);
}
public XMPPConnection(ConnectionConfiguration config, SocketFactory socketFactory) {
init(config, socketFactory);
}
/**
* Package-private default constructor. This constructor is only intended
* for unit testing. Normal classes extending XMPPConnection should override
* one of the other constructors.
*/
XMPPConnection() {
}
/**
* Returns the connection ID for this connection, which is the value set by the server
* when opening a XMPP stream. If the server does not set a connection ID, this value
* will be null.
*
* @return the ID of this connection returned from the XMPP server.
*/
public String getConnectionID() {
return connectionID;
}
/**
* Returns the name of the service provided by the XMPP server for this connection. After
* authenticating with the server the returned value may be different.
*
* @return the name of the service provided by the XMPP server.
*/
public String getServiceName() {
return serviceName;
}
/**
* Returns the host name of the server where the XMPP server is running. This would be the
* IP address of the server or a name that may be resolved by a DNS server.
*
* @return the host name of the server where the XMPP server is running.
*/
public String getHost() {
return host;
}
/**
* Returns the port number of the XMPP server for this connection. The default port
* for normal connections is 5222. The default port for SSL connections is 5223.
*
* @return the port number of the XMPP server.
*/
public int getPort() {
return port;
}
/**
* Returns the full XMPP address of the user that is logged in to the connection or
* <tt>null</tt> if not logged in yet. An XMPP address is in the form
* username@server/resource.
*
* @return the full XMPP address of the user logged in.
*/
public String getUser() {
if (!isAuthenticated()) {
return null;
}
return user;
}
/**
* Logs in to the server using the strongest authentication mode supported by
* the server, then set our presence to available. 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.
*
* @param username the username.
* @param password the password.
* @throws XMPPException if an error occurs.
*/
public void login(String username, String password) throws XMPPException {
login(username, password, "Smack");
}
/**
* Logs in to the server using the strongest authentication mode supported by
* the server, then sets presence to available. 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.
*
* @param username the username.
* @param password the password.
* @param resource the resource.
* @throws XMPPException if an error occurs.
* @throws IllegalStateException if not connected to the server, or already logged in
* to the serrver.
*/
public synchronized void login(String username, String password, String resource)
throws XMPPException
{
login(username, password, resource, true);
}
/**
* Logs in to the server using the strongest authentication mode supported by
* the server. If the server supports SASL authentication then the user will be
* authenticated using SASL if not Non-SASL authentication will be tried. An available
* presence may optionally be sent. If <tt>sendPresence</tt>
* 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.<p>
*
* 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.
* @param resource the resource.
* @param sendPresence if <tt>true</tt> an available presence will be sent automatically
* after login is completed.
* @throws XMPPException if an error occurs.
* @throws IllegalStateException if not connected to the server, or already logged in
* to the serrver.
*/
public synchronized void login(String username, String password, String resource,
boolean sendPresence) throws XMPPException
{
if (!isConnected()) {
throw new IllegalStateException("Not connected to server.");
}
if (authenticated) {
throw new IllegalStateException("Already logged in to server.");
}
// Do partial version of nameprep on the username.
username = username.toLowerCase().trim();
String response;
if (configuration.isSASLAuthenticationEnabled() &&
saslAuthentication.hasNonAnonymousAuthentication()) {
// Authenticate using SASL
response = saslAuthentication.authenticate(username, password, resource);
}
else {
// Authenticate using Non-SASL
response = new NonSASLAuthentication(this).authenticate(username, password, resource);
}
// Set the user.
if (response != null) {
this.user = response;
// Update the serviceName with the one returned by the server
this.serviceName = StringUtils.parseServer(response);
}
else {
this.user = username + "@" + this.serviceName;
if (resource != null) {
this.user += "/" + resource;
}
}
// If compression is enabled then request the server to use stream compression
if (configuration.isCompressionEnabled()) {
useCompression();
}
// 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().setLoginInfo(username, password, resource, sendPresence);
// 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
// will be null
if (configuration.isDebuggerEnabled() && debugger != null) {
debugger.userHasLogged(user);
}
}
/**
* Logs in to the server anonymously. Very few servers are configured to support anonymous
* authentication, so it's fairly likely logging in anonymously will fail. If anonymous login
* does succeed, your XMPP address will likely be in the form "server/123ABC" (where "123ABC"
* is a random value generated by the server).
*
* @throws XMPPException if an error occurs or anonymous logins are not supported by the server.
* @throws IllegalStateException if not connected to the server, or already logged in
* to the serrver.
*/
public synchronized void loginAnonymously() throws XMPPException {
if (!isConnected()) {
throw new IllegalStateException("Not connected to server.");
}
if (authenticated) {
throw new IllegalStateException("Already logged in to server.");
}
String response;
if (configuration.isSASLAuthenticationEnabled() &&
saslAuthentication.hasAnonymousAuthentication()) {
response = saslAuthentication.authenticateAnonymously();
}
else {
// Authenticate using Non-SASL
response = new NonSASLAuthentication(this).authenticateAnonymously();
}
// Set the user value.
this.user = response;
// Update the serviceName with the one returned by the server
this.serviceName = StringUtils.parseServer(response);
// If compression is enabled then request the server to use stream compression
if (configuration.isCompressionEnabled()) {
useCompression();
}
// Anonymous users can't have a roster.
roster = null;
// Set presence to online.
packetWriter.sendPacket(new Presence(Presence.Type.available));
// Indicate that we're now authenticated.
authenticated = true;
anonymous = true;
// 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
// will be null
if (configuration.isDebuggerEnabled() && debugger != null) {
debugger.userHasLogged(user);
}
}
/**
* Returns the roster for the user logged into the server. If the user has not yet
* logged into the server (or if the user is logged in anonymously), this method will return
* <tt>null</tt>.
*
* @return the user's roster, or <tt>null</tt> if the user has not logged in yet.
*/
public Roster getRoster() {
if (roster == null) {
return null;
}
// If this is the first time the user has asked for the roster after calling
// login, we want to wait for the server to send back the user's roster. This
// behavior shields API users from having to worry about the fact that roster
// operations are asynchronous, although they'll still have to listen for
// changes to the roster. Note: because of this waiting logic, internal
// Smack code should be wary about calling the getRoster method, and may need to
// access the roster object directly.
if (!roster.rosterInitialized) {
try {
synchronized (roster) {
long waitTime = SmackConfiguration.getPacketReplyTimeout();
long start = System.currentTimeMillis();
while (!roster.rosterInitialized) {
if (waitTime <= 0) {
break;
}
roster.wait(waitTime);
long now = System.currentTimeMillis();
waitTime -= now - start;
start = now;
}
}
}
catch (InterruptedException ie) {
// Ignore.
}
}
return roster;
}
/**
* Returns an account manager instance for this connection.
*
* @return an account manager for this connection.
*/
public synchronized AccountManager getAccountManager() {
if (accountManager == null) {
accountManager = new AccountManager(this);
}
return accountManager;
}
/**
* Creates a new chat with the specified participant. The participant should
* be a valid XMPP user such as <tt>jdoe@jivesoftware.com</tt> or
* <tt>jdoe@jivesoftware.com/work</tt>.
*
* @param participant the person to start the conversation with.
* @return a new Chat object.
*/
public Chat createChat(String participant) {
if (!isConnected()) {
throw new IllegalStateException("Not connected to server.");
}
return new Chat(this, participant);
}
/**
* Returns true if currently connected to the XMPP server.
*
* @return true if connected.
*/
public boolean isConnected() {
return connected;
}
/**
* Returns true if the connection is a secured one, such as an SSL connection or
* if TLS was negotiated successfully.
*
* @return true if a secure connection to the server.
*/
public boolean isSecureConnection() {
return isUsingTLS();
}
/**
* Returns true if currently authenticated by successfully calling the login method.
*
* @return true if authenticated.
*/
public boolean isAuthenticated() {
return authenticated;
}
/**
* Returns true if currently authenticated anonymously.
*
* @return true if authenticated anonymously.
*/
public boolean isAnonymous() {
return anonymous;
}
/**
* Closes the connection by setting presence to unavailable then closing the stream to
* 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.
*/
protected void shutdown() {
// Set presence to offline.
packetWriter.sendPacket(new Presence(Presence.Type.unavailable));
packetReader.shutdown();
packetWriter.shutdown();
// Wait 150 ms for processes to clean-up, then shutdown.
try {
Thread.sleep(150);
}
catch (Exception e) {
// Ignore.
}
// Close down the readers and writers.
if (reader != null)
{
try { reader.close(); } catch (Throwable ignore) { /* ignore */ }
reader = null;
}
if (writer != null)
{
try { writer.close(); } catch (Throwable ignore) { /* ignore */ }
writer = null;
}
try {
socket.close();
}
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;
}
/**
* Sends the specified packet to the server.
*
* @param packet the packet to send.
*/
public void sendPacket(Packet packet) {
if (!isConnected()) {
throw new IllegalStateException("Not connected to server.");
}
if (packet == null) {
throw new NullPointerException("Packet is null.");
}
packetWriter.sendPacket(packet);
}
/**
* Registers a packet listener with this connection. A packet filter determines
* which packets will be delivered to the listener.
*
* @param packetListener the packet listener to notify of new packets.
* @param packetFilter the packet filter to use.
*/
public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) {
if (!isConnected()) {
throw new IllegalStateException("Not connected to server.");
}
packetReader.addPacketListener(packetListener, packetFilter);
}
/**
* Removes a packet listener from this connection.
*
* @param packetListener the packet listener to remove.
*/
public void removePacketListener(PacketListener packetListener) {
packetReader.removePacketListener(packetListener);
}
/**
* Registers a packet listener with this connection. The listener will be
* notified of every packet that this connection sends. A packet filter determines
* which packets will be delivered to the listener. Note that the thread
* that writes packets will be used to invoke the listeners. Therefore, each
* packet listener should complete all operations quickly or use a different
* thread for processing.
*
* @param packetListener the packet listener to notify of sent packets.
* @param packetFilter the packet filter to use.
*/
public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) {
if (!isConnected()) {
throw new IllegalStateException("Not connected to server.");
}
packetWriter.addPacketListener(packetListener, packetFilter);
}
/**
* Removes a packet listener from this connection.
*
* @param packetListener the packet listener to remove.
*/
public void removePacketWriterListener(PacketListener packetListener) {
packetWriter.removePacketListener(packetListener);
}
/**
* Registers a packet interceptor with this connection. The interceptor will be
* invoked every time a packet is about to be sent by this connection. Interceptors
* may modify the packet to be sent. A packet filter determines which packets
* will be delivered to the interceptor.
*
* @param packetInterceptor the packet interceptor to notify of packets about to be sent.
* @param packetFilter the packet filter to use.
*/
public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor,
PacketFilter packetFilter) {
if (!isConnected()) {
throw new IllegalStateException("Not connected to server.");
}
packetWriter.addPacketInterceptor(packetInterceptor, packetFilter);
}
/**
* Removes a packet interceptor.
*
* @param packetInterceptor the packet interceptor to remove.
*/
public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) {
packetWriter.removePacketInterceptor(packetInterceptor);
}
/**
* Creates a new packet collector for this connection. A packet filter determines
* which packets will be accumulated by the collector.
*
* @param packetFilter the packet filter to use.
* @return a new packet collector.
*/
public PacketCollector createPacketCollector(PacketFilter packetFilter) {
return packetReader.createPacketCollector(packetFilter);
}
/**
* Adds a connection listener to this connection that will be notified when
* the connection closes or fails. The connection needs to already be connected
* or otherwise an IllegalStateException will be thrown.
*
* @param connectionListener a connection listener.
*/
public void addConnectionListener(ConnectionListener connectionListener) {
if (!isConnected()) {
throw new IllegalStateException("Not connected to server.");
}
if (connectionListener == null) {
return;
}
synchronized (packetReader.connectionListeners) {
if (!packetReader.connectionListeners.contains(connectionListener)) {
packetReader.connectionListeners.add(connectionListener);
}
}
}
/**
* Removes a connection listener from this connection.
*
* @param connectionListener a connection listener.
*/
public void removeConnectionListener(ConnectionListener connectionListener) {
synchronized (packetReader.connectionListeners) {
packetReader.connectionListeners.remove(connectionListener);
}
}
/**
* Adds a new listener that will be notified when new XMPPConnections are created. Note
* that newly created connections will not be actually connected to the server.
*
* @param connectionCreationListener a listener interested on new connections.
*/
public static void addConnectionCreationListener(
ConnectionCreationListener connectionCreationListener) {
connectionEstablishedListeners.add(connectionCreationListener);
}
/**
* Removes a listener that was interested in connection creation events.
*
* @param connectionCreationListener a listener interested on new connections.
*/
public static void removeConnectionCreationListener(
ConnectionCreationListener connectionCreationListener) {
connectionEstablishedListeners.remove(connectionCreationListener);
}
private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException {
this.host = config.getHost();
this.port = config.getPort();
try {
if (config.getSocketFactory() == null) {
this.socket = new Socket(host, port);
}
else {
this.socket = config.getSocketFactory().createSocket(host, port);
}
}
catch (UnknownHostException uhe) {
String errorMessage = "Could not connect to " + host + ":" + port + ".";
throw new XMPPException(errorMessage, new XMPPError(
XMPPError.Condition.remote_server_timeout, errorMessage),
uhe);
}
catch (IOException ioe) {
String errorMessage = "XMPPError connecting to " + host + ":"
+ port + ".";
throw new XMPPException(errorMessage, new XMPPError(
XMPPError.Condition.remote_server_error, errorMessage), ioe);
}
this.serviceName = config.getServiceName();
initConnection();
}
/**
* Initializes the connection configuration. This method is only executed
* when the XMPPConnection is created.
*
* @param config the connection configuration options.
* @param socketFactory a factory used for creating sockets to the XMPP server.
*/
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 initConnection() throws XMPPException {
boolean isFirstInitialization = packetReader == null || packetWriter == null;
if (!isFirstInitialization) {
usingCompression = false;
}
// Set the reader and writer instance variables
initReaderAndWriter();
try {
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
// get an opening stream packet back from server.
packetReader.startup();
// Make note of the fact that we're now connected.
connected = true;
// Start keep alive process (after TLS was negotiated - if available)
packetWriter.startKeepAliveProcess();
if (isFirstInitialization) {
// Notify listeners that a new connection has been established
for (ConnectionCreationListener listener : connectionEstablishedListeners) {
listener.connectionCreated(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<Chat> 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));
}
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 */ }
packetWriter = null;
}
if (packetReader != null) {
try {
packetReader.shutdown();
}
catch (Throwable ignore) { /* ignore */ }
packetReader = null;
}
if (reader != null) {
try {
reader.close();
}
catch (Throwable ignore) { /* ignore */ }
reader = null;
}
if (writer != null) {
try {
writer.close();
}
catch (Throwable ignore) { /* ignore */}
writer = null;
}
if (socket != null) {
try {
socket.close();
}
catch (Exception e) { /* ignore */ }
socket = null;
}
this.setWasAuthenticated(authenticated);
authenticated = false;
connected = false;
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"));
}
else {
try {
Class<?> zoClass = Class.forName("com.jcraft.jzlib.ZOutputStream");
Constructor<?> constructor =
zoClass.getConstructor(OutputStream.class, Integer.TYPE);
Object out = constructor.newInstance(socket.getOutputStream(), 9);
Method method = zoClass.getMethod("setFlushMode", Integer.TYPE);
method.invoke(out, 1);
writer =
new BufferedWriter(new OutputStreamWriter((OutputStream) out, "UTF-8"));
Class<?> ziClass = Class.forName("com.jcraft.jzlib.ZInputStream");
constructor = ziClass.getConstructor(InputStream.class);
Object in = constructor.newInstance(socket.getInputStream());
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"));
}
}
}
catch (IOException ioe) {
throw new XMPPException(
"XMPPError establishing connection with server.",
new XMPPError(XMPPError.Condition.remote_server_error,
"XMPPError establishing connection with server."),
ioe);
}
// If debugging is enabled, we open a window and write out all network traffic.
if (configuration.isDebuggerEnabled()) {
if (debugger == null) {
// Detect the debugger class to use.
String className = null;
// Use try block since we may not have permission to get a system
// property (for example, when an applet).
try {
className = System.getProperty("smack.debuggerClass");
}
catch (Throwable t) {
// Ignore.
}
Class<?> debuggerClass = null;
if (className != null) {
try {
debuggerClass = Class.forName(className);
}
catch (Exception e) {
e.printStackTrace();
}
}
if (debuggerClass == null) {
try {
debuggerClass =
Class.forName("org.jivesoftware.smackx.debugger.EnhancedDebugger");
}
catch (Exception ex) {
try {
debuggerClass =
Class.forName("org.jivesoftware.smack.debugger.LiteDebugger");
}
catch (Exception ex2) {
ex2.printStackTrace();
}
}
}
// 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);
debugger = (SmackDebugger) constructor.newInstance(this, writer, reader);
reader = debugger.getReader();
writer = debugger.getWriter();
}
catch (Exception e) {
e.printStackTrace();
DEBUG_ENABLED = false;
}
}
else {
// Obtain new reader and writer from the existing debugger
reader = debugger.newConnectionReader(reader);
writer = debugger.newConnectionWriter(writer);
}
}
}
/***********************************************
* TLS code below
**********************************************/
/**
* Returns true if the connection to the server has successfully negotiated TLS. Once TLS
* has been negotiatied the connection has been secured.
*
* @return true if the connection to the server has successfully negotiated TLS.
*/
public boolean isUsingTLS() {
return usingTLS;
}
/**
* Returns the SASLAuthentication manager that is responsible for authenticating with
* the server.
*
* @return the SASLAuthentication manager that is responsible for authenticating with
* the server.
*/
public SASLAuthentication getSASLAuthentication() {
return saslAuthentication;
}
/**
* Returns the configuration used to connect to the server.
*
* @return the configuration used to connect to the server.
*/
protected ConnectionConfiguration getConfiguration() {
return configuration;
}
/**
* Notification message saying that the server supports TLS so confirm the server that we
* want to secure the connection.
*/
void startTLSReceived() {
if (!configuration.isTLSEnabled()) {
// Do not secure the connection using TLS since TLS was disabled
return;
}
try {
writer.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
writer.flush();
}
catch (IOException e) {
packetReader.notifyConnectionError(e);
}
}
/**
* The server has indicated that TLS negotiation can start. We now need to secure the
* existing plain connection and perform a handshake. This method won't return until the
* connection has finished the handshake or an error occured while securing the connection.
*
* @throws Exception if an exception occurs.
*/
void proceedTLSReceived() throws Exception {
SSLContext context = SSLContext.getInstance("TLS");
// Verify certificate presented by the server
context.init(null, // KeyManager not required
new javax.net.ssl.TrustManager[]{new ServerTrustManager(serviceName, configuration)},
new java.security.SecureRandom());
Socket plain = socket;
// Secure the plain connection
socket = context.getSocketFactory().createSocket(plain,
plain.getInetAddress().getHostName(), plain.getPort(), true);
socket.setSoTimeout(0);
socket.setKeepAlive(true);
// Initialize the reader and writer with the new secured version
initReaderAndWriter();
// Proceed to do the handshake
((SSLSocket) socket).startHandshake();
// Set that TLS was successful
usingTLS = true;
// Set the new writer to use
packetWriter.setWriter(writer);
// Send a new opening stream to the server
packetWriter.openStream();
}
/**
* Sets the available stream compression methods offered by the server.
*
* @param methods compression methods offered by the server.
*/
void setAvailableCompressionMethods(Collection methods) {
compressionMethods = methods;
}
/**
* Returns true if the specified compression method was offered by the server.
*
* @param method the method to check.
* @return true if the specified compression method was offered by the server.
*/
private boolean hasAvailableCompressionMethod(String method) {
return compressionMethods != null && compressionMethods.contains(method);
}
/**
* Returns true if network traffic is being compressed. When using stream compression network
* traffic can be reduced up to 90%. Therefore, stream compression is ideal when using a slow
* speed network connection. However, the server will need to use more CPU time in order to
* un/compress network data so under high load the server performance might be affected.<p>
*
* Note: to use stream compression the smackx.jar file has to be present in the classpath.
*
* @return true if network traffic is being compressed.
*/
public boolean isUsingCompression() {
return usingCompression;
}
/**
* Starts using stream compression that will compress network traffic. Traffic can be
* reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network
* connection. However, the server and the client will need to use more CPU time in order to
* un/compress network data so under high load the server performance might be affected.<p>
*
* Stream compression has to have been previously offered by the server. Currently only the
* zlib method is supported by the client. Stream compression negotiation has to be done
* before authentication took place.<p>
*
* Note: to use stream compression the smackx.jar file has to be present in the classpath.
*
* @return true if stream compression negotiation was successful.
*/
private boolean useCompression() {
// If stream compression was offered by the server and we want to use
// compression then send compression request to the server
if (authenticated) {
throw new IllegalStateException("Compression should be negotiated before authentication.");
}
try {
Class.forName("com.jcraft.jzlib.ZOutputStream");
}
catch (ClassNotFoundException e) {
throw new IllegalStateException("Cannot use compression. Add smackx.jar to the classpath");
}
if (hasAvailableCompressionMethod("zlib")) {
requestStreamCompression();
// Wait until compression is being used or a timeout happened
synchronized (this) {
try {
this.wait(SmackConfiguration.getPacketReplyTimeout() * 5);
}
catch (InterruptedException e) {
// Ignore.
}
}
return usingCompression;
}
return false;
}
/**
* Request the server that we want to start using stream compression. When using TLS
* then negotiation of stream compression can only happen after TLS was negotiated. If TLS
* compression is being used the stream compression should not be used.
*/
private void requestStreamCompression() {
try {
writer.write("<compress xmlns='http://jabber.org/protocol/compress'>");
writer.write("<method>zlib</method></compress>");
writer.flush();
}
catch (IOException e) {
packetReader.notifyConnectionError(e);
}
}
/**
* Start using stream compression since the server has acknowledged stream compression.
*
* @throws Exception if there is an exception starting stream compression.
*/
void startStreamCompression() throws Exception {
// Secure the plain connection
usingCompression = true;
// Initialize the reader and writer with the new secured version
initReaderAndWriter();
// Set the new writer to use
packetWriter.setWriter(writer);
// Send a new opening stream to the server
packetWriter.openStream();
// Notify that compression is being used
synchronized (this) {
this.notify();
}
}
/**
* Notifies the XMPP connection that stream compression was denied so that
* the connection process can proceed.
*/
void streamCompressionDenied() {
synchronized (this) {
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.<p>
*
* 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(),
getConfiguration().getResource(), getConfiguration().isSendPresence());
}
} catch (XMPPException e) {
e.printStackTrace();
}
}
}
/**
* Sets whether the connection has already logged in the server.
*
* @param wasAuthenticated true if the connection has already been authenticated.
*/
private void setWasAuthenticated(boolean wasAuthenticated) {
if (!this.wasAuthenticated) {
this.wasAuthenticated = wasAuthenticated;
}
}
}