/***********************************************************************************
*
* Copyright (c) 2014 Kamil Baczkowicz
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v1.0 which accompany this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Eclipse Distribution License is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* Contributors:
*
* Kamil Baczkowicz - initial API and implementation and/or initial documentation
*
*/
package pl.baczkowicz.mqttspy.connectivity;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.paho.client.mqttv3.IMqttActionListener;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttSecurityException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.baczkowicz.mqttspy.connectivity.topicmatching.TopicMatcher;
import pl.baczkowicz.mqttspy.utils.ConnectionUtils;
import pl.baczkowicz.spy.common.generated.ScriptDetails;
import pl.baczkowicz.spy.exceptions.ExceptionUtils;
import pl.baczkowicz.spy.exceptions.SpyException;
import pl.baczkowicz.spy.scripts.BaseScriptManager;
import pl.baczkowicz.spy.scripts.Script;
import pl.baczkowicz.spy.utils.TimeUtils;
/**
* Base MQTT connection class, encapsulating the Paho's MQTT client and providing some common features.
*/
public abstract class BaseMqttConnection implements IMqttConnection
{
/** Diagnostic logger. */
private static final Logger logger = LoggerFactory.getLogger(BaseMqttConnection.class);
/** Number of connection attempts made. */
private int connectionAttempts = 0;
/** Last connection attempt timestamp. */
private long lastConnectionAttemptTimestamp = ConnectionUtils.NEVER_STARTED;
/** Last successful connection attempt timestamp. */
private Date lastSuccessfulConnectionAttempt;
protected final Map<String, BaseMqttSubscription> subscriptions = new HashMap<>();
private int lastUsedSubscriptionId = 0;
/** The Paho MQTT client. */
protected MqttAsyncClient client;
/** Connection options. */
protected final MqttConnectionDetailsWithOptions connectionDetails;
/** COnnection status. */
private MqttConnectionStatus connectionStatus = MqttConnectionStatus.NOT_CONNECTED;
/** Disconnection reason (if any). */
private String disconnectionReason;
/** Used for matching topics to subscriptions. */
private final TopicMatcher topicMatcher;
/** Used for calling subscription scripts. */
private BaseScriptManager scriptManager;
/**
* Instantiates the BaseMqttConnection.
*
* @param connectionDetails Connection details
*/
public BaseMqttConnection(final MqttConnectionDetailsWithOptions connectionDetails)
{
this.connectionDetails = connectionDetails;
this.topicMatcher = new TopicMatcher();
}
/**
* Creates an asynchronous client with the given callback.
*
* @param callback The callback to be set on the MQTT client
*
* @throws SpyException Thrown when errors detected
*/
public void createClient(final MqttCallback callback) throws SpyException
{
try
{
logger.debug("Creating MQTT client with server URI {} and client ID {}",
connectionDetails.getServerURI().get(0),
connectionDetails.getClientID());
// Creating MQTT client instance
client = new MqttAsyncClient(
connectionDetails.getServerURI().get(0),
connectionDetails.getClientID(),
null);
// Set MQTT callback
client.setCallback(callback);
}
catch (IllegalArgumentException e)
{
throw new SpyException("Cannot instantiate the MQTT client", e);
}
catch (MqttException e)
{
throw new SpyException("Cannot instantiate the MQTT client", e);
}
}
/**
* Asynchronous connection attempt.
*
* TODO: check if this parameter is needed
* @param options The connection options
* @param userContext The user context (used for any callbacks)
* @param callback Connection result callback
*
* @throws SpyException Thrown when errors detected
*/
public void connect(final MqttConnectOptions options, final Object userContext, final IMqttActionListener callback) throws SpyException
{
recordConnectionAttempt();
try
{
client.connect(options, userContext, callback);
}
catch (IllegalArgumentException e)
{
throw new SpyException("Connection attempt failed", e);
}
catch (MqttSecurityException e)
{
throw new SpyException("Connection attempt failed", e);
}
catch (MqttException e)
{
throw new SpyException("Connection attempt failed", e);
}
}
/**
* Synchronous connection attempt.
*
* TODO: check if this parameter is needed
* @param options The connection options
*
* @throws SpyException Thrown when errors detected
*/
public void connectAndWait(final MqttConnectOptions options) throws SpyException
{
recordConnectionAttempt();
try
{
client.connect(options).waitForCompletion();
}
catch (IllegalArgumentException e)
{
throw new SpyException("Connection attempt failed", e);
}
catch (MqttSecurityException e)
{
throw new SpyException("Connection attempt failed", e);
}
catch (MqttException e)
{
throw new SpyException("Connection attempt failed", e);
}
}
/**
* Records a connection attempt.
*/
protected void recordConnectionAttempt()
{
lastConnectionAttemptTimestamp = TimeUtils.getMonotonicTime();
connectionAttempts++;
}
/** Records a successful connection. */
public void recordSuccessfulConnection()
{
lastSuccessfulConnectionAttempt = new Date();
}
/**
* Returns last successful connection attempt.
*
* @return Formatted date of the last successful connection attempt
*/
public String getLastSuccessfulyConnectionAttempt()
{
return TimeUtils.DATE_WITH_SECONDS_SDF.format(lastSuccessfulConnectionAttempt);
}
/**
* Subscribes to the given topic and quality of service.
*
* @param topic The topic to subscribe to
* @param qos The quality of service requested
*
* @throws SpyException Thrown when errors detected
*/
private void subscribeToTopic(final String topic, final int qos) throws SpyException
{
if (client == null || !client.isConnected())
{
// TODO: consider throwing an exception here
logger.warn("Client not connected");
return;
}
try
{
client.subscribe(topic, qos);
topicMatcher.addSubscriptionToStore(topic, "subscription");
}
catch (MqttException e)
{
throw new SpyException("Subscription attempt failed", e);
}
}
/**
* Attempts a subscription to the given topic and quality of service.
*
* @param topic Subscription topic
* @param qos Subscription QoS
*/
// TODO: deprecate? only used for testing
public boolean subscribe(final String topic, final int qos)
{
try
{
subscribeToTopic(topic, qos);
logger.info("Successfully subscribed to " + topic);
return true;
}
catch (SpyException e)
{
logger.error("Subscription attempt failed for topic {}", topic, e);
}
return false;
}
public boolean subscribe(final BaseMqttSubscription subscription)
{
// Subscription are either triggered by configuration or user actions, so default to auto-subscribe
subscription.setSubscriptionRequested(true);
// Record the subscription, regardless of whether further stuff succeeds
addSubscription(subscription);
// If already active, simply ignore
if (subscription.isActive())
{
return false;
}
if (client == null || !client.isConnected())
{
logger.warn("Client not connected");
return false;
}
try
{
// Retained messages can be received very quickly, even so quickly we still haven't set the subscription's state to active
subscription.setSubscribing(true);
logger.debug("Subscribing to " + subscription.getTopic());
client.subscribe(subscription.getTopic(), subscription.getQos());
logger.info("Subscribed to " + subscription.getTopic());
subscription.setActive(true);
subscription.setSubscribing(false);
logger.trace("Subscription " + subscription.getTopic() + " is active = "
+ subscription.isActive());
if (subscription.getDetails() != null
&& subscription.getDetails().getScriptFile() != null
&& !subscription.getDetails().getScriptFile().isEmpty())
{
final Script script = scriptManager.addScript(new ScriptDetails(false, false, subscription.getDetails().getScriptFile()));
subscription.setScript(script);
scriptManager.runScript(script, false);
if (scriptManager.invokeBefore(script))
{
subscription.setScriptActive(true);
}
}
return true;
}
catch (MqttException e)
{
subscription.setSubscribing(false);
logger.error("Cannot subscribe to " + subscription.getTopic(), e);
removeSubscription(subscription);
return false;
}
}
public void addSubscription(final BaseMqttSubscription subscription)
{
// Add it to the store if it hasn't been created before
if (subscriptions.put(subscription.getTopic(), subscription) == null)
{
subscription.setId(lastUsedSubscriptionId++);
}
logger.debug("Adding topic " + subscription.getTopic() + " to the subsciption store");
addSubscriptionToMatcher(subscription);
}
public void removeSubscription(final BaseMqttSubscription subscription)
{
subscriptions.remove(subscription.getTopic());
removeSubscriptionFromMatcher(subscription);
}
public void addSubscriptionToMatcher(final BaseMqttSubscription subscription)
{
getTopicMatcher().addSubscriptionToStore(subscription.getTopic(), "subscription" + subscription.getId());
}
public void removeSubscriptionFromMatcher(final BaseMqttSubscription subscription)
{
getTopicMatcher().removeSubscriptionFromStore(subscription.getTopic(), "subscription" + subscription.getId());
}
public int getLastUsedSubscriptionId()
{
return lastUsedSubscriptionId;
}
public void unsubscribeFromTopic(final String topic) throws SpyException
{
if (client == null || !client.isConnected())
{
// TODO: consider throwing an exception here
logger.warn("Client not connected");
return;
}
try
{
client.unsubscribe(topic);
// topicMatcher.removeSubscriptionFromStore(topic);
}
catch (MqttException e)
{
throw new SpyException("Unsubscription attempt failed", e);
}
}
/**
* Attempts to unsubscribe from the given topic.
*
* @param topic Subscription topic
*/
@Override
public boolean unsubscribe(final String topic)
{
try
{
unsubscribeFromTopic(topic);
logger.info("Successfully unsubscribed from " + topic);
return true;
}
catch (SpyException e)
{
logger.error("Unsubscribe attempt failed for topic {}", topic, e);
}
return false;
}
/**
* Unsubscribes from all topics.
*
* @param manualOverride True if it was requested by user
*
* @return True if successful
*/
public abstract boolean unsubscribeAll(final boolean manualOverride);
/**
* Checks if a message can be published.
*
* @return True if the client is connected
*/
@Override
public boolean canPublish()
{
return client != null && client.isConnected();
}
/**
* Records lost connection.
*
* @param cause The cause of the connection loss
*/
public void connectionLost(Throwable connectionLostCause)
{
setDisconnectionReason(ExceptionUtils.getInfo(connectionLostCause));
setConnectionStatus(MqttConnectionStatus.DISCONNECTED);
}
/**
* Sets the disconnection reason to the given message.
*
* @param message The disconnection reason
*/
public void setDisconnectionReason(final String message)
{
this.disconnectionReason = message;
if (!message.isEmpty())
{
this.disconnectionReason = this.disconnectionReason + " ("
+ TimeUtils.DATE_WITH_SECONDS_SDF.format(new Date()) + ")";
}
}
public void disconnect()
{
try
{
client.disconnect(0);
logger.info("Client {} disconnected", client.getClientId());
}
catch (MqttException e)
{
logger.error("Cannot disconnect", e);
}
}
// ===============================
// === Setters and getters =======
// ===============================
/**
* Gets the last disconnection reason.
*
* @return The disconnection reason
*/
public String getDisconnectionReason()
{
return disconnectionReason;
}
/**
* Gets the MQTT connection details.
*
* @return The MQTT connection details
*/
public MqttConnectionDetailsWithOptions getMqttConnectionDetails()
{
return connectionDetails;
}
/**
* Gets the last connection attempt timestamp.
*
* @return Last connection attempt timestamp
*/
public long getLastConnectionAttemptTimestamp()
{
return lastConnectionAttemptTimestamp;
}
/**
* Gets the number of connections attempts made so far.
*
* @return The number of connection attempts
*/
public int getConnectionAttempts()
{
return connectionAttempts;
}
/**
* Gets the current connection status.
*
* @return Current connection status
*/
public MqttConnectionStatus getConnectionStatus()
{
return connectionStatus;
}
/**
* Sets the new connection status
*
* @param connectionStatus The connection status to set
*/
public void setConnectionStatus(final MqttConnectionStatus connectionStatus)
{
this.connectionStatus = connectionStatus;
}
/**
* Gets the topic matcher.
*
* @return Topic matcher
*/
public TopicMatcher getTopicMatcher()
{
return topicMatcher;
}
/**
* Gets the MQTT client.
*
* @return the client
*/
public MqttAsyncClient getClient()
{
return client;
}
/**
* Sets the MQTT client - primarily for testing.
*
* @param client the client to set
*/
public void setClient(final MqttAsyncClient client)
{
this.client = client;
}
public void setScriptManager(final BaseScriptManager scriptManager)
{
this.scriptManager = scriptManager;
}
// TODO: is that needed?
public BaseMqttSubscription getMqttSubscriptionForTopic(final String topic)
{
return subscriptions.get(topic);
}
}