package com.insightfullogic.honest_profiler.ports.javafx.controller;
import static com.insightfullogic.honest_profiler.ports.javafx.model.ProfileContext.ProfileMode.LOG;
import static com.insightfullogic.honest_profiler.ports.javafx.util.DialogUtil.selectLogFile;
import static com.insightfullogic.honest_profiler.ports.javafx.util.DialogUtil.showErrorDialog;
import static com.insightfullogic.honest_profiler.ports.javafx.util.DialogUtil.showExceptionDialog;
import static com.insightfullogic.honest_profiler.ports.javafx.util.FxUtil.FXML_PROFILE_DIFF_ROOT;
import static com.insightfullogic.honest_profiler.ports.javafx.util.FxUtil.FXML_PROFILE_ROOT;
import static com.insightfullogic.honest_profiler.ports.javafx.util.FxUtil.addProfileNr;
import static com.insightfullogic.honest_profiler.ports.javafx.util.FxUtil.createColoredLabelContainer;
import static com.insightfullogic.honest_profiler.ports.javafx.util.FxUtil.getProgressIndicator;
import static com.insightfullogic.honest_profiler.ports.javafx.util.FxUtil.loaderFor;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.CONTENT_TAB_LOADING;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.HEADER_DIALOG_ERR_ALREADYOPENPROFILE;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.HEADER_DIALOG_ERR_OPENPROFILE;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.INFO_MENU_ROOT;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.INFO_TAB_PROFILE;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.INFO_TAB_PROFILEDIFF;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.MESSAGE_DIALOG_ERR_ALREADYOPENPROFILE;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.MESSAGE_DIALOG_ERR_OPENPROFILE;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.MESSAGE_DIALOG_ERR_TASKCANCELED;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.TITLE_DIALOG_ERR_ALREADYOPENPROFILE;
import static com.insightfullogic.honest_profiler.ports.javafx.util.ResourceUtil.TITLE_DIALOG_ERR_OPENPROFILE;
import static com.insightfullogic.honest_profiler.ports.javafx.view.Icon.LIVE_16;
import static com.insightfullogic.honest_profiler.ports.javafx.view.Icon.LOG_16;
import static com.insightfullogic.honest_profiler.ports.javafx.view.Icon.viewFor;
import static javafx.application.Platform.exit;
import static javafx.application.Platform.runLater;
import static org.slf4j.LoggerFactory.getLogger;
import java.io.File;
import java.io.IOException;
import java.util.function.Consumer;
import com.insightfullogic.honest_profiler.core.MachineListener;
import com.insightfullogic.honest_profiler.core.sources.VirtualMachine;
import com.insightfullogic.honest_profiler.ports.javafx.UserInterfaceConfigurationException;
import com.insightfullogic.honest_profiler.ports.javafx.model.ApplicationContext;
import com.insightfullogic.honest_profiler.ports.javafx.model.ProfileContext;
import com.insightfullogic.honest_profiler.ports.javafx.model.task.InitializeProfileTask;
import com.insightfullogic.honest_profiler.ports.sources.LocalMachineSource;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.Pane;
import javafx.stage.FileChooser;
/**
* Controller which manages the View-independent controls for the application.
*/
public class RootController extends AbstractController implements MachineListener
{
@FXML
private MenuBar menuBar;
@FXML
private Menu fileMenu;
@FXML
private MenuItem openLogItem;
@FXML
private MenuItem openLiveItem;
@FXML
private MenuItem quitItem;
@FXML
private Menu monitorMenu;
@FXML
private TabPane profileTabs;
@FXML
private Label info;
private LocalMachineSource machineSource;
// FXML Implementation
@Override
@FXML
public void initialize()
{
super.initialize();
setApplicationContext(new ApplicationContext(this));
// Bind the InfoBar Node to the ApplicationContext.
info.textProperty().bind(appCtx().getInfo());
// Monitor running VMs on the local machine.
machineSource = new LocalMachineSource(getLogger(getClass()), this);
machineSource.start();
}
// MachineListener Implementation
@Override
public void onNewMachine(final VirtualMachine machine)
{
runLater(() -> addToMachineMenu(machine));
}
@Override
public void onClosedMachine(final VirtualMachine machine)
{
runLater(() -> removeFromMachineMenu(machine));
}
// Machine-related Helper Methods
/**
* Add a JVM to the Monitor menu.
* <p>
*
* @param vm the JVM to be added
*/
private void addToMachineMenu(final VirtualMachine vm)
{
String vmName = vm.getDisplayName();
final String vmId = (vmName.contains(" ") ? vmName.substring(0, vmName.indexOf(" "))
: vmName) + " (" + vm.getId() + ")";
MenuItem vmItem = new MenuItem(vmId);
vmItem.setId(vm.getId());
vmItem.setDisable(!vm.isAgentLoaded());
vmItem.setOnAction(event ->
{
vmItem.setDisable(true);
createNewProfile(vm, true);
});
if (monitorMenu != null)
{
monitorMenu.getItems().add(vmItem);
}
}
/**
* remove a JVM from the Monitor menu.
* <p>
*
* @param vm the JVM to be removed
*/
private void removeFromMachineMenu(final VirtualMachine vm)
{
monitorMenu.getItems().removeIf(node -> vm.getId().equals(node.getId()));
}
/**
* Stop the thread which monitors running VMs.
*/
public void close()
{
appCtx().stop();
machineSource.stop();
}
// Profile-related Helper Methods
/**
* Create a {@link Tab} which will contain the Views for a newly opened profile.
* <p>
*
* @param source the source of the profile
* @param live a boolean indicating whether the source is "live"
*/
private void createNewProfile(Object source, boolean live)
{
Tab tab = newLoadingTab();
ProfileRootController controller = loadViewIntoTab(FXML_PROFILE_ROOT, tab);
tab.getContent().setVisible(false);
Task<ProfileContext> task = new InitializeProfileTask(appCtx(), source, live);
task.setOnSucceeded(event -> handleNewProfile(tab, controller, task.getValue()));
task.setOnFailed(
event ->
{
profileTabs.getTabs().remove(tab);
showExceptionDialog(
appCtx(),
appCtx().textFor(TITLE_DIALOG_ERR_OPENPROFILE),
appCtx().textFor(HEADER_DIALOG_ERR_OPENPROFILE),
appCtx().textFor(MESSAGE_DIALOG_ERR_OPENPROFILE),
task.getException());
});
task.setOnCancelled(
event ->
{
profileTabs.getTabs().remove(tab);
showErrorDialog(
appCtx().textFor(TITLE_DIALOG_ERR_OPENPROFILE),
appCtx().textFor(HEADER_DIALOG_ERR_OPENPROFILE),
appCtx().textFor(MESSAGE_DIALOG_ERR_TASKCANCELED));
});
appCtx().execute(task);
}
/**
* Initializes the {@link ProfileRootController} for a new {@link Tab} with the specified {@link ProfileContext}.
* <p>
*
* @param tab the {@link Tab} in which the profile data will be shown
* @param controller the {@link ProfileRootController} controlling the Views for the profile
* @param profileContext the {@link ProfileContext} for the profile
*/
private void handleNewProfile(Tab tab, ProfileRootController controller,
ProfileContext profileContext)
{
controller.setApplicationContext(appCtx());
controller.setProfileContext(profileContext);
initializeProfileTabTitle(tab, profileContext);
tab.getContent().setVisible(true);
}
/**
* Create a {@link Tab} which will contain the Views for the Diff between two opened profiles.
* <p>
*
* @param baseName the name of the Base {@link ProfileContext}
* @param newName the name of the New {@link ProfileContext}
*/
public void createDiffTab(String baseName, String newName)
{
Tab tab = newLoadingTab();
ProfileDiffRootController controller = loadViewIntoTab(FXML_PROFILE_DIFF_ROOT, tab);
tab.getContent().setVisible(false);
controller.setApplicationContext(appCtx());
ProfileContext baseCtx = appCtx().getProfileContext(baseName);
ProfileContext newCtx = appCtx().getProfileContext(newName);
controller.setProfileContexts(baseCtx, newCtx);
tab.setText(null);
Pane tabInfo = createColoredLabelContainer();
tab.setGraphic(tabInfo);
info(tab, INFO_TAB_PROFILEDIFF, baseName, newName);
addProfileNr(tabInfo, baseCtx);
tabInfo.getChildren().add(new Label("<->"));
addProfileNr(tabInfo, newCtx);
runLater(() -> tab.getContent().setVisible(true));
}
/**
* Set the title of a {@link Tab} for a profile.
* <p>
*
* @param tab the {@link Tab} whose title will be set
* @param profileContext the {@link ProfileContext} for the profile
*/
private void initializeProfileTabTitle(Tab tab, ProfileContext profileContext)
{
tab.setText(null);
Pane tabInfo = createColoredLabelContainer();
tab.setGraphic(tabInfo);
addProfileNr(tabInfo, profileContext);
tabInfo.getChildren().add(viewFor(profileContext.getMode() == LOG ? LOG_16 : LIVE_16));
tabInfo.getChildren().add(new Label(profileContext.getName()));
info(tab, INFO_TAB_PROFILE, profileContext.getName());
}
/**
* Loads the View using the specifie FXML file into the {@link Tab}.
* <p>
*
* @param <T> the type of the resulting controller
* @param fxml the path of the FXML file
* @param tab the {@link Tab} into which the View will be loaded
* @return the controller which was created for the View
*/
private <T extends AbstractController> T loadViewIntoTab(String fxml, Tab tab)
{
try
{
FXMLLoader loader = loaderFor(this, fxml);
tab.setContent(loader.load());
T controller = loader.getController();
profileTabs.getTabs().add(tab);
profileTabs.getSelectionModel().select(tab);
return controller;
}
catch (IOException ioe)
{
throw new UserInterfaceConfigurationException(ioe);
}
}
// UI State Helper Methods
/**
* Create a {@link Tab} with a {@link ProgressIndicator} in the {@link Tab} header.
* <p>
*
* @return a new {@link Tab} with a {@link ProgressIndicator} in the {@link Tab} header
*/
private Tab newLoadingTab()
{
Tab tab = new Tab(appCtx().textFor(CONTENT_TAB_LOADING));
tab.setGraphic(getProgressIndicator(15, 15));
return tab;
}
/**
* Helper method which presents the user with a {@link FileChooser} dialog, and executes an action based on the
* selected {@link File}.
* <p>
*
* @param fileBasedAction the action to be executed if a {@link File} was selected
*/
private void doWithFile(Consumer<File> fileBasedAction)
{
setRootDisabled(true);
File file = selectLogFile(appCtx());
setRootDisabled(false);
if (file != null)
{
Integer id = appCtx().getContextIdByPath(file);
if (id == null)
{
fileBasedAction.accept(file);
}
else
{
showErrorDialog(
appCtx().textFor(TITLE_DIALOG_ERR_ALREADYOPENPROFILE),
appCtx().textFor(HEADER_DIALOG_ERR_ALREADYOPENPROFILE),
appCtx().textFor(MESSAGE_DIALOG_ERR_ALREADYOPENPROFILE, id.toString()));
}
}
}
/**
* Disable or enable the root-level controls.
* <p>
*
* @param disable a boolean indicating whether the controls should be disabled
*/
private void setRootDisabled(boolean disable)
{
menuBar.setDisable(disable);
profileTabs.setDisable(disable);
}
// AbstractController Implementation
@Override
protected void initializeInfoText()
{
info(menuBar, INFO_MENU_ROOT);
}
@Override
protected void initializeHandlers()
{
openLogItem.setOnAction(event -> doWithFile(file -> createNewProfile(file, false)));
openLiveItem.setOnAction(event -> doWithFile(file -> createNewProfile(file, true)));
quitItem.setOnAction(event -> exit());
}
}