Package org.openhab.io.transport.mqtt.internal

Source Code of org.openhab.io.transport.mqtt.internal.MqttBrokerConnection

/**
* Copyright (c) 2010-2014, openHAB.org and others.
*
* 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
*/
package org.openhab.io.transport.mqtt.internal;

import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Properties;
import java.util.Timer;
import java.util.concurrent.CopyOnWriteArrayList;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.lang.StringUtils;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
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.MqttMessage;
import org.eclipse.paho.client.mqttv3.MqttTopic;
import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence;
import org.openhab.io.transport.mqtt.MqttMessageConsumer;
import org.openhab.io.transport.mqtt.MqttMessageProducer;
import org.openhab.io.transport.mqtt.MqttSenderChannel;
import org.openhab.io.transport.mqtt.MqttWillAndTestament;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* An MQTTBrokerConnection represents a single client connection to a MQTT
* broker. The connection is configured by the MQTTService with properties from
* the openhab.cfg file.
*
* When a connection to an MQTT broker is lost, it will try to reconnect every
* 60 seconds.
*
* @author Davy Vanherbergen
* @since 1.3.0
*/
public class MqttBrokerConnection implements MqttCallback {

  private static Logger logger = LoggerFactory
      .getLogger(MqttBrokerConnection.class);

  private static final int RECONNECT_FREQUENCY = 60000;

  private String name;

  private String url;

  private String user;

  private String password;

  private int qos = 0;

  private boolean retain = false;

  private boolean async = true;

  private MqttWillAndTestament lastWill;

  private String clientId;

  private MqttClient client;

  private boolean started;

  private List<MqttMessageConsumer> consumers = new CopyOnWriteArrayList<MqttMessageConsumer>();

  private List<MqttMessageProducer> producers = new CopyOnWriteArrayList<MqttMessageProducer>();

  private Timer reconnectTimer;

  private int keepAliveInterval = 60;

  /**
   * Create a new connection with the given name.
   *
   * @param name
   *            for the connection.
   */
  public MqttBrokerConnection(String name) {
    this.name = name;
  }

  /**
   * Start the connection. This will try to open an MQTT client connection to
   * the MQTT broker and notify all publishers and subscribers on this
   * connection that the connection has become active.
   *
   * @throws Exception
   *             If connection could not be created.
   */
  public synchronized void start() throws Exception {

    if (StringUtils.isEmpty(url)) {
      logger.debug(
          "No url defined for MQTT broker connection '{}'. Not starting.",
          name);
      return;
    }

    logger.info("Starting MQTT broker connection '{}'", name);
    openConnection();

    if (reconnectTimer != null) {
      // we are active, so stop trying to reconnect
      reconnectTimer.cancel();
    }

    // start all consumers
    for (MqttMessageConsumer c : consumers) {
      startConsumer(c);
    }

    // start all producers
    for (MqttMessageProducer p : producers) {
      startProducer(p);
    }

    started = true;
  }

  /**
   * @return name for the connection as defined in openhab.cfg.
   */
  public String getName() {
    return name;
  }

  /**
   * Get the url for the MQTT broker. Valid URL's are in the format:
   * tcp://localhost:1883 or ssl://localhost:8883
   *
   * @return url for the MQTT broker.
   */
  public String getUrl() {
    return url;
  }

  /**
   * Set the url for the MQTT broker. Valid URL's are in the format:
   * tcp://localhost:1883 or ssl://localhost:8883
   *
   * @param url
   *            url string for the MQTT broker.
   */
  public void setUrl(String url) {
    this.url = url;
  }

  /**
   * @return optional user name for the MQTT connection.
   */
  public String getUser() {
    return user;
  }

  /**
   * Set the optional user name to use when connecting to the MQTT broker.
   *
   * @param user
   *            name to use for connection.
   */
  public void setUser(String user) {
    this.user = user;
  }

  /**
   * @return connection password.
   */
  public String getPassword() {
    return password;
  }

