/*
* 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;
import java.util.Hashtable;
//import java.util.Properties; //m2mgo
//import javax.net.SocketFactory; //m2mgo
//import javax.net.ssl.SSLSocketFactory; //m2mgo
import org.eclipse.paho.client.mqttv3.internal.ClientComms;
import org.eclipse.paho.client.mqttv3.internal.DestinationProvider;
import org.eclipse.paho.client.mqttv3.internal.ExceptionHelper;
//import org.eclipse.paho.client.mqttv3.internal.LocalNetworkModule; //m2mgo
import org.eclipse.paho.client.mqttv3.internal.MemoryPersistence;
import org.eclipse.paho.client.mqttv3.internal.NetworkModule;
import org.eclipse.paho.client.mqttv3.internal.SSLNetworkModule;
import org.eclipse.paho.client.mqttv3.internal.TCPNetworkModule;
import org.eclipse.paho.client.mqttv3.internal.comms.MqttDirectException;
//import org.eclipse.paho.client.mqttv3.internal.security.SSLSocketFactoryFactory;//m2mgo
import org.eclipse.paho.client.mqttv3.internal.trace.Trace;
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.MqttSubscribe;
import org.eclipse.paho.client.mqttv3.internal.wire.MqttUnsubscribe;
import com.m2mgo.net.SSLSocketFactory;
import com.m2mgo.net.SocketFactory;
import com.m2mgo.util.Properties;
/**
* Lightweight client for talking to a server via the MQTT version 3 protocol.
* The client allows an application to use publish/subscribe messaging.
*/
public class MqttClient implements DestinationProvider {
private static final int URI_TYPE_TCP = 0;
private static final int URI_TYPE_SSL = 1;
private static final int URI_TYPE_LOCAL = 2;
private String clientId;
private String serverURI;
private int serverURIType;
private ClientComms comms;
private Hashtable topics;
private MqttClientPersistence persistence;
private Trace trace;
/**
* Creates an MqttClient to connect to the specified address, using the
* specified client identifier. The address should be a URI, using a scheme
* of either "tcp://" for a TCP connection or "ssl://" for a TCP connection
* secured by SSL/TLS. For example:
* <ul>
* <li><code>tcp://localhost:1883</code></li>
* <li><code>ssl://localhost:8883</code></li>
* </ul>
* <p>
* If the port is not specified, it will default to 1883 for "tcp://" URIs,
* and 8883 for "ssl://" URIs.
* </p>
* <p>
* The client identifier should be unique across all clients connecting to
* the same server. A convenience method is provided to generate a random
* client id that should satisfy this criteria - {@link #generateClientId()}
* . As the client identifier is used by the server to identify a client
* when it reconnects, the client must use the same identifier between
* connections if durable subscriptions are to be used.
* </p>
* <p>
* In Java SE, SSL can be configured in one of several ways, which the
* client will use in the following order:
* </p>
* <ul>
* <li><strong>Supplying an <code>SSLSocketFactory</code></strong> -
* applications can use
* {@link MqttConnectOptions#setSocketFactory(SocketFactory)} to supply a
* factory with the appropriate SSL settings.</li>
* <li><strong>SSL Properties</strong> - applications can supply SSL
* settings as a simple Java Properties using
* {@link MqttConnectOptions#setSSLProperties(Properties)}.</li>
* <li><strong>Use JVM settings</strong> - There are a number of standard
* Java system properties that can be used to configure key and trust
* stores.</li>
* </ul>
*
* <p>
* In Java ME, the platform settings are used for SSL connections.
* </p>
*
* <p>
* A default instance of {@link MqttDefaultFilePersistence} is used by the
* client. To specify a different persistence implementation, or to turn off
* persistence, use the
* {@link #MqttClient(String, String, MqttClientPersistence)} constructor.
*
* @param serverURI
* the address to connect to, specified as a URI
* @param clientId
* the client ID to use
* @throws IllegalArgumentException
* if the URI does not start with "tcp://", "ssl://" or
* "local://".
* @throws IllegalArgumentException
* if the clientId is null or is greater than 23 characters in
* length
* @throws MqttException
* if any other problem was encountered
*/
// public MqttClient(String serverURI, String clientId) throws MqttException { //m2mgo
// this(serverURI, clientId, new MqttDefaultFilePersistence());
// }
/**
* Creates an MqttClient to connect to the specified address, using the
* specified client identifer and persistence implementation. The address
* should be a URI, using a scheme of either "tcp://" for a TCP connection
* or "ssl://" for a TCP connection secured by SSL/TLS. For example:
* <ul>
* <li><code>tcp://localhost:1883</code></li>
* <li><code>ssl://localhost:8883</code></li>
* <li><code>local://FirstBroker</code></li>
* </ul>
* <p>
* If the port is not specified, it will default to 1883 for "tcp://" URIs,
* and 8883 for "ssl://" URIs.
* </p>
* <p>
* The client identifier should be unique across all clients connecting to
* the same server. A convenience method is provided to generate a random
* client id that should satisfy this criteria - {@link #generateClientId()}
* . As the client identifier is used by the server to identify a client
* when it reconnects, the client must use the same identifier between
* connections if durable subscriptions are to be used.
* </p>
* <p>
* In Java SE, SSL can be configured in one of several ways, which the
* client will use in the following order:
* </p>
* <ul>
* <li><strong>Supplying an <code>SSLSocketFactory</code></strong> -
* applications can use
* {@link MqttConnectOptions#setSocketFactory(SocketFactory)} to supply a
* factory with the appropriate SSL settings.</li>
* <li><strong>SSL Properties</strong> - applications can supply SSL
* settings as a simple Java Properties using
* {@link MqttConnectOptions#setSSLProperties(Properties)}.</li>
* <li><strong>Use JVM settings</strong> - There are a number of standard
* Java system properties that can be used to configure key and trust
* stores.</li>
* </ul>
*
* <p>
* In Java ME, the platform settings are used for SSL connections.
* </p>
*
* The persistence mechanism is used to enable reliable messaging. For
* qualities of server (QoS) 1 or 2 to work, messages must be persisted to
* disk by both the client and the server. If this is not done, then a
* failure in the client or server will result in lost messages. It is the
* application's responsibility to provide an implementation of the
* {@link MqttClientPersistence} interface, which the client can use to
* persist messages. If the application is only sending QoS 0 messages, then
* this is not needed.
*
* <p>
* An implementation of file-based persistence is provided in the class
* {@link MqttDefaultFilePersistence}. If no persistence is needed, it can
* be explicitly set to <code>null</code>.
* </p>
*
* @param serverURI
* the address to connect to, specified as a URI
* @param clientId
* the client ID to use
* @param persistence
* the persistence mechanism to use.
* @throws IllegalArgumentException
* if the URI does not start with "tcp://", "ssl://" or
* "local://".
* @throws IllegalArgumentException
* if the clientId is null or is greater than 23 characters in
* length
* @throws MqttException
* if any other problem was encountered
*/
public MqttClient(String serverURI, String clientId,
MqttClientPersistence persistence) throws MqttException {
if (clientId == null || clientId.length() == 0
|| clientId.length() > 23) {
throw new IllegalArgumentException();
}
this.trace = Trace.getTrace(clientId);
this.serverURI = serverURI;
this.serverURIType = validateURI(serverURI);
this.clientId = clientId;
this.persistence = persistence;
if (this.persistence == null) {
this.persistence = new MemoryPersistence();
}
// @TRACE 101=New Client Instance ClientID={0} ServerURI={1}
// PersistenceType={2}
trace.trace(Trace.FINE, 101, new Object[] { clientId, serverURI,
persistence });
this.persistence.open(clientId, serverURI);
this.comms = new ClientComms(this, this.persistence, trace);
this.persistence.close();
this.topics = new Hashtable();
}
private int validateURI(String serverURI) {
if (serverURI.startsWith("tcp://")) {
return URI_TYPE_TCP;
} else if (serverURI.startsWith("ssl://")) {
return URI_TYPE_SSL;
} else if (serverURI.startsWith("local://")) {
return URI_TYPE_LOCAL;
} else {
throw new IllegalArgumentException();
}
}
/**
* Factory method to create the correct network module, based on the
* supplied address URI.
*
* @param address
* the URI for the server.
* @return a network module appropriate to the specified address.
*/
protected NetworkModule createNetworkModule(String address,
MqttConnectOptions options) throws MqttException {
NetworkModule netModule;
String shortAddress;
String host;
int port;
SocketFactory factory = options.getSocketFactory();
switch (serverURIType) {
case URI_TYPE_TCP:
shortAddress = address.substring(6);
host = getHostName(shortAddress);
port = getPort(shortAddress, 1883);
if (factory == null) {
factory = SocketFactory.getDefault();
options.setSocketFactory(factory);
} else if (factory instanceof SSLSocketFactory) {
throw ExceptionHelper
.createMqttException(MqttException.REASON_CODE_SOCKET_FACTORY_MISMATCH);
}
netModule = new TCPNetworkModule(trace, factory, host, port);
break;
case URI_TYPE_SSL:
shortAddress = address.substring(6);
host = getHostName(shortAddress);
port = getPort(shortAddress, 8883);
// SSLSocketFactoryFactory factoryFactory = null; //m2mgo
if (factory == null) {
// try {
// factoryFactory = new SSLSocketFactoryFactory();//m2mgo
// Properties sslClientProps =
// options.getSSLProperties();//m2mgo
// if (null != sslClientProps)//m2mgo
// factoryFactory.initialize(sslClientProps, null);//m2mgo
// factory = factoryFactory.createSocketFactory(null);//m2mgo
factory = SSLSocketFactory.getDefault();
// }
// catch (MqttDirectException ex) {
/*
* One of cases when you will reach here is when the truststore
* is created in DeviceEE and loaded in J2SE. This will lead to
* a java.io.IOException: Invalid keystore format which is
* encapsulated in the MQTT exception and thrown.
*/
// throw ExceptionHelper.createMqttException(ex.getCause());
// }
} else if ((factory instanceof SSLSocketFactory) == false) {
throw ExceptionHelper
.createMqttException(MqttException.REASON_CODE_SOCKET_FACTORY_MISMATCH);
}
// Create the network module...
netModule = new SSLNetworkModule(trace, (SSLSocketFactory) factory,
host, port);
// ((SSLNetworkModule)netModule).setSSLhandshakeTimeout(options.getConnectionTimeout());//m2mgo
// Ciphers suites need to be set, if they are available
// if (factoryFactory != null) { //m2mgo
// String[] enabledCiphers =
// factoryFactory.getEnabledCipherSuites(null);
// if (enabledCiphers != null) {
// ((SSLNetworkModule) netModule).setEnabledCiphers(enabledCiphers);
// }
// }
break;
// case URI_TYPE_LOCAL:
// netModule = new LocalNetworkModule(address.substring(8));//m2mgo
// break;
default:
// This shouldn't happen, as long as validateURI() has been called.
netModule = null;
}
return netModule;
}
private int getPort(String uri, int defaultPort) {
int port;
int portIndex = uri.lastIndexOf(':');
if (portIndex == -1) {
port = defaultPort;
} else {
port = Integer.valueOf(uri.substring(portIndex + 1)).intValue();
}
return port;
}
private String getHostName(String uri) {
int schemeIndex = uri.lastIndexOf('/');
int portIndex = uri.lastIndexOf(':');
if (portIndex == -1) {
portIndex = uri.length();
}
return uri.substring(schemeIndex + 1, portIndex);
}
/**
* Connects to a server using the default options. It is recommended to call
* {@link #setCallback(MqttCallback)} prior to connecting.
*/
public void connect() throws MqttSecurityException, MqttException {
this.connect(new MqttConnectOptions());
}
/**
* Connects to a server using the specified options. It is recommended to
* call {@link #setCallback(MqttCallback)} prior to connecting.
*/
public void connect(MqttConnectOptions options)
throws MqttSecurityException, MqttException {
if (this.isConnected()) {
throw ExceptionHelper
.createMqttException(MqttException.REASON_CODE_CLIENT_ALREADY_CONNECTED);
}
// @TRACE 103=Connect cleanSession={0} connectionTimeout={1}
// TimekeepAlive={2} userName={3} password={4} will={5}
if (trace.isOn()) {
trace.trace(
Trace.FINE,
103,
new Object[] {
new Boolean(options.isCleanSession()),
new Integer(options.getConnectionTimeout()),
new Integer(options.getKeepAliveInterval()),
options.getUserName(),
((null == options.getPassword()) ? "[null]"
: "[notnull]"),
((null == options.getWillMessage()) ? "[null]"
: "[notnull]") });
}
comms.setNetworkModule(createNetworkModule(serverURI, options));
this.persistence.open(clientId, serverURI);
if (options.isCleanSession()) {
persistence.clear();
}
comms.connect(
new MqttConnect(clientId, options.isCleanSession(), options
.getKeepAliveInterval(), options.getUserName(), options
.getPassword(), options.getWillMessage(), options
.getWillDestination()), options.getConnectionTimeout(),
options.getKeepAliveInterval(), options.isCleanSession());
}
/**
* Disconnects from the server, which quiesces for up to a maximum of thirty
* seconds, to allow the client to finish any work it currently has.
*
* @see #disconnect(long)
*/
public void disconnect() throws MqttException {
this.disconnect(30000);
}
/**
* Disconnects from the server. This method must not be called from inside
* {@link MqttCallback} methods.
* <p>
* Firstly, the client will wait for all {@link MqttCallback} methods to
* complete. It will then quiesce for the specified time, to allow for work
* which has already been accepted to complete - for example, it will wait
* for the QoS 2 flows from earlier publications to complete. After the
* quiesce timeout, the client will disconnect from the server. When the
* client is next connected, any QoS 1 or 2 messages which have not
* completed will be retried.
* </p>
*
* @param quiesceTimeout
* the amount of time in milliseconds to allow for existing work
* to finish before disconnecting. A value of zero or less means
* the client will not quiesce.
*/
public void disconnect(long quiesceTimeout) throws MqttException {
// @TRACE 104=Disconnect quiesceTimeout={0}
trace.trace(Trace.FINE, 104, new Object[] { new Long(quiesceTimeout) });
MqttDisconnect disconnect = new MqttDisconnect();
try {
comms.disconnect(disconnect, quiesceTimeout);
} catch (MqttException ex) {
// @TRACE 105=Disconnect exception
trace.trace(Trace.FINE, 105, null, ex);
throw ex;
}
}
/**
* Determines if this client is currently connected to the server.
*
* @return <code>true</code> if connected, <code>false</code> otherwise.
*/
public boolean isConnected() {
return comms.isConnected();
}
/**
* Returns the client ID used by this client.
*
* @return the client ID used by this client.
*/
public String getClientId() {
return clientId;
}
/**
* Returns the address of the server used by this client, as a URI. The
* format is the same as specified on the constructor.
*
* @return the server's address, as a URI String.
* @see #MqttClient(String, String)
*/
public String getServerURI() {
return serverURI;
}
/**
* Gets a topic object which can be used to publish messages.
* <p>
* When you build an application, the design of the topic tree should take
* into account the following principles of topic name syntax and semantics:
* </p>
*
* <ul>
* <li>A topic must be at least one character long.</li>
* <li>Topic names are case sensitive. For example, <em>ACCOUNTS</em> and
* <em>Accounts</em> are two different topics.</li>
* <li>Topic names can include the space character. For example,
* <em>Accounts
* payable</em> is a valid topic.</li>
* <li>A leading "/" creates a distinct topic. For example,
* <em>/finance</em> is different from <em>finance</em>. <em>/finance</em>
* matches "+/+" and "/+", but not "+".</li>
* <li>Do not include the null character (Unicode<samp class="codeph">
* \x0000</samp>) in any topic.</li>
* </ul>
*
* <p>
* The following principles apply to the construction and content of a topic
* tree:
* </p>
*
* <ul>
* <li>The length is limited to 64k but within that there are no limits to
* the number of levels in a topic tree.</li>
* <li>There can be any number of root nodes; that is, there can be any
* number of topic trees.</li>
* </ul>
* </p>
*
* @param topic
* the topic to use, for example "finance/stock/ibm".
* @return an MqttTopic object, which can be used to publish messages to the
* topic.
* @throws IllegalArgumentException
* if the topic contains a '+' or '#' wildcard character.
*/
public MqttTopic getTopic(String topic) {
if ((topic.indexOf('#') == -1) && (topic.indexOf('+') == -1)) {
MqttTopic result = (MqttTopic) topics.get(topic);
if (result == null) {
result = new MqttTopic(topic, comms);
topics.put(topic, result);
}
return result;
} else {
throw new IllegalArgumentException();
}
}
/**
* Subscribes to a topic, which may include wildcards, using the default
* options. The {@link #setCallback(MqttCallback)} method should be called
* before this method, otherwise any received messages will be discarded.
*
* @see #subscribe(String[], int[])
*/
public void subscribe(String topicFilter) throws MqttException,
MqttSecurityException {
this.subscribe(new String[] { topicFilter }, new int[] { 1 });
}
/**
* Subscribes to multiple topics, each of which may include wildcards, using
* the default options. The {@link #setCallback(MqttCallback)} method should
* be called before this method, otherwise any received messages will be
* discarded.
*
* @see #subscribe(String[], int[])
*/
public void subscribe(String[] topicFilters) throws MqttException,
MqttSecurityException {
int[] qos = new int[topicFilters.length];
for (int i = 0; i < qos.length; i++) {
qos[i] = 1;
}
this.subscribe(topicFilters, qos);
}
/**
* Subscribes to a topic, which may include wildcards, using the specified
* options. The {@link #setCallback(MqttCallback)} method should be called
* before this method, otherwise any received messages will be discarded.
*
* @param topicFilter
* the topic to subscribe to, which can include wildcards.
* @param qos
* the quality of service at which to subscribe.
* @see #subscribe(String[], int[])
*/
public void subscribe(String topicFilter, int qos) throws MqttException,
MqttSecurityException {
this.subscribe(new String[] { topicFilter }, new int[] { qos });
}
/**
* Subscribes to multiple topics, each of which may include wildcards, using
* the specified options. The {@link #setCallback(MqttCallback)} method
* should be called before this method, otherwise any received messages will
* be discarded.
*
* <p>
* The "topic filter" string used when subscribing may contain special
* characters, which allow you to subscribe to multiple topics at once.
* </p>
* <p>
* The topic level separator is used to introduce structure into the topic,
* and can therefore be specified within the topic for that purpose. The
* multi-level wildcard and single-level wildcard can be used for
* subscriptions, but they cannot be used within a topic by the publisher of
* a message.
* <dl>
* <dt>Topic level separator</dt>
* <dd>The forward slash (/) is used to separate each level within a topic
* tree and provide a hierarchical structure to the topic space. The use of
* the topic level separator is significant when the two wildcard characters
* are encountered in topics specified by subscribers.</dd>
*
* <dt>Multi-level wildcard</dt>
* <dd>
* <p>
* The number sign (#) is a wildcard character that matches any number of
* levels within a topic. For example, if you subscribe to <span><span
* class="filepath">finance/stock/ibm/#</span></span>, you receive messages
* on these topics:
*
* <pre>
* finance/stock/ibm<br /> finance/stock/ibm/closingprice<br /> finance/stock/ibm/currentprice
* </pre>
*
* </p>
* <p>
* The multi-level wildcard can represent zero or more levels. Therefore,
* <em>finance/#</em> can also match the singular <em>finance</em>, where
* <em>#</em> represents zero levels. The topic level separator is
* meaningless in this context, because there are no levels to separate.
* </p>
*
* <p>
* The <span>multi-level</span> wildcard can be specified only on its own or
* next to the topic level separator character. Therefore, <em>#</em> and
* <em>finance/#</em> are both valid, but <em>finance#</em> is not valid.
* <span>The multi-level wildcard must be the last character used within the
* topic tree. For example, <em>finance/#</em> is valid but
* <em>finance/#/closingprice</em> is not valid.</span>
* </p>
* </dd>
*
* <dt>Single-level wildcard</dt>
* <dd>
* <p>
* The plus sign (+) is a wildcard character that matches only one topic
* level. For example, <em>finance/stock/+</em> matches
* <em>finance/stock/ibm</em> and <em>finance/stock/xyz</em>, but not
* <em>finance/stock/ibm/closingprice</em>. Also, because the single-level
* wildcard matches only a single level, <em>finance/+</em> does not match
* <em>finance</em>.
* </p>
*
* <p>
* Use the single-level wildcard at any level in the topic tree, and in
* conjunction with the multilevel wildcard. Specify the single-level
* wildcard next to the topic level separator, except when it is specified
* on its own. Therefore, <em>+</em> and <em>finance/+</em> are both valid,
* but <em>finance+</em> is not valid. <span>The single-level wildcard can
* be used at the end of the topic tree or within the topic tree. For
* example, <em>finance/+</em> and <em>finance/+/ibm</em> are both
* valid.</span>
* </p>
* </dd>
* </dl>
* </p>
*
* @param topicFilters
* the topics to subscribe to, which can include wildcards.
* @param qos
* the qualities of service levels at which to subscribe.
* @throws MqttException
* if there was an error registering the subscription.
* @throws IllegalArgumentException
* if the two supplied arrays are not the same size.
*/
public void subscribe(String[] topicFilters, int[] qos)
throws MqttException, MqttSecurityException {
if (topicFilters.length != qos.length) {
throw new IllegalArgumentException();
}
if (trace.isOn()) {
String subs = "";
for (int i = 0; i < topicFilters.length; i++) {
if (i > 0) {
subs += ", ";
}
subs += topicFilters[i] + ":" + qos[i];
}
// @TRACE 106=Subscribe topic={0}
trace.trace(Trace.FINE, 106, new Object[] { subs });
}
MqttSubscribe register = new MqttSubscribe(topicFilters, qos);
comms.sendAndWait(register);
}
/**
* Unsubscribes from a topic.
*
* @param topicFilter
* the topic to unsubscribe from.
*/
public void unsubscribe(String topicFilter) throws MqttException {
unsubscribe(new String[] { topicFilter });
}
/**
* Unsubscribes from multiple topics.
*
* @param topicFilters
* the topics to unsubscribe from.
*/
public void unsubscribe(String[] topicFilters) throws MqttException {
if (trace.isOn()) {
String subs = "";
for (int i = 0; i < topicFilters.length; i++) {
if (i > 0) {
subs += ", ";
}
subs += topicFilters[i];
}
// @TRACE 107=Unsubscribe topic={0}
trace.trace(Trace.FINE, 107, new Object[] { subs });
}
MqttUnsubscribe unregister = new MqttUnsubscribe(topicFilters);
comms.sendAndWait(unregister);
}
/**
* Sets the callback listener to use for asynchronously received messages.
* The {@link MqttCallback#messageArrived(MqttTopic, MqttMessage)} method
* will be called back whenever a message arrives.
*
* @param callback
* the class to callback when a message arrives.
*/
public void setCallback(MqttCallback callback) throws MqttException {
comms.setCallback(callback);
}
/**
* Returns a randomly generated client identifier based on the current
* user's login name and the system time.
* <p>
* When cleanSession is set to false, an application should ensure it uses
* the same client identifier when it reconnects to the server to resume
* state and maintain assured message delivery.
* </p>
*
* @return a generated client identifier
* @see MqttConnectOptions#setCleanSession(boolean)
*/
public static String generateClientId() {
return (System.getProperty("user.name") + "." + System
.currentTimeMillis());
}
/**
* Returns the delivery tokens for any outstanding publish operations.
*/
public MqttDeliveryToken[] getPendingDeliveryTokens() {
return comms.getPendingDeliveryTokens();
}
}