/*********************************************************************************** * * 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.ui.connections; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import java.util.concurrent.LinkedBlockingQueue; import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.control.Tab; import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pl.baczkowicz.mqttspy.common.generated.MessageLog; import pl.baczkowicz.mqttspy.common.generated.MessageLogEnum; import pl.baczkowicz.mqttspy.configuration.ConfigurationManager; import pl.baczkowicz.mqttspy.configuration.ConfiguredConnectionDetails; import pl.baczkowicz.mqttspy.configuration.generated.UserInterfaceMqttConnectionDetails; import pl.baczkowicz.mqttspy.connectivity.BaseMqttSubscription; import pl.baczkowicz.mqttspy.connectivity.MqttAsyncConnection; import pl.baczkowicz.mqttspy.connectivity.MqttAsyncConnectionRunnable; import pl.baczkowicz.mqttspy.connectivity.MqttConnectionStatus; import pl.baczkowicz.mqttspy.connectivity.RuntimeConnectionProperties; import pl.baczkowicz.mqttspy.connectivity.handlers.MqttCallbackHandler; import pl.baczkowicz.mqttspy.connectivity.handlers.MqttDisconnectionResultHandler; import pl.baczkowicz.mqttspy.connectivity.handlers.MqttEventHandler; import pl.baczkowicz.mqttspy.connectivity.reconnection.ReconnectionManager; import pl.baczkowicz.mqttspy.logger.MqttMessageLogger; import pl.baczkowicz.mqttspy.messages.BaseMqttMessage; import pl.baczkowicz.mqttspy.messages.FormattedMqttMessage; import pl.baczkowicz.mqttspy.scripts.MqttScriptManager; import pl.baczkowicz.mqttspy.stats.StatisticsManager; import pl.baczkowicz.mqttspy.ui.ConnectionController; import pl.baczkowicz.mqttspy.ui.MainController; import pl.baczkowicz.mqttspy.ui.SubscriptionController; import pl.baczkowicz.mqttspy.ui.ViewManager; import pl.baczkowicz.mqttspy.ui.events.ConnectionStatusChangeEvent; import pl.baczkowicz.mqttspy.ui.events.queuable.UIEventHandler; import pl.baczkowicz.mqttspy.ui.events.queuable.connectivity.MqttConnectionAttemptFailureEvent; import pl.baczkowicz.mqttspy.ui.events.queuable.connectivity.MqttDisconnectionAttemptFailureEvent; import pl.baczkowicz.mqttspy.ui.scripts.InteractiveScriptManager; import pl.baczkowicz.mqttspy.ui.utils.ConnectivityUtils; import pl.baczkowicz.mqttspy.ui.utils.ContextMenuUtils; import pl.baczkowicz.mqttspy.ui.utils.DialogUtils; import pl.baczkowicz.spy.common.generated.UserCredentials; import pl.baczkowicz.spy.eventbus.IKBus; import pl.baczkowicz.spy.exceptions.ConfigurationException; import pl.baczkowicz.spy.exceptions.SpyException; import pl.baczkowicz.spy.formatting.FormattingManager; import pl.baczkowicz.spy.ui.configuration.UiProperties; import pl.baczkowicz.spy.ui.events.queuable.EventQueueManager; import pl.baczkowicz.spy.ui.panes.PaneVisibilityStatus; import pl.baczkowicz.spy.ui.panes.TabStatus; import pl.baczkowicz.spy.ui.storage.ManagedMessageStoreWithFiltering; import pl.baczkowicz.spy.ui.threading.SimpleRunLaterExecutor; import pl.baczkowicz.spy.ui.utils.DialogFactory; import pl.baczkowicz.spy.ui.utils.FxmlUtils; import pl.baczkowicz.spy.ui.utils.TabUtils; /** * Class for managing connection tabs. */ public class ConnectionManager { /** Diagnostic logger. */ private final static Logger logger = LoggerFactory.getLogger(ConnectionManager.class); /** Global event bus. */ private final IKBus eventBus; /** Global statistics manager .*/ private final StatisticsManager statisticsManager; /** Global configuration manager. */ private final ConfigurationManager configurationManager; // TODO: not sure this is needed /** Map of connections and their IDs. */ private Map<String, MqttAsyncConnection> connections = new HashMap<>(); /** Map of connections and their connection controllers. */ private final Map<MqttAsyncConnection, ConnectionController> connectionControllers = new HashMap<>(); /** Map of connections and their tabs. */ private final Map<MqttAsyncConnection, Tab> connectionTabs = new HashMap<>(); /** Map of connection controllers and their subscription managers. */ private final Map<ConnectionController, SubscriptionManager> subscriptionManagers = new HashMap<>(); /** UI event queue. */ private final EventQueueManager<FormattedMqttMessage> uiEventQueue; private ViewManager viewManager; /** Reconnection manager. */ private ReconnectionManager reconnectionManager; private Set<ConnectionController> offlineConnectionControllers = new HashSet<>(); public ConnectionManager(final IKBus eventBus, final StatisticsManager statisticsManager, final ConfigurationManager configurationManager) { this.uiEventQueue = new EventQueueManager<FormattedMqttMessage>(); this.eventBus = eventBus; this.statisticsManager = statisticsManager; this.configurationManager = configurationManager; this.reconnectionManager = new ReconnectionManager(); new Thread(reconnectionManager).start(); new Thread(new UIEventHandler(uiEventQueue, eventBus)).start(); } public void openConnection(final ConfiguredConnectionDetails configuredConnectionDetails, final MainController mainController) throws ConfigurationException { // Note: this is not a complete ConfiguredConnectionDetails copy but ConnectionDetails copy - any user credentials entered won't be stored in config final ConfiguredConnectionDetails connectionDetails = new ConfiguredConnectionDetails(); //configuredConnectionDetails.copyTo(connectionDetails); connectionDetails.setConnectionDetails(configuredConnectionDetails); connectionDetails.setID(configuredConnectionDetails.getID()); final boolean cancelled = completeUserAuthenticationCredentials(connectionDetails, mainController.getStage()); if (!cancelled) { final String validationResult = ConnectivityUtils.validateConnectionDetails(connectionDetails, true); if (validationResult != null) { DialogFactory.createWarningDialog("Invalid value detected", validationResult); } else { try { final RuntimeConnectionProperties connectionProperties = new RuntimeConnectionProperties(connectionDetails); new Thread(new Runnable() { @Override public void run() { loadConnectionTab(mainController, mainController, connectionProperties); } }).start(); } catch (ConfigurationException e) { logger.error("Cannot create connection properties", e); DialogFactory.createExceptionDialog("Invalid configuration detected", e); } } } } private boolean completeUserAuthenticationCredentials(final UserInterfaceMqttConnectionDetails connectionDetails, final Stage stage) { if (connectionDetails.getUserAuthentication() != null) { // Copy so that we don't store it in the connection and don't save those values final UserCredentials userCredentials = new UserCredentials(); connectionDetails.getUserCredentials().copyTo(userCredentials); // Check if ask for username or password, and then override existing values if confirmed if (connectionDetails.getUserAuthentication().isAskForPassword() || connectionDetails.getUserAuthentication().isAskForUsername()) { // Password is decoded and encoded in this utility method if (!DialogUtils.createMqttUsernameAndPasswordDialog(stage, connectionDetails.getName(), userCredentials)) { return true; } } // Settings user credentials so they can be validated and passed onto the MQTT client library connectionDetails.setUserCredentials(userCredentials); } return false; } /** * Creates and loads a new connection tab. * * @param mainController The main controller * @param parent The UI parent node * @param connectionProperties The connection properties from which to create the connection */ public void loadConnectionTab(final MainController mainController, final Object parent, final RuntimeConnectionProperties connectionProperties) { // Create connection final MqttAsyncConnection connection = createConnection(connectionProperties, uiEventQueue); connection.setOpening(true); connection.setStatisticsManager(statisticsManager); // Load a new tab and connection pane final FXMLLoader loader = FxmlUtils.createFxmlLoaderForProjectFile("ConnectionTab.fxml"); AnchorPane connectionPane = FxmlUtils.loadAnchorPane(loader); final ConnectionController connectionController = (ConnectionController) loader.getController(); connectionController.setConnection(connection); connectionController.setConnectionManager(this); connectionController.setEventBus(eventBus); connectionController.setStatisticsManager(statisticsManager); connectionController.setTabStatus(new TabStatus()); connectionController.getTabStatus().setVisibility(PaneVisibilityStatus.NOT_VISIBLE); connectionController.getResizeMessageContentMenu().setSelected(mainController.getResizeMessagePaneMenu().isSelected()); final Tab connectionTab = createConnectionTab(connection.getProperties().getName(), connectionPane, connectionController); final SubscriptionManager subscriptionManager = new SubscriptionManager(eventBus, configurationManager, viewManager, uiEventQueue); final SubscriptionController subscriptionController = subscriptionManager.createSubscriptionTab( true, connection.getStore(), null, connection, connectionController); subscriptionController.setConnectionController(connectionController); subscriptionController.setFormatting(configurationManager.getConfiguration().getFormatting()); final ConnectionManager connectionManager = this; Platform.runLater(new Runnable() { @Override public void run() { connectionController.init(); subscriptionController.init(); mainController.addConnectionTab(connectionTab); connectionController.getTabStatus().setVisibility(PaneVisibilityStatus.ATTACHED); connectionController.getTabStatus().setParent(connectionTab.getTabPane()); connectionTab.setContextMenu(ContextMenuUtils.createConnectionMenu(connection, eventBus, connectionController, connectionManager)); subscriptionController.getTab().setContextMenu(ContextMenuUtils.createAllSubscriptionsTabContextMenu( connection, eventBus, subscriptionManager, configurationManager, subscriptionController)); eventBus.subscribe(connectionController, connectionController::onConnectionStatusChanged, ConnectionStatusChangeEvent.class, new SimpleRunLaterExecutor(), connection); connection.setOpening(false); connection.setOpened(true); // Connect if (connection.getProperties().isAutoConnect()) { connectToBroker(connection); } else { connection.setConnectionStatus(MqttConnectionStatus.NOT_CONNECTED); } // Add "All" tab connectionController.getSubscriptionTabs().getTabs().add(subscriptionController.getTab()); subscriptionController.getTab().setDisable(true); connectionControllers.put(connection, connectionController); connectionTabs.put(connection, connectionTab); subscriptionManagers.put(connectionController, subscriptionManager); // Populate panes mainController.populateConnectionPanes(connectionProperties.getConfiguredProperties(), connectionController); // Apply perspective viewManager.showPerspective(connectionController); } }); } /** * Creates and loads a message log tab. * * @param mainController The main controller * @param parent The parent UI node * @param name Name of the tab * @param list List of messages to display */ public void loadMessageLogTab(final MainController mainController, final String name, final List<BaseMqttMessage> list) { // Load a new tab and connection pane final FXMLLoader loader = FxmlUtils.createFxmlLoaderForProjectFile("ConnectionTab.fxml"); AnchorPane connectionPane = FxmlUtils.loadAnchorPane(loader); final ConnectionController connectionController = (ConnectionController) loader.getController(); connectionController.setConnectionManager(this); connectionController.setEventBus(eventBus); connectionController.setStatisticsManager(statisticsManager); connectionController.setReplayMode(true); connectionController.setTabStatus(new TabStatus()); connectionController.getTabStatus().setVisibility(PaneVisibilityStatus.NOT_VISIBLE); connectionController.getResizeMessageContentMenu().setSelected(mainController.getResizeMessagePaneMenu().isSelected()); final Tab replayTab = createConnectionTab(name, connectionPane, connectionController); final SubscriptionManager subscriptionManager = new SubscriptionManager(eventBus, configurationManager, viewManager, uiEventQueue); final ManagedMessageStoreWithFiltering<FormattedMqttMessage> store = new ManagedMessageStoreWithFiltering<FormattedMqttMessage>( name, 0, list.size(), list.size(), uiEventQueue, //eventManager, new FormattingManager(new MqttScriptManager(null, null, null)), UiProperties.getSummaryMaxPayloadLength(configurationManager.getUiPropertyFile())); final SubscriptionController subscriptionController = subscriptionManager.createSubscriptionTab( true, store, null, null, connectionController); subscriptionController.setConnectionController(connectionController); subscriptionController.setFormatting(configurationManager.getConfiguration().getFormatting()); subscriptionController.setReplayMode(true); final ConnectionManager manager = this; Platform.runLater(new Runnable() { @Override public void run() { connectionController.init(); subscriptionController.init(); mainController.addConnectionTab(replayTab); replayTab.setContextMenu(ContextMenuUtils.createMessageLogMenu(replayTab, connectionController, manager)); // Add "All" subscription tab connectionController.getSubscriptionTabs().getTabs().clear(); connectionController.getSubscriptionTabs().getTabs().add(subscriptionController.getTab()); connectionController.getTabStatus().setVisibility(PaneVisibilityStatus.ATTACHED); connectionController.getTabStatus().setParent(replayTab.getTabPane()); // TODO: pane status offlineConnectionControllers.add(connectionController); subscriptionManagers.put(connectionController, subscriptionManager); // Apply perspective connectionController.showReplayMode(); // Process the messages for (final BaseMqttMessage mqttMessage : list) { store.messageReceived(new FormattedMqttMessage(mqttMessage, null)); } replayTab.getTabPane().getSelectionModel().select(replayTab); } }); } /** * Gets a collection of all connections. * * @return Collection of MqttAsyncConnection instances */ public Collection<MqttAsyncConnection> getConnections() { // TODO: needs to use the connections list, as the controllers are populated later, // so opening doesn't work properly return connections.values(); //return connectionControllers.keySet(); } /** * Disconnects all connections. */ public void disconnectAll() { for (final MqttAsyncConnection connection : getConnections()) { disconnectFromBroker(connection); } } /** * Disconnects and closes the tab of the given connection. * * @param connection The connection to close */ public void disconnectAndCloseTab(final MqttAsyncConnection connection) { disconnectFromBroker(connection); connection.closeConnection(); if (connection.getMessageLogger() != null && connection.getMessageLogger().isRunning()) { connection.getMessageLogger().stop(); } TabUtils.requestClose(connectionControllers.get(connection).getTab()); subscriptionManagers.remove(connectionControllers.get(connection)); connectionControllers.remove(connection); connectionTabs.remove(connection); logger.debug("Closing connection tab; sm = {}; cc = {}; ct = {}", subscriptionManagers.keySet().size(), connectionControllers.keySet().size(), connectionTabs.keySet().size()); // Stop all scripts connection.getScriptManager().stopScripts(); // for (final Script script : connection.getScriptManager().getScripts()) // { // // Only stop file-based scripts // if (script.getScriptFile() != null) // { // // connection.getScriptManager().stopScriptFile(script.getScriptFile()); // connection.getScriptManager().stopScript(script); // } // } for (final BaseMqttSubscription subscription : connection.getSubscriptions().values()) { subscription.getStore().cleanUp(); } connection.getStore().cleanUp(); } public void closeOfflineTab(final ConnectionController connectionController) { TabUtils.requestClose(connectionController.getTab()); offlineConnectionControllers.remove(connectionController); } /** * Creates a new connection tab. * * @param name Name of the tab * @param content The content of the tab * @param connectionController The connection controller * * @return Created tab */ private Tab createConnectionTab(final String name, final Node content, final ConnectionController connectionController) { final Tab tab = new Tab(); connectionController.setTab(tab); tab.setText(name); tab.setContent(content); return tab; } public ConnectionController getControllerForTab(final Tab tab) { for (final ConnectionController controller : getConnectionControllers()) { if (controller.getTab().equals(tab)) { return controller; } } return null; } public MqttAsyncConnection createConnection(final RuntimeConnectionProperties connectionProperties, final EventQueueManager<FormattedMqttMessage> uiEventQueue) { final InteractiveScriptManager scriptManager = new InteractiveScriptManager(eventBus, null); final FormattingManager formattingManager = new FormattingManager(scriptManager); final MqttAsyncConnection connection = new MqttAsyncConnection(reconnectionManager, connectionProperties, MqttConnectionStatus.DISCONNECTED, eventBus, scriptManager, formattingManager, uiEventQueue, UiProperties.getSummaryMaxPayloadLength(configurationManager.getUiPropertyFile())); formattingManager.initialiseFormatter(connection.getProperties().getFormatter()); scriptManager.setConnection(connection); // Set up message logger final MessageLog messageLog = connectionProperties.getConfiguredProperties().getMessageLog(); if (messageLog != null && !messageLog.getValue().equals(MessageLogEnum.DISABLED) && messageLog.getLogFile() != null && !messageLog.getLogFile().isEmpty()) { final Queue<FormattedMqttMessage> messageQueue= new LinkedBlockingQueue<FormattedMqttMessage>(); if (connection.getMessageLogger() == null) { final MqttMessageLogger messageLogger = new MqttMessageLogger( connection.getId(), messageQueue, messageLog, true, 50); connection.setMessageLogger(messageLogger); } if (!connection.getMessageLogger().isRunning()) { new Thread(connection.getMessageLogger()).start(); } } // connection.setScriptManager(scriptManager); //connection.getStore().setFormattingManager(new FormattingManager(scriptManager)); // Store the created connection connections.put(connectionProperties.getConfiguredProperties().getID(), connection); return connection; } /** * Connects the specified connection to a broker. * * @param connection The connection to connect * * @return True if successfully connected */ public boolean connectToBroker(final MqttAsyncConnection connection) { try { connection.connect(new MqttCallbackHandler(connection), new MqttAsyncConnectionRunnable(connection)); return true; } catch (SpyException e) { // TODO: simplify this Platform.runLater(new MqttEventHandler(new MqttConnectionAttemptFailureEvent(connection, e))); logger.error(e.getMessage(), e); } return false; } /** * Disconnects the specified connection from the broker. * * @param connection The connection to disconnect */ public void disconnectFromBroker(final MqttAsyncConnection connection) { try { connection.disconnect(new MqttDisconnectionResultHandler()); } catch (SpyException e) { // TODO: simplify this Platform.runLater(new MqttEventHandler(new MqttDisconnectionAttemptFailureEvent(connection, e))); logger.error(e.getMessage(), e); } } /** * Disconnects and closes all connections. */ public void disconnectAndCloseAll() { for (final MqttAsyncConnection connection : getConnections()) { disconnectAndCloseTab(connection); } } /** * Gets the connection controllers. * * @return Collection of ConnectionController instances */ public Collection<ConnectionController> getConnectionControllers() { return connectionControllers.values(); } /** * Gets the connection controllers. * * @return Collection of ConnectionController instances */ public Collection<ConnectionController> getOfflineConnectionControllers() { return offlineConnectionControllers; } /** * Gets subscription manager for the given connection. * * @param connectionController The connection controller for which to retrieve the subscription manager * * @return SubscriptionManager instance */ public SubscriptionManager getSubscriptionManager(final ConnectionController connectionController) { return subscriptionManagers.get(connectionController); } public void setViewManager(final ViewManager viewManager) { this.viewManager = viewManager; } }