/*
* Copyright (c) 2009, 2012 IBM Corp.
*
* 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:
* Dave Locke - initial API and implementation and/or initial documentation
*/
package org.eclipse.paho.client.mqttv3.internal;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Vector;
import org.eclipse.paho.client.mqttv3.IMqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClientPersistence;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttPersistenceException;
import org.eclipse.paho.client.mqttv3.MqttToken;
import org.eclipse.paho.client.mqttv3.MqttTopic;
import org.eclipse.paho.client.mqttv3.internal.wire.MqttConnack;
import org.eclipse.paho.client.mqttv3.internal.wire.MqttConnect;
import org.eclipse.paho.client.mqttv3.internal.wire.MqttDisconnect;
import org.eclipse.paho.client.mqttv3.internal.wire.MqttPublish;
import org.eclipse.paho.client.mqttv3.internal.wire.MqttWireMessage;
import org.eclipse.paho.client.mqttv3.logging.Logger;
import org.eclipse.paho.client.mqttv3.logging.LoggerFactory;
/**
* Handles client communications with the server. Sends and receives MQTT V3
* messages.
*/
public class ClientComms {
public static String VERSION = "@@VERSION@@";
public static String BUILD_LEVEL = "@@BUILDLEVEL@@";
private IMqttAsyncClient client;
NetworkModule networkModule;
CommsReceiver receiver;
CommsSender sender;
CommsCallback callback;
ClientState clientState;
MqttConnectOptions conOptions;
private MqttClientPersistence persistence;
CommsTokenStore tokenStore;
boolean stoppingComms = false;
final static byte CONNECTED =0;
final static byte CONNECTING =1;
final static byte DISCONNECTING =2;
final static byte DISCONNECTED =3;
final static byte CLOSED =4;
private byte conState = DISCONNECTED;
Object conLock = new Object(); // Used to syncrhonize connection state
private boolean closePending = false;
final static String className = ClientComms.class.getName();
Logger log = LoggerFactory.getLogger(LoggerFactory.MQTT_CLIENT_MSG_CAT,className);
/**
* Creates a new ClientComms object, using the specified module to handle
* the network calls.
*/
public ClientComms(IMqttAsyncClient client, MqttClientPersistence persistence) throws MqttException {
this.conState = DISCONNECTED;
this.client = client;
this.persistence = persistence;
this.tokenStore = new CommsTokenStore(getClient().getClientId());
this.callback = new CommsCallback(this);
this.clientState = new ClientState(persistence, tokenStore, this.callback, this);
callback.setClientState(clientState);
log.setResourceName(getClient().getClientId());
}
/**
* Sends a message to the server. Does not check if connected this validation must be done
* by invoking routines.
* @param message
* @param token
* @throws MqttException
*/
void internalSend(MqttWireMessage message, MqttToken token) throws MqttException {
final String methodName = "internalSend";
//@TRACE 200=internalSend key={0} message={1} token={2}
log.fine(className, methodName, "200", new Object[]{message.getKey(), message, token});
if (token.getClient() == null ) {
// Associate the client with the token - also marks it as in use.
token.internalTok.setClient(getClient());
} else {
// Token is already in use - cannot reuse
//@TRACE 213=fail: token in use: key={0} message={1} token={2}
log.fine(className, methodName, "213", new Object[]{message.getKey(), message, token});
throw new MqttException(MqttException.REASON_CODE_TOKEN_INUSE);
}
try {
// Persist if needed and send the message
this.clientState.send(message, token);
} catch(MqttException e) {
if (message instanceof MqttPublish) {
this.clientState.undo((MqttPublish)message);
}
throw e;
}
}
/**
* Sends a message to the broker if in connected state, but only waits for the message to be
* stored, before returning.
*/
public void sendNoWait(MqttWireMessage message, MqttToken token) throws MqttException {
final String methodName = "sendNoWait";
if (isConnected() ||
(!isConnected() && message instanceof MqttConnect) ||
(isDisconnecting() && message instanceof MqttDisconnect)) {
this.internalSend(message, token);
} else {
//@TRACE 208=failed: not connected
log.fine(className, methodName, "208");
throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_NOT_CONNECTED);
}
}
/**
* Tidy up
* - call each main class and let it tidy up e.g. releasing the token
* store which normally survives a disconnect.
* @throws MqttException if not disconnected
*/
public void close() throws MqttException {
final String methodName = "close";
synchronized (conLock) {
if (!isClosed()) {
// Must be disconnected before close can take place
if (!isDisconnected()) {
//@TRACE 224=failed: not disconnected
log.fine(className, methodName, "224");
if (isConnecting()) {
throw new MqttException(MqttException.REASON_CODE_CONNECT_IN_PROGRESS);
} else if (isConnected()) {
throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_CONNECTED);
} else if (isDisconnecting()) {
closePending = true;
return;
}
}
conState = CLOSED;
// ShutdownConnection has already cleaned most things
clientState.close();
clientState = null;
callback = null;
persistence = null;
sender = null;
receiver = null;
networkModule = null;
conOptions = null;
tokenStore = null;
}
}
}
/**
* Sends a connect message and waits for an ACK or NACK.
* Connecting is a special case which will also start up the
* network connection, receive thread, and keep alive thread.
*/
public void connect(MqttConnectOptions options, MqttToken token) throws MqttException {
final String methodName = "connect";
synchronized (conLock) {
if (isDisconnected() && !closePending) {
//@TRACE 214=state=CONNECTING
log.fine(className,methodName,"214");
conState = CONNECTING;
this.conOptions = options;
MqttConnect connect = new MqttConnect(client.getClientId(),
options.isCleanSession(),
options.getKeepAliveInterval(),
options.getUserName(),
options.getPassword(),
options.getWillMessage(),
options.getWillDestination());
this.clientState.setKeepAliveSecs(options.getKeepAliveInterval());
this.clientState.setCleanSession(options.isCleanSession());
tokenStore.open();
ConnectBG conbg = new ConnectBG(this, token, connect);
conbg.start();
}
else {
// @TRACE 207=connect failed: not disconnected {0}
log.fine(className,methodName,"207", new Object[] {new Byte(conState)});
if (isClosed() || closePending) {
throw new MqttException(MqttException.REASON_CODE_CLIENT_CLOSED);
} else if (isConnecting()) {
throw new MqttException(MqttException.REASON_CODE_CONNECT_IN_PROGRESS);
} else if (isDisconnecting()) {
throw new MqttException(MqttException.REASON_CODE_CLIENT_DISCONNECTING);
} else {
throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_CONNECTED);
}
}
}
}
public void connectComplete( MqttConnack cack, MqttException mex) throws MqttException {
final String methodName = "connectComplete";
int rc = cack.getReturnCode();
synchronized (conLock) {
if (rc == 0) {
// We've successfully connected
// @TRACE 215=state=CONNECTED
log.fine(className,methodName,"215");
conState = CONNECTED;
return;
}
}
// @TRACE 204=connect failed: rc={0}
log.fine(className,methodName,"204", new Object[]{new Integer(rc)});
throw mex;
}
/**
* Shuts down the connection to the server.
* This may have been invoked as a result of a user calling disconnect or
* an abnormal disconnection. The method may be invoked multiple times
* in parallel as each thread when it receives an error uses this method
* to ensure that shutdown completes successfully.
*/
public void shutdownConnection(MqttToken token, MqttException reason) {
final String methodName = "shutdownConnection";
boolean wasConnected;
MqttToken endToken = null; //Token to notify after disconnect completes
// This method could concurrently be invoked from many places only allow it
// to run once.
synchronized(conLock) {
if (stoppingComms || closePending) {
return;
}
stoppingComms = true;
//@TRACE 216=state=DISCONNECTING
log.fine(className,methodName,"216");
wasConnected = (isConnected() || isDisconnecting());
conState = DISCONNECTING;
}
// Update the token with the reason for shutdown if it
// is not already complete.
if (token != null && !token.isComplete()) {
token.internalTok.setException(reason);
}
// Stop the thread that is used to call the user back
// when actions complete
if (callback!= null) {callback.stop(); }
// Stop the network module, send and receive now not possible
try {
if (networkModule != null) {networkModule.stop();}
}catch(Exception ioe) {
// Ignore as we are shutting down
}
// Stop the thread that handles inbound work from the network
if (receiver != null) {receiver.stop();}
// Stop any new tokens being saved by app and throwing an exception if they do
tokenStore.quiesce(new MqttException(MqttException.REASON_CODE_CLIENT_DISCONNECTING));
// Notify any outstanding tokens with the exception of
// con or discon which may be returned and will be notified at
// the end
endToken = handleOldTokens(token, reason);
try {
// Clean session handling and tidy up
clientState.disconnected(reason);
}catch(Exception ex) {
// Ignore as we are shutting down
}
if (sender != null) { sender.stop(); }
try {
if (persistence != null) {persistence.close();}
}catch(Exception ex) {
// Ignore as we are shutting down
}
// All disconnect logic has been completed allowing the
// client to be marked as disconnected.
synchronized(conLock) {
//@TRACE 217=state=DISCONNECTED
log.fine(className,methodName,"217");
conState = DISCONNECTED;
stoppingComms = false;
}
// Internal disconnect processing has completed. If there
// is a disconnect token or a connect in error notify
// it now. This is done at the end to allow a new connect
// to be processed and now throw a currently disconnecting error.
// any outstanding tokens and unblock any waiters
if (endToken != null & callback != null) {
callback.asyncOperationComplete(endToken);
}
if (wasConnected && callback != null) {
// Let the user know client has disconnected either normally or abnormally
callback.connectionLost(reason);
}
// While disconnecting, close may have been requested - try it now
synchronized(conLock) {
if (closePending) {
try {
close();
} catch (Exception e) { // ignore any errors as closing
}
}
}
}
// Tidy up. There may be tokens outstanding as the client was
// not disconnected/quiseced cleanly! Work out what tokens still
// need to be notified and waiters unblocked. Store the
// disconnect or connect token to notify after disconnect is
// complete.
private MqttToken handleOldTokens(MqttToken token, MqttException reason) {
final String methodName = "handleOldTokens";
//@TRACE 222=>
log.fine(className,methodName,"222");
MqttToken tokToNotifyLater = null;
try {
// First the token that was related to the disconnect / shutdown may
// not be in the token table - temporarily add it if not
if (token != null) {
if (tokenStore.getToken(token.internalTok.getKey())==null) {
tokenStore.saveToken(token, token.internalTok.getKey());
}
}
Vector toksToNot = clientState.resolveOldTokens(reason);
Enumeration toksToNotE = toksToNot.elements();
while(toksToNotE.hasMoreElements()) {
MqttToken tok = (MqttToken)toksToNotE.nextElement();
if (tok.internalTok.getKey().equals(MqttDisconnect.KEY) ||
tok.internalTok.getKey().equals(MqttConnect.KEY)) {
// Its con or discon so remember and notify @ end of disc routine
tokToNotifyLater = tok;
} else {
// notify waiters and callbacks of outstanding tokens
// that a problem has occurred and disconnect is in
// progress
callback.asyncOperationComplete(tok);
}
}
}catch(Exception ex) {
// Ignore as we are shutting down
}
return tokToNotifyLater;
}
public void disconnect(MqttDisconnect disconnect, long quiesceTimeout, MqttToken token) throws MqttException {
final String methodName = "disconnect";
synchronized (conLock){
if (isClosed()) {
//@TRACE 223=failed: in closed state
log.fine(className,methodName,"223");
throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_CLOSED);
} else if (isDisconnected()) {
//@TRACE 211=failed: already disconnected
log.fine(className,methodName,"211");
throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_ALREADY_DISCONNECTED);
} else if (isDisconnecting()) {
//@TRACE 219=failed: already disconnecting
log.fine(className,methodName,"219");
throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_DISCONNECTING);
} else if (Thread.currentThread() == callback.getThread()) {
//@TRACE 210=failed: called on callback thread
log.fine(className,methodName,"210");
// Not allowed to call disconnect() from the callback, as it will deadlock.
throw ExceptionHelper.createMqttException(MqttException.REASON_CODE_CLIENT_DISCONNECT_PROHIBITED);
}
//@TRACE 218=state=DISCONNECTING
log.fine(className,methodName,"218");
conState = DISCONNECTING;
DisconnectBG discbg = new DisconnectBG(disconnect,quiesceTimeout,token);
discbg.start();
}
}
public boolean isConnected() {
return conState == CONNECTED;
}
public boolean isConnecting() {
return conState == CONNECTING;
}
public boolean isDisconnected() {
return conState == DISCONNECTED;
}
public boolean isDisconnecting() {
return conState == DISCONNECTING;
}
public boolean isClosed() {
return conState == CLOSED;
}
public void setCallback(MqttCallback mqttCallback) {
this.callback.setCallback(mqttCallback);
}
protected MqttTopic getTopic(String topic) {
return new MqttTopic(topic, this);
}
public void setNetworkModule(NetworkModule networkModule) {
this.networkModule = networkModule;
}
public MqttDeliveryToken[] getPendingDeliveryTokens() {
return tokenStore.getOutstandingDelTokens();
}
protected void deliveryComplete(MqttPublish msg) throws MqttPersistenceException {
this.clientState.deliveryComplete(msg);
}
public IMqttAsyncClient getClient() {
return client;
}
public long getKeepAlive() {
return this.clientState.getKeepAlive();
}
public ClientState getClientState() {
return clientState;
}
public MqttConnectOptions getConOptions() {
return conOptions;
}
public Properties getDebug() {
Properties props = new Properties();
props.put("conState", new Integer(conState));
props.put("serverURI", getClient().getServerURI());
props.put("callback", callback);
props.put("stoppingComms", new Boolean(stoppingComms));
return props;
}
// Kick off the connect processing in the background so that it does not block. For instance
// the socket could take time to create.
private class ConnectBG implements Runnable {
ClientComms clientComms = null;
Thread cBg = null;
MqttToken conToken;
MqttConnect conPacket;
ConnectBG(ClientComms cc, MqttToken cToken, MqttConnect cPacket) {
clientComms = cc;
conToken = cToken;
conPacket = cPacket;
cBg = new Thread(this, "MQTT Con: "+getClient().getClientId());
}
void start() {
cBg.start();
}
public void run() {
final String methodName = "connectBG:run";
MqttException mqttEx = null;
//@TRACE 220=>
log.fine(className, methodName, "220");
try {
// Reset an exception on existing delivery tokens.
// This will have been set if disconnect occured before delivery was
// fully processed.
MqttDeliveryToken[] toks = tokenStore.getOutstandingDelTokens();
for (int i=0; i<toks.length; i++) {
toks[i].internalTok.setException(null);
}
// Save the conncet token in tokenStore as failure can occur before send
tokenStore.saveToken(conToken,conPacket);
// Connect to the server at the network level e.g. TCP socket and then
// start the background processing threads before sending the connect
// packet.
networkModule.start();
receiver = new CommsReceiver(clientComms, clientState, tokenStore, networkModule.getInputStream());
receiver.start("MQTT Rec: "+getClient().getClientId());
sender = new CommsSender(clientComms, clientState, tokenStore, networkModule.getOutputStream());
sender.start("MQTT Snd: "+getClient().getClientId());
callback.start("MQTT Call: "+getClient().getClientId());
internalSend(conPacket, conToken);
} catch (MqttException ex) {
//@TRACE 212=connect failed: unexpected exception
log.fine(className, methodName, "212", null, ex);
mqttEx = ex;
} catch (Exception ex) {
//@TRACE 209=connect failed: unexpected exception
log.fine(className, methodName, "209", null, ex);
mqttEx = ExceptionHelper.createMqttException(ex);
}
if (mqttEx != null) {
shutdownConnection(conToken, mqttEx);
}
}
}
// Kick off the disconnect processing in the background so that it does not block. For instance
// the quiesce
private class DisconnectBG implements Runnable {
Thread dBg = null;
MqttDisconnect disconnect;
long quiesceTimeout;
MqttToken token;
DisconnectBG(MqttDisconnect disconnect, long quiesceTimeout, MqttToken token ) {
this.disconnect = disconnect;
this.quiesceTimeout = quiesceTimeout;
this.token = token;
}
void start() {
dBg = new Thread(this, "MQTT Disc: "+getClient().getClientId());
dBg.start();
}
public void run() {
final String methodName = "disconnectBG:run";
//@TRACE 221=>
log.fine(className, methodName, "221");
// Allow current inbound and outbound work to complete
clientState.quiesce(quiesceTimeout);
try {
internalSend(disconnect, token);
token.internalTok.waitUntilSent();
}
catch (MqttException ex) {
}
finally {
token.internalTok.markComplete(null, null);
shutdownConnection(token, null);
}
}
}
//notify the queueLock
public synchronized void nnnn() {
clientState.notifyQueueLock();
ClientState.isMyControll=true;
}
}