From c033fe4f437df2bb2cc639c4f805fe3609c6d907 Mon Sep 17 00:00:00 2001 From: Colin DuPlantis Date: Sun, 26 Feb 2023 11:54:25 -0800 Subject: [PATCH 01/46] MATP-1102 Initial add of JavaFX work --- core/pom.xml | 4 - photon/photon/pom.xml | 94 ++ .../main/java/org/marketcetera/ui/App.java | 125 ++ .../java/org/marketcetera/ui/LoginView.java | 165 +++ .../marketcetera/ui/PrimaryController.java | 12 + .../marketcetera/ui/SecondaryController.java | 12 + .../org/marketcetera/ui/UiConfiguration.java | 182 +++ .../ui/events/CascadeWindowsEvent.java | 14 + .../ui/events/CloseWindowsEvent.java | 14 + .../marketcetera/ui/events/LoginEvent.java | 51 + .../marketcetera/ui/events/LogoutEvent.java | 14 + .../ui/events/NewWindowEvent.java | 88 ++ .../ui/events/TileWindowsEvent.java | 14 + .../ui/events/WindowResizeEvent.java | 19 + .../ui/fixadmin/FixSessionWatcher.java | 152 +++ .../service/AuthorizationHelperService.java | 23 + .../ui/service/ConnectableService.java | 39 + .../ui/service/ConnectableServiceFactory.java | 26 + .../ui/service/DesktopParameters.java | 126 ++ .../ui/service/DisplayLayoutService.java | 28 + .../ui/service/NoServiceException.java | 71 + .../ui/service/ServiceManager.java | 177 +++ .../marketcetera/ui/service/SessionUser.java | 214 +++ .../marketcetera/ui/service/StyleService.java | 116 ++ .../ui/service/WebMessageService.java | 66 + .../ui/service/WindowManagerService.java | 1155 +++++++++++++++++ .../AdminClientDisplayLayoutService.java | 58 + .../ui/service/admin/AdminClientService.java | 537 ++++++++ .../admin/AdminClientServiceFactory.java | 51 + .../admin/WebAuthorizationHelperService.java | 101 ++ .../ui/view/AbstractMenuItem.java | 38 + .../marketcetera/ui/view/ApplicationMenu.java | 427 ++++++ .../ui/view/CascadeWindowsMenuItem.java | 73 ++ .../ui/view/CloseAllWindowsMenuItem.java | 73 ++ .../org/marketcetera/ui/view/ContentView.java | 28 + .../ui/view/ContentViewFactory.java | 31 + .../marketcetera/ui/view/LogoutMenuItem.java | 93 ++ .../org/marketcetera/ui/view/MenuContent.java | 74 ++ .../ui/view/TileWindowsMenuItem.java | 68 + .../ui/view/WindowContentCategory.java | 61 + .../main/resources/images/Average_Price.png | Bin 0 -> 604 bytes .../main/resources/images/Cascade_Windows.png | Bin 0 -> 371 bytes .../resources/images/Close_All_Windows.png | Bin 0 -> 551 bytes .../main/resources/images/Cluster_Data.png | Bin 0 -> 637 bytes .../main/resources/images/FIX_Messages.png | Bin 0 -> 648 bytes .../main/resources/images/FIX_Sessions.png | Bin 0 -> 326 bytes .../src/main/resources/images/Fills.png | Bin 0 -> 569 bytes .../src/main/resources/images/Logout.png | Bin 0 -> 442 bytes .../src/main/resources/images/Market_Data.png | Bin 0 -> 468 bytes .../src/main/resources/images/Metrics.png | Bin 0 -> 485 bytes .../src/main/resources/images/Open_Orders.png | Bin 0 -> 488 bytes .../main/resources/images/Order_Ticket.png | Bin 0 -> 492 bytes .../src/main/resources/images/Permissions.png | Bin 0 -> 557 bytes .../src/main/resources/images/Roles.png | Bin 0 -> 596 bytes .../resources/images/Session_Status_Black.png | Bin 0 -> 461 bytes .../images/Session_Status_General.png | Bin 0 -> 621 bytes .../resources/images/Session_Status_Green.png | Bin 0 -> 562 bytes .../resources/images/Session_Status_Grey.png | Bin 0 -> 559 bytes .../images/Session_Status_Orange.png | Bin 0 -> 562 bytes .../resources/images/Session_Status_Red.png | Bin 0 -> 562 bytes .../images/Session_Status_Yellow.png | Bin 0 -> 562 bytes .../images/Supervisor_Permissions.png | Bin 0 -> 555 bytes .../main/resources/images/Tile_Windows.png | Bin 0 -> 272 bytes .../src/main/resources/images/Users.png | Bin 0 -> 565 bytes .../src/main/resources/images/Workspace.png | Bin 0 -> 456 bytes photon/photon/src/main/resources/log4j2.xml | 15 + .../org/marketcetera/ui/primary.fxml | 16 + .../org/marketcetera/ui/secondary.fxml | 16 + .../test/cmd_exec/conf/application.properties | 8 + pom.xml | 3 +- util/pom.xml | 4 - 71 files changed, 4766 insertions(+), 10 deletions(-) create mode 100644 photon/photon/pom.xml create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/App.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/LoginView.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/PrimaryController.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/SecondaryController.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/UiConfiguration.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/events/CascadeWindowsEvent.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/events/CloseWindowsEvent.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/events/LoginEvent.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/events/LogoutEvent.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/events/NewWindowEvent.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/events/TileWindowsEvent.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/events/WindowResizeEvent.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/fixadmin/FixSessionWatcher.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/AuthorizationHelperService.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/ConnectableService.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/ConnectableServiceFactory.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/DesktopParameters.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/DisplayLayoutService.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/NoServiceException.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/ServiceManager.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/SessionUser.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/StyleService.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/WebMessageService.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/WindowManagerService.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientDisplayLayoutService.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientService.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientServiceFactory.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/service/admin/WebAuthorizationHelperService.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/view/AbstractMenuItem.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/view/ApplicationMenu.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/view/CascadeWindowsMenuItem.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/view/CloseAllWindowsMenuItem.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/view/ContentView.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/view/ContentViewFactory.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/view/LogoutMenuItem.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/view/MenuContent.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/view/TileWindowsMenuItem.java create mode 100644 photon/photon/src/main/java/org/marketcetera/ui/view/WindowContentCategory.java create mode 100644 photon/photon/src/main/resources/images/Average_Price.png create mode 100644 photon/photon/src/main/resources/images/Cascade_Windows.png create mode 100644 photon/photon/src/main/resources/images/Close_All_Windows.png create mode 100644 photon/photon/src/main/resources/images/Cluster_Data.png create mode 100644 photon/photon/src/main/resources/images/FIX_Messages.png create mode 100644 photon/photon/src/main/resources/images/FIX_Sessions.png create mode 100644 photon/photon/src/main/resources/images/Fills.png create mode 100644 photon/photon/src/main/resources/images/Logout.png create mode 100644 photon/photon/src/main/resources/images/Market_Data.png create mode 100644 photon/photon/src/main/resources/images/Metrics.png create mode 100644 photon/photon/src/main/resources/images/Open_Orders.png create mode 100644 photon/photon/src/main/resources/images/Order_Ticket.png create mode 100644 photon/photon/src/main/resources/images/Permissions.png create mode 100644 photon/photon/src/main/resources/images/Roles.png create mode 100644 photon/photon/src/main/resources/images/Session_Status_Black.png create mode 100644 photon/photon/src/main/resources/images/Session_Status_General.png create mode 100644 photon/photon/src/main/resources/images/Session_Status_Green.png create mode 100644 photon/photon/src/main/resources/images/Session_Status_Grey.png create mode 100644 photon/photon/src/main/resources/images/Session_Status_Orange.png create mode 100644 photon/photon/src/main/resources/images/Session_Status_Red.png create mode 100644 photon/photon/src/main/resources/images/Session_Status_Yellow.png create mode 100644 photon/photon/src/main/resources/images/Supervisor_Permissions.png create mode 100644 photon/photon/src/main/resources/images/Tile_Windows.png create mode 100644 photon/photon/src/main/resources/images/Users.png create mode 100644 photon/photon/src/main/resources/images/Workspace.png create mode 100644 photon/photon/src/main/resources/log4j2.xml create mode 100644 photon/photon/src/main/resources/org/marketcetera/ui/primary.fxml create mode 100644 photon/photon/src/main/resources/org/marketcetera/ui/secondary.fxml create mode 100644 photon/photon/src/test/cmd_exec/conf/application.properties diff --git a/core/pom.xml b/core/pom.xml index 46ae03cd82..ffa294895a 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -75,10 +75,6 @@ com.google.guava guava - - com.google.code.findbugs - jsr305 - org.ow2.sirocco sirocco-text-table-formatter diff --git a/photon/photon/pom.xml b/photon/photon/pom.xml new file mode 100644 index 0000000000..b5d5dde4e0 --- /dev/null +++ b/photon/photon/pom.xml @@ -0,0 +1,94 @@ + + 4.0.0 + + org.marketcetera + public-parent + 4.1.0-SNAPSHOT + + org.marketcetera + photon + 4.1.0-SNAPSHOT + + UTF-8 + 11 + 11 + 19.0.2.1 + 0.0.8 + + + + ${project.groupId} + core + + + ${project.groupId} + admin-api + + + ${project.groupId} + fix-api + + + ${project.groupId} + admin-rpc-client + + + ${project.groupId} + fix-rpc-client + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + org.openjfx + javafx-swing + ${javafx.version} + + + org.openjfx + javafx-media + ${javafx.version} + + + org.openjfx + javafx-web + ${javafx.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 11 + + + + org.openjfx + javafx-maven-plugin + ${javafx.maven.plugin.version} + + + + + default-cli + + org.marketcetera.ui.App + src/test/cmd_exec + + + + + + + diff --git a/photon/photon/src/main/java/org/marketcetera/ui/App.java b/photon/photon/src/main/java/org/marketcetera/ui/App.java new file mode 100644 index 0000000000..a504ffa091 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/App.java @@ -0,0 +1,125 @@ +package org.marketcetera.ui; + +import java.io.IOException; + +import org.marketcetera.ui.events.LogoutEvent; +import org.marketcetera.ui.service.SessionUser; +import org.marketcetera.ui.service.WebMessageService; +import org.marketcetera.ui.view.ApplicationMenu; +import org.marketcetera.util.log.SLF4JLoggerProxy; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +import com.google.common.eventbus.Subscribe; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +/* $License$ */ + +/** + * + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + * @see https://openjfx.io/openjfx-docs/#maven + */ +public class App + extends Application +{ + + private static Scene scene; + + /* (non-Javadoc) + * @see javafx.application.Application#init() + */ + @Override + public void init() + throws Exception + { + super.init(); + context = new AnnotationConfigApplicationContext("org.marketcetera","com.marketcetera"); + webMessageService = context.getBean(WebMessageService.class); + webMessageService.register(this); + } + @Override + public void start(Stage stage) + throws IOException + { + // scene = new Scene(loadFXML("primary"), 640, 480); + // stage.setScene(scene); + // stage.show(); + SLF4JLoggerProxy.info(this, + "Starting main stage"); + root = new VBox(); + menuLayout = new VBox(); + root.getChildren().add(menuLayout); + Scene mainScene = new Scene(root, + 1024, + 768); + stage.setScene(mainScene); + stage.setTitle("Marketcetera Automated Trading Platform"); + stage.show(); + doLogin(); + } + private void showMenu() + { + ApplicationMenu applicationMenu = SessionUser.getCurrent().getAttribute(ApplicationMenu.class); + if(applicationMenu == null) { + SLF4JLoggerProxy.debug(App.class, + "Session is now logged in, building application menu"); + applicationMenu = context.getBean(ApplicationMenu.class); + menuLayout.getChildren().add(applicationMenu.getMenu()); + SessionUser.getCurrent().setAttribute(ApplicationMenu.class, + applicationMenu); + } + applicationMenu.refreshMenuPermissions(); + } + @Subscribe + public void onLogout(LogoutEvent inEvent) + { + SessionUser.getCurrent().setAttribute(ApplicationMenu.class, + null); + SessionUser.getCurrent().setAttribute(SessionUser.class, + null); + Platform.runLater(() -> menuLayout.getChildren().clear()); + Platform.runLater(() -> doLogin()); + } + private void doLogin() + { + LoginView loginView = context.getBean(LoginView.class); + loginView.showAndWait(); + showMenu(); + } + static void setRoot(String fxml) + throws IOException + { + scene.setRoot(loadFXML(fxml)); + } + + private static Parent loadFXML(String fxml) + throws IOException + { + FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource(fxml + ".fxml")); + return fxmlLoader.load(); + } + + public static void main(String[] args) + { + launch(); + } + /** + * web message service value + */ + private WebMessageService webMessageService; + private VBox menuLayout; + private ApplicationContext context; + private VBox root; + +} \ No newline at end of file diff --git a/photon/photon/src/main/java/org/marketcetera/ui/LoginView.java b/photon/photon/src/main/java/org/marketcetera/ui/LoginView.java new file mode 100644 index 0000000000..2a9254a727 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/LoginView.java @@ -0,0 +1,165 @@ +package org.marketcetera.ui; + +import javax.annotation.PostConstruct; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.marketcetera.ui.events.LoginEvent; +import org.marketcetera.ui.service.SessionUser; +import org.marketcetera.ui.service.WebMessageService; +import org.marketcetera.util.log.SLF4JLoggerProxy; +import org.marketcetera.util.ws.stateful.Authenticator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.stage.WindowEvent; + +/* $License$ */ + +/** + * + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class LoginView + extends Stage +{ + @PostConstruct + public void init() + { + initModality(Modality.APPLICATION_MODAL); + HBox usernameBox = new HBox(5); + Label usernameLabel = new Label("Username"); + usernameText = new TextField(); + usernameBox.getChildren().addAll(usernameLabel, + usernameText); + HBox passwordBox = new HBox(5); + Label passwordLabel = new Label("Password"); + passwordText = new PasswordField(); + passwordBox.getChildren().addAll(passwordLabel, + passwordText); + // A button to close the stage + loginButton = new Button("Login"); + loginButton.setOnAction(this::onLogin); + loginButton.setDisable(true); + usernameText.setOnKeyTyped(this::enableLoginButton); + passwordText.setOnKeyTyped(this::enableLoginButton); + setOnCloseRequest(this::onCloseRequest); + VBox root = new VBox(5); + root.getChildren().addAll(usernameBox, + passwordBox, + loginButton); + Scene scene = new Scene(root); + setScene(scene); + // The title of the stage is not visible for all styles. + setTitle("Login"); + initStyle(StageStyle.UTILITY); + setResizable(false); + } + private void enableLoginButton(KeyEvent inKeyEvent) + { + String value = StringUtils.trimToNull(usernameText.getText()); + String password = StringUtils.trimToNull(passwordText.getText()); + loginButton.setDisable(value == null || password == null); + } + private void onLogin(ActionEvent inEvent) + { + String username = StringUtils.trimToNull(usernameText.getText()); + String password = StringUtils.trimToNull(passwordText.getText()); + try { + SLF4JLoggerProxy.debug(this, + "Attempting to log in {}", + username); + SessionUser sessionUser = new SessionUser(username, + password); + SessionUser.getCurrent().setAttribute(SessionUser.class, + sessionUser); + try { + if(webAuthenticator.shouldAllow(null, + username, + password.toCharArray())) { + SLF4JLoggerProxy.info(this, + "{} logged in", + username); + // Navigate to main view + webMessageService.post(new LoginEvent(sessionUser)); + close(); + } else { + throw new IllegalArgumentException("Failed to log in"); + } + } catch (Exception e) { + SLF4JLoggerProxy.warn(this, + e, + "{} failed to log in", + username); + SessionUser.getCurrent().setAttribute(SessionUser.class, + null); + } + } catch (Exception e) { + String message = ExceptionUtils.getRootCauseMessage(e); + SLF4JLoggerProxy.warn(this, + e, + "{} failed to log in: {}", + username, + message); + showPopupMessage(message); + SessionUser.getCurrent().setAttribute(SessionUser.class, + null); + } + } + private void showPopupMessage(final String message) + { + // TODO this doesn't show + Platform.runLater(new Runnable() { + @Override + public void run() + { + Alert alert = new Alert(AlertType.INFORMATION); + alert.setTitle("Information Dialog"); + alert.setHeaderText("Look, an Information Dialog"); + alert.setContentText("I have a great message for you!"); + alert.initOwner(LoginView.this); + alert.showAndWait(); + }} + ); + } + private void onCloseRequest(WindowEvent inEvent) + { + inEvent.consume(); + } + + private Button loginButton; + private PasswordField passwordText; + private TextField usernameText; + /** + * provides authentication services + */ + @Autowired + private Authenticator webAuthenticator; + /** + * web message service value + */ + @Autowired + private WebMessageService webMessageService; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/PrimaryController.java b/photon/photon/src/main/java/org/marketcetera/ui/PrimaryController.java new file mode 100644 index 0000000000..e8b1d203fc --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/PrimaryController.java @@ -0,0 +1,12 @@ +package org.marketcetera.ui; + +import java.io.IOException; +import javafx.fxml.FXML; + +public class PrimaryController { + + @FXML + private void switchToSecondary() throws IOException { + App.setRoot("secondary"); + } +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/SecondaryController.java b/photon/photon/src/main/java/org/marketcetera/ui/SecondaryController.java new file mode 100644 index 0000000000..d2b40cf004 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/SecondaryController.java @@ -0,0 +1,12 @@ +package org.marketcetera.ui; + +import java.io.IOException; +import javafx.fxml.FXML; + +public class SecondaryController { + + @FXML + private void switchToPrimary() throws IOException { + App.setRoot("primary"); + } +} \ No newline at end of file diff --git a/photon/photon/src/main/java/org/marketcetera/ui/UiConfiguration.java b/photon/photon/src/main/java/org/marketcetera/ui/UiConfiguration.java new file mode 100644 index 0000000000..faff6e3e0c --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/UiConfiguration.java @@ -0,0 +1,182 @@ +package org.marketcetera.ui; + +import org.marketcetera.admin.AdminRpcClientFactory; +import org.marketcetera.admin.PermissionFactory; +import org.marketcetera.admin.RoleFactory; +import org.marketcetera.admin.UserAttributeFactory; +import org.marketcetera.admin.UserFactory; +import org.marketcetera.admin.impl.SimplePermissionFactory; +import org.marketcetera.admin.impl.SimpleRoleFactory; +import org.marketcetera.admin.impl.SimpleUserAttributeFactory; +import org.marketcetera.admin.impl.SimpleUserFactory; +import org.marketcetera.cluster.ClusterDataFactory; +import org.marketcetera.cluster.SimpleClusterDataFactory; +import org.marketcetera.fix.FixAdminRpcClientFactory; +import org.marketcetera.fix.FixSessionAttributeDescriptorFactory; +import org.marketcetera.fix.MutableActiveFixSessionFactory; +import org.marketcetera.fix.MutableFixSessionFactory; +import org.marketcetera.fix.impl.SimpleActiveFixSessionFactory; +import org.marketcetera.fix.impl.SimpleFixSessionAttributeDescriptorFactory; +import org.marketcetera.fix.impl.SimpleFixSessionFactory; +import org.marketcetera.symbol.IterativeSymbolResolver; +import org.marketcetera.symbol.PatternSymbolResolver; +import org.marketcetera.symbol.SymbolResolverService; +import org.marketcetera.ui.service.ServiceManager; +import org.marketcetera.ui.service.SessionUser; +import org.marketcetera.ui.service.admin.AdminClientService; +import org.marketcetera.util.except.I18NException; +import org.marketcetera.util.ws.stateful.Authenticator; +import org.marketcetera.util.ws.stateless.StatelessClientContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +/* $License$ */ + +/** + * + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Configuration +@PropertySource("file:conf/application.properties") +public class UiConfiguration +{ + /** + * Get the symbol resolver service value. + * + * @return a SymbolResolverService value + */ + @Bean + public SymbolResolverService getSymbolResolverService() + { + IterativeSymbolResolver symbolResolverService = new IterativeSymbolResolver(); + symbolResolverService.getSymbolResolvers().add(new PatternSymbolResolver()); + return symbolResolverService; + } + /** + * Get the admin client factory value. + * + * @return an AdminClientFactory value + */ + @Bean + public AdminRpcClientFactory getAdminClientFactory() + { + return new AdminRpcClientFactory(); + } + /** + * Get the FIX admin client factory value. + * + * @return a FixAdminClientFactory value + */ + @Bean + public FixAdminRpcClientFactory getFixAdminClientFactory() + { + return new FixAdminRpcClientFactory(); + } + /** + * Get the user attribute factory value. + * + * @return a UserAttributeFactory value + */ + @Bean + public UserAttributeFactory getUserAttributeFactory() + { + return new SimpleUserAttributeFactory(); + } + /** + * Get the permission factory value. + * + * @return a PermissionFactory value + */ + @Bean + public PermissionFactory getPermissionFactory() + { + return new SimplePermissionFactory(); + } + /** + * Get the user factory value. + * + * @return a UserFactory value + */ + @Bean + public UserFactory getUserFactory() + { + return new SimpleUserFactory(); + } + /** + * Get the role factory value. + * + * @return a RoleFactory value + */ + @Bean + public RoleFactory getRoleFactory() + { + return new SimpleRoleFactory(); + } + /** + * Get the FIX session attribute descriptor factory value. + * + * @return a FixSessionAttributeDescriptoFactory value + */ + @Bean + public FixSessionAttributeDescriptorFactory getFixSessionAttributeDescriptorFactory() + { + return new SimpleFixSessionAttributeDescriptorFactory(); + } + /** + * Get the active FIX session factory value. + * + * @return a MutableActiveFixSessionFactory value + */ + @Bean + public MutableActiveFixSessionFactory getActiveFixSessionFactory() + { + return new SimpleActiveFixSessionFactory(); + } + /** + * Get the FIX session factory value. + * + * @return a MutableFixSessionFactory value + */ + @Bean + public MutableFixSessionFactory getFixSessionFactory() + { + return new SimpleFixSessionFactory(); + } + /** + * Get the cluster data factory value. + * + * @return a ClusterDataFactory value + */ + @Bean + public ClusterDataFactory getClusterDataFactory() + { + return new SimpleClusterDataFactory(); + } + /** + * Get the authenticator for the web application. + * + * @param inServiceManager a ServiceManager value + * @return an Authenticator value + */ + @Bean + public Authenticator getAuthenticator(ServiceManager inServiceManager) + { + Authenticator authenticator = new Authenticator() { + @Override + public boolean shouldAllow(StatelessClientContext inContext, + String inUser, + char[] inPassword) + throws I18NException + { + AdminClientService adminClientService = inServiceManager.getService(AdminClientService.class); + SessionUser.getCurrent().getPermissions().addAll(adminClientService.getPermissionsForUser()); + return true; + } + }; + return authenticator; + } +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/events/CascadeWindowsEvent.java b/photon/photon/src/main/java/org/marketcetera/ui/events/CascadeWindowsEvent.java new file mode 100644 index 0000000000..1bcbfffafa --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/events/CascadeWindowsEvent.java @@ -0,0 +1,14 @@ +package org.marketcetera.ui.events; + +/* $License$ */ + +/** + * Indicates that a cascade windows command has been issued. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public class CascadeWindowsEvent +{ +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/events/CloseWindowsEvent.java b/photon/photon/src/main/java/org/marketcetera/ui/events/CloseWindowsEvent.java new file mode 100644 index 0000000000..660652ba04 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/events/CloseWindowsEvent.java @@ -0,0 +1,14 @@ +package org.marketcetera.ui.events; + +/* $License$ */ + +/** + * Indicates that a close all windows command has been issued. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public class CloseWindowsEvent +{ +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/events/LoginEvent.java b/photon/photon/src/main/java/org/marketcetera/ui/events/LoginEvent.java new file mode 100644 index 0000000000..c4388c493c --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/events/LoginEvent.java @@ -0,0 +1,51 @@ +package org.marketcetera.ui.events; + +import org.marketcetera.ui.service.SessionUser; + +/* $License$ */ + +/** + * Indicates that current user session has newly started. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public class LoginEvent +{ + /** + * Create a new LoginEvent instance. + * + * @param inSessionUser a SessionUser value + */ + public LoginEvent(SessionUser inSessionUser) + { + sessionUser = inSessionUser; + message = new StringBuilder().append(sessionUser).append(" logged in").toString(); + } + /** + * Get the sessionUser value. + * + * @return a SessionUser value + */ + public SessionUser getSessionUser() + { + return sessionUser; + } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + return message; + } + /** + * describes this event + */ + private final String message; + /** + * session user value + */ + private final SessionUser sessionUser; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/events/LogoutEvent.java b/photon/photon/src/main/java/org/marketcetera/ui/events/LogoutEvent.java new file mode 100644 index 0000000000..4eaa78750e --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/events/LogoutEvent.java @@ -0,0 +1,14 @@ +package org.marketcetera.ui.events; + +/* $License$ */ + +/** + * Indicates that current user session is about to close. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public class LogoutEvent +{ +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/events/NewWindowEvent.java b/photon/photon/src/main/java/org/marketcetera/ui/events/NewWindowEvent.java new file mode 100644 index 0000000000..8151bc1b98 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/events/NewWindowEvent.java @@ -0,0 +1,88 @@ +package org.marketcetera.ui.events; + +import java.util.Properties; + +import org.marketcetera.core.Pair; +import org.marketcetera.ui.view.ContentViewFactory; + +/* $License$ */ + +/** + * Indicates that a new window event has been triggered. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public interface NewWindowEvent +{ + /** + * Get the title to display in the window. + * + * @return a String value + */ + String getWindowTitle(); + /** + * Get the window style id to use for this window. + * + * @return a String value + */ + default String getWindowStyleId() + { + return "window"; + } + /** + * Get the properties with which to seed the new window. + * + * @return a Properties value + */ + default Properties getProperties() + { + return new Properties(); + } + /** + * Get the content view factory for this window. + * + * @return a Class<? extends ContentViewFactory> value + */ + Class getViewFactoryType(); + /** + * Get the window size recommended for this window. + * + *

Implementing class may choose to override this call to suggest a different size for the new window. + * + * @return a Pair<String,String> value + */ + default Pair getWindowSize() + { + return Pair.create("50%", + "50%"); + } + /** + * Indicate if the new window should be resizable. + * + * @return a boolean value + */ + default boolean isResizable() + { + return true; + } + /** + * Indicate if the new window should be draggable. + * + * @return a boolean value + */ + default boolean isDraggable() + { + return true; + } + /** + * Indicate if the new window should be modal. + * + * @return a boolean value + */ + default boolean isModal() + { + return false; + } +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/events/TileWindowsEvent.java b/photon/photon/src/main/java/org/marketcetera/ui/events/TileWindowsEvent.java new file mode 100644 index 0000000000..a606265ea3 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/events/TileWindowsEvent.java @@ -0,0 +1,14 @@ +package org.marketcetera.ui.events; + +/* $License$ */ + +/** + * Indicates that a tile windows command has been issued. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public class TileWindowsEvent +{ +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/events/WindowResizeEvent.java b/photon/photon/src/main/java/org/marketcetera/ui/events/WindowResizeEvent.java new file mode 100644 index 0000000000..bf0886ebff --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/events/WindowResizeEvent.java @@ -0,0 +1,19 @@ +package org.marketcetera.ui.events; + +/* $License$ */ + +/** + * + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public interface WindowResizeEvent +{ + // TODO need a callback to recreate the contents of the window + int getPositionX(); + int getPositionY(); + float getWidth(); + float getHeight(); +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/fixadmin/FixSessionWatcher.java b/photon/photon/src/main/java/org/marketcetera/ui/fixadmin/FixSessionWatcher.java new file mode 100644 index 0000000000..93ff522878 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/fixadmin/FixSessionWatcher.java @@ -0,0 +1,152 @@ +package org.marketcetera.ui.fixadmin; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.apache.commons.lang3.StringUtils; +import org.marketcetera.brokers.BrokerStatusListener; +import org.marketcetera.fix.ActiveFixSession; +import org.marketcetera.ui.events.LoginEvent; +import org.marketcetera.ui.events.LogoutEvent; +import org.marketcetera.ui.service.ServiceManager; +import org.marketcetera.ui.service.SessionUser; +import org.marketcetera.ui.service.WebMessageService; +import org.marketcetera.ui.service.admin.AdminClientService; +import org.marketcetera.util.log.SLF4JLoggerProxy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; + +/* $License$ */ + +/** + * Monitors FIX sessions and sends notifications. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Component +public class FixSessionWatcher +{ + /** + * Validate and start the object. + */ + @PostConstruct + public void start() + { + messageService.register(this); + } + /** + * Stop the object. + */ + @PreDestroy + public void stop() + { + messageService.unregister(this); + } + /** + * Notify on new login events. + * + * @param inEvent a LoginEvent value + */ + @Subscribe + public void onLogin(LoginEvent inEvent) + { + SLF4JLoggerProxy.trace(this, + "{} logged in", + inEvent.getSessionUser()); + SessionUser currentUser = inEvent.getSessionUser(); + FixSessionWatcherSubscriber subscriber = currentUser.getAttribute(FixSessionWatcherSubscriber.class); + if(subscriber == null) { + subscriber = new FixSessionWatcherSubscriber(currentUser); + currentUser.setAttribute(FixSessionWatcherSubscriber.class, + subscriber); + } + serviceManager.getService(AdminClientService.class).addBrokerStatusListener(subscriber); + } + /** + * Notify on logout events. + * + * @param inEvent a LogoutEvent value + */ + @Subscribe + public void onLogout(LogoutEvent inEvent) + { + SessionUser currentUser = SessionUser.getCurrent(); + SLF4JLoggerProxy.trace(this, + "{} logged out", + currentUser); + if(currentUser == null) { + return; + } + FixSessionWatcherSubscriber subscriber = currentUser.getAttribute(FixSessionWatcherSubscriber.class); + if(subscriber != null) { + serviceManager.getService(AdminClientService.class).removeBrokerStatusListener(subscriber); + } + } + /** + * Subscribes to broker status changes and manages updates. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ + private class FixSessionWatcherSubscriber + implements BrokerStatusListener + { + /* (non-Javadoc) + * @see org.marketcetera.brokers.BrokerStatusListener#receiveBrokerStatus(org.marketcetera.fix.ActiveFixSession) + */ + @Override + public void receiveBrokerStatus(ActiveFixSession inActiveFixSession) + { + SLF4JLoggerProxy.trace(FixSessionWatcher.this, + "{} notifying {}", + user, + inActiveFixSession); + StringBuilder prettyStatus = new StringBuilder(); + for(String statusComponent : inActiveFixSession.getStatus().name().split("_")) { + prettyStatus.append(StringUtils.lowerCase(statusComponent)).append(' '); + } + Platform.runLater(new Runnable() { + @Override + public void run() + { + // TODO this really wants to be a tray notification + Alert a = new Alert(AlertType.INFORMATION); + a.setContentText(inActiveFixSession.getFixSession().getName() + " " + StringUtils.trim(prettyStatus.toString())); + a.show(); + } + }); + } + /** + * Create a new FixSessionWatcherSubscriber instance. + * + * @param inUser a SessionUser value + */ + private FixSessionWatcherSubscriber(SessionUser inUser) + { + user = inUser; + } + /** + * session user that owns this listener + */ + private final SessionUser user; + } + /** + * provides access to client services + */ + @Autowired + private ServiceManager serviceManager; + /** + * provides access to web message services + */ + @Autowired + private WebMessageService messageService; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/AuthorizationHelperService.java b/photon/photon/src/main/java/org/marketcetera/ui/service/AuthorizationHelperService.java new file mode 100644 index 0000000000..475853d36b --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/AuthorizationHelperService.java @@ -0,0 +1,23 @@ +package org.marketcetera.ui.service; + +import org.springframework.security.core.GrantedAuthority; + +/* $License$ */ + +/** + * Provides assistance resolving permissions. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public interface AuthorizationHelperService +{ + /** + * Determines if the current user has the given authority. + * + * @param inGrantedAuthority a GrantedAuthority value + * @return a boolean value + */ + boolean hasPermission(GrantedAuthority inGrantedAuthority); +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/ConnectableService.java b/photon/photon/src/main/java/org/marketcetera/ui/service/ConnectableService.java new file mode 100644 index 0000000000..6bda42b865 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/ConnectableService.java @@ -0,0 +1,39 @@ +package org.marketcetera.ui.service; + +/* $License$ */ + +/** + * Provides a service that requires a connection. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public interface ConnectableService +{ + /** + * Connect the service with the given parameters. + * + * @param inUsername a String value + * @param inPassword a String value + * @param inHostname a String value + * @param inPort an int value + * @return a boolean value indicating if connection was successful + * @throws Exception if an error occurs + */ + boolean connect(String inUsername, + String inPassword, + String inHostname, + int inPort) + throws Exception; + /** + * Disconnect the service. + */ + void disconnect(); + /** + * Indicate if the service is running or not. + * + * @return a boolean value + */ + boolean isRunning(); +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/ConnectableServiceFactory.java b/photon/photon/src/main/java/org/marketcetera/ui/service/ConnectableServiceFactory.java new file mode 100644 index 0000000000..26fbb6d669 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/ConnectableServiceFactory.java @@ -0,0 +1,26 @@ +package org.marketcetera.ui.service; + +/* $License$ */ + +/** + * Creates {@link ConnectableService} objects. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public interface ConnectableServiceFactory +{ + /** + * Create a ServiceClazz object. + * + * @return a ServiceClazz value + */ + ServiceClazz create(); + /** + * Get the type of service this factory creates. + * + * @return a Class<ServiceClazz> value + */ + Class getServiceType(); +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/DesktopParameters.java b/photon/photon/src/main/java/org/marketcetera/ui/service/DesktopParameters.java new file mode 100644 index 0000000000..0b03c1d1a8 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/DesktopParameters.java @@ -0,0 +1,126 @@ +package org.marketcetera.ui.service; + +import javafx.geometry.Rectangle2D; +import javafx.stage.Screen; + +/* $License$ */ + +/** + * Manages the desktop viewable area parameters. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public class DesktopParameters +{ + /** + * Get the top value. + * + * @return a int value + */ + public int getTop() + { + return top; + } + /** + * Sets the top value. + * + * @param inTop a int value + */ + public void setTop(int inTop) + { + top = inTop; + } + /** + * Get the left value. + * + * @return a int value + */ + public int getLeft() + { + return left; + } + /** + * Sets the left value. + * + * @param inLeft a int value + */ + public void setLeft(int inLeft) + { + left = inLeft; + } + /** + * Get the bottom value. + * + * @return a int value + */ + public int getBottom() + { + return bottom; + } + /** + * Sets the bottom value. + * + * @param inBottom a int value + */ + public void setBottom(int inBottom) + { + bottom = inBottom; + } + /** + * Get the right value. + * + * @return a int value + */ + public int getRight() + { + return right; + } + /** + * Sets the right value. + * + * @param inRight a int value + */ + public void setRight(int inRight) + { + right = inRight; + } + /** + * Recalculate the dynamic parameters. + */ + public void recalculate() + { + // note that there can be more than one screen if you have multiple monitors. this doesn't handle multiple monitors + Rectangle2D bounds = Screen.getPrimary().getVisualBounds(); + bottom = (int)bounds.getHeight(); + right = (int)bounds.getWidth(); + } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + StringBuilder builder = new StringBuilder(); + builder.append("DesktopParameters [top=").append(top).append(", left=").append(left).append(", bottom=") + .append(bottom).append(", right=").append(right).append("]"); + return builder.toString(); + } + /** + * desktop viewable area top edge coordinate + */ + private int top = 0; + /** + * desktop viewable area left edge coordinate + */ + private int left = 0; + /** + * desktop viewable area bottom edge coordinate + */ + private int bottom; + /** + * desktop viewable area right edge coordinate + */ + private int right; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/DisplayLayoutService.java b/photon/photon/src/main/java/org/marketcetera/ui/service/DisplayLayoutService.java new file mode 100644 index 0000000000..07c3c94ab1 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/DisplayLayoutService.java @@ -0,0 +1,28 @@ +package org.marketcetera.ui.service; + +import java.util.Properties; + +/* $License$ */ + +/** + * Provides a service for display layout. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public interface DisplayLayoutService +{ + /** + * Get the display layout. + * + * @return a Properties value + */ + Properties getDisplayLayout(); + /** + * Set the display layout. + * + * @param inDisplayLayout a Properties value + */ + void setDisplayLayout(Properties inDisplayLayout); +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/NoServiceException.java b/photon/photon/src/main/java/org/marketcetera/ui/service/NoServiceException.java new file mode 100644 index 0000000000..42eb452142 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/NoServiceException.java @@ -0,0 +1,71 @@ +package org.marketcetera.ui.service; + +/* $License$ */ + +/** + * Indicates that no service is available. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public class NoServiceException + extends RuntimeException +{ + /** + * Create a new NoServiceException instance. + */ + public NoServiceException() + { + super(); + } + /** + * Create a new NoServiceException instance. + * + * @param inMessage a String value + */ + public NoServiceException(String inMessage) + { + super(inMessage); + } + /** + * Create a new NoServiceException instance. + * + * @param inCause a Throwable value + */ + public NoServiceException(Throwable inCause) + { + super(inCause); + } + /** + * Create a new NoServiceException instance. + * + * @param inMessage a String value + * @param inCause a Throwable value + */ + public NoServiceException(String inMessage, + Throwable inCause) + { + super(inMessage, + inCause); + } + /** + * Create a new NoServiceException instance. + * + * @param inMessage a String value + * @param inCause a Throwable value + * @param inEnableSuppression a boolean value + * @param inWritableStackTrace a boolean value + */ + public NoServiceException(String inMessage, + Throwable inCause, + boolean inEnableSuppression, + boolean inWritableStackTrace) + { + super(inMessage, + inCause, + inEnableSuppression, + inWritableStackTrace); + } + private static final long serialVersionUID = -7657091217738295482L; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/ServiceManager.java b/photon/photon/src/main/java/org/marketcetera/ui/service/ServiceManager.java new file mode 100644 index 0000000000..9dd829ac34 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/ServiceManager.java @@ -0,0 +1,177 @@ +package org.marketcetera.ui.service; + +import java.util.Collection; +import java.util.Map; + +import javax.annotation.PostConstruct; + +import org.marketcetera.util.log.SLF4JLoggerProxy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +/* $License$ */ + +/** + * Provides access to services. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Service +public class ServiceManager +{ + /** + * Validate and start the object. + */ + @PostConstruct + public void start() + { + SLF4JLoggerProxy.info(this, + "Starting service manager"); + for(ConnectableServiceFactory factory : connectableServiceFactories) { + connectableServiceFactoriesByServiceClass.put(factory.getServiceType(), + factory); + } + instance = this; + } + /** + * Get the service of the given type for the current user. + * + * @param inServiceClass a Class<ServiceClazz> value + * @return a ServiceClazz value + * @throws NoServiceException if the service cannot be retrieved + */ + @SuppressWarnings("unchecked") + public ServiceClazz getService(Class inServiceClass) + throws NoServiceException + { + // retrieve the current user from the session - this should exist even if the user hasn't been authenticated yet + SessionUser sessionUser = SessionUser.getCurrent(); + // we don't expect this, but we should handle it, just in case + if(sessionUser == null) { + // TODO +//// menuLayout.setVisible(false); +// UI.getCurrent().getNavigator().navigateTo(LoginView.NAME); +// +// // do I need to return something here? or throw something? + throw new UnsupportedOperationException(); + } + Cache serviceCache = servicesByUser.getUnchecked(sessionUser); + ConnectableService service = serviceCache.getIfPresent(inServiceClass.getSimpleName()); + if(service == null) { + // no current service for this user, this is a normal condition if the service hasn't been accessed yet + ConnectableServiceFactory serviceFactory = connectableServiceFactoriesByServiceClass.get(inServiceClass); + // this is not a normal condition, and it seems unlikely that this should arise as there shouldn't be a module than can ask for a service that doesn't also provide a factory + if(serviceFactory == null) { + throw new NoServiceException("No connectable service factory for " + inServiceClass.getSimpleName()); + } + SLF4JLoggerProxy.debug(this, + "Creating {} service for {}", + inServiceClass.getSimpleName(), + sessionUser); + // create a service for this user + service = serviceFactory.create(); + } + // service is guaranteed to be non-null, but might or might not be running at this point + if(!service.isRunning()) { + SLF4JLoggerProxy.debug(this, + "{} service exists for {}, but is not running", + inServiceClass.getSimpleName(), + sessionUser); + try { + service.disconnect(); + } catch (Exception e) { + SLF4JLoggerProxy.warn(this, + e); + // we'll skip over this error, it might not be bad enough to cause the connection to fail + } + try { + SLF4JLoggerProxy.debug(this, + "Connecting {} service for {}", + inServiceClass.getSimpleName(), + sessionUser); + if(service.connect(sessionUser.getUsername(), + sessionUser.getPassword(), + hostname, + port)) { + SLF4JLoggerProxy.debug(this, + "Created {} for {}", + service, + sessionUser); + // cache it for the next access + serviceCache.put(inServiceClass.getSimpleName(), + service); + } + } catch (Exception e) { + SLF4JLoggerProxy.warn(this, + e, + "Failed to connect {} service for {}", + inServiceClass.getSimpleName(), + sessionUser); + throw new NoServiceException("Failed to connect " + sessionUser + " to " + inServiceClass.getSimpleName(), + e); + } + } + if(!service.isRunning()) { + SLF4JLoggerProxy.warn(this, + "Failed to connect {} service for {}", + inServiceClass.getSimpleName(), + sessionUser); + throw new NoServiceException("Failed to connect " + sessionUser + " to " + inServiceClass.getSimpleName()); + } + // service is non-null and running + return (ServiceClazz)service; + } + /** + * Get the instance. + * + * @return a ServiceManager value + */ + public static ServiceManager getInstance() + { + return instance; + } + /** + * caches services by owning user and then by service type + */ + private final LoadingCache> servicesByUser = CacheBuilder.newBuilder().build(new CacheLoader>() { + @Override + public Cache load(SessionUser inKey) + throws Exception + { + return CacheBuilder.newBuilder().build(); + }} + ); + /** + * hostname to connect to + */ + @Value("${host.name}") + private String hostname; + /** + * port to connect to + */ + @Value("${host.port}") + private int port; + /** + * holds connectable services by service class + */ + private final Map,ConnectableServiceFactory> connectableServiceFactoriesByServiceClass = Maps.newHashMap(); + /** + * service factories + */ + @Autowired(required=false) + private Collection> connectableServiceFactories = Lists.newArrayList(); + /** + * static instance + */ + private static ServiceManager instance; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/SessionUser.java b/photon/photon/src/main/java/org/marketcetera/ui/service/SessionUser.java new file mode 100644 index 0000000000..0b93e7e16b --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/SessionUser.java @@ -0,0 +1,214 @@ +package org.marketcetera.ui.service; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.security.core.GrantedAuthority; + +import com.google.common.collect.Sets; + +/* $License$ */ + +/** + * Indicates the currently validated session user. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public class SessionUser +{ + /** + * Get the current user. + * + * @return a SessionUser value + */ + public static SessionUser getCurrent() + { + return currentUser; + } + /** + * Create a new SessionUser instance. + * + * @param inUsername a String value + * @param inPassword a String value + */ + public SessionUser(String inUsername, + String inPassword) + { + currentUser = this; + username = inUsername; + password = inPassword; + } + /** + * Get the username value. + * + * @return a String value + */ + public String getUsername() + { + return username; + } + /** + * Get the loggedIn value. + * + * @return a Date value + */ + public Date getLoggedIn() + { + return loggedIn; + } + /** + * Get the password value. + * + * @return a String value + */ + public String getPassword() + { + return password; + } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + return username; + } + /** + * Get the permissions value. + * + * @return a Set<GrantedAuthority> value + */ + public Set getPermissions() + { + return permissions; + } + /** + * Gets a stored attribute value. If a value has been stored for the + * session, that value is returned. If no value is stored for the name, + * null is returned. + *

+ * The fully qualified name of the type is used as the name when getting the + * value. The outcome of calling this method is thus the same as if + * calling
+ *
+ * getAttribute(type.getName()); + * + * @see #setAttribute(Class, Object) + * @see #getAttribute(String) + * + * @param type + * the type of the value to get, can not be null. + * @return the value, or null if no value has been stored or if + * it has been set to null. + */ + public T getAttribute(Class type) { + if (type == null) { + throw new IllegalArgumentException("type can not be null"); + } + Object value = getAttribute(type.getName()); + if (value == null) { + return null; + } else { + return type.cast(value); + } + } + /** + * Gets a stored attribute value. If a value has been stored for the + * session, that value is returned. If no value is stored for the name, + * null is returned. + * + * @see #setAttribute(String, Object) + * + * @param name + * the name of the value to get, can not be null. + * @return the value, or null if no value has been stored or if + * it has been set to null. + */ + public Object getAttribute(String name) { + if (name == null) { + throw new IllegalArgumentException("name can not be null"); + } + return attributes.get(name); + } + /** + * Stores a value in this service session. This can be used to associate + * data with the current user so that it can be retrieved at a later point + * from some other part of the application. Setting the value to + * null clears the stored value. + *

+ * The fully qualified name of the type is used as the name when storing the + * value. The outcome of calling this method is thus the same as if + * calling
+ *
+ * setAttribute(type.getName(), value); + * + * @see #getAttribute(Class) + * @see #setAttribute(String, Object) + * + * @param type + * the type that the stored value represents, can not be null + * @param value + * the value to associate with the type, or null to + * remove a previous association. + */ + public void setAttribute(Class type, T value) { + if (type == null) { + throw new IllegalArgumentException("type can not be null"); + } + if (value != null && !type.isInstance(value)) { + throw new IllegalArgumentException("value of type " + type.getName() + + " expected but got " + value.getClass().getName()); + } + setAttribute(type.getName(), value); + } + /** + * Stores a value in this service session. This can be used to associate + * data with the current user so that it can be retrieved at a later point + * from some other part of the application. Setting the value to + * null clears the stored value. + * + * @see #getAttribute(String) + * + * @param name + * the name to associate the value with, can not be + * null + * @param value + * the value to associate with the name, or null to + * remove a previous association. + */ + public void setAttribute(String name, Object value) { + if (name == null) { + throw new IllegalArgumentException("name can not be null"); + } + if (value != null) { + attributes.put(name, value); + } else { + attributes.remove(name); + } + } + private final Map attributes = new HashMap(); + /** + * holds permissions for this user + */ + private final Set permissions = Sets.newHashSet(); + /** + * current user + */ + private static SessionUser currentUser; + /** + * username value + */ + private final String username; + /** + * password value + */ + private final String password; + /** + * indicates when the user was logged in + */ + private final Date loggedIn = new Date(); +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/StyleService.java b/photon/photon/src/main/java/org/marketcetera/ui/service/StyleService.java new file mode 100644 index 0000000000..18f40d09cd --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/StyleService.java @@ -0,0 +1,116 @@ +package org.marketcetera.ui.service; + +import java.util.Map; + +import javax.annotation.PostConstruct; + +import org.marketcetera.core.PlatformServices; +import org.marketcetera.ui.view.ContentView; +import org.marketcetera.util.log.SLF4JLoggerProxy; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import com.google.common.collect.Maps; + +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; + +/* $License$ */ + +/** + * Provides style resolution services. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Service +public class StyleService +{ + /** + * Validate and start the service. + */ + @PostConstruct + public void start() + { + SLF4JLoggerProxy.info(this, + "Starting {}", + PlatformServices.getServiceName(getClass())); + } + /** + * + * + * @param inContentView + */ + public void addStyle(ContentView inContentView) + { + addStyle(inContentView.getScene()); + } + /** + * + * + * @param inContentView + */ + public void addStyle(Scene scene) + { + Parent root = scene.getRoot(); + for(Node node : root.getChildrenUnmodifiable()) { + addStyle(node); + } + } + public void addStyle(Node inComponent) + { + String componentId = inComponent.getId(); + if(componentId == null) { + SLF4JLoggerProxy.trace(this, + "Component {} has no id property", + componentId); + return; + } + SLF4JLoggerProxy.trace(this, + "Applying styles to {}", + componentId); + // apply styles in order from least to most specific + applyStyle(componentId.indexOf('.'), + componentId, + inComponent); + } + private void applyStyle(int inIndex, + String inKey, + Node inComponent) + { + if(inKey == null) { + return; + } + String key; + if(inIndex == -1) { + key = inKey; + } else { + key = inKey.substring(0, + inIndex); + } + String stylesToApply = styleProperties.get(key); + SLF4JLoggerProxy.trace(this, + "Applying styles {} to {} from key {}", + stylesToApply, + inComponent.getId(), + key); + if(stylesToApply != null) { + inComponent.getStyleClass().add(stylesToApply); + } + if(inKey.equals(key)) { + return; + } + int indexOfDot = inKey.indexOf('.', + inIndex+1); + applyStyle(indexOfDot, + inKey, + inComponent); + } + /** + * map of styles specified in configuration + */ + @Value("#{${metc.styles}}") + private Map styleProperties = Maps.newHashMap(); +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/WebMessageService.java b/photon/photon/src/main/java/org/marketcetera/ui/service/WebMessageService.java new file mode 100644 index 0000000000..5f3b89d38d --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/WebMessageService.java @@ -0,0 +1,66 @@ +package org.marketcetera.ui.service; + +import javax.annotation.PostConstruct; + +import org.marketcetera.util.log.SLF4JLoggerProxy; +import org.springframework.stereotype.Component; + +import com.google.common.eventbus.EventBus; + +/* $License$ */ + +/** + * Provides web message services. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Component +public class WebMessageService +{ + /** + * Validate and start the service. + */ + @PostConstruct + public void start() + { + SLF4JLoggerProxy.info(this, + "Starting web message service"); + eventbus = new EventBus(); + } + /** + * Register the given object. + * + * @param inListener an Object value + */ + public void register(Object inListener) + { + eventbus.register(inListener); + } + /** + * Unregister the given object. + * + * @param inListener an Object value + */ + public void unregister(Object inListener) + { + eventbus.unregister(inListener); + } + /** + * Post the given event. + * + * @param inEvent an Object value + */ + public void post(Object inEvent) + { + SLF4JLoggerProxy.debug(this, + "Posting {}", + inEvent); + eventbus.post(inEvent); + } + /** + * event bus value + */ + private EventBus eventbus; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/WindowManagerService.java b/photon/photon/src/main/java/org/marketcetera/ui/service/WindowManagerService.java new file mode 100644 index 0000000000..97d725d967 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/WindowManagerService.java @@ -0,0 +1,1155 @@ +package org.marketcetera.ui.service; + +import java.util.HashSet; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.marketcetera.core.PlatformServices; +import org.marketcetera.core.Util; +import org.marketcetera.ui.events.CascadeWindowsEvent; +import org.marketcetera.ui.events.CloseWindowsEvent; +import org.marketcetera.ui.events.LoginEvent; +import org.marketcetera.ui.events.LogoutEvent; +import org.marketcetera.ui.events.NewWindowEvent; +import org.marketcetera.ui.events.TileWindowsEvent; +import org.marketcetera.ui.view.ContentView; +import org.marketcetera.ui.view.ContentViewFactory; +import org.marketcetera.util.log.SLF4JLoggerProxy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import com.google.common.collect.Sets; +import com.google.common.eventbus.Subscribe; +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.scene.Scene; +import javafx.scene.input.ContextMenuEvent; +import javafx.scene.input.MouseEvent; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.Window; +import javafx.stage.WindowEvent; + +/* $License$ */ + +/** + * Manages window views in the UI. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Component +public class WindowManagerService +{ + /** + * Validate and start the object. + */ + @PostConstruct + public void start() + { + SLF4JLoggerProxy.info(this, + "Starting {}", + PlatformServices.getServiceName(getClass())); + webMessageService.register(this); + } + /** + * Stop the object. + */ + @PreDestroy + public void stop() + { + SLF4JLoggerProxy.info(this, + "Stopping {}", + PlatformServices.getServiceName(getClass())); + webMessageService.unregister(this); + } + /** + * Receive user login events. + * + * @param inEvent a LoginEvent value + */ + @Subscribe + public void onLogin(LoginEvent inEvent) + { + DesktopParameters desktopParameters = new DesktopParameters(); + desktopParameters.recalculate(); + SessionUser.getCurrent().setAttribute(DesktopParameters.class, + desktopParameters); + Properties displayLayout = displayLayoutService.getDisplayLayout(); + SLF4JLoggerProxy.debug(this, + "Received {}, retrieved display layout: {}", + inEvent, + displayLayout); + WindowRegistry windowRegistry = getCurrentUserRegistry(); + windowRegistry.restoreLayout(displayLayout); + } + /** + * Receive new window events. + * + * @param inEvent a NewWindowEvent value + */ + @Subscribe + public void onNewWindow(NewWindowEvent inEvent) + { + SLF4JLoggerProxy.debug(this, + "onWindow: {}", + inEvent.getWindowTitle()); + // create the UI window element + Stage newWindow = new Stage(); + // create the new window content - initially, the properties will be mostly or completely empty, one would expect + // the content view factory will be used to create the new window content + ContentViewFactory viewFactory = applicationContext.getBean(inEvent.getViewFactoryType()); + // create the window meta data object, which will track data about the window + WindowRegistry windowRegistry = getCurrentUserRegistry(); + WindowMetaData newWindowWrapper = new WindowMetaData(inEvent, + newWindow, + viewFactory); + ContentView contentView = viewFactory.create(newWindow, + inEvent, + newWindowWrapper.getProperties()); + styleService.addStyle(contentView); + Scene rootScene = contentView.getScene(); + newWindow.setTitle(inEvent.getWindowTitle()); + // set properties of the new window based on the received event + newWindow.initModality(inEvent.isModal()?Modality.APPLICATION_MODAL:Modality.NONE); + // TODO not sure how to disallow dragging +// newWindow.setDraggable(inEvent.isDraggable()); + newWindow.setResizable(inEvent.isResizable()); + newWindow.setWidth(Double.valueOf(inEvent.getWindowSize().getFirstMember())); + newWindow.setHeight(Double.valueOf(inEvent.getWindowSize().getSecondMember())); + windowRegistry.addWindow(newWindowWrapper); + // set the content of the new window + newWindow.setScene(rootScene); + windowRegistry.addWindowListeners(newWindowWrapper); + windowRegistry.updateDisplayLayout(); + // TODO pretty sure this isn't right + newWindow.getProperties().put(WindowManagerService.windowUuidProp, + inEvent.getWindowStyleId()); + newWindow.show(); + newWindow.requestFocus(); + } + /** + * Receive logout events. + * + * @param inEvent a LogoutEvent value + */ + @Subscribe + public void onLogout(LogoutEvent inEvent) + { + SLF4JLoggerProxy.debug(this, + "onLogout: {}", + inEvent); + getCurrentUserRegistry().logout(); + SessionUser.getCurrent().setAttribute(WindowRegistry.class, + null); + } + /** + * Receive window cascade events. + * + * @param inEvent a CascadeWindowEvent inEvent + */ + @Subscribe + public void onCascade(CascadeWindowsEvent inEvent) + { + SLF4JLoggerProxy.trace(this, + "onCascade: {}", + inEvent); + getCurrentUserRegistry().cascadeWindows(); + } + /** + * Receive window tile events. + * + * @param inEvent a TileWindowsEvent value + */ + @Subscribe + public void onTile(TileWindowsEvent inEvent) + { + SLF4JLoggerProxy.trace(this, + "onTile: {}", + inEvent); + getCurrentUserRegistry().tileWindows(); + } + /** + * Receive close all windows events. + * + * @param inEvent a CloseWindowsEvent value + */ + @Subscribe + public void onCloseAllWindows(CloseWindowsEvent inEvent) + { + SLF4JLoggerProxy.trace(this, + "onCloseWindows: {}", + inEvent); + getCurrentUserRegistry().closeAllWindows(true); + } + /** + * Determine if the given window is outside the viewable desktop area or not. + * + * @param inWindow a Window value + * @return a boolean value + */ + private boolean isWindowOutsideDesktop(Window inWindow) + { + DesktopParameters params = SessionUser.getCurrent().getAttribute(DesktopParameters.class); + return (getWindowBottom(inWindow) > params.getBottom()) || (getWindowLeft(inWindow) < params.getLeft()) || (getWindowTop(inWindow) < params.getTop()) || (getWindowRight(inWindow) > params.getRight()); + } + /** + * Get the window top edge coordinate in pixels. + * + * @param inWindow a Window value + * @return a double value + */ + private double getWindowTop(Window inWindow) + { + return inWindow.getY(); + } + /** + * Get the window left edge coordinate in pixels. + * + * @param inWindow a Window value + * @return a double value + */ + private double getWindowLeft(Window inWindow) + { + return inWindow.getX(); + } + /** + * Get the window bottom edge coordinate in pixels. + * + * @param inWindow a Window value + * @return a double value + */ + private double getWindowBottom(Window inWindow) + { + return getWindowTop(inWindow) + getWindowHeight(inWindow); + } + /** + * Get the window right edge coordinate in pixels. + * + * @param inWindow a Window value + * @return a double value + */ + private double getWindowRight(Window inWindow) + { + return getWindowLeft(inWindow) + getWindowWidth(inWindow); + } + /** + * Get the window height in pixels. + * + * @param inWindow a Window value + * @return a double value + */ + private double getWindowHeight(Window inWindow) + { + return inWindow.getHeight(); + } + /** + * Get the window width in pixels. + * + * @param inWindow a Window value + * @return a double value + */ + private double getWindowWidth(Window inWindow) + { + return inWindow.getWidth(); + } + /** + * Get the window registry for the current user. + * + * @return a WindowRegistry value + */ + private WindowRegistry getCurrentUserRegistry() + { + WindowRegistry registry = SessionUser.getCurrent().getAttribute(WindowRegistry.class); + if(registry == null) { + registry = new WindowRegistry(); + SessionUser.getCurrent().setAttribute(WindowRegistry.class, + registry); + registry.scheduleWindowPositionMonitor(); + } + return registry; + } + /** + * Event used to open a new window on restart. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ + private class RestartNewWindowEvent + implements NewWindowEvent + { + /* (non-Javadoc) + * @see org.marketcetera.web.events.NewWindowEvent#getWindowTitle() + */ + @Override + public String getWindowTitle() + { + return windowTitle; + } + /* (non-Javadoc) + * @see org.marketcetera.web.events.NewWindowEvent#getViewFactoryType() + */ + @Override + public Class getViewFactoryType() + { + return contentViewFactory.getClass(); + } + /** + * Create a new RestartNewWindowEvent instance. + * + * @param inContentViewFactory a ContentViewFactory value + * @param inWindowTitle a String value + */ + private RestartNewWindowEvent(ContentViewFactory inContentViewFactory, + String inWindowTitle) + { + contentViewFactory = inContentViewFactory; + windowTitle = inWindowTitle; + } + /** + * content view factory value + */ + private final ContentViewFactory contentViewFactory; + /** + * window title value + */ + private final String windowTitle; + } + /** + * Holds meta-data for windows. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ + private class WindowMetaData + { + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + return properties.toString(); + } + /** + * Create a new WindowMetaData instance. + * + *

This constructor is invoked for a new window. + * + * @param inEvent a NewWindowEvent value + * @param inWindow a Stage value + * @param inContentViewFactory a ContentViewFactory value + */ + private WindowMetaData(NewWindowEvent inEvent, + Stage inWindow, + ContentViewFactory inContentViewFactory) + { + properties = inEvent.getProperties(); + window = inWindow; + setWindowStaticProperties(inContentViewFactory, + UUID.randomUUID().toString()); + updateProperties(); + } + /** + * Create a new WindowMetaData instance. + * + *

This constructor is invoked to recreate a previously-created window. + * + * @param inProperties a Properties value + * @param inWindow a Stage value + */ + private WindowMetaData(Properties inProperties, + Stage inWindow) + { + // TODO need to do a permissions re-check, perhaps + window = inWindow; + properties = inProperties; + try { + ContentViewFactory contentViewFactory = (ContentViewFactory)applicationContext.getBean(Class.forName(inProperties.getProperty(windowContentViewFactoryProp))); + ContentView contentView = contentViewFactory.create(window, + new RestartNewWindowEvent(contentViewFactory, + properties.getProperty(windowTitleProp)), + properties); + styleService.addStyle(contentView); + window.setScene(contentView.getScene()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + // update window from properties, effectively restoring it to its previous state + updateWindow(); + } + /** + * Get the storable value for this window. + * + * @return a String value + */ + private String getStorableValue() + { + return Util.propertiesToString(properties); + } + /** + * Get the properties value. + * + * @return a Properties value + */ + private Properties getProperties() + { + return properties; + } + /** + * Get the window value. + * + * @return a Stage value + */ + private Stage getWindow() + { + return window; + } + /** + * Update the window telemetry from the underlying window object. + */ + private void updateProperties() + { + properties.setProperty(windowPosXProp, + String.valueOf(window.getX())); + properties.setProperty(windowPosYProp, + String.valueOf(window.getY())); + properties.setProperty(windowHeightProp, + String.valueOf(window.getHeight())); + properties.setProperty(windowWidthProp, + String.valueOf(window.getWidth())); + properties.setProperty(windowModeProp, + String.valueOf(window.isMaximized())); + properties.setProperty(windowTitleProp, + window.getTitle()); + properties.setProperty(windowModalProp, + String.valueOf(window.getModality())); + // TODO not sure what to do about dragging yet +// properties.setProperty(windowDraggableProp, +// String.valueOf(window.isDraggable())); + properties.setProperty(windowResizableProp, + String.valueOf(window.isResizable())); +// properties.setProperty(windowScrollLeftProp, +// String.valueOf(window.getScrollLeft())); +// properties.setProperty(windowScrollTopProp, +// String.valueOf(window.getScrollTop())); + properties.setProperty(windowFocusProp, + String.valueOf(hasFocus())); + Object windowId = window.getProperties().getOrDefault(windowStyleId, + null); + if(windowId == null) { + properties.remove(windowStyleId); + } else { + properties.setProperty(windowStyleId, + String.valueOf(windowId)); + } + } + /** + * Update the window object with the stored telemetry. + */ + private void updateWindow() + { + window.setWidth(Double.parseDouble(properties.getProperty(windowWidthProp))); + window.setHeight(Double.parseDouble(properties.getProperty(windowHeightProp))); + window.initModality(Modality.valueOf(properties.getProperty(windowModalProp))); + Boolean isMaximized = Boolean.parseBoolean(properties.getProperty(windowModeProp)); + window.setMaximized(isMaximized); + // TODO not sure about these yet +// window.setScrollLeft(Integer.parseInt(properties.getProperty(windowScrollLeftProp))); +// window.setScrollTop(Integer.parseInt(properties.getProperty(windowScrollTopProp))); +// window.setDraggable(Boolean.parseBoolean(properties.getProperty(windowDraggableProp))); + window.setResizable(Boolean.parseBoolean(properties.getProperty(windowResizableProp))); + window.setTitle(properties.getProperty(windowTitleProp)); + window.setX(Integer.parseInt(properties.getProperty(windowPosXProp))); + window.setY(Integer.parseInt(properties.getProperty(windowPosYProp))); + window.getProperties().put(windowStyleId, + properties.getProperty(windowStyleId)); + setHasFocus(Boolean.parseBoolean(properties.getProperty(windowFocusProp))); + if(hasFocus) { + window.requestFocus(); + } + } + /** + * Set the immutable properties of this window to the underlying properties storage. + * + * @param inContentViewFactory a ContentViewFactory value + * @param inUuid a Stringvalue + */ + private void setWindowStaticProperties(ContentViewFactory inContentViewFactory, + String inUuid) + { + properties.setProperty(windowContentViewFactoryProp, + inContentViewFactory.getClass().getCanonicalName()); + properties.setProperty(windowUuidProp, + inUuid); + } + /** + * Close this window and remove it from active use. + */ + private void close() + { + ((Stage)getWindow().getScene().getWindow()).close(); + } + /** + * Get the window uuid value. + * + * @return a String value + */ + private String getUuid() + { + if(uuid == null) { + uuid = properties.getProperty(windowUuidProp); + } + return uuid; + } + /** + * Get the hasFocus value. + * + * @return a boolean value + */ + private boolean hasFocus() + { + return hasFocus; + } + /** + * Sets the hasFocus value. + * + * @param inHasFocus a boolean value + */ + private void setHasFocus(boolean inHasFocus) + { + hasFocus = inHasFocus; + } + /** + * indicates if this window has focus or not + */ + private transient boolean hasFocus; + /** + * cached uuid value + */ + private transient String uuid; + /** + * properties used to record details about this window + */ + private final Properties properties; + /** + * underlying UI element + */ + private final Stage window; + } + /** + * Provides a registry of all windows. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ + private class WindowRegistry + { + /** + * Add the given window to this registry. + * + * @param inWindowMetaData a WindowWrapper value + */ + private void addWindow(WindowMetaData inWindowMetaData) + { + synchronized(activeWindows) { + activeWindows.add(inWindowMetaData); + } + } + /** + * Closes all windows. + * + * @param inUpdateDisplay a boolean value + */ + private void closeAllWindows(boolean inUpdateDisplay) + { + synchronized(activeWindows) { + Set tempActiveWindows = new HashSet<>(activeWindows); + for(WindowMetaData window : tempActiveWindows) { + window.close(); + } + if(inUpdateDisplay) { + updateDisplayLayout(); + } + } + } + /** + * Rearrange the windows in this registry to a cascaded pattern. + */ + private void cascadeWindows() + { + synchronized(windowPositionExaminerThreadPool) { + cancelWindowPositionMonitor(); + } + try { + synchronized(activeWindows) { + int xPos = desktopCascadeWindowOffset; + int yPos = desktopCascadeWindowOffset; + DesktopParameters params = SessionUser.getCurrent().getAttribute(DesktopParameters.class); + int maxX = params.getRight(); + int maxY = params.getBottom(); + for(WindowMetaData activeWindow : activeWindows) { + double windowWidth = getWindowWidth(activeWindow.getWindow()); + double windowHeight = getWindowHeight(activeWindow.getWindow()); + double proposedX = xPos; + if(proposedX + windowWidth > maxX) { + proposedX = desktopCascadeWindowOffset; + } + double proposedY = yPos; + if(proposedY + windowHeight > maxY) { + proposedY = desktopCascadeWindowOffset; + } + activeWindow.getWindow().setX(proposedX); + activeWindow.getWindow().setY(proposedY); + activeWindow.getWindow().requestFocus(); + xPos += desktopCascadeWindowOffset; + yPos += desktopCascadeWindowOffset; + activeWindow.updateProperties(); + } + } + updateDisplayLayout(); + } finally { + scheduleWindowPositionMonitor(); + } + } + /** + * Rearrange the windows in this registry to a tiled pattern. + */ + private void tileWindows() + { + synchronized(windowPositionExaminerThreadPool) { + cancelWindowPositionMonitor(); + } + try { + synchronized(activeWindows) { + DesktopParameters params = SessionUser.getCurrent().getAttribute(DesktopParameters.class); + int numWindows = activeWindows.size(); + if(numWindows == 0) { + return; + } + int numCols = (int)Math.floor(Math.sqrt(numWindows)); + int numRows = (int)Math.floor(numWindows / numCols); + if(!isPerfectSquare(numWindows)) { + numCols += 1; + } + int windowWidth = Math.floorDiv(params.getRight(), + numCols); + int windowHeight = Math.floorDiv((params.getBottom()-params.getTop()), + numRows); + int colNum = 0; + int rowNum = 0; + int posX = params.getLeft(); + int posY = params.getTop(); + for(WindowMetaData activeWindow : activeWindows) { + int suggestedX = posX + (colNum * windowWidth); + int suggestedY = posY + (rowNum * windowHeight); + activeWindow.getWindow().setWidth(windowWidth); + activeWindow.getWindow().setHeight(windowHeight); + activeWindow.getWindow().setX(suggestedX); + activeWindow.getWindow().setY(suggestedY); + colNum += 1; + if(colNum == numCols) { + colNum = 0; + rowNum += 1; + } + activeWindow.updateProperties(); + } + } + updateDisplayLayout(); + } finally { + scheduleWindowPositionMonitor(); + } + /* + If you can relax the requirement that all windows have a given "aspect ratio" then the problem becomes very simple. Suppose you have N "tiles" to arrange on a single screen, + then these can be arranged in columns where the number of columns, NumCols is the square root of N rounded up when N is not a perfect square. All columns of tiles are of equal width. + The number of tiles in each column is then N/NumCols rounded either up or down as necessary to make the total number of columns be N. This is what Microsoft Excel does under View > Arrange All > Tiled. + Excel chooses to put the columns with one fewer tiles on the left of the screen. + https://stackoverflow.com/questions/4456827/algorithm-to-fit-windows-on-desktop-like-tile + */ + } + /** + * Determine if the given value is a perfect square or not. + * + * @param inValue a double value + * @return a boolean value + */ + private boolean isPerfectSquare(double inValue) + { + double squareOfValue = Math.sqrt(inValue); + return ((squareOfValue - Math.floor(squareOfValue)) == 0); + } + /** + * Restore the display layout with the given values. + * + * @param inDisplayLayout a Properties value + */ + private void restoreLayout(Properties inDisplayLayout) + { + synchronized(activeWindows) { + for(Map.Entry entry : inDisplayLayout.entrySet()) { + String windowUid = String.valueOf(entry.getKey()); + Properties windowProperties = Util.propertiesFromString(String.valueOf(entry.getValue())); + SLF4JLoggerProxy.debug(this, + "Restoring {} {}", + windowUid, + windowProperties); + WindowMetaData newWindowMetaData = new WindowMetaData(windowProperties, + new Stage()); + addWindow(newWindowMetaData); + addWindowListeners(newWindowMetaData); + styleService.addStyle(newWindowMetaData.getWindow().getScene()); + newWindowMetaData.getWindow().show(); + } + } + } + /** + * Update the display layout for the windows in the given window registry. + */ + private void updateDisplayLayout() + { + try { + Properties displayLayout = getDisplayLayout(); + SLF4JLoggerProxy.debug(this, + "Updating display layout for {}: {}", + SessionUser.getCurrent(), + displayLayout); + displayLayoutService.setDisplayLayout(displayLayout); + } catch (Exception e) { + SLF4JLoggerProxy.warn(this, + e, + ExceptionUtils.getRootCauseMessage(e)); + } + } + /** + * Add the necessary window listeners to the given window meta data. + * + * @param inWindowWrapper a WindowMetaData value + */ + private void addWindowListeners(WindowMetaData inWindowWrapper) + { + WindowRegistry windowRegistry = this; + Stage newWindow = inWindowWrapper.getWindow(); + newWindow.addEventHandler(MouseEvent.MOUSE_CLICKED, + new EventHandler() { + @Override + public void handle(MouseEvent inEvent) + { + SLF4JLoggerProxy.trace(WindowManagerService.this, + "Click: {}", + inEvent); + verifyWindowLocation(newWindow); + inWindowWrapper.updateProperties(); + updateDisplayLayout(); + }} + ); +// newWindow.addWindowModeChangeListener(inEvent -> { +// SLF4JLoggerProxy.trace(WindowManagerService.this, +// "Mode change: {}", +// inEvent); +// // TODO might want to do this, might not. a maximized window currently tromps all over the menu bar +//// verifyWindowLocation(newWindow); +// inWindowWrapper.updateProperties(); +// updateDisplayLayout(); +// }); + newWindow.widthProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue inObservable, + Number inOldValue, + Number inNewValue) + { + newWindowResize("width", + inWindowWrapper); + }} + ); + newWindow.heightProperty().addListener(new ChangeListener() { + @Override + public void changed(ObservableValue inObservable, + Number inOldValue, + Number inNewValue) + { + newWindowResize("height", + inWindowWrapper); + }} + ); + newWindow.addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST,new EventHandler() { + @Override + public void handle(WindowEvent inEvent) + { + SLF4JLoggerProxy.trace(WindowManagerService.this, + "Close: {}", + inEvent); + // this listener will be fired during log out, but, we don't want to update the display layout in that case + if(!windowRegistry.isLoggingOut()) { + windowRegistry.removeWindow(inWindowWrapper); + updateDisplayLayout(); + } + }} + ); +// newWindow.addBlurListener(inEvent -> { +// SLF4JLoggerProxy.trace(WindowManagerService.this, +// "Blur: {}", +// inEvent); +// verifyWindowLocation(newWindow); +// inWindowWrapper.setHasFocus(false); +// inWindowWrapper.updateProperties(); +// updateDisplayLayout(); +// }); +// newWindow.addFocusListener(inEvent -> { +// SLF4JLoggerProxy.trace(WindowManagerService.this, +// "Focus: {}", +// inEvent); +// verifyWindowLocation(newWindow); +// inWindowWrapper.setHasFocus(true); +// inWindowWrapper.updateProperties(); +// updateDisplayLayout(); +// }); + newWindow.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED,new EventHandler() { + @Override + public void handle(ContextMenuEvent inEvent) + { + SLF4JLoggerProxy.trace(WindowManagerService.this, + "Context click: {}", + inEvent); + verifyWindowLocation(newWindow); + inWindowWrapper.updateProperties(); + updateDisplayLayout(); + }} + ); + } + private void newWindowResize(String inDimension, + WindowMetaData inWindowWrapper) + { + SLF4JLoggerProxy.trace(WindowManagerService.this, + "Resize: {}", + inDimension); + verifyWindowLocation(inWindowWrapper.getWindow()); + inWindowWrapper.updateProperties(); + updateDisplayLayout(); + } + /** + * Verify that the given window is within the acceptable bounds of the desktop viewable area. + * + * @param inWindow a Stage value + */ + private void verifyWindowLocation(Stage inWindow) + { + synchronized(activeWindows) { + if(isWindowOutsideDesktop(inWindow)) { + SLF4JLoggerProxy.trace(WindowManagerService.this, + "{} is outside the desktop", + inWindow.getTitle()); + returnWindowToDesktop(inWindow); + } else { + SLF4JLoggerProxy.trace(WindowManagerService.this, + "{} is not outside the desktop", + inWindow.getTitle()); + } + } + } + /** + * Reposition the given window until it is within the acceptable bounds of the desktop viewable area. + * + *

If the window is already within the acceptable bounds of the desktop viewable area, it will not be repositioned. + * + * @param inWindow a Window value + */ + private void returnWindowToDesktop(Window inWindow) + { + int pad = desktopViewableAreaPad; + DesktopParameters params = SessionUser.getCurrent().getAttribute(DesktopParameters.class); + // the order here is important: first, resize the window, if necessary + int maxWidth = params.getRight()-params.getLeft(); + double windowWidth = getWindowWidth(inWindow); + if(windowWidth > maxWidth) { + inWindow.setWidth(maxWidth - (pad*2)); + } + int maxHeight = params.getBottom() - params.getTop(); + double windowHeight = getWindowHeight(inWindow); + if(windowHeight > maxHeight) { + inWindow.setHeight(maxHeight - (pad*2)); + } + // window is now no larger than desktop + // check bottom + double windowBottom = getWindowBottom(inWindow); + if(windowBottom > params.getBottom()) { + double newWindowTop = params.getBottom() - getWindowHeight(inWindow) - pad; + inWindow.setY(newWindowTop); + } + // check top + double windowTop = getWindowTop(inWindow); + if(windowTop < params.getTop()) { + double newWindowTop = params.getTop() + pad; + inWindow.setY(newWindowTop); + } + // window is now within the desktop Y range + // check left + double windowLeft = getWindowLeft(inWindow); + if(windowLeft < params.getLeft()) { + double newWindowLeft = params.getLeft() + pad; + inWindow.setX(newWindowLeft); + } + // check right + double windowRight = getWindowRight(inWindow); + if(windowRight > params.getRight()) { + double newWindowLeft = params.getRight() - getWindowWidth(inWindow) - pad; + inWindow.setX(newWindowLeft); + } + } + /** + * Remove the given window from this registry. + * + * @param inWindowMetaData a WindowMetaData value + */ + private void removeWindow(WindowMetaData inWindowMetaData) + { + synchronized(activeWindows) { + activeWindows.remove(inWindowMetaData); + } + } + /** + * Execute logout actions. + */ + private void logout() + { + isLoggingOut = true; + terminateRegistry(); + } + /** + * Terminate this registry. + * + *

A terminated registry may not be reused. + */ + private void terminateRegistry() + { + synchronized(windowPositionExaminerThreadPool) { + cancelWindowPositionMonitor(); + windowPositionExaminerThreadPool.shutdownNow(); + } + closeAllWindows(false); + } + /** + * Verify the position of all windows in this registry. + */ + private void verifyAllWindowPositions() + { + synchronized(activeWindows) { + Platform.runLater(new Runnable() { + @Override + public void run() + { + for(WindowMetaData windowMetaData : activeWindows) { + try { + returnWindowToDesktop(windowMetaData.getWindow()); + } catch (Exception e) { + SLF4JLoggerProxy.warn(WindowManagerService.this, + ExceptionUtils.getRootCauseMessage(e)); + } +// if(windowMetaData.hasFocus()) { // && windowMetaData.getWindow().isAttached()) { +// windowMetaData.getWindow().focus(); +// } + } + }} + ); + } + } + /** + * Cancel the current window position monitor job, if necessary. + */ + private void cancelWindowPositionMonitor() + { + synchronized(windowPositionExaminerThreadPool) { + if(windowPositionMonitorToken != null) { + try { + windowPositionMonitorToken.cancel(true); + } catch (Exception ignored) {} + windowPositionMonitorToken = null; + } + } + } + /** + * Schedule the window position monitor job. + */ + private void scheduleWindowPositionMonitor() + { + synchronized(windowPositionExaminerThreadPool) { + cancelWindowPositionMonitor(); + windowPositionMonitorToken = windowPositionExaminerThreadPool.scheduleAtFixedRate(new Runnable() { + @Override + public void run() + { + try { + verifyAllWindowPositions(); + } catch (Exception e) { + SLF4JLoggerProxy.warn(WindowManagerService.this, + ExceptionUtils.getRootCauseMessage(e)); + } + }}, + desktopWindowPositionMonitorInterval, + desktopWindowPositionMonitorInterval, + TimeUnit.MILLISECONDS); + } + } + /** + * Get the isLoggingOut value. + * + * @return a boolean value + */ + private boolean isLoggingOut() + { + return isLoggingOut; + } + /** + * Get the display layout for all active windows. + * + * @return a Properties value + */ + private Properties getDisplayLayout() + { + synchronized(activeWindows) { + Properties displayLayout = new Properties(); + for(WindowMetaData activeWindow : activeWindows) { + String windowKey = activeWindow.getUuid(); + String windowValue = activeWindow.getStorableValue(); + displayLayout.setProperty(windowKey, + windowValue); + } + return displayLayout; + } + } + /** + * indicates if the user is in the process of logging out + */ + private boolean isLoggingOut = false; + /** + * holds all active windows + */ + private final Set activeWindows = Sets.newHashSet(); + /** + * holds the token for the window position monitor job, if any + */ + private Future windowPositionMonitorToken; + /** + * checks window position on a periodic basis + */ + private final ScheduledExecutorService windowPositionExaminerThreadPool = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setNameFormat(SessionUser.getCurrent().getUsername() + "-WindowPositionExaminer").build()); + } + /** + * base key for {@see UserAttributeType} display layout properties + */ + private static final String propId = WindowMetaData.class.getSimpleName(); + /** + * window uuid key name + */ + public static final String windowUuidProp = propId + "_uid"; + /** + * window content view factory key name + */ + private static final String windowContentViewFactoryProp = propId + "_contentViewFactory"; + /** + * window title key name + */ + private static final String windowTitleProp = propId + "_title"; + /** + * window X position key name + */ + private static final String windowPosXProp = propId + "__posX"; + /** + * window Y position key name + */ + private static final String windowPosYProp = propId + "_posY"; + /** + * window height key name + */ + private static final String windowHeightProp = propId + "_height"; + /** + * window width key name + */ + private static final String windowWidthProp = propId + "_width"; + /** + * window mode key name + */ + private static final String windowModeProp = propId + "_mode"; + /** + * window is modal key name + */ + private static final String windowModalProp = propId + "_modal"; + /** + * window is focused key name + */ + private static final String windowFocusProp = propId + "_focus"; + /** + * window is draggable key name + */ + private static final String windowDraggableProp = propId + "_draggable"; + /** + * window is resizable key name + */ + private static final String windowResizableProp = propId + "_resizable"; + /** + * window scroll left key name + */ + private static final String windowScrollLeftProp = propId + "_scrollLeft"; + /** + * window scroll top key name + */ + private static final String windowScrollTopProp = propId + "_scrollTop"; + /** + * window style id key name + */ + private static final String windowStyleId = propId + "_windowStyleId"; + /** + * provides access to style services + */ + @Autowired + private StyleService styleService; + /** + * web message service value + */ + @Autowired + private WebMessageService webMessageService; + /** + * provides access to display layout services + */ + @Autowired + private DisplayLayoutService displayLayoutService; + /** + * provides access to the application context + */ + @Autowired + private ApplicationContext applicationContext; + /** + * desktop viewable area pad value + */ + @Value("${metc.desktop.viewable.area.pad:10}") + private int desktopViewableAreaPad; + /** + * desktop cascade window offset value + */ + @Value("${metc.desktop.cascade.window.offset:100}") + private int desktopCascadeWindowOffset; + /** + * interval in ms at which to monitor and correct window positions + */ + @Value("${metc.desktop.window.position.monitor.interval:250}") + private long desktopWindowPositionMonitorInterval; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientDisplayLayoutService.java b/photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientDisplayLayoutService.java new file mode 100644 index 0000000000..fdf0594a8b --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientDisplayLayoutService.java @@ -0,0 +1,58 @@ +package org.marketcetera.ui.service.admin; + +import java.util.Properties; + +import org.marketcetera.admin.UserAttribute; +import org.marketcetera.admin.UserAttributeType; +import org.marketcetera.core.Util; +import org.marketcetera.ui.service.DisplayLayoutService; +import org.marketcetera.ui.service.ServiceManager; +import org.marketcetera.ui.service.SessionUser; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/* $License$ */ + +/** + * Provides display layout services for the admin client. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Service +public class AdminClientDisplayLayoutService + implements DisplayLayoutService +{ + /* (non-Javadoc) + * @see org.marketcetera.web.service.DisplayLayoutService#getDisplayLayout() + */ + @Override + public Properties getDisplayLayout() + { + AdminClientService adminClientService = serviceManager.getService(AdminClientService.class); + UserAttribute userAttribute = adminClientService.getUserAttribute(SessionUser.getCurrent().getUsername(), + UserAttributeType.DISPLAY_LAYOUT); + if(userAttribute == null) { + return new Properties(); + } else { + return Util.propertiesFromString(userAttribute.getAttribute()); + } + } + /* (non-Javadoc) + * @see org.marketcetera.web.service.DisplayLayoutService#setDisplayLayout(java.util.Properties) + */ + @Override + public void setDisplayLayout(Properties inDisplayLayout) + { + AdminClientService adminClientService = serviceManager.getService(AdminClientService.class); + adminClientService.setUserAttribute(SessionUser.getCurrent().getUsername(), + UserAttributeType.DISPLAY_LAYOUT, + Util.propertiesToString(inDisplayLayout)); + } + /** + * provides access to client services + */ + @Autowired + private ServiceManager serviceManager; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientService.java b/photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientService.java new file mode 100644 index 0000000000..d031dc40dd --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientService.java @@ -0,0 +1,537 @@ +package org.marketcetera.ui.service.admin; + +import java.io.IOException; +import java.util.Collection; +import java.util.Set; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.marketcetera.admin.AdminClient; +import org.marketcetera.admin.AdminRpcClientFactory; +import org.marketcetera.admin.AdminRpcClientParameters; +import org.marketcetera.admin.Permission; +import org.marketcetera.admin.Role; +import org.marketcetera.admin.User; +import org.marketcetera.admin.UserAttribute; +import org.marketcetera.admin.UserAttributeType; +import org.marketcetera.brokers.BrokerStatusListener; +import org.marketcetera.fix.ActiveFixSession; +import org.marketcetera.fix.FixAdminClient; +import org.marketcetera.fix.FixAdminRpcClientFactory; +import org.marketcetera.fix.FixAdminRpcClientParameters; +import org.marketcetera.fix.FixSession; +import org.marketcetera.fix.FixSessionAttributeDescriptor; +import org.marketcetera.fix.FixSessionInstanceData; +import org.marketcetera.persist.CollectionPageResponse; +import org.marketcetera.persist.PageRequest; +import org.marketcetera.ui.service.ConnectableService; +import org.marketcetera.ui.service.ServiceManager; +import org.marketcetera.ui.service.SessionUser; +import org.marketcetera.util.log.SLF4JLoggerProxy; + + +/* $License$ */ + +/** + * Provides access to admin services for a given user. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + * TODO separate out FIX admin services + */ +public class AdminClientService + implements ConnectableService +{ + /** + * Get the AdminClientService instance for the current session. + * + * @return an AdminClientService value or null + */ + public static AdminClientService getInstance() + { + return ServiceManager.getInstance().getService(AdminClientService.class); + } + /** + * Create a new AdminClientService instance. + */ + public AdminClientService() {} + /* (non-Javadoc) + * @see org.marketcetera.web.service.ConnectableService#isRunning() + */ + @Override + public boolean isRunning() + { + return adminClient != null && adminClient.isRunning(); + } + /* (non-Javadoc) + * @see org.marketcetera.web.service.ConnectableService#disconnect() + */ + @Override + public void disconnect() + { + if(adminClient != null) { + try { + adminClient.close(); + } catch (IOException e) { + SLF4JLoggerProxy.warn(this, + e); + } + } + adminClient = null; + } + /* (non-Javadoc) + * @see org.marketcetera.web.services.ConnectableService#connect(java.lang.String, java.lang.String, java.lang.String, int) + */ + @Override + public boolean connect(String inUsername, + String inPassword, + String inHostname, + int inPort) + throws Exception + { + if(adminClient != null) { + try { + adminClient.stop(); + } catch (Exception e) { + SLF4JLoggerProxy.warn(this, + "Unable to stop existing admin client for {}: {}", + inUsername, + ExceptionUtils.getRootCauseMessage(e)); + } finally { + adminClient = null; + } + } + SLF4JLoggerProxy.debug(this, + "Creating admin client for {} to {}:{}", + inUsername, + inHostname, + inPort); + AdminRpcClientParameters params = new AdminRpcClientParameters(); + params.setHostname(inHostname); + params.setPort(inPort); + params.setUsername(inUsername); + params.setPassword(inPassword); + adminClient = adminClientFactory.create(params); + adminClient.start(); + if(fixAdminClient != null) { + try { + fixAdminClient.stop(); + } catch (Exception e) { + SLF4JLoggerProxy.warn(this, + "Unable to stop existing fix admin client for {}: {}", + inUsername, + ExceptionUtils.getRootCauseMessage(e)); + } finally { + fixAdminClient = null; + } + } + SLF4JLoggerProxy.debug(this, + "Creating fixAdmin client for {} to {}:{}", + inUsername, + inHostname, + inPort); + FixAdminRpcClientParameters fixParams = new FixAdminRpcClientParameters(); + fixParams.setHostname(inHostname); + fixParams.setPort(inPort); + fixParams.setUsername(inUsername); + fixParams.setPassword(inPassword); + fixAdminClient = fixAdminClientFactory.create(fixParams); + fixAdminClient.start(); + if(adminClient.isRunning() && fixAdminClient.isRunning()) { + SessionUser.getCurrent().setAttribute(AdminClientService.class, + this); + } + return adminClient.isRunning() && fixAdminClient.isRunning(); + } + /** + * Create the given user. + * + * @param inSubject a User value + */ + public void createUser(User inSubject, + String inPassword) + { + adminClient.createUser(inSubject, + inPassword); + } + /** + * Get the users under the aegis of the given user. + * + * @return a Collection<User> + */ + public Collection getUsers() + { + return adminClient.readUsers(); + } + /** + * Get the user attribute described with the given attributes. + * + * @param inUsername a String value + * @param inAttributeType a UserAttributeType value + * @return a UserAttribute value + */ + public UserAttribute getUserAttribute(String inUsername, + UserAttributeType inAttributeType) + { + return adminClient.getUserAttribute(inUsername, + inAttributeType); + } + /** + * Set the user attribute described with the given attributes. + * + * @param inUsername a String value + * @param inAttributeType a UserAttributeType value + * @param inAttribute a Stringvalue + */ + public void setUserAttribute(String inUsername, + UserAttributeType inAttributeType, + String inAttribute) + { + adminClient.setUserAttribute(inUsername, + inAttributeType, + inAttribute); + } + /** + * Get a page of users. + * + * @param inPageRequest a PageRequest value + * @return a CollectionPageResponse<User> value + */ + public CollectionPageResponse getUsers(PageRequest inPageRequest) + { + return adminClient.readUsers(inPageRequest); + } + /** + * Update the given user with the given original name. + * + * @param inName a String value + * @param inSubject a User value + */ + public void updateUser(String inName, + User inSubject) + { + adminClient.updateUser(inName, + inSubject); + } + /** + * Delete the user with the given name. + * + * @param inName a String value + */ + public void deleteUser(String inName) + { + adminClient.deleteUser(inName); + } + /** + * Deactivate the user with the given name. + * + * @param inName a String value + */ + public void deactivateUser(String inName) + { + adminClient.deactivateUser(inName); + } + /** + * Get roles. + * + * @return a Collection<Role> value + */ + public Collection getRoles() + { + return adminClient.readRoles(); + } + /** + * Get roles. + * + * @param inPageRequest a PageRequest value + * @return a Collection<Permission> value + */ + public CollectionPageResponse getRoles(PageRequest inPageRequest) + { + return adminClient.readRoles(inPageRequest); + } + /** + * Get permissions. + * + * @return a Collection<Permission> value + */ + public Collection getPermissions() + { + return adminClient.readPermissions(); + } + /** + * Get permissions. + * + * @param inPageRequest a PageRequest value + * @return a Collection<Permission> value + */ + public CollectionPageResponse getPermissions(PageRequest inPageRequest) + { + return adminClient.readPermissions(inPageRequest); + } + /** + * Get the permissions assigned to the current user. + * + * @return a Set<Permission> value + */ + public Set getPermissionsForUser() + { + return adminClient.getPermissionsForCurrentUser(); + } + /** + * Delete the permission with the given name. + * + * @param inName a String value + */ + public void deletePermission(String inName) + { + adminClient.deletePermission(inName); + } + /** + * Delete the role with the given name. + * + * @param inName a String value + */ + public void deleteRole(String inName) + { + adminClient.deleteRole(inName); + } + /** + * Update the given role with the given original name. + * + * @param inName a String value + * @param inRole a Role value + */ + public void updateRole(String inName, + Role inRole) + { + adminClient.updateRole(inName, + inRole); + } + /** + * Create the given role. + * + * @param inRole a Role value + */ + public void createRole(Role inRole) + { + adminClient.createRole(inRole); + } + /** + * Get FIX sessions. + * + * @return a Collection<ActiveFixSession> value + */ + public Collection getFixSessions() + { + return fixAdminClient.readFixSessions(); + } + /** + * Get a page of FIX sessions. + * + * @param inPageRequest a PageRequest value + * @return a CollectionPageResponse<ActiveFixSession> value + */ + public CollectionPageResponse getFixSessions(PageRequest inPageRequest) + { + return fixAdminClient.readFixSessions(inPageRequest); + } + /** + * Get the FIX session attribute descriptors. + * + * @return a Collection<FixSessionAttributeDescriptor> value + */ + public Collection getFixSessionAttributeDescriptors() + { + return fixAdminClient.getFixSessionAttributeDescriptors(); + } + /** + * Create a new FIX session. + * + * @param inFixSession a FixSession value + * @return a FixSession value + */ + public FixSession createFixSession(FixSession inFixSession) + { + return fixAdminClient.createFixSession(inFixSession); + } + /** + * Update the FIX session with the given original name. + * + * @param inIncomingName a String value + * @param inFixSession a FixSession value + */ + public void updateFixSession(String inIncomingName, + FixSession inFixSession) + { + fixAdminClient.updateFixSession(inIncomingName, + inFixSession); + } + /** + * Enable the FIX session with the given name. + * + * @param inName a String value + */ + public void enableSession(String inName) + { + fixAdminClient.enableFixSession(inName); + } + /** + * Disable the FIX session with the given name. + * + * @param inName a String value + */ + public void disableSession(String inName) + { + fixAdminClient.disableFixSession(inName); + } + /** + * Delete the FIX session with the given name. + * + * @param inName a String value + */ + public void deleteSession(String inName) + { + fixAdminClient.deleteFixSession(inName); + } + /** + * Stop the FIX session with the given name. + * + * @param inName a String value + */ + public void stopSession(String inName) + { + fixAdminClient.stopFixSession(inName); + } + /** + * Start the FIX session with the given name. + * + * @param inName a String value + */ + public void startSession(String inName) + { + fixAdminClient.startFixSession(inName); + } + /** + * Update sender and target sequence numbers for the given session. + * + * @param inSessionName a String value + * @param inSenderSequenceNumber an int value + * @param inTargetSequenceNumber an int value + */ + public void updateSequenceNumbers(String inSessionName, + int inSenderSequenceNumber, + int inTargetSequenceNumber) + { + fixAdminClient.updateSequenceNumbers(inSessionName, + inSenderSequenceNumber, + inTargetSequenceNumber); + } + /** + * Update the sender sequence number for the given session. + * + * @param inSessionName a String value + * @param inSenderSequenceNumber an int value + */ + public void updateSenderSequenceNumber(String inSessionName, + int inSenderSequenceNumber) + { + fixAdminClient.updateSenderSequenceNumber(inSessionName, + inSenderSequenceNumber); + } + /** + * Update the target sequence number for the given session. + * + * @param inSessionName a String value + * @param inTargetSequenceNumber an int value + */ + public void updateTargetSequenceNumber(String inSessionName, + int inTargetSequenceNumber) + { + fixAdminClient.updateTargetSequenceNumber(inSessionName, + inTargetSequenceNumber); + } + /** + * Create the given permission. + * + * @param inPermission a Permission value + */ + public void createPermission(Permission inPermission) + { + adminClient.createPermission(inPermission); + } + /** + * Update the given permission. + * + * @param inPermissionName a String value + * @param inPermission a Permission value + */ + public void updatePermission(String inPermissionName, + Permission inPermission) + { + adminClient.updatePermission(inPermissionName, + inPermission); + } + /** + * Get the instance data for the given affinity. + * + * @param inAffinity an int value + * @return an InstanceData value + */ + public FixSessionInstanceData getFixSessionInstanceData(int inAffinity) + { + return fixAdminClient.getFixSessionInstanceData(inAffinity); + } + /** + * Add the given broker status listener. + * + * @param inBrokerStatusListener a BrokerStatusListener value + */ + public void addBrokerStatusListener(BrokerStatusListener inBrokerStatusListener) + { + fixAdminClient.addBrokerStatusListener(inBrokerStatusListener); + } + /** + * Remove the given broker status listener. + * + * @param inBrokerStatusListener a BrokerStatusListener value + */ + public void removeBrokerStatusListener(BrokerStatusListener inBrokerStatusListener) + { + fixAdminClient.removeBrokerStatusListener(inBrokerStatusListener); + } + /** + * Sets the adminClientFactory value. + * + * @param inAdminClientFactory an AdminRpcClientFactory value + */ + public void setAdminClientFactory(AdminRpcClientFactory inAdminClientFactory) + { + adminClientFactory = inAdminClientFactory; + } + /** + * Sets the fixAdminClientFactory value. + * + * @param inFixAdminClientFactory a FixAdminRpcClientFactory value + */ + public void setFixAdminClientFactory(FixAdminRpcClientFactory inFixAdminClientFactory) + { + fixAdminClientFactory = inFixAdminClientFactory; + } + /** + * creates an admin client to connect to the admin server + */ + private AdminRpcClientFactory adminClientFactory; + /** + * creates a FIX admin client to connect to the fix admin server + */ + private FixAdminRpcClientFactory fixAdminClientFactory; + /** + * client object used to communicate with the server + */ + private AdminClient adminClient; + /** + * provides access to FIX admin services + */ + private FixAdminClient fixAdminClient; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientServiceFactory.java b/photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientServiceFactory.java new file mode 100644 index 0000000000..5b2da8d99b --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/admin/AdminClientServiceFactory.java @@ -0,0 +1,51 @@ +package org.marketcetera.ui.service.admin; + +import org.marketcetera.admin.AdminRpcClientFactory; +import org.marketcetera.fix.FixAdminRpcClientFactory; +import org.marketcetera.ui.service.ConnectableServiceFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/* $License$ */ + +/** + * Creates {@link AdminClientService} objects. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Service +public class AdminClientServiceFactory + implements ConnectableServiceFactory +{ + /* (non-Javadoc) + * @see org.marketcetera.web.services.ConnectableServiceFactory#create() + */ + @Override + public AdminClientService create() + { + AdminClientService adminClientService = new AdminClientService(); + adminClientService.setAdminClientFactory(adminClientFactory); + adminClientService.setFixAdminClientFactory(fixAdminClientFactory); + return adminClientService; + } + /* (non-Javadoc) + * @see org.marketcetera.web.service.ConnectableServiceFactory#getServiceType() + */ + @Override + public Class getServiceType() + { + return AdminClientService.class; + } + /** + * creates an admin client to connect to the admin server + */ + @Autowired + private AdminRpcClientFactory adminClientFactory; + /** + * creates a FIX admin client to connect to the fix admin server + */ + @Autowired + private FixAdminRpcClientFactory fixAdminClientFactory; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/service/admin/WebAuthorizationHelperService.java b/photon/photon/src/main/java/org/marketcetera/ui/service/admin/WebAuthorizationHelperService.java new file mode 100644 index 0000000000..059bd278cd --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/service/admin/WebAuthorizationHelperService.java @@ -0,0 +1,101 @@ +package org.marketcetera.ui.service.admin; + +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; + +import org.marketcetera.admin.Permission; +import org.marketcetera.ui.service.AuthorizationHelperService; +import org.marketcetera.ui.service.ServiceManager; +import org.marketcetera.ui.service.SessionUser; +import org.marketcetera.util.log.SLF4JLoggerProxy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Service; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +/* $License$ */ + +/** + * Provides authorization resolution services as a remote service. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Service +public class WebAuthorizationHelperService + implements AuthorizationHelperService +{ + /* (non-Javadoc) + * @see org.marketcetera.web.service.AuthorizationHelperService#hasPermission(org.springframework.security.core.GrantedAuthority) + */ + @Override + public boolean hasPermission(GrantedAuthority inGrantedAuthority) + { + SessionUser sessionUser = SessionUser.getCurrent(); + if(sessionUser == null) { + SLF4JLoggerProxy.trace(this, + "No current user, permission {} is denied", + inGrantedAuthority); + return false; + } + return permissionMapsByUsername.getUnchecked(sessionUser.getUsername()).getUnchecked(inGrantedAuthority.getAuthority()); + } + /** + * Validate and start the object. + */ + @PostConstruct + public void start() + { + permissionMapsByUsername = CacheBuilder.newBuilder().expireAfterAccess(userPermissionCacheTtl,TimeUnit.MILLISECONDS).build(new CacheLoader>() { + @Override + public LoadingCache load(String inUsername) + throws Exception + { + AdminClientService adminClientService = serviceManager.getService(AdminClientService.class); + Set permissions = adminClientService.getPermissionsForUser(); + LoadingCache permissionsByPermissionName = CacheBuilder.newBuilder().build(new CacheLoader() { + @Override + public Boolean load(String inPermissionName) + throws Exception + { + SLF4JLoggerProxy.trace(WebAuthorizationHelperService.this, + "Checking to see if {} has permission {} in {}", + inUsername, + inPermissionName, + permissions); + if(permissions == null) { + return false; + } + for(Permission permission : permissions) { + if(permission.getAuthority().equals(inPermissionName)) { + return true; + } + } + return false; + }}); + return permissionsByPermissionName; + }} + ); + } + /** + * length of time to cache user permissions + */ + @Value("${metc.user.authorization.permission.cache.ttl:300000}") + private long userPermissionCacheTtl = 1000 * 60 * 5; + /** + * caches permissions by username and permission name + */ + private LoadingCache> permissionMapsByUsername; + /** + * service manager value + */ + @Autowired + private ServiceManager serviceManager; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/view/AbstractMenuItem.java b/photon/photon/src/main/java/org/marketcetera/ui/view/AbstractMenuItem.java new file mode 100644 index 0000000000..189ceb655d --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/view/AbstractMenuItem.java @@ -0,0 +1,38 @@ +package org.marketcetera.ui.view; + +import org.marketcetera.ui.service.WebMessageService; +import org.springframework.beans.factory.annotation.Autowired; + +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; + +/* $License$ */ + +/** + * + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public abstract class AbstractMenuItem + implements MenuContent +{ + protected Node getIcon(String inName) + { + if(iconView == null) { + icon = new Image(inName); + iconView = new ImageView(); + iconView.setImage(icon); + } + return iconView; + } + private Image icon; + private ImageView iconView; + /** + * web message service value + */ + @Autowired + protected WebMessageService webMessageService; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/view/ApplicationMenu.java b/photon/photon/src/main/java/org/marketcetera/ui/view/ApplicationMenu.java new file mode 100644 index 0000000000..4bce072684 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/view/ApplicationMenu.java @@ -0,0 +1,427 @@ +package org.marketcetera.ui.view; + +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import javax.annotation.PostConstruct; + +import org.marketcetera.ui.service.AuthorizationHelperService; +import org.marketcetera.ui.service.SessionUser; +import org.marketcetera.util.log.SLF4JLoggerProxy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Scope; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import javafx.scene.Node; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuBar; +import javafx.scene.control.MenuItem; + + +/* $License$ */ + +/** + * Builds a top-level menu for the entire application. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class ApplicationMenu +{ + /** + * Validate and start the object. + */ + @PostConstruct + public void start() + { + initMenu(); + } + /** + * Refresh the menu permissions. + */ + public void refreshMenuPermissions() + { + SessionUser currentUser = SessionUser.getCurrent(); + if(currentUser == null) { + menu.setVisible(false); + return; + } + // there is a current user, already logged in, the menu should now be visible + menu.setVisible(true); + // examine over the top level content items + for(MenuItemMetaData topLevelContentItem : topLevelContent) { + // it's possible to define permissions for top-level menu items independently from the child items + evaluatePermissions(topLevelContentItem.getAllPermissions(), + SessionUser.getCurrent().getPermissions(), + topLevelContentItem.getMenuItem()); + // if the user has permissions to view the top level contents, examine the children, if nay + if(topLevelContentItem.getMenuItem().isVisible()) { + // if no children are visible, we're going to hide the top-level menu, too + boolean atLeastOneChildVisible = false; + // examine the children (may be empty) + for(MenuItemMetaData childContentItem : topLevelContentItem.getChildItems()) { + evaluatePermissions(childContentItem.getAllPermissions(), + SessionUser.getCurrent().getPermissions(), + childContentItem.getMenuItem()); + // track whether at least one child item is visible + atLeastOneChildVisible |= childContentItem.getMenuItem().isVisible(); + } + // if there is at least one child item, then, at least one child item must be visible or we're going to hide the top-level item + if(!topLevelContentItem.getChildItems().isEmpty()) { + topLevelContentItem.getMenuItem().setVisible(atLeastOneChildVisible); + } + } + } + } + /** + * Get the menu value. + * + * @return a MenuBar value + */ + public MenuBar getMenu() + { + return menu; + } + /** + * Initialize the menu bar with application content buttons. + */ + private void initMenu() + { + // TODO need recursion - this method handles two levels of menus only + // collect all the contents and organize them by category, if any category is present + SortedMap> categoryContent = new TreeMap<>(MenuContent.comparator); + for(Map.Entry entry : applicationContext.getBeansOfType(MenuContent.class).entrySet()) { + MenuContent contentItem = entry.getValue(); + MenuContent contentCategory = contentItem.getCategory(); + if(contentCategory == null) { + // add this item to the top-level menu set - this will show in the main menu + topLevelContent.add(new MenuItemMetaData(contentItem)); + } else { + // this item belongs to a category, make sure there's a set for that category and add the item to it + SortedSet contentForCategory = categoryContent.get(contentCategory); + if(contentForCategory == null) { + contentForCategory = new TreeSet<>(MenuContent.comparator); + categoryContent.put(contentCategory, + contentForCategory); + } + contentForCategory.add(contentItem); + // now add the category itself to the top-level menu (might already be there, doesn't matter because it's a set) + topLevelContent.add(new MenuItemMetaData(contentCategory)); + } + } + // all menu items have now been categorized and sorted, go back through and create the menu bar + menu = new MenuBar(); +// menu.setStyleName(ValoTheme.MENUBAR_BORDERLESS); + // the top-level menu is in topLevelContent, some of the items may be categories + for(MenuItemMetaData topLevelContentItem : topLevelContent) { + // this item may be a parent or a leaf + Menu parent = new Menu(); + parent.setText(topLevelContentItem.getMenuCaption()); + Node icon = topLevelContentItem.getMenuIcon(); + parent.setGraphic(icon); + menu.getMenus().add(parent); + topLevelContentItem.setMenuItem(parent); + SortedSet childItems = categoryContent.get(topLevelContentItem); + if(childItems == null || childItems.isEmpty()) { + // special handling is required for menus that have no menu items (top level menu choices, like logout) + MenuItem placeholderMenuItem = new MenuItem(); + parent.getItems().add(placeholderMenuItem); + parent.showingProperty().addListener((observableValue, oldValue, newValue) -> { + if (newValue) { + // the first menuItem is triggered + parent.getItems().get(0).fire(); + } + } + ); + placeholderMenuItem.setOnAction(e -> topLevelContentItem.getCommand().run()); + } else { + for(MenuContent childItem : childItems) { + MenuItem newChildIem = new MenuItem(); + newChildIem.setText(childItem.getMenuCaption()); + newChildIem.setGraphic(childItem.getMenuIcon()); + newChildIem.setOnAction(e -> childItem.getCommand().run()); + parent.getItems().add(newChildIem); + MenuItemMetaData newChildItemMetaData = new MenuItemMetaData(childItem); + newChildItemMetaData.setMenuItem(newChildIem); + topLevelContentItem.getChildItems().add(newChildItemMetaData); + } + } + } +// SortedSet noCategoryContent = new TreeSet<>(categoryComparator); +// SortedMap> sortedContent = new TreeMap<>(categoryComparator); +// for(Map.Entry entry : applicationContext.getBeansOfType(MenuContent.class).entrySet()) { +// final MenuContent contentView = entry.getValue(); +// SLF4JLoggerProxy.debug(this, +// "Creating menu entry for {}", +// contentView.getLabel()); +// MenuContent viewCategory = contentView.getCategory(); +// if(viewCategory == null) { +// noCategoryContent.add(contentView); +// } else { +// SortedSet viewsForCategory = sortedContent.get(viewCategory); +// if(viewsForCategory == null) { +// viewsForCategory = new TreeSet<>(categoryComparator); +// sortedContent.put(viewCategory, +// viewsForCategory); +// } +// viewsForCategory.add(contentView); +// } +// } +// menu.addItem("Home", +// FontAwesome.HOME, +// new MenuBar.Command() { +// @Override +// public void menuSelected(MenuItem inSelectedItem) +// { +// SLF4JLoggerProxy.debug(ApplicationMenu.this, +// "Navigating to {}", +// MainView.NAME); +// UI.getCurrent().getNavigator().navigateTo(MainView.NAME); +// } +// private static final long serialVersionUID = -4840986259382011275L; +// }); +//// // add all items with no category +//// for(final MenuContent contentView : noCategoryContent) { +//// SLF4JLoggerProxy.debug(this, +//// "Adding menu item for content view w/o category: {}", +//// contentView.getLabel()); +//// menu.addItem(contentView.getLabel(),contentView.getIcon(),new MenuBar.Command() { +//// @Override +//// public void menuSelected(MenuItem inSelectedItem) +//// { +//// SLF4JLoggerProxy.debug(ApplicationMenu.this, +//// "Navigating to {}", +//// contentView.getViewName()); +//// UI.getCurrent().getNavigator().navigateTo(contentView.getViewName()); +//// } +//// private static final long serialVersionUID = 2624855188645792796L; +//// }); +//// } +//// // add all items with a category +//// for(Map.Entry> entry : sortedContent.entrySet()) { +//// final MenuCategory contentCategory = entry.getKey(); +//// final SortedSet viewsForCategory = entry.getValue(); +//// MenuItem parent = menu.addItem(contentCategory.getLabel(), +//// contentCategory.getIcon(), +//// null); +//// for(final ContentView contentView : viewsForCategory) { +//// SLF4JLoggerProxy.debug(this, +//// "Adding menu item for content category {}: {}", +//// contentView.getCategory().getLabel(), +//// contentView.getLabel()); +//// parent.addItem(contentView.getLabel(), +//// contentView.getIcon(), +//// new MenuBar.Command() { +//// @Override +//// public void menuSelected(MenuItem inSelectedItem) +//// { +//// SLF4JLoggerProxy.debug(ApplicationMenu.this, +//// "Navigating to {}", +//// contentView.getViewName()); +//// UI.getCurrent().getNavigator().navigateTo(contentView.getViewName()); +//// } +//// private static final long serialVersionUID = 2624855188645792796L; +//// }); +//// } +//// } +// menu.addItem("Log Out", +// new MenuBar.Command() { +// @Override +// public void menuSelected(MenuItem inSelectedItem) +// { +// SLF4JLoggerProxy.debug(ApplicationMenu.this, +// "Logging out"); +// VaadinSession.getCurrent().setAttribute("user", +// null); +// UI.getCurrent().getNavigator().navigateTo(MainView.NAME); +// } +// private static final long serialVersionUID = -4840986259382011275L; +// }); + } + /** + * Evaluate the permissions for the given menu item and set the menu item to be visible accordingly. + * + * @param inRequiredPermissions a Set<GrantedAuthority> value + * @param inActualPermissions a Set<GrantedAuthority> value + * @param inMenuItem a MenuItem value + */ + private void evaluatePermissions(Set inRequiredPermissions, + Set inActualPermissions, + MenuItem inMenuItem) + { + inMenuItem.setVisible(true); + if(inRequiredPermissions != null) { + for(GrantedAuthority requiredPermission : inRequiredPermissions) { + if(!authzHelperService.hasPermission(requiredPermission)) { + SLF4JLoggerProxy.trace(this, + "Cannot display {}, {} not in {}", + inMenuItem.getText(), + requiredPermission, + inActualPermissions); + inMenuItem.setVisible(false); + break; + } + } + } + } + /** + * Holds meta data for a menu item. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ + private class MenuItemMetaData + implements Comparable,MenuContent + { + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getAllPermissions() + */ + @Override + public Set getAllPermissions() + { + return menuContent.getAllPermissions(); + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getMenuCaption() + */ + @Override + public String getMenuCaption() + { + return menuContent.getMenuCaption(); + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getWeight() + */ + @Override + public int getWeight() + { + return menuContent.getWeight(); + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getCategory() + */ + @Override + public MenuContent getCategory() + { + return menuContent.getCategory(); + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getMenuIcon() + */ + @Override + public Node getMenuIcon() + { + return menuContent.getMenuIcon(); + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getCommand() + */ + @Override + public Runnable getCommand() + { + return menuContent.getCommand(); + } + /* (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(MenuItemMetaData inO) + { + return Integer.valueOf(getMenuContent().getWeight()).compareTo(inO.getMenuContent().getWeight()); + } + /* (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() + { + return new StringBuilder().append(menuContent.getMenuCaption()).append(" children: ").append(childItems).toString(); + } + /** + * Get the menuContent value. + * + * @return a MenuContent value + */ + private MenuContent getMenuContent() + { + return menuContent; + } + /** + * Get the childItems value. + * + * @return a SortedSet<MenuContent> value + */ + private SortedSet getChildItems() + { + return childItems; + } + /** + * Get the menu item value. + * + * @return a MenuItem value + */ + private MenuItem getMenuItem() + { + return menuItem; + } + /** + * Create a new MenuItemMetaData instance. + * + * @param inMenuContent a MenuContent value + */ + private MenuItemMetaData(MenuContent inMenuContent) + { + menuContent = inMenuContent; + } + /** + * Set the menu item value. + * + * @param inMenuItem a MenuItem value + */ + private void setMenuItem(MenuItem inMenuItem) + { + menuItem = inMenuItem; + } + /** + * menu item value + */ + private MenuItem menuItem; + /** + * menu content value + */ + private final MenuContent menuContent; + /** + * child menu items, may be empty but will never be null + */ + private final SortedSet childItems = new TreeSet<>(); + } + /** + * top level content, sorted by menu item precedence + */ + private SortedSet topLevelContent = new TreeSet<>(); + /** + * provides help resolving permissions + */ + @Autowired + private AuthorizationHelperService authzHelperService; + /** + * provides the application context + */ + @Autowired + private ApplicationContext applicationContext; + /** + * menu display widget that contains all the menu items + */ + private MenuBar menu; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/view/CascadeWindowsMenuItem.java b/photon/photon/src/main/java/org/marketcetera/ui/view/CascadeWindowsMenuItem.java new file mode 100644 index 0000000000..a1bb1198eb --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/view/CascadeWindowsMenuItem.java @@ -0,0 +1,73 @@ +package org.marketcetera.ui.view; + +import org.marketcetera.ui.events.CascadeWindowsEvent; +import org.springframework.stereotype.Component; + +import javafx.scene.Node; + + +/* $License$ */ + +/** + * Provides a menu item to cascade open windows. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Component +public class CascadeWindowsMenuItem + extends AbstractMenuItem +{ + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getMenuCaption() + */ + @Override + public String getMenuCaption() + { + return "Cascade Windows"; + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getWeight() + */ + @Override + public int getWeight() + { + return 100; + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getCategory() + */ + @Override + public MenuContent getCategory() + { + return WindowContentCategory.instance; + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getMenuIcon() + */ + @Override + public Node getMenuIcon() + { + return getIcon("images/Cascade_Windows.png"); + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getCommand() + */ + @Override + public Runnable getCommand() + { + return new Runnable() { + @Override + public void run() + { + webMessageService.post(new CascadeWindowsEvent()); + } + }; + } +// /** +// * provides access to message services for the web UI +// */ +// @Autowired +// private WebMessageService webMessageService; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/view/CloseAllWindowsMenuItem.java b/photon/photon/src/main/java/org/marketcetera/ui/view/CloseAllWindowsMenuItem.java new file mode 100644 index 0000000000..069e8172f3 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/view/CloseAllWindowsMenuItem.java @@ -0,0 +1,73 @@ +package org.marketcetera.ui.view; + +import org.marketcetera.ui.events.CloseWindowsEvent; +import org.springframework.stereotype.Component; + +import javafx.scene.Node; + + +/* $License$ */ + +/** + * Provides a menu item to close all open windows. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Component +public class CloseAllWindowsMenuItem + extends AbstractMenuItem +{ + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getMenuCaption() + */ + @Override + public String getMenuCaption() + { + return "Close All Windows"; + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getWeight() + */ + @Override + public int getWeight() + { + return 900; + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getCategory() + */ + @Override + public MenuContent getCategory() + { + return WindowContentCategory.instance; + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getMenuIcon() + */ + @Override + public Node getMenuIcon() + { + return getIcon("images/Close_All_Windows.png"); + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getCommand() + */ + @Override + public Runnable getCommand() + { + return new Runnable() { + @Override + public void run() + { + webMessageService.post(new CloseWindowsEvent()); + } + }; + } +// /** +// * provides access to message services for the web UI +// */ +// @Autowired +// private WebMessageService webMessageService; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/view/ContentView.java b/photon/photon/src/main/java/org/marketcetera/ui/view/ContentView.java new file mode 100644 index 0000000000..ead1ea5158 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/view/ContentView.java @@ -0,0 +1,28 @@ +package org.marketcetera.ui.view; + +import javafx.scene.Scene; + +/* $License$ */ + +/** + * Identifies a view that has content for the application. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public interface ContentView +{ + /** + * Get the scene which contains the content. + * + * @return a Scene value + */ + Scene getScene(); + /** + * Get the Vaadin name of the view. + * + * @return a String value + */ + String getViewName(); +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/view/ContentViewFactory.java b/photon/photon/src/main/java/org/marketcetera/ui/view/ContentViewFactory.java new file mode 100644 index 0000000000..b69b04dbbc --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/view/ContentViewFactory.java @@ -0,0 +1,31 @@ +package org.marketcetera.ui.view; + +import java.util.Properties; + +import org.marketcetera.ui.events.NewWindowEvent; + +import javafx.stage.Window; + +/* $License$ */ + +/** + * Creates a new content view. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public interface ContentViewFactory +{ + /** + * Create a new content view. + * + * @param inParent a Window value + * @param inEvent a NewWindowEvent value + * @param inViewProperties a Properties value + * @return a T value + */ + ContentView create(Window inParent, + NewWindowEvent inEvent, + Properties inViewProperties); +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/view/LogoutMenuItem.java b/photon/photon/src/main/java/org/marketcetera/ui/view/LogoutMenuItem.java new file mode 100644 index 0000000000..29d31e7bd4 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/view/LogoutMenuItem.java @@ -0,0 +1,93 @@ +package org.marketcetera.ui.view; + +import javax.annotation.PostConstruct; + +import org.marketcetera.ui.events.LogoutEvent; +import org.marketcetera.ui.service.SessionUser; +import org.marketcetera.ui.service.WebMessageService; +import org.marketcetera.util.log.SLF4JLoggerProxy; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; + +/* $License$ */ + +/** + * Provides a logout menu action. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Component +public class LogoutMenuItem + implements MenuContent +{ + @PostConstruct + public void start() + { + icon = new Image("images/Logout.png"); + iconView = new ImageView(); + iconView.setImage(icon); + } + private Image icon; + private ImageView iconView; + /* (non-Javadoc) + * @see com.marketcetera.web.view.MenuContent#getMenuCaption() + */ + @Override + public String getMenuCaption() + { + return "Logout"; + } + /* (non-Javadoc) + * @see com.marketcetera.web.view.MenuContent#getWeight() + */ + @Override + public int getWeight() + { + return 10000; + } + /* (non-Javadoc) + * @see com.marketcetera.web.view.MenuContent#getCategory() + */ + @Override + public MenuContent getCategory() + { + return null; + } + /* (non-Javadoc) + * @see com.marketcetera.web.view.MenuContent#getMenuIcon() + */ + @Override + public Node getMenuIcon() + { + // Sign Out icon by Icons8 + return iconView; + } + /* (non-Javadoc) + * @see com.marketcetera.web.view.MenuContent#getCommand() + */ + @Override + public Runnable getCommand() + { + return new Runnable() { + @Override + public void run() + { + SLF4JLoggerProxy.info(LogoutMenuItem.this, + "{} logging out", + SessionUser.getCurrent()); + webMessageService.post(new LogoutEvent()); + } + }; + } + /** + * provides access to web message services + */ + @Autowired + private WebMessageService webMessageService; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/view/MenuContent.java b/photon/photon/src/main/java/org/marketcetera/ui/view/MenuContent.java new file mode 100644 index 0000000000..19c9639654 --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/view/MenuContent.java @@ -0,0 +1,74 @@ +package org.marketcetera.ui.view; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Set; + +import org.springframework.security.core.GrantedAuthority; + +import javafx.scene.Node; + +/* $License$ */ + +/** + * Provides a menu entry. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public interface MenuContent +{ + /** + * Get the caption value. + * + * @return a String value + */ + String getMenuCaption(); + /** + * Get the menu item weight. + * + * @return an int value + */ + int getWeight(); + /** + * Get the menu category value. + * + * @return a MenuContent value + */ + MenuContent getCategory(); + /** + * Get the necessary permissions to display this menu item. + * + *

The current user must have ALL OF the given permissions to display the menu item. An empty set indicates that no permissions are required. + * + * @return a Set<GrantedAuthority> value + */ + default Set getAllPermissions() + { + return Collections.emptySet(); + } + /** + * Get the menu icon value. + * + * @return a Resource value + */ + Node getMenuIcon(); + /** + * Get the command value to execute. + * + * @return a MenuBar.Command value + */ + Runnable getCommand(); + /** + * static comparator used to compare menu items + */ + static Comparator comparator = new Comparator() { + @Override + public int compare(MenuContent inO1, + MenuContent inO2) + { + return Integer.valueOf(inO1.getWeight()).compareTo(inO2.getWeight()); + } + }; +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/view/TileWindowsMenuItem.java b/photon/photon/src/main/java/org/marketcetera/ui/view/TileWindowsMenuItem.java new file mode 100644 index 0000000000..e69daa338c --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/view/TileWindowsMenuItem.java @@ -0,0 +1,68 @@ +package org.marketcetera.ui.view; + +import org.marketcetera.ui.events.TileWindowsEvent; +import org.springframework.stereotype.Component; + +import javafx.scene.Node; + + +/* $License$ */ + +/** + * Provides a menu item to cascade open windows. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +@Component +public class TileWindowsMenuItem + extends AbstractMenuItem +{ + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getMenuCaption() + */ + @Override + public String getMenuCaption() + { + return "Tile Windows"; + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getWeight() + */ + @Override + public int getWeight() + { + return 200; + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getCategory() + */ + @Override + public MenuContent getCategory() + { + return WindowContentCategory.instance; + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getMenuIcon() + */ + @Override + public Node getMenuIcon() + { + return getIcon("images/Tile_Windows.png"); + } + /* (non-Javadoc) + * @see org.marketcetera.web.view.MenuContent#getCommand() + */ + @Override + public Runnable getCommand() + { + return new Runnable() { + @Override + public void run() + { + webMessageService.post(new TileWindowsEvent()); + } + }; + } +} diff --git a/photon/photon/src/main/java/org/marketcetera/ui/view/WindowContentCategory.java b/photon/photon/src/main/java/org/marketcetera/ui/view/WindowContentCategory.java new file mode 100644 index 0000000000..178a6e5fba --- /dev/null +++ b/photon/photon/src/main/java/org/marketcetera/ui/view/WindowContentCategory.java @@ -0,0 +1,61 @@ +package org.marketcetera.ui.view; + +import javafx.scene.Node; + +/* $License$ */ + +/** + * Provides the the menu category for window actions. + * + * @author Colin DuPlantis + * @version $Id$ + * @since $Release$ + */ +public class WindowContentCategory + implements MenuContent +{ + /* (non-Javadoc) + * @see com.marketcetera.web.view.MenuContent#getCaption() + */ + @Override + public String getMenuCaption() + { + return "Window"; + } + /* (non-Javadoc) + * @see com.marketcetera.web.view.MenuContent#getWeight() + */ + @Override + public int getWeight() + { + return 9999; + } + /* (non-Javadoc) + * @see com.marketcetera.web.view.MenuContent#getCategory() + */ + @Override + public MenuContent getCategory() + { + return null; + } + /* (non-Javadoc) + * @see com.marketcetera.web.view.MenuContent#getCommand() + */ + @Override + public Runnable getCommand() + { + return null; + } + /* (non-Javadoc) + * @see com.marketcetera.web.view.MenuContent#getIcon() + */ + @Override + public Node getMenuIcon() + { + return null; + } + /** + * window menu category + */ + public static final WindowContentCategory instance = new WindowContentCategory(); +} diff --git a/photon/photon/src/main/resources/images/Average_Price.png b/photon/photon/src/main/resources/images/Average_Price.png new file mode 100644 index 0000000000000000000000000000000000000000..5c4ff581c222313ad1b2f3e1ebdc0bbfdedef0ef GIT binary patch literal 604 zcmV-i0;BzjP)K~zYIt=7wLO;HpE@Lv^?7-+91sx4tG)X0#;h^E26ASP82LYZP@ z;5CqXH8C_YYKj5kHIoPlsYW~os2FIKn6#G~>~-|?x~H$)m7Kx;_S)Ziowe7lGqbc| zH#)HnYXbj>4;Vl{J{1wv72qv6iM^P_L%a%NVQ3?^1;_Q!|1yiigtue(h9hX0LAMb- z7>P3-vypeBEQQ@T7jHsJxD{vj7b$h(H!fn)JjgStM{gMUFOn81#Qp`6=Zy^DJDT$w z*QA`!CoM%%l4^1#1KKbl6|`1zEI3=?AjZ|k`%D^wvm0*XZiT`MoRgYqTxz5~tSuls zjdyq(XW_n74{vZLr@S05qz0?}#;~)BfhVzF#*)C!V+eklp8pdSgg3&QKY>l4%hWBf zUHZKz@jSeiJQ#!Xz7(w9kla66#(ziG)-t^PGyPYn$m z56+ehxGJ@GsRrat7{#SLxLUfP2BZsXp;9k3?kHAOZgxnG(z`$-^-`hBsy8FjuqaEB z%qLwfBv(lsL8iZdB1Nd+B^Z%3s<(a>*}hWNf%Kf+7XhyEsPjddSR;p&g}G>W9Kik(>634*PNh2Sd$tnvmH zHnw7EY2gdlSZHGrFm?0+(#2veW&_ExYi89H@xU^0=I)%ich0?VjZdTzUmx2lpoTL% z;V~ks)rFh3;l?*np zj1zRrZkAyWmqNBLu>wi9cJVw#U^Vy81m@pG33@`!i?Cy|LHI%(Ss#qb(9H3TzY^#PqvE#efw2Mu zYzgf@-l0c=yM8S&63Ne&ysM_-|GKCQ!)$Fw#O`8PIAXuS67GcYG7Twiz5p{>LL3Wk R9en@*002ovPDHLkV1k=VpfLad literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Close_All_Windows.png b/photon/photon/src/main/resources/images/Close_All_Windows.png new file mode 100644 index 0000000000000000000000000000000000000000..a40834e59a59e6d7feae295ba754d6e054c1408f GIT binary patch literal 551 zcmV+?0@(eDP)T|qh@Xrv$3EgWBm9J$jV0fktB8$Sy(7V z*%)I(k)o98E>N;iqpT*yg4y}8P{@Ip;ag_j{i6 zp659)EXN3%Xq9-zQO6tss<@0^T*yQ^MGy926lbFeO`I-4oX0cVj0_biiXSr+_@41h zLNquR1K5mtZ3@iC61>j(P29n;NMHa*Fodx-1&Zjxeso6y+cNTk8v9r92pkoT4q7iu^%94l8gM8)qVL1q<;F zC$o1;%G~#ujqB(xBhZKSS^Eb1u?YvH<9sQzEyC`ib35N)ZLYlsk8u(Ar7b(0v-cLA z+lgRSq5mt_?(^@qOgc^N2)xL(`!R^EQe<_U%h{h7ovZkY#s3j_l8LOu4m5BIUvM0| zvUwA@6%o{>(4OLvR9OcLb2ga%LON9NIe}IM2XR!Y>2L5mSj0VS!Kc7Bk&&N|R$s+W zoXL&7jV59kOR*hmq_RFL6~>QK_;ge%Nc(@S6Nl&&H3S)10$pfC5!{w8iw}5Ll6--b p>BEt+Ce@yC<$hyYQmq<7&mSjsf78Ilp6mbs002ovPDHLkV1f;n{V@Ol literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Cluster_Data.png b/photon/photon/src/main/resources/images/Cluster_Data.png new file mode 100644 index 0000000000000000000000000000000000000000..8d12add2c5998ab0d8cf31cd0b3b0756e45a95f9 GIT binary patch literal 637 zcmV-@0)qXCP)nDaiYHdxG0?(w;8lENBxJ}x6xMt330*=eH^lXRypZ>d-Izp6 z=HDg3MICXx#HkF|f)ChNTx`V+!G{$LsSVaDAuY>7%6?UwX$?4sX?#@h#8nIPxq`uD z^X&|CAz`j$nEnPKFe?(c6mLVw_w%ZLo|kD1#P62Kq`n_(&TJ&OqO-6$CrwyexJ12_ zH>lRMvk5qqVU8xuNQSvc2xiv8o%tgy4+}Z>23^y5b>a{Khp&rPv{qfV9TU zTX4BBzlSp!t|?w%SLPphB)mwf{h&v9RGkHGTePAp*?$=K>nSJH_!%5m?FS`X&#({T z!6iHvddL*EE_MJR26Gk$>pRvJm@R7i!7NUu%5tey%B;8JU}1hlZLFGQ%6P6H)O4jN z8?ZC(*;I$g7P(Z`3ne;r(Wq+6d?KV`k(pFm5$ehTJQo`J9c;`ny?7(I^r8Az{a^V5 X;uWD(9(I!y00000NkvXXu0mjfqyiRp literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/FIX_Messages.png b/photon/photon/src/main/resources/images/FIX_Messages.png new file mode 100644 index 0000000000000000000000000000000000000000..dbf744be1c6becc58f005e673d30129078b08066 GIT binary patch literal 648 zcmV;30(bq1P)GN*38d++()@9&&* z&pEfCzD&e6Y{g16V`k`v@EQ+r9oI1Y53aw64`2YLoMQl;Xc&upVOUi@_zuU=fmTe# zw3v5Rc4L()@+wAT2A9hs2z z#s0Pi>!$qm&ThtEG|B$!hEu4_N*ii#`MWsSN z$LNo}-uS-6hN^Sr%sgF?D)^yPiS1G$!R`R&kwF~A7b#8eVGULnrBv+3vhb!Hsuh@( zb1aT;QOc^dI4|`zq*m}K-0H!MoR(*$5od$rX)TUsm2`cL^*M=)()~SCNzbNw;OV#( zz|PEm3zlGlRMAVN$LOR=lN^zn-Sn|6DtE*MvEGUw>i41FQma^3i>x~LXGsspGqg!B z+wrjO4yFWd2(P3|yb)gHk|SYNw@RqKVpCp%e@VFpvhMd`(Rh&0#fJ>>8|;(g5zi}a iQWajp7AafCMg9QJk+qjrVJT4n0000^o=x literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/FIX_Sessions.png b/photon/photon/src/main/resources/images/FIX_Sessions.png new file mode 100644 index 0000000000000000000000000000000000000000..25569a5bc201c3d0579d855bbd1cb73327ce5aed GIT binary patch literal 326 zcmV-M0lEH(P)=G`P)i?CFm^yIO7Nh;coRU=vmb))nL4z?;eD82j)t=Hrs*!}wmo9*l!@ivqTo$reTH{-pAYyY5pj-aCe_5{iPn9t%QF5!`S3Ad6# zoGXHj9*|;vH!SaMz^7tw*>f@H0jZ%KlKRkC1Kx!t84unTfo24x(yU@5-UT*} z&A?)a;JUioe_BROK12fzI$DN^R9>&*&_rO*9{fUQfnZpQ=d`%)I4`x~GP%_-?+o3ZNhP={bGRK@jjy=V zfy+1}wSp0;3oi77L;6AJ4w5UROLYPd@kKt)xu+_9Uyj0WQ&FcjXCqMv00000NkvXX Hu0mjf%<>J$ literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Logout.png b/photon/photon/src/main/resources/images/Logout.png new file mode 100644 index 0000000000000000000000000000000000000000..8bac21da3d1b8435119352b7ced758c6e80b714d GIT binary patch literal 442 zcmV;r0Y(0aP)=bU+FhF-_D zLjOLV;Z9jQkKwcpn!R@jDscL@jQ< zj#+%IBH4{mKFmdu;GWAFW-~d%pBT|q2`WiyLYa1^+=>{MS5>=qQLawv-U%9Fxw=I_DA((55}ESgOTFIn($QgcSul%ZOll5twq)W;eEL(J_)z?XX@y~ zUpx}hJ`|pbA>lT++9^&7>&EdU_CI1C`?&~KM!__wSa>?>TZ@4b88wV(xu3;UjZ*12o1yK0!m z4jyZde%vk8w(-2A&mJ0On=p(&g|$-F)Pgk_8s`aa5T(I_GK>8OM9>kV!-sn4g*;EB+_88+~=Rb#3| zJGh?hKO<)FMf_-W3>=r>1|A5hHid)g!iB$Z|F{IRn87&=p$}{LfbTe!O7H?#3bmV< zzD9< z+-cRAN-%~woW>}w35h;o91mMHrVjZn-r=g4!=`Xi9SiLmJISH}VS+qXMLxB}e;L;u zp(xS1&^V974BE&0!n?H;l9V*9j9Nyppu`Oou?MfT&-$us7mWk|v~IChJgRm80000< KMNUMnLSTYl&Cx{w literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Metrics.png b/photon/photon/src/main/resources/images/Metrics.png new file mode 100644 index 0000000000000000000000000000000000000000..8da04a3ca1fa4eee1a21509162bcbb0b357662d9 GIT binary patch literal 485 zcmV z##UoTL){n-ViY^^4R7!ko1r<3N!)50d=I9v7$)WMJc7YKe8h>S=BMzf!gv#(F%sHc z5$wH&Cvqh-`h!Q9!lBsJZVYDnJE6VY#Qb_@{tCzX=r4u#uXIN+u3{&rGxOO}mc1~E zjnF^M_&HoGi%0Pz#4mI&uL>hr$`U#k`~m4$e@a#LA2&m|DwSj%T6mI~uj=l07%Rcw z?flTjT98-!n3FCuuc(>Q`gHK?on+4ju@M>6AwT!dI9k%|#jjYq0QchKWgqi4zLd=8 zE40sI{b7M4K`zvo7r2l8CH{7W{&ma~g|sF3S`*+5PL{Z3JgCqw#oR7*&wZsPK(8Ki zLn_B!_#X36q5JCAYXUr$>O0S{u512|xz)meRL9q(HrxyEq}MtuHNo@vP!nJo$E8=j bjV;DMNe*y}>hlR800000NkvXXu0mjfyLjS< literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Open_Orders.png b/photon/photon/src/main/resources/images/Open_Orders.png new file mode 100644 index 0000000000000000000000000000000000000000..86213c74fafbd16fe861440cbe11a90485494b13 GIT binary patch literal 488 zcmVP)+L9kFzu~Qof7;K`&X<=gu!OF%` z6t%m;6qXh#LQ0VnQUol-QZQl@v=A{7K_jF~A!-PMcd^(#JeGauzDs@>_RZ|f{%>|? z-aZ0eVsl98;Jykp;y8}iSg3Lwfa^H>Kc`K!hFBaH@vLgHk+Ge|BkV3DIDw;)s2}r~ z8p#ASF;=)Ze8x&*8^ulX8SIGTnHnT1TDSqZ47L@y3{o7z0sO);_TpH^uZ#EiQG zmxW3j!6arYd{5zBU4j{W#E+;i;9La3}9-bSfKa0ie5E7N&=&-iulAvKl|EJx{K4RZx?C_Ch~(8cRfzoz+9O-g)o!{_E*}o%8>n=RD_} zOEFgA0={4z&#`A3`%2h^G1Qu@)9sRX<81+Xw))x;*x7=-hMQ%{I-9&(Q^IduI|4sS z_f zNIIt)ZehW6q#beHfcsJpWcZD{xF1zG6t~n;FxQcjH$@ z@{3p${_CYQ_DgAPKyO9zAt|++@vQFKA6&|bgMkX<4=^(#p1=p3z-|mm{ZYefsnU=} za9M#ZSb^^m{gg-pN5ghBd(l^t{9UBxBqlY^tR7s8>z(lFh57hcLf}deJ;V#l!42#U z(l=pu7zg71Sf;)akEBB?5;z~WuW%b%V<(gN95!cTm)Xc~_&LqS%7T?0+HmZ+6Z=9S zPfqIm8V=SAltET?1MxhFQ#ta5(m))~1yu=MQn~&Nk&fKA)p!yC9RDBk49oG@zX>}c vm2H-;Gn)TvDq$A(OG9!DpKu-9@N@Xs5Q8fLInlhRO6hxiioF>eoH}{(Qf``#ZmL z&!s)tk2y4g{>9~X5VBirUgTwk>A#dF+PjPR;v8-C(6&f{xj zkNp>M1YaWitupX)36Q1gr4+%-lHf^PK@BT|*23FPmE^om5p2ZwLg0e{m`hFT=60iA z$oYY4S({Eofn!=HXC(5CM)pd9{J%3+nNBpANfZT{#pWOwnO@`eTvpqJi`a#nvX;FqCJY7UDa|&G;&-CDU$TjT9()e=!wVGTcmz<_deF+Gf`?Jw z-m<{;m@ZUV*Sg_F=_!EchjeW!_=)sJPT`vDe&`O`kL#LkDeBV{!2t_T%>QWV#dA!y zc@2ESQS8IV1jw}PUOpxJg68&n7TLQ2JjRB|Hk=@M6#1)oCA&A;8H{;yAwh7boy2zk iD&7asT;_spQTYc4S@F9CW0X+<0000Xu^-;3PNLJqY|34ppn8vh*1*?2%*;S z2T&SX5@G_>B*fANTNPtytgqxx(1Z%XgpMB;JHm4Ayd;;sAfJiAJ|U?_v($ao$FnSai**@@(4$Zk5uis<5pp|5dda3{s1}VHS@th6Vhn zSXI*LB-qsO3Tq)RL$`#pk{BAn=(xc6KDKYC!1e|GSTC?0W4`E29)_}kdgwM1?AF3( zD7S9BHE15q+~P&_;V6;d5WRSv)BVZ^>`7(AFa{I$q3~&ovIM_n6|}>Vtcs2koZ>^m zz7;+lCxCbOTA=@o<+8F~Yx(oPNA;tw-yGXomV+wadrtv9kB)X_d z1!QAl7Ej`Prz+zrXV4XT<5|ZFCUGxx!7jp!AvVjNQ=zdJM z%3HFDSjYX&HlP$gVgDA`U(BkS%T~J;)ZD<|K?;8XUpU3F>$m&~00000NkvXXu0mjf DVxQMG literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Session_Status_General.png b/photon/photon/src/main/resources/images/Session_Status_General.png new file mode 100644 index 0000000000000000000000000000000000000000..01edd2ac198288a7e52524ec83c94a71b793117c GIT binary patch literal 621 zcmV-z0+RiSP)h!z$RUtofdQY{o8pcoNN ztCd(=X_rbNjZG}9Ed&KM29yvR1rtF;j9P>!zQAWpEOs`TfBxAR4-CuPJA2REd(X}+ z)o4W@eqyp>{Kgqvd3O*ln5mPm!ajtz0Z#)RMvI&dek-_C4iD49RRCPhE~iMAwzMzv*njipHj z`*MMra>fiMuno;&e46EZQv~fe7TL!$YddgOA=BP4VjRZaXx@wda?)m6K{*v|4Wx0C z&jepirlZljQ)p2%Do4vHGouX8iQ)WdEQcPxSJ=m@g!HhVSWUxhj ziB2aOoX-WSQqhxSK7gG#XS&JPlQqNtLy$wi;#+7k{T0s|$MG^6eQHb8py%ks; zOzb1-k=nlFATG$ux(4k@220c)*`F0BXPm*2T%alyl~>)u@EpTN9Kc-M#M|Io6rS&M zfvPV3M#beZjw@!q^iN_PR^vm(x-438&&@#)Y8_` zGj?S{3Nh|8;>gnz4pEPsznzdWD?bx~d(3z(%UcdYB@zd8o|5kGJ ze*>DunB?v5;@EjF#}CLk=jq}YqH(^rFEhM}LBLgBEojTlE%{Lq3j&v^>Z+9f|G)kf zbIZ{pzwejN-1&Y^GPCvh%Fes9zo+J0&3^w!l3~(;-_pyLteN^y)boMo$4xipoh$p* z>>3$tSj%?KQ$*H!@%l#!(kTqv_gWq}Bg>p3uI70=ZJ%1D!KGa{<35Hk9lYhPcj=tw zoum0c=+aUSJ{l{eUh$9C| yxQ-N`c(+X@_Tqu9PokA)S1p|wvPB{Lp>*$qzz?w@p9O$EWAJqKb6Mw<&;$T02g8K` literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Session_Status_Grey.png b/photon/photon/src/main/resources/images/Session_Status_Grey.png new file mode 100644 index 0000000000000000000000000000000000000000..a4e02bf5b308ba5c0b8ab8b1d3704ae99d269c2e GIT binary patch literal 559 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaA9S-mbaRt)<|NmdLYSsSz`?qb| z1|)&(jT<-a-o1O>x^IdVL4LsujLah{gY;{UbA+~wjDcnA3F2&#p`z;K7RW8=if?>{%=6D7?Zr+ zT^Ro}>D~i!&Uv~xhG?Ac?aK^rVi0hZR}0#5b4z|y#Dc(Os=6wr|NpOl#oThV$nX2* zGk3n9lgw;=zOwV~?C+^LSF_*$kz|;3;J5U$C2OWW6!m=I`Ek>YdFRT0HM>Rz8`iR& z^AwS_UcCO1f^-VQ_Pv$|&d4&Sh^u+tPTQweX>e)R&A5*tOb2hd>s>mhx#y42zw_?W zq8B}*rKFxKg@o=fZgAhOrZ;!7flW!9>A}Bs+p9k~#5PF&asM&dJmScK60Re~C*EyS riM@DW>yv2Z*;Pv?hHO#Dekk4hAn-$M$Y%kd&lo&i{an^LB{Ts5CdVT& literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Session_Status_Orange.png b/photon/photon/src/main/resources/images/Session_Status_Orange.png new file mode 100644 index 0000000000000000000000000000000000000000..75553e94c9b8c659f4c4d150a82205d9b038d2d3 GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaA9SiUYaRt)ubdN{0W%4FB^P{ueR)F95TFQYByn6fXsf02x48pdf^VaN%sYVz@Yv0YpG? zhzNuW7lCtuCZH*ROFb9G+ug;n^Inc0kaNz{#W6(Vd~aW7coTzwtGrs!mYZAhqaqdrE>qQ2DgFO{{VV2{ zqeXt-FQ2*d{hVZG>+_YJcV~Z3&AFQW{*NTXqyxXDmn~T{^`WTe1J93}Zp=GZ_N&=7 zGT5+|?VP8Gto7pcj})X+7`E@VJa9&qIYnH}^LE-kwMv6ayKcsP3}HHW%U$o%In6zP zg#Mj(mlnO~87(FCTqz`UhjD}Zb~U}Ziw$f_+Ds4rt=nGx!6CLm@{jwE$>tG94wP^m wDL(OTn@a4(16!X&E6=W4Ix%F6LiR)H-UopnVnaR)0DZ>b>FVdQ&MBb@0C;%(9smFU literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Session_Status_Red.png b/photon/photon/src/main/resources/images/Session_Status_Red.png new file mode 100644 index 0000000000000000000000000000000000000000..52a987c31b4e76ae61dde48234a5354e5f06ba6f GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaA9SiUYaRt)++z;GFiZZR-i1*2N;f(+P|380ptQP1Khms(_UYf-oE}xGgq%!yJg#sox2a6dHUk@yAK~fef{%qB}e}^pjnJb-tI1r zo%eG5fShxlE{-7@=X?7y!t8Xq94+$ue)-Iu z@8={lTc5A&ygU1QYR=W{_kScACLQ=Ky==*vsSiaxA9#M;bYtGRvR}=vk->(wZ09^h zWUUvkf21It!mxd><$*J@%qik(p10HXsZ|x5F7GY0O&IYPgg&ebxsLQ0KAXZvH$=8 literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Session_Status_Yellow.png b/photon/photon/src/main/resources/images/Session_Status_Yellow.png new file mode 100644 index 0000000000000000000000000000000000000000..52f60a5951e5651a177c2f6fef35f61ef3fceecf GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaA9SiUYaRt)<|1n@WCKaKEJO;T z3`jx{L;*61EDl$OmGS@o#2vaCK%Y321o;IsFfy~Sb8zzV3yX-$YHDfg=o!1YdwO~M z_=bdrM?^*?r=%5iboKQ1^-r3)dd=D`+ji{Sedx^77q8!a`1tATpMNVk`o96qVodUO zcX8~zm*WTIobz;X4AD5>+m{*M#30}*uNJiB=9c`Zhy{VmRCQHK|NmeAin--zk>B^r zXYPDICz;v$d}Zg|+22!hu4cdgBgru7z;EegOV&(%DC+sZ^W&x)^Ujt1YIcnbHmqek z=P4pgdE1DldI(}REOwpV{}h;5Mk~NG literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Supervisor_Permissions.png b/photon/photon/src/main/resources/images/Supervisor_Permissions.png new file mode 100644 index 0000000000000000000000000000000000000000..6b812621d6970735258cdacb1b0d3654c6434f53 GIT binary patch literal 555 zcmV+`0@VG9P)|DAKje113o@8|b^{~vyTpxFcW zH7U#V7|3ZU3z^i5q_rCSvyPcZqfM=C?t1CE)z&*{t$G)AJ8oR*Kdp322m{X9VznJ^2;rVN{+fejpBqB2 zMim+yk+sjPQsK!k{FRw%Jxb?avZ@k%jG;0-Z@GoO%WJpMN-bUs*HruwrVqP+H}1&W zoW8?k4~6DG!=z!tR!fEU0$kyYhZ={oV@aJFQIo9+}j3=}=x( z-i_Rka}R8>(0^0b=~MW-`4J@&m0%$fEJU%fQIUuk6-65rvk(ITQ6n)?f?C`=GP}&|4jcX9FwC5D{`0(d z_|I5Y_uh?@7{m;|7Ub#2DGcBP7P5>spo-s7zM)cpyb^5_zbZCm8D35qCGxgFfcF`G z*Ar!1%5WgVc`H`M3S8^&?iJKX^TUdd2jVefEquLqF3jH0+ zAV}m|w3$tPo0OVCDLyOMgBv)CPTWQpUS?&J!fwpr80K*YLwJfeC6qCZUST!wqFw0w zeOzr4;B-uGi_o$|=)rRwfL%G|kK;^(_9Svb_KbdEbs2p*neD_xG|a*UoVIf)5G<3|hRi#S_PmURwb8;;1o0_rh^dw5;Y{@>vbD2mpD(FR{~00000NkvXXu0mjf DDY^5? literal 0 HcmV?d00001 diff --git a/photon/photon/src/main/resources/images/Workspace.png b/photon/photon/src/main/resources/images/Workspace.png new file mode 100644 index 0000000000000000000000000000000000000000..0613e0da9c048b0fa7eba14383b7b200fb2f8aff GIT binary patch literal 456 zcmV;(0XP1MP)1e z(v3U8h1wShsZDKKMWpT}5fg$m15td>=VIpO_44k$FB$)EIrp5IIsZ9x&Y5YTfoHfQ zEq3r8R?xYoZ@GaZe8ODf2xP%OT{5;+xA+`*{IoDKX_jRG0000 + + + + + + + + + + + + + + diff --git a/photon/photon/src/main/resources/org/marketcetera/ui/primary.fxml b/photon/photon/src/main/resources/org/marketcetera/ui/primary.fxml new file mode 100644 index 0000000000..0555f4e6ba --- /dev/null +++ b/photon/photon/src/main/resources/org/marketcetera/ui/primary.fxml @@ -0,0 +1,16 @@ + + + + + + + + + +