/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.mqtt.inbound;
import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.ScheduledFuture;
import org.eclipse.paho.client.mqttv3.IMqttClient;
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.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.integration.mqtt.core.ConsumerStopAction;
import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory;
import org.springframework.integration.mqtt.core.MqttPahoClientFactory;
import org.springframework.integration.mqtt.event.MqttConnectionFailedEvent;
import org.springframework.integration.mqtt.event.MqttSubscribedEvent;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessagingException;
import org.springframework.util.Assert;
/**
* Eclipse Paho Implementation.
*
* @author Gary Russell
* @since 1.0
*
*/
public class MqttPahoMessageDrivenChannelAdapter extends AbstractMqttMessageDrivenChannelAdapter
implements MqttCallback, ApplicationEventPublisherAware {
private static final int DEFAULT_COMPLETION_TIMEOUT = 30000;
private static final int DEFAULT_RECOVERY_INTERVAL = 10000;
private final MqttPahoClientFactory clientFactory;
private volatile IMqttClient client;
private volatile ScheduledFuture<?> reconnectFuture;
private volatile boolean connected;
private volatile int completionTimeout = DEFAULT_COMPLETION_TIMEOUT;
private volatile int recoveryInterval = DEFAULT_RECOVERY_INTERVAL;
private volatile boolean cleanSession;
private volatile ConsumerStopAction consumerStopAction;
private ApplicationEventPublisher applicationEventPublisher;
/**
* Use this constructor for a single url (although it may be overridden
* if the server URI(s) are provided by the {@link MqttConnectOptions#getServerURIs()}
* provided by the {@link MqttPahoClientFactory}).
* @param url the URL.
* @param clientId The client id.
* @param clientFactory The client factory.
* @param topic The topic(s).
*/
public MqttPahoMessageDrivenChannelAdapter(String url, String clientId, MqttPahoClientFactory clientFactory,
String... topic) {
super(url, clientId, topic);
this.clientFactory = clientFactory;
}
/**
* Use this constructor if the server URI(s) are provided by the {@link MqttConnectOptions#getServerURIs()}
* provided by the {@link MqttPahoClientFactory}.
* @param clientId The client id.
* @param clientFactory The client factory.
* @param topic The topic(s).
* @since 4.1
*/
public MqttPahoMessageDrivenChannelAdapter(String clientId, MqttPahoClientFactory clientFactory,
String... topic) {
super(null, clientId, topic);
this.clientFactory = clientFactory;
}
/**
* Use this URL when you don't need additional {@link MqttConnectOptions}.
* @param url The URL.
* @param clientId The client id.
* @param topic The topic(s).
*/
public MqttPahoMessageDrivenChannelAdapter(String url, String clientId, String... topic) {
this(url, clientId, new DefaultMqttPahoClientFactory(), topic);
}
/**
* Set the completion timeout for operations. Not settable using the namespace.
* Default 30000 milliseconds.
* @param completionTimeout The timeout.
* @since 4.1
*/
public void setCompletionTimeout(int completionTimeout) {
this.completionTimeout = completionTimeout;
}
/**
* The time (ms) to wait between reconnection attempts.
* Default {@value #DEFAULT_RECOVERY_INTERVAL}.
* @param recoveryInterval the interval.
* @since 4.2.2
*/
public void setRecoveryInterval(int recoveryInterval) {
this.recoveryInterval = recoveryInterval;
}
/**
* @since 4.2.2
*/
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher; // NOSONAR (inconsistent synchronization)
}
@Override
protected void doStart() {
Assert.state(getTaskScheduler() != null, "A 'taskScheduler' is required");
super.doStart();
try {
connectAndSubscribe();
}
catch (Exception e) {
logger.error("Exception while connecting and subscribing, retrying", e);
this.scheduleReconnect();
}
}
@Override
protected synchronized void doStop() {
cancelReconnect();
super.doStop();
if (this.client != null) {
try {
if (this.consumerStopAction.equals(ConsumerStopAction.UNSUBSCRIBE_ALWAYS)
|| (this.consumerStopAction.equals(ConsumerStopAction.UNSUBSCRIBE_CLEAN)
&& this.cleanSession)) {
this.client.unsubscribe(getTopic());
}
}
catch (MqttException e) {
logger.error("Exception while unsubscribing", e);
}
try {
this.client.disconnectForcibly(this.completionTimeout);
}
catch (MqttException e) {
logger.error("Exception while disconnecting", e);
}
try {
this.client.close();
}
catch (MqttException e) {
logger.error("Exception while closing", e);
}
this.connected = false;
this.client = null;
}
}
@Override
public void addTopic(String topic, int qos) {
this.topicLock.lock();
try {
super.addTopic(topic, qos);
if (this.client != null && this.client.isConnected()) {
this.client.subscribe(topic, qos);
}
}
catch (MqttException e) {
super.removeTopic(topic);
throw new MessagingException("Failed to subscribe to topic " + topic, e);
}
finally {
this.topicLock.unlock();
}
}
@Override
public void removeTopic(String... topic) {
this.topicLock.lock();
try {
if (this.client != null && this.client.isConnected()) {
this.client.unsubscribe(topic);
}
super.removeTopic(topic);
}
catch (MqttException e) {
throw new MessagingException("Failed to unsubscribe from topic " + Arrays.asList(topic), e);
}
finally {
this.topicLock.unlock();
}
}
private synchronized void connectAndSubscribe() throws MqttException {
MqttConnectOptions connectionOptions = this.clientFactory.getConnectionOptions();
this.cleanSession = connectionOptions.isCleanSession();
this.consumerStopAction = this.clientFactory.getConsumerStopAction();
if (this.consumerStopAction == null) {
this.consumerStopAction = ConsumerStopAction.UNSUBSCRIBE_CLEAN;
}
Assert.state(getUrl() != null || connectionOptions.getServerURIs() != null,
"If no 'url' provided, connectionOptions.getServerURIs() must not be null");
this.client = this.clientFactory.getClientInstance(getUrl(), getClientId());
this.client.setCallback(this);
if (this.client instanceof MqttClient) {
((MqttClient) this.client).setTimeToWait(this.completionTimeout);
}
this.topicLock.lock();
String[] topics = getTopic();
try {
this.client.connect(connectionOptions);
int[] requestedQos = getQos();
int[] grantedQos = Arrays.copyOf(requestedQos, requestedQos.length);
this.client.subscribe(topics, grantedQos);
for (int i = 0; i < requestedQos.length; i++) {
if (grantedQos[i] != requestedQos[i]) {
if (logger.isWarnEnabled()) {
logger.warn("Granted QOS different to Requested QOS; topics: " + Arrays.toString(topics)
+ " requested: " + Arrays.toString(requestedQos)
+ " granted: " + Arrays.toString(grantedQos));
}
break;
}
}
}
catch (MqttException e) {
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, e));
}
logger.error("Error connecting or subscribing to " + Arrays.toString(topics), e);
this.client.disconnectForcibly(this.completionTimeout);
throw e;
}
finally {
this.topicLock.unlock();
}
if (this.client.isConnected()) {
this.connected = true;
String message = "Connected and subscribed to " + Arrays.toString(topics);
if (logger.isDebugEnabled()) {
logger.debug(message);
}
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(new MqttSubscribedEvent(this, message));
}
}
}
private synchronized void cancelReconnect() {
if (this.reconnectFuture != null) {
this.reconnectFuture.cancel(false);
this.reconnectFuture = null;
}
}
private void scheduleReconnect() {
try {
this.reconnectFuture = getTaskScheduler().schedule(() -> {
try {
if (logger.isDebugEnabled()) {
logger.debug("Attempting reconnect");
}
synchronized (MqttPahoMessageDrivenChannelAdapter.this) {
if (!MqttPahoMessageDrivenChannelAdapter.this.connected) {
connectAndSubscribe();
MqttPahoMessageDrivenChannelAdapter.this.reconnectFuture = null;
}
}
}
catch (MqttException e) {
logger.error("Exception while connecting and subscribing", e);
scheduleReconnect();
}
}, new Date(System.currentTimeMillis() + this.recoveryInterval));
}
catch (Exception e) {
logger.error("Failed to schedule reconnect", e);
}
}
@Override
public synchronized void connectionLost(Throwable cause) {
this.logger.error("Lost connection:" + cause.getMessage() + "; retrying...");
this.connected = false;
scheduleReconnect();
if (this.applicationEventPublisher != null) {
this.applicationEventPublisher.publishEvent(new MqttConnectionFailedEvent(this, cause));
}
}
@Override
public void messageArrived(String topic, MqttMessage mqttMessage) throws Exception {
Message<?> message = this.getConverter().toMessage(topic, mqttMessage);
try {
sendMessage(message);
}
catch (RuntimeException e) {
logger.error("Unhandled exception for " + message.toString(), e);
throw e;
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
}
}