  /**
   * Set the optional password to use when connecting to the MQTT broker.
   *
   * @param password
   */
  public void setPassword(String password) {
    this.password = password;
  }

  /**
   * @return quality of service level.
   */
  public int getQos() {
    return qos;
  }

  /**
   * Set quality of service. Valid values are 0,1,2
   *
   * @param qos
   *            level.
   */
  public void setQos(int qos) {
    if (qos >= 0 && qos <= 2) {
      this.qos = qos;
    }
  }

  /**
   * @return true if messages sent to the broker should be retained by the
   *         broker.
   */
  public boolean isRetain() {
    return retain;
  }

  /**
   * Set whether any published messages should be retained by the broker.
   *
   * @param retain
   *            true to retain.
   */
  public void setRetain(boolean retain) {
    this.retain = retain;
  }

  public MqttWillAndTestament getLastWill() {
    return lastWill;
  }

  public void setLastWill(MqttWillAndTestament lastWill) {
    this.lastWill = lastWill;
  }

  /**
   * @return true if messages are sent asynchronously.
   */
  public boolean isAsync() {
    return async;
  }

  /**
   * Set whether messages should be sent synchronously (the message is sent
   * and the thread waits until delivery to the broker has completed) or
   * asynchronously (the message is sent and the sendign thread does not wait
   * for delivery completion). In the case of async, the sending thread
   * currently does not receive any feedback when delivery is completed.
   *
   * @param async
   */
  public void setAsync(boolean async) {
    this.async = async;
  }

  /**
   * Set client id to use when connecting to the broker. If none is specified,
   * a default is generated.
   *
   * @param value
   *            clientId to use.
   */
  public void setClientId(String value) {
    this.clientId = value;
  }

  /**
   * Open an MQTT client connection.
   *
   * @throws Exception
   */
  private void openConnection() throws Exception {
    if (client != null && client.isConnected()) {
      return;
    }

    if (StringUtils.isBlank(url)) {
      throw new Exception("Missing url");
    }

    if (client == null) {
      if (StringUtils.isBlank(clientId) || clientId.length() > 23) {
        clientId = MqttClient.generateClientId();
      }
     
      String tmpDir = System.getProperty("java.io.tmpdir") + "/" + name;
      MqttDefaultFilePersistence dataStore = new MqttDefaultFilePersistence(
          tmpDir);
      logger.debug(
          "Creating new client for '{}' using id '{}' and file store '{}'",
          url, clientId, tmpDir);
      client = new MqttClient(url, clientId, dataStore);
      client.setCallback(this);
    }

    MqttConnectOptions options = new MqttConnectOptions();

    if (!StringUtils.isBlank(user)) {
      options.setUserName(user);
    }
    if (!StringUtils.isBlank(password)) {
      options.setPassword(password.toCharArray());
    }   
    if (url.toLowerCase().contains("ssl")) {

      if (StringUtils.isNotBlank(System
          .getProperty("com.ibm.ssl.protocol"))) {

        // get all com.ibm.ssl properties from the system properties
        // and set them as the SSL properties to use.

        Properties sslProps = new Properties();
        addSystemProperty("com.ibm.ssl.protocol", sslProps);
        addSystemProperty("com.ibm.ssl.contextProvider", sslProps);
        addSystemProperty("com.ibm.ssl.keyStore", sslProps);
        addSystemProperty("com.ibm.ssl.keyStorePassword", sslProps);
        addSystemProperty("com.ibm.ssl.keyStoreType", sslProps);
        addSystemProperty("com.ibm.ssl.keyStoreProvider", sslProps);
        addSystemProperty("com.ibm.ssl.trustStore", sslProps);
        addSystemProperty("com.ibm.ssl.trustStorePassword", sslProps);
        addSystemProperty("com.ibm.ssl.trustStoreType", sslProps);
        addSystemProperty("com.ibm.ssl.trustStoreProvider", sslProps);
        addSystemProperty("com.ibm.ssl.enabledCipherSuites", sslProps);
        addSystemProperty("com.ibm.ssl.keyManager", sslProps);
        addSystemProperty("com.ibm.ssl.trustManager", sslProps);

        options.setSSLProperties(sslProps);

      } else {

        // use standard JSSE available in the runtime and
        // use TLSv1.2 which is the default for a secured mosquitto
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(null,
            new TrustManager[] { getVeryTrustingTrustManager() },
            new java.security.SecureRandom());
        SSLSocketFactory socketFactory = sslContext.getSocketFactory();
        options.setSocketFactory(socketFactory);
      }
    }

    if (lastWill != null) {
      options.setWill(lastWill.getTopic(), lastWill.getPayload(),
          lastWill.getQos(), lastWill.isRetain());
    }

    options.setKeepAliveInterval(keepAliveInterval);
   
    client.connect(options);
  }

