package com.voxeo.rayo.client; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.Socket; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.voxeo.rayo.client.auth.AuthenticationHandler; import com.voxeo.rayo.client.auth.AuthenticationListener; import com.voxeo.rayo.client.auth.SimpleAuthenticationHandler; import com.voxeo.rayo.client.filter.XmppObjectExtensionNameFilter; import com.voxeo.rayo.client.filter.XmppObjectFilter; import com.voxeo.rayo.client.filter.XmppObjectIdFilter; import com.voxeo.rayo.client.filter.XmppObjectNameFilter; import com.voxeo.rayo.client.io.SimpleXmppReader; import com.voxeo.rayo.client.io.SimpleXmppWriter; import com.voxeo.rayo.client.io.XmppReader; import com.voxeo.rayo.client.io.XmppWriter; import com.voxeo.rayo.client.listener.StanzaListener; import com.voxeo.rayo.client.response.FilterCleaningResponseHandler; import com.voxeo.rayo.client.response.ResponseHandler; import com.voxeo.rayo.client.xmpp.extensions.Extension; import com.voxeo.rayo.client.xmpp.stanza.Error; import com.voxeo.rayo.client.xmpp.stanza.Stanza; import com.voxeo.rayo.client.xmpp.stanza.XmppObject; import com.voxeo.rayo.client.xmpp.stanza.Error.Condition; import com.voxeo.rayo.client.xmpp.stanza.Error.Type; public class SimpleXmppConnection implements XmppConnection { private Logger log = LoggerFactory.getLogger(SimpleXmppConnection.class); private XmppReader reader; private XmppWriter writer; private ConnectionConfiguration config; private Socket socket; private String serviceName; private String connectionId; private String username; private String resource; private AuthenticationHandler authenticationHandler; private boolean loggingIn; private boolean connected; private int DEFAULT_TIMEOUT = XmppObjectFilter.DEFAULT_TIMEOUT; private List<XmppConnectionListener> listeners = new ArrayList<XmppConnectionListener>(); public SimpleXmppConnection(String serviceName) { this(serviceName, null); } public SimpleXmppConnection(String serviceName, Integer port) { this.serviceName = serviceName; //TODO: Lots of things to be handled. Security, compression, proxies. All already done in Smack. Reuse!! this.config = new ConnectionConfiguration(serviceName, port); authenticationHandler = new SimpleAuthenticationHandler(this); reader = new SimpleXmppReader(); } @Override public ConnectionConfiguration getConfiguration() { return config; } @Override public boolean isConnected() { return connected; } @Override public boolean isAuthenticated() { return authenticationHandler.isAuthenticated(); } @Override public void connect() throws XmppException { connect(5); } @Override public void connect(int timeout) throws XmppException { String host = config.getHostname(); int port = config.getPort(); try { this.socket = new Socket(host, port); } catch (UnknownHostException uhe) { throw new XmppException(String.format("Could not connect to %s:%s",host,port), Error.Condition.remote_server_timeout); } catch (IOException ioe) { throw new XmppException(String.format("Error while connecting to %s:%s",host,port), Error.Condition.service_unavailable, ioe); } initConnection(timeout); } private void initConnection(int timeout) throws XmppException { if (connected) { return; } try { initIO(); initAuth(); startReader(); // Blocks until we get an open stream final CountDownLatch latch = new CountDownLatch(1); XmppConnectionListener connectionListener =new XmppConnectionAdapter() { @Override public void connectionEstablished(String connectionId) { connected = true; latch.countDown(); } }; reader.addXmppConnectionListener(connectionListener); openStream(); try { latch.await(timeout, TimeUnit.SECONDS); } catch (InterruptedException e1) { } reader.removeXmppConnectionListener(connectionListener); if (!connected) { throw new XmppException(new Error(Condition.service_unavailable, Type.cancel, "Could not connect to server.")); } //TODO: Keep alive // Wait a little bit to let connection id get populated on the listener try { Thread.sleep(150); } catch (Exception e) {} } catch (XmppException xmpe) { disconnect(); throw xmpe; } } private void initAuth() { addAuthenticationListener(authenticationHandler); } @Override public void disconnect() throws XmppException { if (!connected) { return; } connected = false; if (writer != null) { try{ writer.close(); } catch(Exception ex){ log.warn("Exception when Closing XMPP connection", ex); } } if (reader != null) { try{ reader.close(); } catch(Exception ex){ log.warn("Exception when Closing XMPP connection", ex); } } // We close the socket first as otherwise closing the reader may enter a deadlock with the // threads that are listening for socket data, specially if there is no incoming activity // from the socket // TODO: Check if implementing keep alive solves this issue try { log.debug("Closing XMPP socket connection"); socket.close(); } catch (IOException e) { e.printStackTrace(); } // Wait a little bit for cleanup try { Thread.sleep(150); } catch (InterruptedException e) { e.printStackTrace(); } cleanup(); } @Override public void send(XmppObject object) throws XmppException { if (object == null) { return; } if (!connected) { throw new XmppException(new Error(Condition.service_unavailable, Type.cancel, "Not connected to the server. You need to connect first.")); } if (!loggingIn && !authenticationHandler.isAuthenticated()) { throw new XmppException(new Error(Condition.not_authorized, Type.cancel, "Not authenticated. You need to authenticate first.")); } log.debug(String.format("[OUT] [%s]",object)); writer.write(object); for (XmppConnectionListener listener: listeners) { listener.messageSent(object); } } @Override public void send(XmppObject object, ResponseHandler handler) throws XmppException { // This wrapping response handler will remove the filter once we get the result from the server // This helps to clean up resources FilterCleaningResponseHandler filterHandler = new FilterCleaningResponseHandler(handler,this); XmppObjectIdFilter filter = new XmppObjectIdFilter(object.getId(), filterHandler); filterHandler.setFilter(filter); addFilter(filter); send(object); } @Override public XmppObject sendAndWait(XmppObject object) throws XmppException { return sendAndWait(object, DEFAULT_TIMEOUT); } @Override public XmppObject sendAndWait(XmppObject object, int timeout) throws XmppException { XmppObjectIdFilter filter = new XmppObjectIdFilter(object.getId()); addFilter(filter); send(object); XmppObject response = filter.poll(timeout); removeFilter(filter); return response; } private void openStream() throws XmppException { writer.openStream(serviceName); } private void startReader() throws XmppException { reader.addXmppConnectionListener(new XmppConnectionAdapter() { @Override public void connectionReset(String connectionId) { try { openStream(); } catch (XmppException e) { e.printStackTrace(); } } @Override public void connectionEstablished(String connectionId) { setConnectionId(connectionId); } @Override public void connectionError(String connectionId, Exception e) { try { disconnect(); } catch (XmppException xe) { xe.printStackTrace(); } } }); reader.start(); } private void initIO() throws XmppException { try { reader.init(new BufferedReader( new InputStreamReader(socket.getInputStream(), "UTF-8"))); writer = new SimpleXmppWriter(new BufferedWriter( new OutputStreamWriter(socket.getOutputStream(), "UTF-8"))); } catch (IOException ioe) { throw new XmppException("Could not initialise IO system", Error.Condition.remote_server_error, ioe); } } @Override public void login(String username, String password, String resourceName) throws XmppException { login(username, password, resourceName, 5); } @Override public void login(String username, String password, String resourceName, int timeout) throws XmppException { loggingIn = true; log.debug("Logging in as " + username + " and resource " + resourceName); authenticationHandler.login(username, password, resourceName, timeout); loggingIn = false; this.username = username; this.resource = resourceName; } @Override public XmppObject waitFor(String node) throws XmppException { return waitFor(node, DEFAULT_TIMEOUT); } @Override public XmppObject waitFor(String node, Integer timeout) throws XmppException { XmppObjectNameFilter filter = null; try { filter = new XmppObjectNameFilter(node); addFilter(filter); XmppObject response = null; if (timeout != null) { response = filter.poll(timeout); } else { response = filter.poll(); } if (response == null) { throw new XmppException(String.format("Timed out while waiting for [%s]",node)); } return response; } finally { removeFilter(filter); } } @Override public Extension waitForExtension(String extensionName) throws XmppException { return waitForExtension(extensionName, DEFAULT_TIMEOUT); } @Override public Extension waitForExtension(String extensionName, Integer timeout) throws XmppException { XmppObjectExtensionNameFilter filter = null; try { filter = new XmppObjectExtensionNameFilter(extensionName); addFilter(filter); XmppObject response = null; if (timeout != null) { response = filter.poll(timeout); } else { response = filter.poll(); } if (response == null) { throw new XmppException(String.format("Timed out while waiting for [%s]",extensionName)); } return ((Stanza<?>)response).getExtension(); } finally { removeFilter(filter); } } private void cleanup() { config = null; socket = null; reader = null; writer = null; connectionId = null; serviceName = null; username = null; resource = null; connected = false; loggingIn = false; } @Override public void addStanzaListener(StanzaListener stanzaListener) { if (reader != null) { reader.addStanzaListener(stanzaListener); } } @Override public void removeStanzaListener(StanzaListener stanzaListener) { if (reader != null) { reader.removeStanzaListener(stanzaListener); } } @Override public void addAuthenticationListener(AuthenticationListener authListener) { if (reader != null) { reader.addAuthenticationListener(authListener); } } @Override public void removeAuthenticationListener(AuthenticationListener authListener) { if (reader != null) { reader.removeAuthenticationListener(authListener); } } @Override public void addXmppConnectionListener(XmppConnectionListener connectionListener) { // Bad smell. Two lists with almost the same listeners. This needs refactoring listeners.add(connectionListener); reader.addXmppConnectionListener(connectionListener); } @Override public void removeXmppConnectionListener(XmppConnectionListener connectionListener) { // Bad smell. Two lists with almost the same listeners. This needs refactoring listeners.remove(connectionListener); reader.removeXmppConnectionListener(connectionListener); } @Override public void addFilter(XmppObjectFilter filter) { if (reader != null) { reader.addFilter(filter); } } @Override public void removeFilter(XmppObjectFilter filter) { if (reader != null) { reader.removeFilter(filter); } } void setConnectionId(String connectionId) { this.connectionId = connectionId; } @Override public String getConnectionId() { return connectionId; } @Override public String getServiceName() { return serviceName; } @Override public String getResource() { return resource; } @Override public String getUsername() { return username; } public void setDefaultTimeout(int timeout) { this.DEFAULT_TIMEOUT = timeout; } }