/*********************************************************************************** * * 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; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.ResourceBundle; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Pos; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.RadioMenuItem; import javafx.scene.control.SplitPane; import javafx.scene.control.Tab; import javafx.scene.control.TextField; import javafx.scene.control.ToggleGroup; import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.HBox; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pl.baczkowicz.mqttspy.configuration.ConfigurationManager; import pl.baczkowicz.mqttspy.connectivity.MqttAsyncConnection; import pl.baczkowicz.mqttspy.messages.FormattedMqttMessage; import pl.baczkowicz.mqttspy.scripts.MqttScriptManager; import pl.baczkowicz.spy.eventbus.IKBus; import pl.baczkowicz.spy.formatting.FormattingManager; import pl.baczkowicz.spy.messages.FormattedMessage; import pl.baczkowicz.spy.scripts.Script; import pl.baczkowicz.spy.ui.configuration.UiProperties; import pl.baczkowicz.spy.ui.events.MessageAddedEvent; import pl.baczkowicz.spy.ui.events.MessageFormatChangeEvent; import pl.baczkowicz.spy.ui.events.MessageIndexChangeEvent; import pl.baczkowicz.spy.ui.events.MessageIndexIncrementEvent; import pl.baczkowicz.spy.ui.events.MessageIndexToFirstEvent; import pl.baczkowicz.spy.ui.events.queuable.ui.BrowseReceivedMessageEvent; import pl.baczkowicz.spy.ui.properties.MessageContentProperties; import pl.baczkowicz.spy.ui.search.InlineScriptMatcher; import pl.baczkowicz.spy.ui.search.ScriptMatcher; import pl.baczkowicz.spy.ui.search.SearchMatcher; import pl.baczkowicz.spy.ui.search.SearchOptions; import pl.baczkowicz.spy.ui.search.SimplePayloadMatcher; import pl.baczkowicz.spy.ui.search.UniqueContentOnlyFilter; import pl.baczkowicz.spy.ui.storage.FilteredMessageStore; import pl.baczkowicz.spy.ui.storage.ManagedMessageStoreWithFiltering; import pl.baczkowicz.spy.ui.threading.SimpleRunLaterExecutor; /** * Controller for the search pane. */ public class SearchPaneController implements Initializable { /** Diagnostic logger. */ private final static Logger logger = LoggerFactory.getLogger(SearchPaneController.class); private final static int MAX_SEARCH_VALUE_CHARACTERS = 15; @FXML private TextField searchField; /** * The name of this field needs to be set to the name of the pane + * Controller (i.e. <fx:id>Controller). */ @FXML private MessageController messagePaneController; /** * The name of this field needs to be set to the name of the pane + * Controller (i.e. <fx:id>Controller). */ @FXML private MessageListTableController messageListTablePaneController; /** * The name of this field needs to be set to the name of the pane + * Controller (i.e. <fx:id>Controller). */ @FXML private MessageNavigationController messageNavigationPaneController; @FXML private CheckBox autoRefreshCheckBox; @FXML private CheckBox caseSensitiveCheckBox; @FXML private ToggleGroup searchMethod; @FXML private Menu searchWithScriptsMenu; @FXML private RadioMenuItem defaultSearch; @FXML private RadioMenuItem inlineScriptSearch; @FXML private Label textLabel; @FXML private AnchorPane messagePane; @FXML private SplitPane splitPane; private IKBus eventBus; private ManagedMessageStoreWithFiltering<FormattedMqttMessage> store; private FilteredMessageStore<FormattedMqttMessage> foundMessageStore; private Tab tab; private final ObservableList<MessageContentProperties<FormattedMqttMessage>> foundMessages = FXCollections.observableArrayList(); private int seachedCount; private MqttAsyncConnection connection; private MqttScriptManager scriptManager; private UniqueContentOnlyFilter<FormattedMqttMessage> uniqueContentOnlyFilter; private ConfigurationManager configurationManager; private FormattingManager formattingManager; public void initialize(URL location, ResourceBundle resources) { searchField.addEventFilter(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() { @Override public void handle(KeyEvent keyEvent) { switch (keyEvent.getCode()) { case ENTER: { search(); break; } default: break; } } }); } public void init() { foundMessageStore = new FilteredMessageStore<FormattedMqttMessage>(store.getMessageList(), store.getMessageList().getPreferredSize(), store.getMessageList().getMaxSize(), "search-" + store.getName(), store.getFormatter(), formattingManager, UiProperties.getSummaryMaxPayloadLength(configurationManager.getUiPropertyFile())); uniqueContentOnlyFilter = new UniqueContentOnlyFilter<FormattedMqttMessage>(store, store.getUiEventQueue()); uniqueContentOnlyFilter.setUniqueContentOnly(messageNavigationPaneController.getUniqueOnlyMenu().isSelected()); foundMessageStore.addMessageFilter(uniqueContentOnlyFilter); messageNavigationPaneController.getUniqueOnlyMenu().setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { uniqueContentOnlyFilter.setUniqueContentOnly(messageNavigationPaneController.getUniqueOnlyMenu().isSelected()); search(); eventBus.publish(new MessageIndexToFirstEvent(foundMessageStore)); } }); messageListTablePaneController.setItems(foundMessages); messageListTablePaneController.setStore(foundMessageStore); messageListTablePaneController.setConnection(connection); messageListTablePaneController.setEventBus(eventBus); messageListTablePaneController.init(); eventBus.subscribe(messageListTablePaneController, messageListTablePaneController::onMessageIndexChange, MessageIndexChangeEvent.class, new SimpleRunLaterExecutor(), foundMessageStore); messagePaneController.setStore(foundMessageStore); messagePaneController.setConfingurationManager(configurationManager); messagePaneController.setFormattingManager(formattingManager); messagePaneController.setStyled(true); messagePaneController.init(); // The search pane's message browser wants to know about changing indices and format eventBus.subscribe(messagePaneController, messagePaneController::onMessageIndexChange, MessageIndexChangeEvent.class, new SimpleRunLaterExecutor(), foundMessageStore); eventBus.subscribe(messagePaneController, messagePaneController::onFormatChange, MessageFormatChangeEvent.class, new SimpleRunLaterExecutor(), foundMessageStore); messageNavigationPaneController.setStore(foundMessageStore); messageNavigationPaneController.setEventBus(eventBus); messageNavigationPaneController.init(); // The search pane's message browser wants to know about show first, index change and update index events eventBus.subscribe(messageNavigationPaneController, messageNavigationPaneController::onMessageIndexChange, MessageIndexChangeEvent.class, new SimpleRunLaterExecutor(), foundMessageStore); eventBus.subscribe(messageNavigationPaneController, messageNavigationPaneController::onNavigateToFirst, MessageIndexToFirstEvent.class, new SimpleRunLaterExecutor(), foundMessageStore); eventBus.subscribe(messageNavigationPaneController, messageNavigationPaneController::onMessageIndexIncrement, MessageIndexIncrementEvent.class, new SimpleRunLaterExecutor(), foundMessageStore); scriptManager = new MqttScriptManager(null, null, connection); refreshList(); eventBus.subscribeWithFilterOnly(this, this::onMessageAdded, MessageAddedEvent.class, store.getMessageList()); eventBus.subscribe(this, this::onFormatChange, MessageFormatChangeEvent.class, new SimpleRunLaterExecutor(), store); } /** * @param formattingManager the formattingManager to set */ public void setFormattingManager(FormattingManager formattingManager) { this.formattingManager = formattingManager; } public void toggleMessagePayloadSize(final boolean resize) { if (resize) { messagePane.setMaxHeight(Double.MAX_VALUE); } else { messagePane.setMaxHeight(50); } } private void refreshList() { // If in offline mode if (connection == null) { searchWithScriptsMenu.setDisable(true); } else if (connection.getProperties() == null) { logger.warn("Connection's properties are null"); } else if (connection.getProperties().getConfiguredProperties() == null) { logger.warn("Connection's configured properties are null"); } else { final String directory = connection.getProperties().getConfiguredProperties().getSearchScripts(); if (directory != null && !directory.isEmpty()) { scriptManager.addScripts(directory); onScriptListChange(); } } } public void onScriptListChange() { final Collection<Script> scripts = scriptManager.getScripts(); final List<Script> pubScripts = new ArrayList<>(); for (final Script script : scripts) { pubScripts.add(script); } NewPublicationController.updateScriptList(pubScripts, searchWithScriptsMenu, searchMethod, "Search with '%s' script", new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { onScriptSearch(((Script) searchMethod.getSelectedToggle().getUserData()).getName()); } }); } @FXML private void toggleAutoRefresh() { updateTabTitle(); } @FXML private void onMessagePayloadSearch() { textLabel.setText("Text to find"); searchField.setText(""); searchField.setPromptText("type some text and press Enter to search"); searchField.setDisable(false); caseSensitiveCheckBox.setVisible(true); } private void onScriptSearch(final String scriptName) { textLabel.setText("Search with script"); searchField.setText(scriptName); searchField.setDisable(true); caseSensitiveCheckBox.setVisible(false); } @FXML private void onInlineScriptSearch() { textLabel.setText("Inline script"); searchField.setText(""); searchField.setPromptText("type inline JavaScript and press Enter to search"); searchField.setDisable(false); caseSensitiveCheckBox.setVisible(false); } public void requestSearchFocus() { searchField.requestFocus(); } private void processMessages(final List<FormattedMqttMessage> messages) { final SearchMatcher matcher = getSearchMatcher(); final int firstIndex = store.getMessages().size() - 1; for (int i = firstIndex; i >= 0; i--) { processMessage(store.getMessages().get(i), matcher); if (firstIndex == i && !matcher.isValid()) { break; } } } private SearchMatcher getSearchMatcher() { if (defaultSearch.isSelected()) { return new SimplePayloadMatcher(searchField.getText(), caseSensitiveCheckBox.isSelected()); } else if (inlineScriptSearch.isSelected()) { return new InlineScriptMatcher(scriptManager, searchField.getText()); } else { final Script script = ((Script) searchMethod.getSelectedToggle().getUserData()); return new ScriptMatcher(scriptManager, script); } } private boolean processMessage(final FormattedMqttMessage message, final SearchMatcher matcher) { seachedCount++; boolean found = matcher.matches(message); if (found) { messageFound(message); return true; } return false; } private void messageFound(final FormattedMqttMessage message) { foundMessages.add(0, new MessageContentProperties<FormattedMqttMessage>(message, /*store.getFormatter(), */ UiProperties.getSummaryMaxPayloadLength(configurationManager.getUiPropertyFile()))); if (!uniqueContentOnlyFilter.filter(message, foundMessageStore.getMessageList(), true)) { // If an old message has been deleted from the store, remove it from the list as well if (foundMessageStore.storeMessage(message) != null) { foundMessages.remove(foundMessages.size() - 1); } } } private void clearMessages() { seachedCount = 0; foundMessages.clear(); foundMessageStore.clear(); uniqueContentOnlyFilter.reset(); } @FXML private void search() { clearMessages(); processMessages(store.getMessages()); updateTabTitle(); messagePaneController.setSearchOptions(new SearchOptions(searchField.getText(), caseSensitiveCheckBox.isSelected())); eventBus.publish(new MessageIndexToFirstEvent(foundMessageStore)); } private void updateTabTitle() { final HBox title = new HBox(); title.setAlignment(Pos.CENTER); if (isAutoRefresh()) { final ProgressIndicator progressIndicator = new ProgressIndicator(); progressIndicator.setMaxSize(15, 15); title.getChildren().add(progressIndicator); title.getChildren().add(new Label(" ")); } // If the search value is too long, show only a substring final String searchValue = searchField.getText().length() > MAX_SEARCH_VALUE_CHARACTERS ? searchField.getText().substring(0, MAX_SEARCH_VALUE_CHARACTERS) + "..." : searchField.getText(); title.getChildren().add(new Label("Search: \"" + searchValue + "\"" + " [" + foundMessages.size() + " found / " + seachedCount + " searched]")); tab.setText(null); tab.setGraphic(title); } public void onFormatChange(final MessageFormatChangeEvent event) { foundMessageStore.setFormatter(store.getFormatter()); eventBus.publish(new MessageFormatChangeEvent(foundMessageStore)); } // TODO: optimise message handling public void onMessageAdded(final MessageAddedEvent<FormattedMessage> event) { for (final BrowseReceivedMessageEvent<FormattedMessage> message : event.getMessages()) { onMessageAdded((FormattedMqttMessage) message.getMessage()); } } public void onMessageAdded(final FormattedMqttMessage message) { // TODO: is that ever deregistered? if (autoRefreshCheckBox.isSelected()) { final boolean matchingSearch = processMessage(message, getSearchMatcher()); if (matchingSearch) { if (messageNavigationPaneController.showLatest()) { eventBus.publish(new MessageIndexToFirstEvent(foundMessageStore)); } else { eventBus.publish(new MessageIndexIncrementEvent(1, foundMessageStore)); } } updateTabTitle(); } } public void cleanup() { disableAutoSearch(); // TODO: need to check this eventBus.unsubscribeConsumer(this, MessageFormatChangeEvent.class); eventBus.unsubscribeConsumer(this, MessageAddedEvent.class); } public void disableAutoSearch() { autoRefreshCheckBox.setSelected(false); updateTabTitle(); } @FXML private void copyMessageToClipboard() { messageNavigationPaneController.copyMessageToClipboard(); } @FXML private void copyMessagesToClipboard() { messageNavigationPaneController.copyMessagesToClipboard(); } @FXML private void copyMessageToFile() { messageNavigationPaneController.copyMessageToFile(); } @FXML private void copyMessagesToFile() { messageNavigationPaneController.copyMessagesToFile(); } // =============================== // === Setters and getters ======= // =============================== public void setTab(Tab tab) { this.tab = tab; } public boolean isAutoRefresh() { return autoRefreshCheckBox.isSelected(); } public void setStore(final ManagedMessageStoreWithFiltering<FormattedMqttMessage> store) { this.store = store; } public void setConnection(MqttAsyncConnection connection) { this.connection = connection; } public void setConfingurationManager(final ConfigurationManager configurationManager) { this.configurationManager = configurationManager; } public void setEventBus(final IKBus eventBus) { this.eventBus = eventBus; } }