  /**
   * Create a trust manager which is not too concerned about validating
   * certificates.
   *
   * @return a trusting trust manager
   */
  private TrustManager getVeryTrustingTrustManager() {

    return new X509TrustManager() {

      @Override
      public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
      }

      @Override
      public void checkClientTrusted(X509Certificate[] certs,
          String authType) {
      }

      @Override
      public void checkServerTrusted(X509Certificate[] certs,
          String authType) {
      }
    };

  }

  private Properties addSystemProperty(String key, Properties props) {
    String value = System.getProperty(key);
    if (StringUtils.isNotBlank(value)) {
      props.put(key, value);
    }
    return props;
  }

  /**
   * Add a new message producer to this connection.
   *
   * @param publisher
   *            to add.
   */
  public synchronized void addProducer(MqttMessageProducer publisher) {
    producers.add(publisher);
    if (started) {
      startProducer(publisher);
    }
  }

  /**
   * Start a registered producer, so that it can start sending messages.
   *
   * @param publisher
   *            to start.
   */
  private void startProducer(MqttMessageProducer publisher) {

    logger.trace("Starting message producer for broker '{}'", name);

    publisher.setSenderChannel(new MqttSenderChannel() {

      @Override
      public void publish(String topic, byte[] payload) throws Exception {

        if (!started) {
          logger.warn(
              "Broker connection not started. Cannot publish message to topic '{}'",
              topic);
          return;
        }

        // Create and configure a message
        MqttMessage message = new MqttMessage(payload);
        message.setQos(qos);
        message.setRetained(retain);

        // publish message asynchronously
        MqttTopic mqttTopic = client.getTopic(topic);
        MqttDeliveryToken deliveryToken = mqttTopic.publish(message);

        logger.debug("Publishing message {} to topic '{}'",
            deliveryToken.getMessageId(), topic);
        if (!async) {
          // wait for publish confirmation
          deliveryToken.waitForCompletion(10000);
          if (!deliveryToken.isComplete()) {
            logger.error(
                "Did not receive completion message within timeout limit whilst publishing to topic '{}'",
                topic);
          }
        }

      }
    });

  }

  /**
   * Add a new message consumer to this connection.
   *
   * @param consumer
   *            to add.
   */
  public synchronized void addConsumer(MqttMessageConsumer subscriber) {
    consumers.add(subscriber);
    if (started) {
      startConsumer(subscriber);
    }
  }

  /**
   * Start a registered consumer, so that it can start receiving messages.
   *
   * @param subscriber
   *            to start.
   */
  private void startConsumer(MqttMessageConsumer subscriber) {

    String topic = subscriber.getTopic();
    logger.debug("Starting message consumer for broker '{}' on topic '{}'",
        name, topic);

    try {
      client.subscribe(topic, qos);
    } catch (Exception e) {
      logger.error("Error starting consumer", e);
    }
  }

  /**
   * Remove a previously registered producer from this connection.
   *
   * @param publisher
   *            to remove.
   */
  public synchronized void removeProducer(MqttMessageProducer publisher) {
    logger.debug("Removing message producer for broker '{}'", name);
    publisher.setSenderChannel(null);
    producers.remove(publisher);
  }

  /**
   * Remove a previously registered consumer from this connection.
   *
   * @param subscriber
   *            to remove.
   */
  public synchronized void removeConsumer(MqttMessageConsumer subscriber) {
    logger.debug("Unsubscribing message consumer for topic '{}' from broker '{}'", subscriber.getTopic(), name);

    try {
      if (started) {
        client.unsubscribe(subscriber.getTopic());
      }
    } catch (Exception e) {
      logger.error("Error unsubscribing topic from broker", e);
    }
    consumers.remove(subscriber);

  }

  /**
   * Close the MQTT connection.
   */
  public synchronized void close() {
  logger.debug("Closing connection to broker '{}'", name);
    try {
      if (started) {
        client.disconnect();
      }
    } catch (MqttException e) {
      logger.error("Error closing connection to broker", e);
    }
    started = false;
  }

  @Override
  public synchronized void connectionLost(Throwable t) {
 
    logger.error("MQTT connection to broker was lost", t);
 
    if (t instanceof MqttException) {
      MqttException e = (MqttException) t;
      logger.error("MQTT connection to '{}' was lost: {} : ReasonCode {} : Cause : {}",
          new Object[] { name, e.getMessage(), e.getReasonCode(), (e.getCause() == null ? "Unknown" : e.getCause().getMessage()) });
    } else {     
      logger.error("MQTT connection to '{}' was lost: {}", name, t.getMessage());
    }
   
    started = false;
    logger.info(
        "Starting connection helper to periodically try restore connection to broker '{}'",
        name);

    MqttBrokerConnectionHelper helper = new MqttBrokerConnectionHelper(this);
    reconnectTimer = new Timer(true);
    reconnectTimer.schedule(helper, 10000, RECONNECT_FREQUENCY);

  }

  @Override
  public void deliveryComplete(IMqttDeliveryToken token) {
    logger.trace("Message with id {} delivered.", token.getMessageId());
  }

  @Override
  public void messageArrived(String topic, MqttMessage message)
      throws Exception {

    logger.trace("Received message on topic '{}' : {}", topic, new String(
        message.getPayload()));
    for (MqttMessageConsumer consumer : consumers) {
      if (isTopicMatch(topic, consumer.getTopic())) {
        consumer.processMessage(topic, message.getPayload());
      }
    }
  }

  /**
   * Check if the topic on which a message was received matches provided
   * target topic. The matching will take into account the + and # wildcards.
   *
   * @param source
   *            topic from received message
   * @param target
   *            topic as defined with possible wildcards.
   * @return true if both topics match.
   */
  private boolean isTopicMatch(String source, String target) {
    if (source.equals(target)) {
      return true;
    }
    if (target.indexOf('+') == -1 && target.indexOf('#') == -1) {
      return false;
    } else {

      String regex = target;
      regex = StringUtils.replace(regex, "+", "[^/]*");
      regex = StringUtils.replace(regex, "#", ".*");
      boolean result = source.matches(regex);
      if (result) {
        logger.trace("Topic match for '{}' and '{}' using regex {}",
            source, target, regex);
        return true;
      } else {
        logger.trace("No topic match for '{}' and '{}' using regex {}",
            source, target, regex);
        return false;
      }
    }

  }

  /**
   * Set the keep alive interval. The default interval is 60 seconds.
   * If no heartbeat is received within this timeframe, the connection
   * will be considered dead. Set this to a higher value on systems which may
   * not always be able to process the heartbeat in time.
   * @param keepAliveInterval interval in seconds
   */
  public void setKeepAliveInterval(int keepAliveInterval) {
    this.keepAliveInterval = keepAliveInterval;
  }

 
}
TOP

Related Classes of org.openhab.io.transport.mqtt.internal.MqttBrokerConnection

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.