/*********************************************************************************** * * 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.io.File; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.paho.client.mqttv3.MqttException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pl.baczkowicz.mqttspy.connectivity.reconnection.ReconnectionManager; import pl.baczkowicz.mqttspy.logger.MqttMessageLogger; import pl.baczkowicz.mqttspy.messages.FormattedMqttMessage; import pl.baczkowicz.mqttspy.stats.StatisticsManager; import pl.baczkowicz.mqttspy.ui.events.ConnectionStatusChangeEvent; import pl.baczkowicz.mqttspy.ui.scripts.InteractiveScriptManager; import pl.baczkowicz.spy.common.generated.ScriptDetails; import pl.baczkowicz.spy.eventbus.IKBus; import pl.baczkowicz.spy.formatting.FormattingManager; import pl.baczkowicz.spy.scripts.Script; import pl.baczkowicz.spy.ui.events.queuable.EventQueueManager; import pl.baczkowicz.spy.ui.storage.ManagedMessageStoreWithFiltering; import pl.baczkowicz.spy.utils.ConversionUtils; /** * Asynchronous MQTT connection with the extra UI elements required. */ public class MqttAsyncConnection extends MqttConnectionWithReconnection { private final static Logger logger = LoggerFactory.getLogger(MqttAsyncConnection.class); private final RuntimeConnectionProperties properties; private boolean isOpened; private boolean isOpening; private final ManagedMessageStoreWithFiltering<FormattedMqttMessage> store; /** Maximum number of messages to store for this connection in each message store. */ private int preferredStoreSize; private StatisticsManager statisticsManager; // private final EventManager<FormattedMqttMessage> eventManager; private IKBus eventBus; private final InteractiveScriptManager scriptManager; private MqttMessageLogger messageLogger; public MqttAsyncConnection(final ReconnectionManager reconnectionManager, final RuntimeConnectionProperties properties, final MqttConnectionStatus status, final IKBus eventBus, final InteractiveScriptManager scriptManager, final FormattingManager formattingManager, final EventQueueManager<FormattedMqttMessage> uiEventQueue, final int summaryMaxPayloadLength) { super(reconnectionManager, properties); setScriptManager(scriptManager); // Max size is double the preferred size store = new ManagedMessageStoreWithFiltering<FormattedMqttMessage>(properties.getName(), properties.getConfiguredProperties().getMinMessagesStoredPerTopic(), properties.getMaxMessagesStored(), properties.getMaxMessagesStored() * 2, uiEventQueue, //eventManager, formattingManager, summaryMaxPayloadLength); this.setPreferredStoreSize(properties.getMaxMessagesStored()); this.properties = properties; // this.eventManager = eventManager; this.eventBus = eventBus; this.scriptManager = scriptManager; setConnectionStatus(status); } public void messageReceived(final FormattedMqttMessage receivedMessage) { // TODO: we should only delete from the topic matcher when a subscription is closed for good, not when just unsubscribed final List<String> matchingSubscriptionTopics = getTopicMatcher().getMatchingSubscriptions(receivedMessage.getTopic()); logger.trace("Matching subscriptions = " + matchingSubscriptionTopics); final FormattedMqttMessage message = new FormattedMqttMessage(receivedMessage); final List<String> matchingActiveSubscriptions = new ArrayList<String>(); final BaseMqttSubscription lastMatchingSubscription = matchMessageToSubscriptions(matchingSubscriptionTopics, receivedMessage, matchingActiveSubscriptions); // If logging is enabled if (messageLogger != null && messageLogger.isRunning()) { message.setMatchingSubscriptionTopics(matchingActiveSubscriptions); messageLogger.getQueue().add(message); } statisticsManager.messageReceived(getId(), matchingActiveSubscriptions); if (lastMatchingSubscription != null) { message.setSubscription(lastMatchingSubscription.getTopic()); } else { logger.warn("Cannot find a matching subscription for " + receivedMessage.getTopic()); } // Pass the message to the "all" message store store.messageReceived(message); } private BaseMqttSubscription matchMessageToSubscriptions(final List<String> matchingSubscriptionTopics, final FormattedMqttMessage receivedMessage, final List<String> matchingActiveSubscriptions) { BaseMqttSubscription lastMatchingSubscription = matchMessageToSubscriptions( matchingSubscriptionTopics, receivedMessage, matchingActiveSubscriptions, false); // If no active subscriptions available, use the first one that matches (as we might be still receiving messages for a non-active subscription) if (matchingActiveSubscriptions.isEmpty()) { logger.debug("No active subscription available for {}, trying to find first matching...", receivedMessage.getTopic()); lastMatchingSubscription = matchMessageToSubscriptions(matchingSubscriptionTopics, receivedMessage, matchingActiveSubscriptions, true); logger.debug("First matching = {} {}", lastMatchingSubscription, matchingSubscriptionTopics); } return lastMatchingSubscription; } private BaseMqttSubscription matchMessageToSubscriptions(final List<String> matchingSubscriptionTopics, final FormattedMqttMessage receivedMessage, final List<String> matchingSubscriptions, final boolean anySubscription) { BaseMqttSubscription foundMqttSubscription = null; // For all found subscriptions for (final String matchingSubscriptionTopic : matchingSubscriptionTopics) { logger.trace("Message on topic " + receivedMessage.getTopic() + " matched to " + matchingSubscriptionTopic); // Get the mqtt-spy's subscription object final BaseMqttSubscription mqttSubscription = getMqttSubscriptionForTopic(matchingSubscriptionTopic); // If a match has been found, and the subscription is active or we don't care if (mqttSubscription != null && (anySubscription || mqttSubscription.isSubscribing() || mqttSubscription.isActive())) { matchingSubscriptions.add(matchingSubscriptionTopic); // Create a copy of the message for each subscription final FormattedMqttMessage message = new FormattedMqttMessage(receivedMessage); // Set subscription reference on the message message.setSubscription(mqttSubscription.getTopic()); foundMqttSubscription = mqttSubscription; if (mqttSubscription.isScriptActive()) { scriptManager.runScriptWithReceivedMessage(mqttSubscription.getScript(), message); } // Pass the message for subscription handling mqttSubscription.getStore().messageReceived(message); // Find only one matching subscription if checking non-active ones if (anySubscription) { logger.trace("Found one match - exiting..."); break; } } } return foundMqttSubscription; } public boolean publish(final String publicationTopic, final String data, final int qos) { return publish(publicationTopic, ConversionUtils.stringToArray(data), qos, false); } public boolean publish(final String publicationTopic, final String data, final int qos, final boolean retained) { return publish(publicationTopic, ConversionUtils.stringToArray(data), qos, retained); } public boolean publish(final String publicationTopic, final byte[] data, final int qos, final boolean retained) { if (canPublish()) { try { logger.info("Publishing message on topic \"" + publicationTopic + "\". Payload size = \"" + data.length + "\""); client.publish(publicationTopic, data, qos, retained); logger.trace("Published message on topic \"" + publicationTopic + "\". Payload size = \"" + data.length + "\""); statisticsManager.messagePublished(getId(), publicationTopic); return true; } catch (MqttException e) { logger.error("Cannot publish message on " + publicationTopic, e); } } else { logger.warn("Publication attempt failure - no connection available..."); } return false; } public void connectionLost(Throwable cause) { super.connectionLost(cause); unsubscribeAll(false); } public void startBackgroundScripts() { final boolean firstConnection = getConnectionAttempts() == 1; // Attempt starting background scripts if (firstConnection) { for (final ScriptDetails scriptDetails : properties.getConfiguredProperties().getBackgroundScript()) { final File scriptFile = new File(scriptDetails.getFile()); final Script script = scriptManager.getScriptsMap().get(Script.getScriptIdFromFile(scriptFile)); if (scriptDetails.isAutoStart() && scriptFile.exists() && script != null) { scriptManager.runScript(script, true); } else if (!scriptFile.exists()) { logger.warn("File " + scriptDetails.getFile() + " does not exist"); } else if (script == null) { logger.warn("Couldn't retrieve a script for " + scriptDetails.getFile()); } } } } public boolean resubscribeAll(final boolean requestedOnly) { final boolean firstConnection = getConnectionAttempts() == 1; final boolean resubscribeEnabled = connectionDetails.getReconnectionSettings() != null && connectionDetails.getReconnectionSettings().isResubscribe(); final boolean tryAutoSubscribe = firstConnection || resubscribeEnabled; // Attempt re-subscription for (final BaseMqttSubscription subscription : subscriptions.values()) { logger.debug("Subscription {} status [requestedOnly = {}, firstConnection = {}, resubscribeEnabled = {}, subscriptionRequested = {}", subscription.getTopic(), requestedOnly, firstConnection, resubscribeEnabled, subscription.getSubscriptionRequested()); if (!requestedOnly || (tryAutoSubscribe && subscription.getSubscriptionRequested())) { resubscribe(subscription); } } return true; } public boolean resubscribe(final BaseMqttSubscription subscription) { return subscribe(subscription); } public boolean unsubscribeAll(final boolean manualOverride) { // Copy the set of values so that we can start removing them from the 'subscriptions' map final Set<BaseMqttSubscription> allSubscriptions = new HashSet<>(subscriptions.values()); for (final BaseMqttSubscription subscription : allSubscriptions) { unsubscribe(subscription, manualOverride); } return true; } public boolean unsubscribe(final BaseMqttSubscription subscription, final boolean manualOverride) { // If this is a user action, set it not to auto-subscribe if (manualOverride && subscription.getSubscriptionRequested()) { subscription.setSubscriptionRequested(false); } // If already unsubscribed, ignore if (!subscription.isActive()) { return false; } logger.debug("Unsubscribing from " + subscription.getTopic()); removeSubscriptionFromMatcher(subscription); final boolean unsubscribed = unsubscribe(subscription.getTopic()); // Run 'after' for script - TODO: move to BaseMqttConnection? if (subscription.isScriptActive()) { scriptManager.invokeAfter(subscription.getScript()); } subscription.setActive(false); logger.trace("Subscription " + subscription.getTopic() + " is active = " + subscription.isActive()); return unsubscribed; } public boolean unsubscribeAndRemove(final BaseMqttSubscription subscription) { final boolean unsubscribed = unsubscribe(subscription, true); removeSubscription(subscription); logger.info("Subscription " + subscription.getTopic() + " removed"); return unsubscribed; } @Override public boolean subscribe(final BaseMqttSubscription subscription) { final boolean subscribed = super.subscribe(subscription); if (subscribed) { StatisticsManager.newSubscription(); } return subscribed; } public void setConnectionStatus(MqttConnectionStatus connectionStatus) { super.setConnectionStatus(connectionStatus); eventBus.publish(new ConnectionStatusChangeEvent(this)); //eventManager.notifyConnectionStatusChanged(this); } public RuntimeConnectionProperties getProperties() { return properties; } public Map<String, BaseMqttSubscription> getSubscriptions() { return subscriptions; } public int getPreferredStoreSize() { return preferredStoreSize; } public void setPreferredStoreSize(int preferredStoreSize) { this.preferredStoreSize = preferredStoreSize; } public String getId() { return properties.getId(); } public boolean isOpened() { return isOpened; } public void closeConnection() { setOpened(false); } public void setOpened(boolean isOpened) { this.isOpened = isOpened; eventBus.publish(new ConnectionStatusChangeEvent(this)); //eventManager.notifyConnectionStatusChanged(this); } public boolean isOpening() { return isOpening; } public void setOpening(boolean isOpening) { this.isOpening = isOpening; eventBus.publish(new ConnectionStatusChangeEvent(this)); // eventManager.notifyConnectionStatusChanged(this); } public ManagedMessageStoreWithFiltering<FormattedMqttMessage> getStore() { return store; } public String getName() { return store.getName(); } public void setStatisticsManager(final StatisticsManager statisticsManager) { this.statisticsManager = statisticsManager; } public InteractiveScriptManager getScriptManager() { return this.scriptManager; } public void setMessageLogger(final MqttMessageLogger messageLogger) { this.messageLogger = messageLogger; } public MqttMessageLogger getMessageLogger() { return messageLogger; } }