/***********************************************************************************
*
* Copyright (c) 2015 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.List;
import java.util.Optional;
import java.util.ResourceBundle;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.util.Callback;
import javax.script.ScriptException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.baczkowicz.mqttspy.configuration.ConfigurationManager;
import pl.baczkowicz.mqttspy.configuration.ConfiguredConnectionDetails;
import pl.baczkowicz.mqttspy.connectivity.BaseMqttConnection;
import pl.baczkowicz.mqttspy.messages.FormattedMqttMessage;
import pl.baczkowicz.mqttspy.scripts.MqttScriptManager;
import pl.baczkowicz.mqttspy.ui.events.FormattersChangedEvent;
import pl.baczkowicz.spy.common.generated.ConversionMethod;
import pl.baczkowicz.spy.common.generated.FormatterDetails;
import pl.baczkowicz.spy.common.generated.FormatterFunction;
import pl.baczkowicz.spy.common.generated.ScriptExecutionDetails;
import pl.baczkowicz.spy.eventbus.IKBus;
import pl.baczkowicz.spy.formatting.FormattingManager;
import pl.baczkowicz.spy.formatting.FormattingUtils;
import pl.baczkowicz.spy.formatting.ScriptBasedFormatter;
import pl.baczkowicz.spy.scripts.Script;
import pl.baczkowicz.spy.ui.utils.DialogFactory;
import pl.baczkowicz.spy.ui.utils.TooltipFactory;
import pl.baczkowicz.spy.ui.utils.UiUtils;
import pl.baczkowicz.spy.utils.ConversionUtils;
/**
* Controller for the converter window.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public class FormattersController implements Initializable
{
final static Logger logger = LoggerFactory.getLogger(FormattersController.class);
@FXML
private Label detailsLabel;
@FXML
private TextArea sampleInput;
@FXML
private TextArea sampleOutput;
@FXML
private TextArea formatterDetails;
@FXML
private TextField formatterName;
@FXML
private TextField formatterType;
@FXML
private ListView<FormatterDetails> formattersList;
@FXML
private Button newButton;
@FXML
private Button applyChangesButton;
@FXML
private Button deleteButton;
private FormatterDetails selectedFormatter = FormattingUtils.createBasicFormatter("default", "Plain", "", ConversionMethod.PLAIN);
private ConfigurationManager configurationManager;
private ScriptBasedFormatter scriptBasedFormatter;
private BaseMqttConnection connection;
private final ChangeListener basicOnChangeListener = new ChangeListener()
{
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue)
{
onChange();
}
};
private FormatterDetails newFormatter;
private boolean ignoreChanges;
private IKBus eventBus;
@FXML
private AnchorPane formattersWindow;
@Override
public void initialize(URL location, ResourceBundle resources)
{
formatterName.textProperty().addListener(basicOnChangeListener);
formatterDetails.textProperty().addListener(basicOnChangeListener);
sampleInput.textProperty().addListener(new ChangeListener<String>()
{
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue)
{
final FormattedMqttMessage message = new FormattedMqttMessage(0, "", new MqttMessage(sampleInput.getText().getBytes()), connection);
formatInput(message);
}
});
formattersList.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<FormatterDetails>()
{
@Override
public void changed(ObservableValue<? extends FormatterDetails> observable,
FormatterDetails oldValue, FormatterDetails newValue)
{
selectedFormatter = newValue;
showFormatterInfo();
}
});
formattersList.setCellFactory(new Callback<ListView<FormatterDetails>, ListCell<FormatterDetails>>()
{
@Override
public ListCell<FormatterDetails> call(ListView<FormatterDetails> l)
{
return new ListCell<FormatterDetails>()
{
@Override
protected void updateItem(FormatterDetails item, boolean empty)
{
super.updateItem(item, empty);
if (item == null || empty)
{
setText(null);
}
else
{
setText(item == newFormatter ? UiUtils.MODIFIED_ITEM + item.getName() : item.getName());
}
}
};
}
});
}
public void init()
{
scriptBasedFormatter = new ScriptBasedFormatter(new MqttScriptManager(null, null, connection));
formattersList.getItems().clear();
final List<FormatterDetails> defaultFormatters = FormattingUtils.createBaseFormatters();
defaultFormatters.addAll(FormattingManager.createDefaultScriptFormatters());
formattersList.getItems().addAll(defaultFormatters);
addFormattersToList(configurationManager.getConfiguration().getFormatting().getFormatter(), formattersList.getItems());
// Select first on the list
formattersList.getSelectionModel().select(0);
}
private void readValues(final FormatterDetails formatter)
{
formatter.setName(formatterName.getText());
if (FormattingUtils.isScriptBased(formatter))
{
formatter.getFunction().get(0).getScriptExecution().setInlineScript(ConversionUtils.stringToBase64(formatterDetails.getText()));
}
}
private void checkInlineScript(final FormatterDetails formatter) throws ScriptException
{
// TODO: should this be a new script object?
final Script script = scriptBasedFormatter.getScript(formatter);
script.setScriptContent(formatterDetails.getText());
// Evaluate it
scriptBasedFormatter.evaluate(script);
if (script.getScriptRunner().getLastThrownException() != null)
{
formatterDetails.getStyleClass().add("invalid");
}
else
{
sampleOutput.setText(scriptBasedFormatter.formatMessage(formatter,
new FormattedMqttMessage(0, "", new MqttMessage(sampleInput.getText().getBytes()), connection), true));
formatterDetails.getStyleClass().add("valid");
}
}
private void onChange()
{
// TODO: split this method into individual on change
if (ignoreChanges || (newFormatter == null && selectedFormatter == null))
{
return;
}
formatterDetails.getStyleClass().removeAll("valid", "invalid");
applyChangesButton.setDisable(true);
// New formatter
if (newFormatter != null)
{
try
{
checkInlineScript(newFormatter);
applyChangesButton.setDisable(false);
}
catch (ScriptException e)
{
formatterDetails.getStyleClass().add("invalid");
logger.error("Script error", e);
}
return;
}
// Modifying existing formatter
if (selectedFormatter.getID().startsWith(FormattingUtils.SCRIPT_PREFIX))
{
try
{
if (!formatterName.getText().equals(selectedFormatter.getName())
|| !formatterDetails.getText().equals(scriptBasedFormatter.getScript(selectedFormatter).getScriptContent()))
{
checkInlineScript(selectedFormatter);
applyChangesButton.setDisable(false);
}
}
catch (ScriptException e)
{
logger.error("Script error: ", e);
}
}
}
public static void addFormattersToList(final List<FormatterDetails> formatters,
final ObservableList<FormatterDetails> observableList)
{
for (final FormatterDetails formatterDetails : formatters)
{
// Make sure the element we're trying to add is not on the list already
boolean found = false;
for (final FormatterDetails existingFormatterDetails : observableList)
{
if (existingFormatterDetails.getID().equals(formatterDetails.getID()))
{
found = true;
break;
}
}
if (!found)
{
observableList.add(formatterDetails);
}
}
}
@FXML
private void deleteFormatter()
{
if (selectedFormatter == null)
{
return;
}
int count = 0;
for (final ConfiguredConnectionDetails connectionDetails : configurationManager.getConnections())
{
if (connectionDetails.getFormatter() == null)
{
continue;
}
if (selectedFormatter.getID().equals(((FormatterDetails) connectionDetails.getFormatter()).getID()))
{
count++;
}
}
Optional<ButtonType> result = null;
if (count > 0)
{
result = DialogFactory.createQuestionDialog("Formatter is still in use",
"There are " + count + " connections configured with this formatter. Are you sure you want to delete it?",
false);
}
if (count == 0 || result.get() == ButtonType.YES)
{
for (final ConfiguredConnectionDetails connectionDetails : configurationManager.getConnections())
{
connectionDetails.setFormatter(null);
}
configurationManager.getConfiguration().getFormatting().getFormatter().remove(selectedFormatter);
if (configurationManager.saveConfiguration())
{
TooltipFactory.createTooltip(deleteButton, "Formatter deleted. Changes saved.");
init();
formattersList.getSelectionModel().selectFirst();
eventBus.publish(new FormattersChangedEvent());
}
}
}
@FXML
private void newFormatter() throws ScriptException
{
ignoreChanges = true;
selectedFormatter = null;
newFormatter = new FormatterDetails();
Optional<String> name = DialogFactory.createInputDialog(
formattersWindow.getScene().getWindow(), "Enter formatter name", "Name for the new formatter");
boolean cancelled = !name.isPresent();
boolean valid = false;
while (!cancelled && !valid)
{
newFormatter.setID(FormattingUtils.SCRIPT_PREFIX + "-" + name.get().replace(" ", "-").toLowerCase());
newFormatter.setName(name.get());
valid = true;
for (FormatterDetails formatter : formattersList.getItems())
{
logger.info("{}, {}, {}, {}", formatter.getName(), newFormatter.getName(), formatter.getID(), newFormatter.getID());
if (formatter.getName().equals(newFormatter.getName()) || formatter.getID().equals(newFormatter.getID()))
{
DialogFactory.createWarningDialog("Invalid name", "Entered formatter name/ID already exists. Please chose a different one.");
name = DialogFactory.createInputDialog(
formattersWindow.getScene().getWindow(), "Enter formatter name", "Name for the new formatter");
cancelled = name.isPresent();
valid = false;
break;
}
}
}
if (!cancelled & valid)
{
newFormatter.setID(FormattingUtils.SCRIPT_PREFIX + "-" + name.get().replace(" ", "-").toLowerCase());
newFormatter.setName(name.get());
formattersList.getItems().add(newFormatter);
formattersList.getSelectionModel().select(newFormatter);
// TODO: load a proper script here with FileUtils.loadFileByNameBase64Encoded
final ScriptExecutionDetails scriptExecution = new ScriptExecutionDetails("ZnVuY3Rpb24gZm9ybWF0KCkKewkKCXJldHVybiByZWNlaXZlZE1lc3NhZ2UuZ2V0UGF5bG9hZCgpICsgIi0gbW9kaWZpZWQhIjsKfQ==");
newFormatter.getFunction().add(new FormatterFunction(null, null, null, null, null, scriptExecution));
formatterName.setText(newFormatter.getName());
formatterType.setText("Script-based");
detailsLabel.setText("Inline script");
scriptBasedFormatter.addFormatter(newFormatter);
formatterDetails.setText(scriptBasedFormatter.getScript(newFormatter).getScriptContent());
sampleOutput.setText(scriptBasedFormatter.formatMessage(newFormatter,
new FormattedMqttMessage(0, "", new MqttMessage(sampleInput.getText().getBytes()), connection),
true));
newButton.setDisable(true);
applyChangesButton.setDisable(false);
deleteButton.setDisable(true);
eventBus.publish(new FormattersChangedEvent());
}
ignoreChanges = false;
}
@FXML
private void applyChanges()
{
if (newFormatter != null)
{
readValues(newFormatter);
configurationManager.getConfiguration().getFormatting().getFormatter().add(newFormatter);
}
else
{
readValues(selectedFormatter);
}
if (configurationManager.saveConfiguration())
{
TooltipFactory.createTooltip(newButton, "Formatter added. Changes saved.");
init();
formattersList.getSelectionModel().selectFirst();
eventBus.publish(new FormattersChangedEvent());
}
}
private void formatInput(final FormattedMqttMessage message)
{
if (FormattingUtils.isScriptBased(selectedFormatter))
{
// logger.debug("Formatting using {}", formatter.getName());
message.setFormattedPayload(scriptBasedFormatter.formatMessage(selectedFormatter, message, false));
message.setPrettyPayload(scriptBasedFormatter.formatMessage(selectedFormatter, message, true));
}
else
{
// Use the raw payload to make sure any formatting/encoding that is applied is correct
message.setFormattedPayload(FormattingUtils.checkAndFormatText(selectedFormatter, message.getRawBinaryPayload()));
message.setPrettyPayload(message.getFormattedPayload());
}
sampleOutput.setText(message.getPrettyPayload());
}
private void showFormatterInfo()
{
// If both are null or the same
if (selectedFormatter == newFormatter)
{
return;
}
ignoreChanges = true;
if (newFormatter != null)
{
formattersList.getItems().remove(newFormatter);
newFormatter = null;
}
else if (selectedFormatter != null)
{
newButton.setDisable(false);
deleteButton.setDisable(false);
detailsLabel.setText("Details");
formatterName.setText(selectedFormatter.getName());
if (FormattingUtils.isDefault(selectedFormatter))
{
formatterType.setText("Built-in");
formatterDetails.setText(selectedFormatter.getDescription());
deleteButton.setDisable(true);
if (selectedFormatter.getName().startsWith("Plain"))
{
sampleInput.setText("hello from mqtt-spy!");
}
else if (selectedFormatter.getName().contains("JSON"))
{
sampleInput.setText("{hello: {from: \"mqtt-spy\", message: \"welcome!\"}}");
}
else if (selectedFormatter.getName().contains("XML"))
{
sampleInput.setText("<hello><from>mqtt-spy</from><message>welcome!</message></hello>");
}
else if (selectedFormatter.getName().startsWith("Base64 decoder"))
{
sampleInput.setText(ConversionUtils.stringToBase64("hello from mqtt-spy!"));
}
else if (selectedFormatter.getName().startsWith("Base64 encoder"))
{
sampleInput.setText("hello from mqtt-spy!");
}
else if (selectedFormatter.getName().startsWith("HEX decoder"))
{
sampleInput.setText(ConversionUtils.stringToHex("hello from mqtt-spy!"));
}
else if (selectedFormatter.getName().startsWith("HEX encoder"))
{
sampleInput.setText("hello from mqtt-spy!");
}
FormattedMqttMessage message = new FormattedMqttMessage(0, "", new MqttMessage(sampleInput.getText().getBytes()), connection);
if (selectedFormatter.getName().startsWith("Eclipse Kura"))
{
final String base64encoded = "H4sIAAAAAAAAAHWR227TQBCGoSU9bB0aSkFNA8JIXLSAjZ0DQeKqpEGINk6V0qJeWZv1Jiz1HrQ+tOHJuObJmHUSkSDwjUf7zcw/889ObfPO7HswD6rz4NfPlX1UJlIISlImRchUpfRh/SI4CfpfA6AHaON7zkOBOQWw/xnn2P4k03Ml04MvvUO7EzMqUvuyB7lltCITyCqdMpHdwoODtrFSMSN42jsydK/TDz46l/7rTu/Y/I67Z6f9K4ig4D3aMmpKyxGLjeBLI1gInXftQSZSxqndFTnTUnAj7LvvXM8Z+o06lFcRkkmYU52AHFRvNl3fbTt5+xXACloHiDX5BmQVa14UbGNCqEpDKoiMmBgDuzf+wRTAZ6icUM1wHIqMD6kGZA1woiDUE+eMFd5tcBnRGFb7B62iyohpfoM1XRhqNXhzBHAHWRFLVIwnc3PNMR4ha8iWdpilv5g68+f9Yb1lFm97Nme3NLLNHEWDtUwZl8z6vve23va9lgfgOdpdPLNIqR5hQpevXUO74DeL8TCm5gqEJonU5mp3m0WPxzIZs3CkYeIbqa8X5imB064R2kNWKlNwjVMu9QTQWr3VaDYNe4LQ1K/Zyn879hRtKazT/xteQ/eXJzDzd0nMVEKL/tZ1pvHCWOjkYnAU+m7D9X4DdrXTyAkDAAA=";
sampleInput.setText(ConversionUtils.base64ToString(base64encoded));
message = new FormattedMqttMessage(0, "", new MqttMessage(ConversionUtils.base64ToArray(base64encoded)), connection);
}
formatInput(message);
}
else if (selectedFormatter.getID().startsWith(FormattingUtils.SCRIPT_PREFIX))
{
// Add just in case
try
{
scriptBasedFormatter.addFormatter(selectedFormatter);
}
catch (ScriptException e)
{
logger.error("Script error", e);
}
formatterType.setText("Script-based");
detailsLabel.setText("Inline script");
try
{
formatterDetails.setText(scriptBasedFormatter.getScript(selectedFormatter).getScriptContent());
}
catch (ScriptException e)
{
logger.error("Script error", e);
}
formatInput(new FormattedMqttMessage(0, "", new MqttMessage(sampleInput.getText().getBytes()), connection));
}
else
{
logger.debug("Formatter = {} {}", selectedFormatter.getID(), selectedFormatter.getName());
formatterType.setText("Function-based");
formatterDetails.setText("(this formatter type has been deprecated)");
sampleOutput.setText(FormattingUtils.formatText(selectedFormatter, sampleInput.getText(), sampleInput.getText().getBytes()));
}
}
applyChangesButton.setDisable(true);
ignoreChanges = false;
}
// ===============================
// === Setters and getters =======
// ===============================
public void setConfigurationManager(final ConfigurationManager configurationManager)
{
this.configurationManager = configurationManager;
}
/**
* @param connection the connection to set
*/
public void setConnection(final BaseMqttConnection connection)
{
this.connection = connection;
}
/**
* Sets the event bus.
*
* @param eventBus the eventBus to set
*/
public void setEventBus(final IKBus eventBus)
{
this.eventBus = eventBus;
}
}