From 4b8f3db8c2334ac5c1484871817d9be0d3563d15 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Thu, 20 Nov 2025 19:10:48 -0500 Subject: [PATCH 001/103] removed README file --- README.md | 253 +----------------------------------------------------- 1 file changed, 1 insertion(+), 252 deletions(-) diff --git a/README.md b/README.md index 9487008b..574b7def 100644 --- a/README.md +++ b/README.md @@ -1,252 +1 @@ -# Clean Architecture Team Lab Activity: Login and Logout - -In this team lab activity, your team will: -- explore an existing use case (login) -- add a new use case (logout). - -To earn credit: -- your team must demo your working `logout` use case. - -Your demo should be similar to the below example: - -![](images/sample-logout.gif) - ---- - -## Task 0: Fork this repo on GitHub -**To get started, one team member should fork this repo on GitHub and share it with the team. -All team members should then clone it.** - - -**Suggested logistics:** One of you should invite the others to collaborate on their fork of the original repo on GitHub. You can do this in your repo on GitHub under `Settings -> Collaborators`. This will allow you to push branches to a common repo and then use pull requests to contribute your code and review. To prevent others from pushing directly to the main branch, we recommend that you set branch protection rules on GitHub. Below are how the settings might look if you add branch protection rules: - -![image of branch protection rules for main with a requirement of two approvers to merge in pull requests.](images/branch_protection_rules.png) - ---- - -## Task 1: Understanding the Program - -Open the project in IntelliJ. Open `app.Main` and read it as a team. -- What are the currently implemented Views and Use Cases in the program? -- Which Use Cases are triggered from each View? -- Which version of the DAO is `app.Main` using? - -> Observe that the main method makes use of the `app.AppBuilder` class which -is responsible for constructing our CA engine for each use case of the application. To answer the last two questions above, you will need to look inside the details of the `app.AppBuilder` class. - -**Make sure that each member of your team can successfully run `app/Main.java`.** -- Ensure that you are each able to create a new user and log in using the username and password. - -> Note: you may need to set the Project SDK in the `Project Structure...` menu, and possibly -> also manually link the Maven project if the app won't run when you try to run Main. - -### Task 1.1: Exploring the login use case - -Let's take a tour of the login use case code: - -- In IntelliJ, find the `LoginController` class and open it. - -- Set a breakpoint inside its `execute` method. - -- Run the program in debug mode. - -- On the login page, attempt to log in with an existing account. When you click the button, the breakpoint that you set will be triggered. - -- **Step through the code to trace the execution of the login use case.** - Importantly, pay extra close attention to what the Presenter does to ensure that the LoggedInView gets displayed after the user successfully gets logged into the application. - -The code is designed based on our CA Engine that was introduced in the reading this week. For reference, here is our CA Engine diagram: - -![The Clean Architecture Engine diagram](images/CA-Engine.png) - -> Pay attention to the classes involved and the flow of execution. When your team implements the logout use case next, your code will need to have a very similar structure. - -To better understand how the view gets updated, your team may find it useful to review the [Extra Advice about the Presenters, Views, and ViewModels](#extra-advice-about-the-presenters-views-and-viewmodels) section at the end of this README. - -## Task 2: Implementing the Logout Use Case - -Currently, you'll notice that the "Log Out" button in the `LoggedInView` still doesn't actually log you out of the program. Let's fix this. - -We have created all the classes for your team, but some of the code is missing. **As a team, your task is to fill in the missing code so that the logout use case is functional.** - -> The next part of the readme describes how your team will do this. - -Your team will know when you are done when: - -- Clicking the "Log Out" button takes the user back to the Login View when you use the program. -- On the Login View, the username of the logged-out user is filled in. -- The provided `LogoutInteractorTest` test passes. - -### Task 2.1: Dividing up the work - -There are `TODO` comments left in the files. - -> Recall that you can use the TODO tool window to conveniently pull up a complete list. - -Once all TODOs are complete, the "Log Out" button _should_ work! - -**As a team, split up the TODOs (see below) between the members of your team.** - -> Optionally, your team can make GitHub Issues and assign them to each team member. - -Make sure that each member has at least one TODO that they will be responsible for completing. -If your team prefers to work in pairs, that is fine too. - -The TODOs are summarized below (by file) to help your team decide how to split them up: - ---- - -- `Main.java` (tip: look at how other use cases have been added) - -[ ] TODO: add the logout use case to the app - ---- - -- `LoggedInView.java` (tip: refer to the other views for similar code) - -[ ] TODO: save the logout controller in the instance variable. - -[ ] TODO: execute the logout use case through the Controller - ---- - -- `LogoutController.java` (tip: refer to the other controllers for similar code) - -[ ] TODO: Save the interactor in the instance variable. - -[ ] TODO: run the use case interactor for the logout use case - -> Note: there is no input data necessary for this use case. - ---- - -- `LogoutInteractor.java` (tip: refer to `ChangePasswordInteractor.java` for similar code) - -[ ] TODO: save the DAO and Presenter in the instance variables. - -[ ] TODO: implement the logic of the Logout Use Case - -> Note: there is no input data necessary for this use case. - ---- - -- `LogoutPresenter.java` (tip: refer to `SignupPresenter.java` for similar code) - -[ ] TODO: assign to the three instance variables. - -[ ] TODO: have prepareSuccessView update the LoggedInState - -[ ] TODO: have prepareSuccessView update the LoginState - ---- - -### Task 2.2: Complete your TODOs! -With the work divided up, your team should complete the TODOs through a sequence of PRs. - -1. Make a branch for your work. - -> Make sure that you switch to your new branch! - -2. Complete your assigned TODO and make a pull request on GitHub. In your pull request, - briefly describe what your TODO was and how you implemented it. If you aren't sure - about part of it, include this in your pull request so that everyone knows what to look - for when reviewing — or you can of course discuss with your team before making your - pull request. - -3. Review all pull requests to ensure each TODO is correctly implemented. - -4. Once all TODOs are completed, your team should debug as needed to ensure the - correctness of the code. Setting a breakpoint where the logout use case - interactor starts its work will likely be a great place to start when debugging. - -And that's it; your team should now have a working logout use case! - -**Demo your working code to your TA to earn credit.** - ---- - -# Extra Advice about the Presenters, Views, and ViewModels - -One of the trickiest parts of the code will be the flow of information between these pieces of the program. Below briefly explains how these pieces fit together and work in the context of the login use case. - -## ViewModels and States - -In the design of this program, `ViewModel` is written using generics to allow -for different "state" objects to be stored. For a `LoginViewModel`, the state is an instance of class `LoginState`. Each state object will just contain a basic constructor, getters, and setters to store the data of the view model. - -## A View and its ViewModel - -In the constructor of `LoginView`, the following line of code connects this instance of `LoginView` to its associated `LoginViewModel`: - -```java -this.loginViewModel.addPropertyChangeListener(this); -``` - -This should remind you of the code we write when adding an action listener to a button. The code is following the same structure. - -> We'll talk more about this "pattern" of _events_ and _listeners_ in our next module. - -When the presenter updates the view model later, an event will be triggered — resulting in the view's `propertyChange` method getting called, with a `PropertyChangeEvent` object being passed through as the argument to the call. - -For example, the `LoginView.propertyChange` method looks like: - -```java -public void propertyChange(PropertyChangeEvent evt) { - final LoginState state = (LoginState) evt.getNewValue(); - setFields(state); - usernameErrorField.setText(state.getLoginError()); - } -``` - -The `LoginView` gets the `LoginState` object stored in the `LoginViewModel` and updates itself with that information. - -## A Presenter and its ViewModel(s) - -A presenter may have one or more view models associated with it. For example, the login use case's presenter has a reference to a `LoginViewModel` and a `LoggedInViewModel`, since it will need to update both view models. Additionally, our implementation makes use of a `ViewManager` and `ViewManagerModel` to keep track of which view -the user should currently see. - -Let's take a look at the `LoginPresenter.prepareSuccessView` method as an example: - -```java -public void prepareSuccessView(LoginOutputData response) { - // On success, update the loggedInViewModel's state - final LoggedInState loggedInState = loggedInViewModel.getState(); - loggedInState.setUsername(response.getUsername()); - this.loggedInViewModel.firePropertyChanged(); - - // and clear everything from the LoginViewModel's state - this.loginViewModel.setState(new LoginState()); - this.loginViewModel.firePropertyChanged(); - - // switch to the logged in view - this.viewManagerModel.setState(loggedInViewModel.getViewName()); - this.viewManagerModel.firePropertyChanged(); -} -``` - -The first part of the code updates the view model for the logged-in view so that the newly logged-in username will be displayed. Once the state is updated, the `firePropertyChanged` method is called, which is what will trigger the call to the view's `propertyChange` method which will update the view based on the updated view model. - -This can be visualized as a sequence diagram as follows: - -> Note: this diagram has been simplified to focus on the high-level flow of information; the actual stack trace includes some additional intermediate calls which you can see if you step through the code in the debugger or manually click through the code. - -![sequence diagram of the LoginPresenter code](images/login_presenter_sequence_diagram.png) - -We then do the same, but for the login view model whose state we want to clear. - -Lastly, we update the state of the `viewManagerModel`, and alert the viewManager that it should switch to displaying the logged-in view. - -> Setting a breakpoint in the code and stepping through can help you see how the information flows through the system. Pay attention to the contents of the call stack to help you track where you are in the execution of the use case. - -## The ViewManager - -This class may stand out as a bit unclear about how it fits into our architecture, as it isn't in the CA Engine diagram at all. Remember that the CA Engine is representing a single use case in our program. Once our program has _multiple_ use cases, we naturally need some kind of additional code to connect them together. As we have seen, one use case can lead to a change in which view is presented to the user. To facilitate this, our implementation used a `ViewManager` and associated `ViewManagerModel` to take care of this switching for us. The state of a `ViewManagerModel` object is simply a string that indicates the name of the currently visible view (`JPanel` in this implementation). The `ViewManager` uses a `CardLayout` to conveniently display only the currently active view at a given time. - -When the `ViewManager` is alerted of a change to its associated `ViewManagerModel`, its `propertyChange` method is executed: - -```java -public void propertyChange(PropertyChangeEvent evt) { - if (evt.getPropertyName().equals("state")) { - final String viewModelName = (String) evt.getNewValue(); - cardLayout.show(views, viewModelName); - } - } -``` - -This code will update the application to display the view corresponding to the `viewModelName` string. - -> In the `AppBuilder` code, you can see how the views are originally added to the `cardLayout`. - -> Thought Question: Can you think of any alternatives to our `ViewManager` implementation for managing multiple views? - ---- +# Stock Project \ No newline at end of file From f7eeeac0dd2db516698178886f3caa6a080c89d6 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Fri, 21 Nov 2025 20:54:56 -0500 Subject: [PATCH 002/103] Added Filter Search Files --- .../filter_search/FilterSearchController.java | 25 ++++++++++++++++ .../filter_search/FilterSearchPresenter.java | 25 ++++++++++++++++ .../filter_search/FilterSearchState.java | 4 +++ .../filter_search/FilterSearchViewModel.java | 4 +++ .../FilterSearchInputBoundary.java | 15 ++++++++++ .../filter_search/FilterSearchInputData.java | 29 +++++++++++++++++++ .../filter_search/FilterSearchInteractor.java | 26 +++++++++++++++++ .../FilterSearchOutputBoundary.java | 15 ++++++++++ .../filter_search/FilterSearchOutputData.java | 8 +++++ 9 files changed, 151 insertions(+) create mode 100644 src/main/java/interface_adapter/filter_search/FilterSearchController.java create mode 100644 src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java create mode 100644 src/main/java/interface_adapter/filter_search/FilterSearchState.java create mode 100644 src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java create mode 100644 src/main/java/use_case/filter_search/FilterSearchInputBoundary.java create mode 100644 src/main/java/use_case/filter_search/FilterSearchInputData.java create mode 100644 src/main/java/use_case/filter_search/FilterSearchInteractor.java create mode 100644 src/main/java/use_case/filter_search/FilterSearchOutputBoundary.java create mode 100644 src/main/java/use_case/filter_search/FilterSearchOutputData.java diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchController.java b/src/main/java/interface_adapter/filter_search/FilterSearchController.java new file mode 100644 index 00000000..76693514 --- /dev/null +++ b/src/main/java/interface_adapter/filter_search/FilterSearchController.java @@ -0,0 +1,25 @@ +package interface_adapter.filter_search; + +import use_case.filter_search.FilterSearchInputBoundary; +import use_case.filter_search.FilterSearchInputData; + +public class FilterSearchController { + private final FilterSearchInputBoundary filterSearchInputBoundary; + + public FilterSearchController(FilterSearchInputBoundary filterSearchInputBoundary) { + this.filterSearchInputBoundary = filterSearchInputBoundary; + } + + /** + * Executes the Change Password Use Case. + * @param exchange the exchange + * @param mic the mic + * @param securityType the security type + * @param currency the currency + */ + public void execute(String exchange, String mic, String securityType, String currency) { + final FilterSearchInputData filterSearchInputData = new FilterSearchInputData(exchange, mic, securityType, currency); + + filterSearchInputBoundary.execute(filterSearchInputData); + } +} diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java b/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java new file mode 100644 index 00000000..45073b9a --- /dev/null +++ b/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java @@ -0,0 +1,25 @@ +package interface_adapter.filter_search; + +import interface_adapter.ViewManagerModel; +import use_case.filter_search.FilterSearchOutputBoundary; +import use_case.filter_search.FilterSearchOutputData; + +/** + * The Presenter for the Filter Search Use Case. + */ + +public class FilterSearchPresenter implements FilterSearchOutputBoundary{ + private final FilterSearchViewModel filterSearchViewModel; + private final ViewManagerModel viewManagerModel; + + public FilterSearchPresenter(FilterSearchViewModel filterSearchViewModel, ViewManagerModel viewManagerModel) { + this.viewManagerModel = viewManagerModel; + this.filterSearchViewModel = filterSearchViewModel; + } + + @Override + public void prepareSuccessView(FilterSearchOutputData filterSearchOutputData) { + filterSearchViewModel.getState(); + + } +} diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchState.java b/src/main/java/interface_adapter/filter_search/FilterSearchState.java new file mode 100644 index 00000000..4c972ac4 --- /dev/null +++ b/src/main/java/interface_adapter/filter_search/FilterSearchState.java @@ -0,0 +1,4 @@ +package interface_adapter.filter_search; + +public class FilterSearchState { +} diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java b/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java new file mode 100644 index 00000000..38490db5 --- /dev/null +++ b/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java @@ -0,0 +1,4 @@ +package interface_adapter.filter_search; + +public class FilterSearchViewModel { +} diff --git a/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java b/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java new file mode 100644 index 00000000..1016ab44 --- /dev/null +++ b/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java @@ -0,0 +1,15 @@ +package use_case.filter_search; + +import use_case.login.LoginInputData; + +/** + * Input Boundary for the Filter Search Use Case. + */ + +public interface FilterSearchInputBoundary { + /** + * Executes the filter search use case. + * @param filterSearchInputData the input data + */ + void execute(FilterSearchInputData filterSearchInputData); +} diff --git a/src/main/java/use_case/filter_search/FilterSearchInputData.java b/src/main/java/use_case/filter_search/FilterSearchInputData.java new file mode 100644 index 00000000..dacea92b --- /dev/null +++ b/src/main/java/use_case/filter_search/FilterSearchInputData.java @@ -0,0 +1,29 @@ +package use_case.filter_search; + +/** + * The Input Data for the Filter Search Use Case. + */ + +public class FilterSearchInputData { + + private final String exchange; + private final String mic; + private final String securityType; + private final String currency; + + public FilterSearchInputData(String exchange, String mic, String securityType, String currency) { + this.exchange = exchange; + this.mic = mic; + this.securityType = securityType; + this.currency = currency; + } + + String getExchange() { return this.exchange; } + + String getMic() { return this.mic; } + + String getSecurityType() { return this.securityType; } + + String getCurrency() { return this.currency; } + +} diff --git a/src/main/java/use_case/filter_search/FilterSearchInteractor.java b/src/main/java/use_case/filter_search/FilterSearchInteractor.java new file mode 100644 index 00000000..cde81075 --- /dev/null +++ b/src/main/java/use_case/filter_search/FilterSearchInteractor.java @@ -0,0 +1,26 @@ +package use_case.filter_search; + +import use_case.login.LoginInputBoundary; + +/** + * The Filter Search Interactor. + */ + +public class FilterSearchInteractor implements FilterSearchInputBoundary { + private final FilterSearchOutputBoundary filterSearchPresenter; + + public FilterSearchInteractor(FilterSearchOutputBoundary filterSearchOutputBoundary) { + this.filterSearchPresenter = filterSearchOutputBoundary; + } + + @Override + public void execute(FilterSearchInputData filterSearchInputData) { + final String exchange = filterSearchInputData.getExchange(); + final String mic = filterSearchInputData.getMic(); + final String securityType = filterSearchInputData.getSecurityType(); + final String currency = filterSearchInputData.getCurrency(); + + final FilterSearchOutputData filterSearchOutputData = new FilterSearchOutputData(); + filterSearchPresenter.prepareSuccessView(filterSearchOutputData); + } +} diff --git a/src/main/java/use_case/filter_search/FilterSearchOutputBoundary.java b/src/main/java/use_case/filter_search/FilterSearchOutputBoundary.java new file mode 100644 index 00000000..ab9b2fc2 --- /dev/null +++ b/src/main/java/use_case/filter_search/FilterSearchOutputBoundary.java @@ -0,0 +1,15 @@ +package use_case.filter_search; + +import use_case.filter_search.FilterSearchOutputData; + +/** + * The output boundary for the Filter Search Use Case. + */ + +public interface FilterSearchOutputBoundary { + /** + * Prepares the success view for the Filter Search Use Case. + * @param outputData the output data + */ + void prepareSuccessView(FilterSearchOutputData outputData); +} diff --git a/src/main/java/use_case/filter_search/FilterSearchOutputData.java b/src/main/java/use_case/filter_search/FilterSearchOutputData.java new file mode 100644 index 00000000..21d25f52 --- /dev/null +++ b/src/main/java/use_case/filter_search/FilterSearchOutputData.java @@ -0,0 +1,8 @@ +package use_case.filter_search; + +/** + * Output Data for the Filter Search Use Case. + */ + +public class FilterSearchOutputData { +} From 9edd16b6c614c9f7d451b228bbcdd773f91fa3bc Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sun, 23 Nov 2025 20:31:25 -0500 Subject: [PATCH 003/103] Added more Filter Search Files --- .../filter_search/FilterSearchPresenter.java | 6 +- .../filter_search/FilterSearchViewModel.java | 13 +- src/main/java/view/FilterSearchView.java | 165 ++++++++++++++++++ 3 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 src/main/java/view/FilterSearchView.java diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java b/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java index 45073b9a..aa07a160 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java @@ -19,7 +19,11 @@ public FilterSearchPresenter(FilterSearchViewModel filterSearchViewModel, ViewMa @Override public void prepareSuccessView(FilterSearchOutputData filterSearchOutputData) { - filterSearchViewModel.getState(); + this.filterSearchViewModel.firePropertyChange(); + filterSearchViewModel.setState(new FilterSearchState()); + + this.viewManagerModel.setState(filterSearchViewModel.getViewName()); + this.viewManagerModel.firePropertyChange(); } } diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java b/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java index 38490db5..d655ed6e 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java @@ -1,4 +1,15 @@ package interface_adapter.filter_search; -public class FilterSearchViewModel { +import interface_adapter.ViewModel; + +/** + * The View Model for the Filter Search View. + */ + +public class FilterSearchViewModel extends ViewModel { + + public FilterSearchViewModel() { + super("Filter Search"); + setState(new FilterSearchState()); + } } diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java new file mode 100644 index 00000000..f2e34632 --- /dev/null +++ b/src/main/java/view/FilterSearchView.java @@ -0,0 +1,165 @@ +package view; + +import interface_adapter.filter_search.FilterSearchController; +import interface_adapter.filter_search.FilterSearchState; +import interface_adapter.filter_search.FilterSearchViewModel; +import interface_adapter.logout.LogoutController; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +/** + * The View for when the user is in the Filter Search Page. + */ + +public class FilterSearchView extends JPanel implements ActionListener, PropertyChangeListener { + private final String viewName = "Filter Search"; + private final FilterSearchViewModel filterSearchViewModel; + private final FilterSearchController filterSearchController = null; + + private final JLabel exchange; + private final JLabel mic; + private final JLabel securityType; + private final JLabel currency; + private final Button search; + + private final String[] exchangeOptions = {"AD" , + "AS" , "AT" , "AX" , "BA" , "BC" , "BD" , "BE" , "BH" , "BK" , "BO" , "BR" , + "CA" , "CN" , "CO" , "CR" , "CS" , "DB" , "DE" , "DU" , "F" , "HE" , "HK" , + "HM" , "IC" , "IR" , "IS" , "JK" , "JO" , "KL" , "KQ" , "KS" , "KW" , "L" , + "LS" , "MC" , "ME" , "MI" , "MT" , "MU" , "MX" , "NE" , "NL" , "NS" , "NZ" , + "OL" , "OM" , "PA" , "PM" , "PR" , "QA" , "RO" , "RG" , "SA" , "SG" , "SI" , + "SN" , "SR" , "SS" , "ST" , "SW" , "SZ" , "T" , "TA" , "TL" , "TO" , "TW" , + "TWO" , "US" , "V" , "VI" , "VN" , "VS" , "WA" , "HA" , "SX" , "TG" , "SC"}; + private final String[] micOptions = {"XADS", "XAMS", "ASEX", "XASX", "XBUE", "XBOG", "XBUD", "XBER", "XBAH", "XBKK", + "XBOM", "XBRU", "XCAI", "XCNQ", "XCSE", "BVCA", "XCAS", "XDFM", "XETR", "XDUS", "XFRA", "XHEL", "XHKG", + "XHAM", "XICE", "XDUB", "XIST", "XIDX", "XJSE", "XKLS", "XKOS", "XKRX", "XKUW", "XLON", "XLIS", "XMAD", + "MISX", "XMIL", "XMAL", "XMUN", "XMEX", "NEOE", "XNSA", "XNSE", "XNZE", "XOSL", "XMUS", "XPAR", "XPHS", + "XPRA", "DSMD", "XBSE", "XRIS", "BVMF", "XSTU", "XSES", "XSGO", "XSAU", "XSHG", "XSTO", "WSWX", "XSHE", + "WJPX", "XTAE", "XTAL", "XTSE", "XTAI", "FNSE", "ROCO", "XTSX", "XWBO", "HSTC", "XLIT", "XWAR", "XHAN", + "XNYS", "XGAT", "XASE", "BATS", "ARCX", "XNMS", "XNCM", "XNGS", "IEXG", "XNAS", "OTCM", "OOTC", "XSTC", + "XHNX", "FNIS", "FNDK", "NFI"}; + private final String[] securityOptions = {"ABS Auto", "ABS Card", "ABS Home", "ABS Other", "ACCEPT BANCARIA", + "ADJ CONV. TO FIXED", "ADJ CONV. TO FIXED, OID", "ADJUSTABLE", "ADJUSTABLE, OID", "ADR", "Agncy ABS Home", + "Agncy ABS Other", "Agncy CMBS", "Agncy CMO FLT", "Agncy CMO INV", "Agncy CMO IO", "Agncy CMO Other", + "Agncy CMO PO", "Agncy CMO Z", "Asset-Based", "ASSET-BASED BRIDGE", "ASSET-BASED BRIDGE REV", + "ASSET-BASED BRIDGE TERM", "ASSET-BASED DELAY-DRAW TERM", "ASSET-BASED DIP", "ASSET-BASED DIP DELAY-DRAW", + "ASSET-BASED DIP REV", "ASSET-BASED DIP TERM", "ASSET-BASED LOC", "ASSET-BASED PIK REV", + "ASSET-BASED PIK TERM", "ASSET-BASED REV", "ASSET-BASED TERM", "AUSTRALIAN", "AUSTRALIAN CD", + "AUSTRALIAN CP", "Austrian Crt", "BANK ACCEPT BILL", "BANK BILL", "BANK NOTE", "BANKERS ACCEPT", + "BANKERS ACCEPTANCE", "BASIS SWAP", "BASIS TRADE ON CLOSE", "Basket WRT", "BDR", "BEARER DEP NOTE", + "Belgium Cert", "BELGIUM CP", "BILL OF EXCHANGE", "BILLET A ORDRE", "Bond", "BRAZIL GENERIC", + "BRAZILIAN CDI", "BRIDGE", "BRIDGE DELAY-DRAW", "BRIDGE DELAY-DRAW TERM", "BRIDGE DIP TERM", + "BRIDGE GUARANTEE FAC", "BRIDGE ISLAMIC", "BRIDGE ISLAMIC TERM", "BRIDGE PIK", "BRIDGE PIK REV", + "BRIDGE PIK TERM", "BRIDGE REV", "BRIDGE REV GUARANTEE FAC", "BRIDGE STANDBY TERM", "BRIDGE TERM", + "BRIDGE TERM GUARANTEE FAC", "BRIDGE TERM VAT-TRNCH", "BRIDGE VAT-TRNCH", "BULLDOG", "BUTTERFLY SWAP", + "CAD INT BEAR CP", "CALC_INSTRUMENT", "Calendar Spread Option", "CALL LOANS", "CALLABLE CP", "CANADIAN", + "Canadian", "CANADIAN CD", "CANADIAN CP", "Canadian DR", "CAPS & FLOORS", "Car Forward", "CASH", + "CASH FLOW", "CASH FLOW, OID", "CASH RATE", "CBLO", "CD", "CDI", "CDR", "CEDEAR", "CF", "CHILEAN CD", + "CHILEAN DN", "Closed-End Fund", "CMBS", "Cmdt Fut WRT", "Cmdt Idx WRT", "COLLAT CALL NOTE", "COLOMBIAN CD", + "COMMERCIAL NOTE", "COMMERCIAL PAPER", "Commodity Index", "Common Stock", "CONTRACT FOR DIFFERENCE", + "CONTRACT FRA", "Conv Bond", "Conv Prfd", "Corp Bnd WRT", "Cover Pool", "CP-LIKE EXT NOTE", "CPI LINKED", + "CROSS", "Crypto", "Currency future.", "Currency option.", "Currency spot.", "Currency WRT", "CURVE_ROLL", + "DELAY-DRAW", "DELAY-DRAW ISLAMIC", "DELAY-DRAW ISLAMIC LOC", "DELAY-DRAW ISLAMIC TERM", "DELAY-DRAW LOC", + "DELAY-DRAW PIK TERM", "DELAY-DRAW STANDBY TERM", "DELAY-DRAW TERM", "DELAY-DRAW TERM GUARANTEE F", + "DELAY-DRAW TERM VAT-TRNCH", "DEPOSIT", "DEPOSIT NOTE", "DIM SUM BRIDGE TERM", "DIM SUM DELAY-DRAW TERM", + "DIM SUM REV", "DIM SUM TERM", "DIP", "DIP DELAY-DRAW ISLAMIC TERM", "DIP DELAY-DRAW PIK TERM", + "DIP DELAY-DRAW TERM", "DIP LOC", "DIP PIK TERM", "DIP REV", "DIP STANDBY LOC", "DIP SYNTH LOC", + "DIP TERM", "DISCOUNT FIXBIS", "DISCOUNT NOTES", "DIVIDEND NEUTRAL STOCK FUTURE", "DOMESTC TIME DEP", + "DOMESTIC", "DOMESTIC MTN", "Dutch Cert", "DUTCH CP", "EDR", "Equity Index", "Equity Option", "Equity WRT", + "ETP", "EURO CD", "EURO CP", "EURO MTN", "EURO NON-DOLLAR", "EURO STRUCTRD LN", "EURO TIME DEPST", + "EURO-DOLLAR", "EURO-ZONE", "EXTEND COMM NOTE", "EXTEND. NOTE MTN", "FDIC", "FED FUNDS", "FIDC", + "Financial commodity future.", "Financial commodity generic.", "Financial commodity option.", + "Financial commodity spot.", "Financial index future.", "Financial index generic.", + "Financial index option.", "FINNISH CD", "FINNISH CP", "FIXED", "Fixed Income Index", "FIXED, OID", + "FIXING RATE", "FLOATING", "FLOATING CP", "FLOATING, OID", "FNMA FHAVA", "Foreign Sh.", "FORWARD", + "FORWARD CROSS", "FORWARD CURVE", "FRA", "FRENCH CD", "French Cert", "FRENCH CP", "Fund of Funds", + "Futures Monthly Ticker", "FWD SWAP", "FX Curve", "FX DISCOUNT NOTE", "GDR", "Generic currency future.", + "Generic index future.", "German Cert", "GERMAN CP", "GLOBAL", "GUARANTEE FAC", "HB", "HDR", "HONG KONG CD", + "I.R. Fut WRT", "I.R. Swp WRT", "IDR", "IMM FORWARD", "IMM SWAP", "Index", "Index Option", "Index WRT", + "INDIAN CD", "INDIAN CP", "INDONESIAN CP", "Indx Fut WRT", "INFLATION SWAP", "INT BEAR FIXBIS", + "Int. Rt. WRT", "INTER. APPRECIATION", "INTER. APPRECIATION, OID", "ISLAMIC", "ISLAMIC BA", "ISLAMIC CP", + "ISLAMIC GUARANTEE FAC", "ISLAMIC LOC", "ISLAMIC REV", "ISLAMIC STANDBY", "ISLAMIC STANDBY REV", + "ISLAMIC STANDBY TERM", "ISLAMIC TERM", "ISLAMIC TERM GUARANTEE FAC", "ISLAMIC TERM VAT-TRNCH", "JUMBO CD", + "KOREAN CD", "KOREAN CP", "LEBANESE CP", "LIQUIDITY NOTE", "LOC", "LOC GUARANTEE FAC", "LOC TERM", + "Ltd Part", "MALAYSIAN CP", "Managed Account", "MARGIN TERM DEP", "MASTER NOTES", "MBS 10yr", "MBS 15yr", + "MBS 20yr", "MBS 30yr", "MBS 35yr", "MBS 40yr", "MBS 50yr", "MBS 5yr", "MBS 7yr", "MBS ARM", "MBS balloon", + "MBS Other", "MED TERM NOTE", "MEDIUM TERM CD", "MEDIUM TERM ECD", "MEXICAN CP", "MEXICAN PAGARE", "Misc.", + "MLP", "MONETARY BILLS", "MONEY MARKET CALL", "MUNI CP", "MUNI INT BEAR CP", "MUNI SWAP", "MURABAHA", + "Mutual Fund", "MV", "MX CERT BURSATIL", "NDF SWAP", "NEG EURO CP", "NEG INST DEPOSIT", "NEGOTIABLE CD", + "NEW ZEALAND CD", "NEW ZEALAND CP", "NON-DELIVERABLE FORWARD", "NON-DELIVERABLE IRS SWAP", "NVDR", + "NY Reg Shrs", "OID", "ONSHORE FORWARD", "ONSHORE SWAP", "Open-End Fund", "OPTION", + "Option on Equity Future", "OPTION VOLATILITY", "OTHER", "OVER/NIGHT", "OVERDRAFT", + "OVERNIGHT INDEXED SWAP", "PANAMANIAN CP", "Participate Cert", "PHILIPPINE CP", + "Physical commodity forward.", "Physical commodity future.", "Physical commodity generic.", + "Physical commodity option.", "Physical commodity spot.", "Physical index future.", + "Physical index option.", "PIK", "PIK LOC", "PIK REV", "PIK SYNTH LOC", "PIK TERM", "PLAZOS FIJOS", + "PORTUGUESE CP", "Preference", "Preferred", "PRES", "Prfd WRT", "PRIV PLACEMENT", "PRIVATE", "Private Comp", + "Private-equity backed", "PROMISSORY NOTE", "PROV T-BILL", "Prvt CMBS", "Prvt CMO FLT", "Prvt CMO INV", + "Prvt CMO IO", "Prvt CMO Other", "Prvt CMO PO", "Prvt CMO Z", "PUBLIC", "Pvt Eqty Fund", "RDC", "Receipt", + "REIT", "REPO", "RESERVE-BASED DIP REV", "RESERVE-BASED LOC", "RESERVE-BASED REV", "RESERVE-BASED TERM", + "RESTRUCTURD DEBT", "RETAIL CD", "RETURN IDX", "REV", "REV GUARANTEE FAC", "REV VAT-TRNCH", "Revolver", + "Right", "Royalty Trst", "S.TERM LOAN NOTE", "SAMURAI", "Savings Plan", "Savings Share", "SBA Pool", "SDR", + "Sec Lending", "SHOGUN", "SHORT TERM BN", "SHORT TERM DN", "SINGAPORE CP", "Singapore DR", + "SINGLE STOCK DIVIDEND FUTURE", "SINGLE STOCK FORWARD", "SINGLE STOCK FUTURE", "SINGLE STOCK FUTURE SPREAD", + "SN", "SPANISH CP", "SPECIAL LMMK PGM", "SPOT", "Spot index.", "STANDBY", "STANDBY LOC", + "STANDBY LOC GUARANTEE FAC", "STANDBY REV", "STANDBY TERM", "Stapled Security", "STERLING CD", + "STERLING CP", "Strategy Trade.", "SWAP" , "SWAP SPREAD" , "SWAPTION VOLATILITY" , "SWEDISH CP" , + "SWINGLINE" , "Swiss Cert" , "SYNTH LOC" , "SYNTH REV" , "SYNTH TERM" , "Synthetic Term" , "TAIWAN CP" , + "TAIWAN CP GUAR" , "TAIWAN NEGO CD" , "TAIWAN TIME DEPO" , "TAX CREDIT" , "TAX CREDIT, OID" , "TDR" , + "TERM" , "TERM DEPOSITS" , "TERM GUARANTEE FAC" , "TERM REV" , "TERM VAT-TRNCH" , "TEST" , "THAILAND CP" , + "TLTRO TERM" , "Tracking Stk" , "TREASURY BILL" , "U.S. CD" , "U.S. CP" , "U.S. INT BEAR CP" , "UIT" , + "UK GILT STOCK" , "UMBS MBS Other" , "Unit" , "Unit Inv Tst" , "UNITRANCHE" , "UNITRANCHE ASSET-BASED REV", + "UNITRANCHE DELAY-DRAW PIK T" , "UNITRANCHE DELAY-DRAW TERM" , "UNITRANCHE PIK REV" , "UNITRANCHE PIK TERM", + "UNITRANCHE REV" , "UNITRANCHE TERM" , "US DOMESTIC" , "US GOVERNMENT" , "US NON-DOLLAR" , + "VAR RATE DEM OBL" , "VAT-TRNCH" , "VENEZUELAN CP" , "VIETNAMESE CD" , "VOLATILITY DERIVATIVE" , "Warrant", + "YANKEE" , "YANKEE CD" , "YEN CD" , "YEN CP" , "Yield Curve" , "ZERO COUPON" , "ZERO COUPON, OID"}; + private final String[] currencyOptions = {"ADP", "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "ATS", "AUD", + "AUd", "AWG", "AZM", "AZN", "BAM", "BBD", "BDT", "BEF", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", + "BRl", "BSD", "BTN", "BWP", "BWp", "BYN", "BYR", "BYS", "BZD", "CAD", "CAd", "CDF", "CER", "CHF", "CHf", + "CLF", "CLP", "CNH", "CNT", "CNY", "COP", "COU", "CRC", "CRS", "CUP", "CVE", "CYP", "CZK", "DEM", "DJF", + "DKK", "DOP", "DZD", "ECS", "EEK", "EES", "EGD", "EGP", "ERN", "ESP", "ETB", "EUA", "EUR", "EUr", "FIM", + "FJD", "FKP", "FRF", "GBP", "GBp", "GEL", "GHC", "GHS", "GIP", "GLD", "GMD", "GNF", "GRD", "GTQ", "GWP", + "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "IEP", "ILS", "ILs", "INR", "IQD", "IRR", "ISK", "ITL", + "JEP", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KWd", "KYD", "KZT", "LAK", + "LBP", "LKR", "LRD", "LSL", "LTL", "LUF", "LVL", "LYD", "MAD", "MDL", "MGA", "MGF", "MKD", "MLF", "MMK", + "MNT", "MOP", "MRO", "MRU", "MTL", "MULTI", "MUR", "MVR", "MWK", "MWk", "MXN", "MYR", "MYr", "MZM", "MZN", + "NAD", "NAd", "NGN", "NIC", "NID", "NIO", "NLG", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", + "PKR", "PLD", "PLN", "PLT", "PTE", "PYG", "QAR", "ROL", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", + "SDD", "SDG", "SDP", "SDR", "SEK", "SGD", "SGd", "SHP", "SIT", "SKK", "SLE", "SLL", "SLV", "SOS", "SPL", + "SRD", "SRG", "SSP", "STD", "STN", "SVC", "SYP", "SZL", "SZl", "THB", "THO", "TJS", "TMM", "TMT", "TND", + "TOP", "TPE", "TRL", "TRY", "TTD", "TVD", "TWD", "TZS", "UAH", "UDI", "UGX", "US", "USD", "USd", "UVR", + "UYI", "UYU", "UYW", "UZS", "VEB", "VEE", "VEF", "VES", "VND", "VUV", "WST", "X0S", "X1S", "X2S", "X3S", + "X4S", "X5S", "X6S", "X7S", "X8S", "X9S", "XAD", "XAF", "XAG", "XAL", "XAO", "XAS", "XAU", "XAV", "XBA", + "XBI", "XBN", "XBS", "XBT", "XBW", "XCD", "XCG", "XCR", "XCS", "XCU", "XDG", "XDH", "XDI", "XDO", "XDR", + "XDT", "XEG", "XEN", "XEO", "XET", "XEU", "XFI", "XFL", "XFM", "XFT", "XGZ", "XHB", "XIC", "XIN", "XIO", + "XLC", "XLI", "XLM", "XLU", "XMA", "XMK", "XMN", "XMR", "XNI", "XOF", "XPB", "XPD", "XPF", "XPT", "XRA", + "XRH", "XRI", "XRP", "XRU", "XSA", "XSN", "XSO", "XST", "XSU", "XSZ", "XTH", "XTK", "XTR", "XUC", "XUN", + "XUT", "XVC", "XVV", "XXT", "XZC", "XZI", "YER", "ZAR", "ZAr", "ZMK", "ZMW", "ZWD", "ZWd", "ZWF", "ZWG", + "ZWg", "ZWL", "ZWN", "ZWR"}; + + + + public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { + this.filterSearchViewModel = filterSearchViewModel; + this.filterSearchViewModel.addPropertyChangeListener(this); + + final JLabel title = new JLabel("Filter Search"); + title.setAlignmentX(Component.CENTER_ALIGNMENT); + + final JComboBox exchangeDrop = new JComboBox(exchangeOptions); + this.add(exchangeDrop); + } + + + + + + +} From 8211c6f9258a50932f225548d5f8d05816a23a33 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Mon, 24 Nov 2025 16:15:20 -0500 Subject: [PATCH 004/103] Added more Filter Search Files + AppBuilder functions --- src/main/java/app/AppBuilder.java | 31 ++++++++-- .../filter_search/FilterSearchPresenter.java | 1 + src/main/java/view/FilterSearchView.java | 57 +++++++++++++++++-- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 3c654cad..84cfdba7 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -3,6 +3,9 @@ import data_access.FileUserDataAccessObject; import entity.UserFactory; import interface_adapter.ViewManagerModel; +import interface_adapter.filter_search.FilterSearchController; +import interface_adapter.filter_search.FilterSearchPresenter; +import interface_adapter.filter_search.FilterSearchViewModel; import interface_adapter.logged_in.ChangePasswordController; import interface_adapter.logged_in.ChangePasswordPresenter; import interface_adapter.logged_in.LoggedInViewModel; @@ -17,6 +20,9 @@ import use_case.change_password.ChangePasswordInputBoundary; import use_case.change_password.ChangePasswordInteractor; import use_case.change_password.ChangePasswordOutputBoundary; +import use_case.filter_search.FilterSearchInputBoundary; +import use_case.filter_search.FilterSearchInteractor; +import use_case.filter_search.FilterSearchOutputBoundary; import use_case.login.LoginInputBoundary; import use_case.login.LoginInteractor; import use_case.login.LoginOutputBoundary; @@ -26,10 +32,7 @@ import use_case.signup.SignupInputBoundary; import use_case.signup.SignupInteractor; import use_case.signup.SignupOutputBoundary; -import view.LoggedInView; -import view.LoginView; -import view.SignupView; -import view.ViewManager; +import view.*; import javax.swing.*; import java.awt.*; @@ -56,6 +59,8 @@ public class AppBuilder { private LoggedInViewModel loggedInViewModel; private LoggedInView loggedInView; private LoginView loginView; + private FilterSearchViewModel filterSearchViewModel; + private FilterSearchView filterSearchView; public AppBuilder() { cardPanel.setLayout(cardLayout); @@ -82,6 +87,13 @@ public AppBuilder addLoggedInView() { return this; } + public AppBuilder addFilterSearchView() { + filterSearchViewModel = new FilterSearchViewModel(); + filterSearchView = new FilterSearchView(filterSearchViewModel); + cardPanel.add(filterSearchView, filterSearchViewModel.getViewName()); + return this; + } + public AppBuilder addSignupUseCase() { final SignupOutputBoundary signupOutputBoundary = new SignupPresenter(viewManagerModel, signupViewModel, loginViewModel); @@ -116,6 +128,17 @@ public AppBuilder addChangePasswordUseCase() { return this; } + public AppBuilder addFilterSearchUseCase() { + final FilterSearchOutputBoundary filterSearchOutputBoundary = new FilterSearchPresenter(filterSearchViewModel, + viewManagerModel); + final FilterSearchInputBoundary filterSearchInteractor = new FilterSearchInteractor( + filterSearchOutputBoundary); + + FilterSearchController filterSearchController = new FilterSearchController(filterSearchInteractor); + filterSearchView.setFilterSearchController(filterSearchController); + return this; + } + /** * Adds the Logout Use Case to the application. * @return this builder diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java b/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java index aa07a160..b0856cc6 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java @@ -13,6 +13,7 @@ public class FilterSearchPresenter implements FilterSearchOutputBoundary{ private final ViewManagerModel viewManagerModel; public FilterSearchPresenter(FilterSearchViewModel filterSearchViewModel, ViewManagerModel viewManagerModel) { + this.viewManagerModel = viewManagerModel; this.filterSearchViewModel = filterSearchViewModel; } diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java index f2e34632..dd3ce2bd 100644 --- a/src/main/java/view/FilterSearchView.java +++ b/src/main/java/view/FilterSearchView.java @@ -3,7 +3,9 @@ import interface_adapter.filter_search.FilterSearchController; import interface_adapter.filter_search.FilterSearchState; import interface_adapter.filter_search.FilterSearchViewModel; +import interface_adapter.logged_in.ChangePasswordController; import interface_adapter.logout.LogoutController; +import interface_adapter.signup.SignupState; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -21,12 +23,12 @@ public class FilterSearchView extends JPanel implements ActionListener, PropertyChangeListener { private final String viewName = "Filter Search"; private final FilterSearchViewModel filterSearchViewModel; - private final FilterSearchController filterSearchController = null; + private FilterSearchController filterSearchController = null; - private final JLabel exchange; - private final JLabel mic; - private final JLabel securityType; - private final JLabel currency; + private final JLabel exchange = new JLabel("Exchange"); + private final JLabel mic = new JLabel("MIC:"); + private final JLabel securityType = new JLabel("Security Type:"); + private final JLabel currency = new JLabel("Currency:"); private final Button search; private final String[] exchangeOptions = {"AD" , @@ -154,12 +156,55 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { title.setAlignmentX(Component.CENTER_ALIGNMENT); final JComboBox exchangeDrop = new JComboBox(exchangeOptions); - this.add(exchangeDrop); + final JComboBox micDrop = new JComboBox(micOptions); + final JComboBox securityDrop = new JComboBox(securityOptions); + final JComboBox currencyDrop = new JComboBox(currencyOptions); + this.search = new Button("Search"); + + JPanel panel_e = new JPanel(); + panel_e.add(exchange); + panel_e.add(exchangeDrop); + + JPanel panel_m = new JPanel(); + panel_m.add(mic); + panel_m.add(micDrop); + + JPanel panel_s = new JPanel(); + panel_s.add(securityType); + panel_s.add(securityDrop); + + JPanel panel_c = new JPanel(); + panel_c.add(currency); + panel_c.add(currencyDrop); + + this.add(panel_e, BorderLayout.PAGE_START); + this.add(panel_m, BorderLayout.PAGE_START); + this.add(panel_s, BorderLayout.PAGE_START); + this.add(panel_c, BorderLayout.PAGE_START); + this.add(search, BorderLayout.PAGE_START); + + + + } + @Override + public void actionPerformed(ActionEvent evt) { + /* TODO add search results */ + } + public String getViewName() { + return viewName; + } + public void setFilterSearchController(FilterSearchController filterSearchController) { + this.filterSearchController = filterSearchController; + } + @Override + public void propertyChange(PropertyChangeEvent evt) { + final FilterSearchState state = (FilterSearchState) evt.getNewValue(); + } } From 805151c13228bd44ab69d8f9f019770aa163b937 Mon Sep 17 00:00:00 2001 From: samario Date: Mon, 24 Nov 2025 17:06:54 -0500 Subject: [PATCH 005/103] Copy and Paste from old repo --- .../data_access/NewsDataAccessObject.java | 179 ++++++++++++ src/main/java/entity/NewsArticle.java | 53 ++++ .../NewsPage/NewsController.java | 31 ++ .../NewsPage/NewsPresenter.java | 33 +++ .../NewsPage/NewsViewModel.java | 47 +++ .../News/NewsDataAccessInterface.java | 17 ++ .../java/use_case/News/NewsInputBoundary.java | 12 + .../java/use_case/News/NewsInteractor.java | 70 +++++ .../use_case/News/NewsOutputBoundary.java | 11 + .../java/use_case/News/NewsRequestModel.java | 46 +++ .../java/use_case/News/NewsResponseModel.java | 21 ++ src/main/java/view/NewsView.java | 272 ++++++++++++++++++ 12 files changed, 792 insertions(+) create mode 100644 src/main/java/data_access/NewsDataAccessObject.java create mode 100644 src/main/java/entity/NewsArticle.java create mode 100644 src/main/java/interface_adapter/NewsPage/NewsController.java create mode 100644 src/main/java/interface_adapter/NewsPage/NewsPresenter.java create mode 100644 src/main/java/interface_adapter/NewsPage/NewsViewModel.java create mode 100644 src/main/java/use_case/News/NewsDataAccessInterface.java create mode 100644 src/main/java/use_case/News/NewsInputBoundary.java create mode 100644 src/main/java/use_case/News/NewsInteractor.java create mode 100644 src/main/java/use_case/News/NewsOutputBoundary.java create mode 100644 src/main/java/use_case/News/NewsRequestModel.java create mode 100644 src/main/java/use_case/News/NewsResponseModel.java create mode 100644 src/main/java/view/NewsView.java diff --git a/src/main/java/data_access/NewsDataAccessObject.java b/src/main/java/data_access/NewsDataAccessObject.java new file mode 100644 index 00000000..3673427b --- /dev/null +++ b/src/main/java/data_access/NewsDataAccessObject.java @@ -0,0 +1,179 @@ +package data_access; + +import entity.NewsArticle; +import use_case.News.NewsDataAccessInterface; + +import org.json.JSONObject; +import org.json.JSONArray; +import org.json.JSONTokener; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Data access implementation for Finnhub news endpoints using org.json. + *

+ * Endpoints: + * - General market news: + * - Company news: + */ +public class NewsDataAccessObject implements NewsDataAccessInterface { + + private static final String BASE_URL = "https://finnhub.io/api/v1"; + + private final String apiKey; + + public NewsDataAccessObject(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public List loadMarketNews() throws Exception { + String endpoint = BASE_URL + "/news?category=general&token=" + + URLEncoder.encode(apiKey, StandardCharsets.UTF_8); + JSONArray array = fetchJsonArray(endpoint); + return parseNewsArray(array); + } + + @Override + public List loadCompanyNews(String symbol, + String fromDate, + String toDate) throws Exception { + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol must not be empty for company news."); + } + + // It is okay if fromDate or toDate are null/blank: Finnhub may require them, + // so we can set defaults here (for example: last 7 days) + StringBuilder urlBuilder = new StringBuilder(BASE_URL) + .append("/company-news") + .append("?symbol=").append(URLEncoder.encode(symbol, StandardCharsets.UTF_8)); + + if (fromDate != null && !fromDate.isBlank()) { + urlBuilder.append("&from=").append(URLEncoder.encode(fromDate, StandardCharsets.UTF_8)); + } + if (toDate != null && !toDate.isBlank()) { + urlBuilder.append("&to=").append(URLEncoder.encode(toDate, StandardCharsets.UTF_8)); + } + + urlBuilder.append("&token=").append(URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); + + JSONArray array = fetchJsonArray(urlBuilder.toString()); + return parseNewsArray(array); + } + + /** + * Helper: performs HTTP GET and returns the response as a JSONArray. + */ + private JSONArray fetchJsonArray(String urlString) throws IOException { + HttpURLConnection connection = null; + InputStream inputStream = null; + + try { + URL url = new URL(urlString); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + // Possible timeout + connection.setConnectTimeout(10_000); + connection.setReadTimeout(10_000); + + int status = connection.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + // Read errors + String errorBody = readStream(connection.getErrorStream()); + throw new IOException("HTTP error " + status + " from Finnhub: " + errorBody); + } + + inputStream = connection.getInputStream(); + JSONTokener tokener = new JSONTokener(inputStream); + return new JSONArray(tokener); + + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ignored) {} + } + if (connection != null) { + connection.disconnect(); + } + } + } + + /** + * Helper: read an InputStream fully into a String (for error messages). + */ + private String readStream(InputStream stream) throws IOException { + if (stream == null) return ""; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(stream, StandardCharsets.UTF_8))) { + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString(); + } + } + + /** + * Parse JSON array into a list of NewsArticle entities. + *

+ * Each object typically has fields: + * - id (long) + * - category (String) + * - datetime (long) + * - headline (String) + * - image (String) + * - related (String) // comma-separated symbols + * - source (String) + * - summary (String) + * - url (String) + */ + private List parseNewsArray(JSONArray array) { + List result = new ArrayList<>(); + if (array == null) { + return result; + } + + for (int i = 0; i < array.length(); i++) { + JSONObject obj = array.optJSONObject(i); + if (obj == null) continue; + + long id = obj.optLong("id", 0L); + String category = obj.optString("category", ""); + long datetime = obj.optLong("datetime", 0L); + String headline = obj.optString("headline", ""); + String image = obj.optString("image", ""); + String related = obj.optString("related", ""); + String source = obj.optString("source", ""); + String summary = obj.optString("summary", ""); + String url = obj.optString("url", ""); + + NewsArticle article = new NewsArticle( + id, + category, + datetime, + headline, + image, + related, + source, + summary, + url + ); + result.add(article); + } + + return result; + } +} diff --git a/src/main/java/entity/NewsArticle.java b/src/main/java/entity/NewsArticle.java new file mode 100644 index 00000000..c8214f79 --- /dev/null +++ b/src/main/java/entity/NewsArticle.java @@ -0,0 +1,53 @@ +package entity; + +public class NewsArticle { + + // Fields from Finnhub API + private final long id; // unique article id + private final String category; // e.g. "general", "crypto", etc. + private final long datetime; // UNIX timestamp (seconds) + private final String headline; // article title + private final String image; // image URL + private final String related; // related stock and companies, e.g. "AAPL,Apple.Inc" + private final String source; // e.g., "Bloomberg" + private final String summary; // short description + private final String url; // link to full article + + public NewsArticle(long id, + String category, + long datetime, + String headline, + String image, + String related, + String source, + String summary, + String url) { + this.id = id; + this.category = category; + this.datetime = datetime; + this.headline = headline; + this.image = image; + this.related = related; + this.source = source; + this.summary = summary; + this.url = url; + } + + public long getId() {return id;} + + public String getCategory() {return category;} + + public long getDatetime() {return datetime;} + + public String getHeadline() {return headline;} + + public String getImage() {return image;} + + public String getRelated() {return related;} + + public String getSource() {return source;} + + public String getSummary() {return summary;} + + public String getUrl() {return url;} +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/NewsPage/NewsController.java b/src/main/java/interface_adapter/NewsPage/NewsController.java new file mode 100644 index 00000000..cd370962 --- /dev/null +++ b/src/main/java/interface_adapter/NewsPage/NewsController.java @@ -0,0 +1,31 @@ +package interface_adapter.NewsPage; + +import use_case.News.NewsInputBoundary; +import use_case.News.NewsRequestModel; + +/** + * read from UI + * build a NewsRequestModel + * "send" the request through input boundary + */ +public class NewsController { + + private final NewsInputBoundary interactor; + + public NewsController(NewsInputBoundary interactor) { + this.interactor = interactor; + } + + // Load general market news (category=general) + public void loadMarketNews() { + NewsRequestModel request = new NewsRequestModel(); // market news ctor + interactor.executeMarketNews(request); + } + + // Load company-specific news (for a symbol like "AAPL") + public void loadCompanyNews(String symbol, String fromDate, String toDate) { + NewsRequestModel request = new NewsRequestModel(symbol, fromDate, toDate); + interactor.executeCompanyNews(request); + } +} + diff --git a/src/main/java/interface_adapter/NewsPage/NewsPresenter.java b/src/main/java/interface_adapter/NewsPage/NewsPresenter.java new file mode 100644 index 00000000..db5237aa --- /dev/null +++ b/src/main/java/interface_adapter/NewsPage/NewsPresenter.java @@ -0,0 +1,33 @@ +package interface_adapter.NewsPage; + +import use_case.News.NewsInputBoundary; +import use_case.News.NewsOutputBoundary; +import use_case.News.NewsResponseModel; + +/** + * receives output data from interactor + * writes the data through ViewModel nad then update the state + */ +public class NewsPresenter implements NewsOutputBoundary { + + private final NewsViewModel viewModel; + + public NewsPresenter(NewsViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void prepareSuccessView(NewsResponseModel responseModel) { + viewModel.setArticles(responseModel.getArticles()); + viewModel.setErrorMessage(null); + viewModel.firePropertyChanged(); + } + + @Override + public void prepareFailView(String errorMessage) { + viewModel.setArticles(null); + viewModel.setErrorMessage(errorMessage); + viewModel.firePropertyChanged(); + } +} + diff --git a/src/main/java/interface_adapter/NewsPage/NewsViewModel.java b/src/main/java/interface_adapter/NewsPage/NewsViewModel.java new file mode 100644 index 00000000..ce0d1358 --- /dev/null +++ b/src/main/java/interface_adapter/NewsPage/NewsViewModel.java @@ -0,0 +1,47 @@ +package interface_adapter.NewsPage; + +import entity.NewsArticle; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.List; + +/** + * The Presenter can update the states, so the View will changer correspondingly + */ +public class NewsViewModel { + + private final PropertyChangeSupport support = new PropertyChangeSupport(this); + + // UI state fields (were in NewsState before) + private List articles; + private String errorMessage; + + // listeners + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + public void firePropertyChanged() { + // property name can be anything, keep "state" to avoid changing other code too much + support.firePropertyChange("state", null, this); + } + + + public List getArticles() { + return articles; + } + + public void setArticles(List articles) { + this.articles = articles; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} + diff --git a/src/main/java/use_case/News/NewsDataAccessInterface.java b/src/main/java/use_case/News/NewsDataAccessInterface.java new file mode 100644 index 00000000..9416c245 --- /dev/null +++ b/src/main/java/use_case/News/NewsDataAccessInterface.java @@ -0,0 +1,17 @@ +package use_case.News; + +import java.util.List; +import entity.NewsArticle; + +public interface NewsDataAccessInterface { + + // Maps to Finnhub endpoint: /news?category=general + List loadMarketNews() throws Exception; + + //Maps to Finnhub endpoint: + // /company-news?symbol={symbol}&from={fromDate}&to={toDate} + // Dates are "YYYY-MM-DD". + List loadCompanyNews(String symbol, + String fromDate, + String toDate) throws Exception; +} \ No newline at end of file diff --git a/src/main/java/use_case/News/NewsInputBoundary.java b/src/main/java/use_case/News/NewsInputBoundary.java new file mode 100644 index 00000000..18522fb7 --- /dev/null +++ b/src/main/java/use_case/News/NewsInputBoundary.java @@ -0,0 +1,12 @@ +package use_case.News; + +/** + * Define what actions this user case supports + */ +public interface NewsInputBoundary { + // Load general market news (category=general). + void executeMarketNews(NewsRequestModel requestModel); + + // Load company-specific news for the given symbol. + void executeCompanyNews(NewsRequestModel requestModel); +} diff --git a/src/main/java/use_case/News/NewsInteractor.java b/src/main/java/use_case/News/NewsInteractor.java new file mode 100644 index 00000000..644ddd96 --- /dev/null +++ b/src/main/java/use_case/News/NewsInteractor.java @@ -0,0 +1,70 @@ +package use_case.News; + +import entity.NewsArticle; +import java.util.List; + +/** + * Implement "what happens when user asks for news" + *

+ * 1. Validate the NewsRequestModel + * 2. Call NewsDataAccessInterface + * 3. Build a NewsResponse Model + * 4. Call the presenter -- NewsOutputBoundary + */ +public class NewsInteractor implements NewsInputBoundary{ + private final NewsDataAccessInterface newsDataAccess; + private final NewsOutputBoundary newsPresenter; + + public NewsInteractor(NewsDataAccessInterface newsDataAccess, + NewsOutputBoundary newsPresenter) { + this.newsDataAccess = newsDataAccess; + this.newsPresenter = newsPresenter; + } + + @Override + public void executeMarketNews(NewsRequestModel requestModel) { + try { + List articles = newsDataAccess.loadMarketNews(); + + if (articles == null || articles.isEmpty()) { + newsPresenter.prepareFailView("No market news available."); + return; + } + + NewsResponseModel response = new NewsResponseModel(articles); + newsPresenter.prepareSuccessView(response); + + } catch (Exception e) { + newsPresenter.prepareFailView("Unable to load market news: " + e.getMessage()); + } + } + + @Override + public void executeCompanyNews(NewsRequestModel requestModel) { + String symbol = requestModel.getSymbol(); + + if (symbol == null || symbol.isBlank()) { + newsPresenter.prepareFailView("Symbol is required for company news."); + return; + } + + try { + List articles = newsDataAccess.loadCompanyNews( + symbol, + requestModel.getFromDate(), + requestModel.getToDate() + ); + + if (articles == null || articles.isEmpty()) { + newsPresenter.prepareFailView("No news found for symbol: " + symbol); + return; + } + + NewsResponseModel response = new NewsResponseModel(articles); + newsPresenter.prepareSuccessView(response); + + } catch (Exception e) { + newsPresenter.prepareFailView("Unable to load company news: " + e.getMessage()); + } + } +} diff --git a/src/main/java/use_case/News/NewsOutputBoundary.java b/src/main/java/use_case/News/NewsOutputBoundary.java new file mode 100644 index 00000000..91fc34f2 --- /dev/null +++ b/src/main/java/use_case/News/NewsOutputBoundary.java @@ -0,0 +1,11 @@ +package use_case.News; + +/** + * Interface implemented by the presenter + */ +public interface NewsOutputBoundary { + + void prepareSuccessView(NewsResponseModel responseModel); + + void prepareFailView(String errorMessage); +} diff --git a/src/main/java/use_case/News/NewsRequestModel.java b/src/main/java/use_case/News/NewsRequestModel.java new file mode 100644 index 00000000..d097e50e --- /dev/null +++ b/src/main/java/use_case/News/NewsRequestModel.java @@ -0,0 +1,46 @@ +package use_case.News; + +/** + * Request model for news. + * For market news, symbol/fromDate/toDate can all be null. + * For company news, symbol must be non-null; dates may be null (use default range). + * Input Data holder, the data comes from the UI + */ +public class NewsRequestModel { + + private final String symbol; // e.g. "AAPL" or null for market news + private final String fromDate; // "YYYY-MM-DD" or null + private final String toDate; // "YYYY-MM-DD" or null + + // Constructor for general market news + public NewsRequestModel() { + this.symbol = null; + this.fromDate = null; + this.toDate = null; + } + + // Constructor for company news + public NewsRequestModel(String symbol, String fromDate, String toDate) { + this.symbol = symbol; + this.fromDate = fromDate; + this.toDate = toDate; + } + + public String getSymbol() { + return symbol; + } + + public String getFromDate() { + return fromDate; + } + + public String getToDate() { + return toDate; + } + + // Market news don't have a symbol + public boolean isMarketNewsRequest() { + return symbol == null || symbol.isBlank(); + } +} + diff --git a/src/main/java/use_case/News/NewsResponseModel.java b/src/main/java/use_case/News/NewsResponseModel.java new file mode 100644 index 00000000..ec9eb102 --- /dev/null +++ b/src/main/java/use_case/News/NewsResponseModel.java @@ -0,0 +1,21 @@ +package use_case.News; + +import entity.NewsArticle; +import java.util.List; + +/** + * Data holder for the presenter to build the View + * Created by the Interactor and passed to OutputBoundary + */ +public class NewsResponseModel { + + private final List articles; + + public NewsResponseModel(List articles) { + this.articles = articles; + } + + public List getArticles() { + return articles; + } +} diff --git a/src/main/java/view/NewsView.java b/src/main/java/view/NewsView.java new file mode 100644 index 00000000..295d5e40 --- /dev/null +++ b/src/main/java/view/NewsView.java @@ -0,0 +1,272 @@ +package view; + +import interface_adapter.NewsPage.NewsController; +import interface_adapter.NewsPage.NewsViewModel; +import entity.NewsArticle; + +import javax.swing.*; +import javax.swing.table.DefaultTableModel; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; +import javax.swing.JTextArea; + + +public class NewsView extends JPanel implements PropertyChangeListener { + + private final NewsController controller; + private final NewsViewModel viewModel; + + private final JButton backToHomeButton; + private final JButton marketNewsButton; + private final JButton companyNewsButton; + private final JTextField symbolField; + private final JTextField fromDateField; + private final JTextField toDateField; + private final JTable newsTable; + private final DefaultTableModel tableModel; + private final JLabel errorLabel; + + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + // column indices for later use + private static final int COL_DATE = 0; + private static final int COL_SYMBOL = 1; + private static final int COL_TITLE = 2; + private static final int COL_SUMMARY = 3; + private static final int COL_SOURCE = 4; + private static final int COL_LINK = 5; + private static final int COL_IMAGE = 6; + + public NewsView(NewsController controller, NewsViewModel viewModel) { + this.controller = controller; + this.viewModel = viewModel; + + this.viewModel.addPropertyChangeListener(this); + + setLayout(new BorderLayout()); + + // ===== TOP PANEL ===== + JPanel topPanel = new JPanel(new BorderLayout()); + + // Left: Back to Home button + JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + backToHomeButton = new JButton("Back to Home"); + leftPanel.add(backToHomeButton); + topPanel.add(leftPanel, BorderLayout.WEST); + + // Right: symbol/date filters and news buttons + JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + + symbolField = new JTextField(8); + fromDateField = new JTextField(10); + toDateField = new JTextField(10); + companyNewsButton = new JButton("Company News"); + marketNewsButton = new JButton("Market News"); + + rightPanel.add(new JLabel("Symbol:")); + rightPanel.add(symbolField); + rightPanel.add(new JLabel("From (YYYY-MM-DD):")); + rightPanel.add(fromDateField); + rightPanel.add(new JLabel("To (YYYY-MM-DD):")); + rightPanel.add(toDateField); + rightPanel.add(companyNewsButton); + rightPanel.add(marketNewsButton); + + topPanel.add(rightPanel, BorderLayout.EAST); + + add(topPanel, BorderLayout.NORTH); + + // ===== CENTRE TABLE ===== + String[] columnNames = {"Date", "Symbol", "Title", "Summary", "Source", "Link", "Image"}; + tableModel = new DefaultTableModel(columnNames, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + + // Create the NewsTable right here + newsTable = new JTable(tableModel); + + // set renderer for headline column to enable wrapping + newsTable.getColumnModel() + .getColumn(COL_SUMMARY) + .setCellRenderer(new TextAreaRenderer()); + + newsTable.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent e) { + int row = newsTable.rowAtPoint(e.getPoint()); + int col = newsTable.columnAtPoint(e.getPoint()); + + if (row < 0 || currentArticles == null || row >= currentArticles.size()) { + return; + } + + NewsArticle article = currentArticles.get(row); + + try { + if (col == COL_LINK) { // column index for "Link" + String url = article.getUrl(); + if (url != null && !url.isBlank()) { + java.awt.Desktop.getDesktop().browse(new java.net.URI(url)); + } + } else if (col == COL_IMAGE) { // column index for "Image" + String imgUrl = article.getImage(); + if (imgUrl != null && !imgUrl.isBlank()) { + java.awt.Desktop.getDesktop().browse(new java.net.URI(imgUrl)); + } + } + } catch (Exception ex) { + JOptionPane.showMessageDialog( + NewsView.this, + "Unable to open link: " + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE + ); + } + } + }); + + // turn off auto-resize of chart + newsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + + //set preferred width + newsTable.getColumnModel().getColumn(COL_DATE).setPreferredWidth(130); + newsTable.getColumnModel().getColumn(COL_SYMBOL).setPreferredWidth(80); + newsTable.getColumnModel().getColumn(COL_TITLE).setPreferredWidth(160); + newsTable.getColumnModel().getColumn(COL_SUMMARY).setPreferredWidth(360); + newsTable.getColumnModel().getColumn(COL_SOURCE).setPreferredWidth(100); + newsTable.getColumnModel().getColumn(COL_LINK).setPreferredWidth(60); + newsTable.getColumnModel().getColumn(COL_IMAGE).setPreferredWidth(60); + + JScrollPane scrollPane = new JScrollPane(newsTable); + add(scrollPane, BorderLayout.CENTER); + + // ===== BOTTOM ERROR LABEL ============================================== + errorLabel = new JLabel(" "); + errorLabel.setForeground(Color.RED); + add(errorLabel, BorderLayout.SOUTH); + + // ===== BUTTON ACTIONS ================================================== + + // Back to Home – placeholder for now + backToHomeButton.addActionListener(e -> { + // TODO: implement navigation back to the main/home view. + // For now, you can leave this empty or show a message: + JOptionPane.showMessageDialog( + this, + "Back to Home is not implemented yet.", + "Info", + JOptionPane.INFORMATION_MESSAGE + ); + }); + + // Market news – load top market news into centre table + marketNewsButton.addActionListener(e -> controller.loadMarketNews()); + + // Company news – use filters and show results in centre table + companyNewsButton.addActionListener(e -> { + String symbol = symbolField.getText().trim(); + String from = fromDateField.getText().trim(); + String to = toDateField.getText().trim(); + controller.loadCompanyNews(symbol, from, to); + }); + + // automatically show top market news when this view is created + controller.loadMarketNews(); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!"state".equals(evt.getPropertyName())) { + return; + } + + updateTable(viewModel.getArticles()); + updateError(viewModel.getErrorMessage()); + } + + private java.util.List currentArticles; + + private void updateTable(List articles) { + tableModel.setRowCount(0); // clear + currentArticles = articles; + + if (articles == null) { + return; + } + + for (NewsArticle article : articles) { + String dateStr = formatDate(article.getDatetime()); + String symbol = article.getRelated(); + String title = article.getHeadline(); + String summary = article.getSummary(); + String source = article.getSource(); + + String linkLabel = (article.getUrl() == null || article.getUrl().isBlank()) ? "" : "Open"; + String imageLabel = (article.getImage() == null || article.getImage().isBlank()) ? "" : "View"; + + tableModel.addRow(new Object[]{dateStr, symbol, title, summary, source, linkLabel, imageLabel}); + } + } + + private void updateError(String errorMessage) { + if (errorMessage == null || errorMessage.isBlank()) { + errorLabel.setText(" "); + } else { + errorLabel.setText(errorMessage); + } + } + + private String formatDate(long unixSeconds) { + if (unixSeconds <= 0) { + return ""; + } + LocalDateTime dt = LocalDateTime.ofInstant( + Instant.ofEpochSecond(unixSeconds), + ZoneId.systemDefault() + ); + return dt.format(DATE_FORMATTER); + } + + private static class TextAreaRenderer extends JTextArea implements javax.swing.table.TableCellRenderer { + + public TextAreaRenderer() { + setLineWrap(true); + setWrapStyleWord(true); + setOpaque(true); + } + + @Override + public java.awt.Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { + + setText(value == null ? "" : value.toString()); + + if (isSelected) { + setBackground(table.getSelectionBackground()); + setForeground(table.getSelectionForeground()); + } else { + setBackground(table.getBackground()); + setForeground(table.getForeground()); + } + + setSize(table.getColumnModel().getColumn(column).getWidth(), Short.MAX_VALUE); + int preferredHeight = getPreferredSize().height; + if (table.getRowHeight(row) != preferredHeight) { + table.setRowHeight(row, preferredHeight); + } + + return this; + } + } +} From 1e34e64076066d0e1050f0e700b156efa7b58307 Mon Sep 17 00:00:00 2001 From: samario Date: Mon, 24 Nov 2025 17:21:17 -0500 Subject: [PATCH 006/103] Copy and Paste from old repo --- .../MarketStatusDataAccessObject.java | 108 ++++++++++++++++++ src/main/java/entity/MarketStatus.java | 36 ++++++ .../MarketStatus/MarketStatusController.java | 16 +++ .../MarketStatus/MarketStatusPresenter.java | 59 ++++++++++ .../MarketStatus/MarketStatusViewModel.java | 88 ++++++++++++++ .../MarketStatusDataAccessInterface.java | 7 ++ .../MarketStatusInputBoundary.java | 6 + .../MarketStatus/MarketStatusInteractor.java | 32 ++++++ .../MarketStatusOutputBoundary.java | 8 ++ .../MarketStatusResponseModel.java | 14 +++ 10 files changed, 374 insertions(+) create mode 100644 src/main/java/data_access/MarketStatusDataAccessObject.java create mode 100644 src/main/java/entity/MarketStatus.java create mode 100644 src/main/java/interface_adapter/MarketStatus/MarketStatusController.java create mode 100644 src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java create mode 100644 src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java create mode 100644 src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java create mode 100644 src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java create mode 100644 src/main/java/use_case/MarketStatus/MarketStatusInteractor.java create mode 100644 src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java create mode 100644 src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java diff --git a/src/main/java/data_access/MarketStatusDataAccessObject.java b/src/main/java/data_access/MarketStatusDataAccessObject.java new file mode 100644 index 00000000..37375ddb --- /dev/null +++ b/src/main/java/data_access/MarketStatusDataAccessObject.java @@ -0,0 +1,108 @@ +package data_access; + +import entity.MarketStatus; +import org.json.JSONObject; +import org.json.JSONTokener; +import use_case.MarketStatus.MarketStatusDataAccessInterface; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +public class MarketStatusDataAccessObject implements MarketStatusDataAccessInterface { + private static final String BASE_URL = "https://finnhub.io/api/v1/stock/market-status"; + + private final String apiKey; + + public MarketStatusDataAccessObject(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public MarketStatus loadStatus() throws Exception { + String urlString = BASE_URL + + "?exchange=US" + + "&token=" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8); + + JSONObject json = fetchJsonObject(urlString); + return parseMarketStatus(json); + } + + // Perform HTTP GET and return the response as a JSONObject. + private JSONObject fetchJsonObject(String urlString) throws IOException { + HttpURLConnection connection = null; + InputStream inputStream = null; + + try { + URL url = new URL(urlString); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + connection.setConnectTimeout(10_000); + connection.setReadTimeout(10_000); + + int status = connection.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + String errorBody = readStream(connection.getErrorStream()); + throw new IOException("HTTP error " + status + " from Finnhub: " + errorBody); + } + + inputStream = connection.getInputStream(); + JSONTokener tokener = new JSONTokener(inputStream); + return new JSONObject(tokener); + + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ignored) {} + } + if (connection != null) { + connection.disconnect(); + } + } + } + + // Convert errorMessage + private String readStream(InputStream stream) throws IOException { + if (stream == null) return ""; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(stream, StandardCharsets.UTF_8))) { + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString(); + } + } + + + private MarketStatus parseMarketStatus(JSONObject obj) { + if (obj == null) { + return null; + } + + String exchange = obj.optString("exchange", "US"); + boolean isOpen = obj.optBoolean("isOpen", false); + String session = obj.optString("session", ""); + String holiday = obj.optString("holiday", ""); + long timestamp = obj.optLong("t", 0L); + String timezone = obj.optString("timezone", ""); + + return new MarketStatus( + exchange, + isOpen, + session, + holiday, + timestamp, + timezone + ); + } +} diff --git a/src/main/java/entity/MarketStatus.java b/src/main/java/entity/MarketStatus.java new file mode 100644 index 00000000..9f24c44a --- /dev/null +++ b/src/main/java/entity/MarketStatus.java @@ -0,0 +1,36 @@ +package entity; + +public class MarketStatus { + private final String exchange; // e.g. "US" + private final boolean open; // true if market is open + private final String session; // "pre", "regular", "post" + private final String holiday; // holiday name if closed, else "" + private final long timestamp; // UNIX seconds + private final String timezone; // e.g. "America/New_York" + + public MarketStatus(String exchange, + boolean open, + String session, + String holiday, + long timestamp, + String timezone) { + this.exchange = exchange; + this.open = open; + this.session = session; + this.holiday = holiday; + this.timestamp = timestamp; + this.timezone = timezone; + } + + public String getExchange() {return exchange;} + + public boolean isOpen() {return open;} + + public String getSession() {return session;} + + public String getHoliday() {return holiday;} + + public long getTimestamp() {return timestamp;} + + public String getTimezone() {return timezone;} +} diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusController.java b/src/main/java/interface_adapter/MarketStatus/MarketStatusController.java new file mode 100644 index 00000000..f6c18f77 --- /dev/null +++ b/src/main/java/interface_adapter/MarketStatus/MarketStatusController.java @@ -0,0 +1,16 @@ +package interface_adapter.MarketStatus; + +import use_case.MarketStatus.MarketStatusInputBoundary; + +public class MarketStatusController { + + private final MarketStatusInputBoundary interactor; + + public MarketStatusController(MarketStatusInputBoundary interactor) { + this.interactor = interactor; + } + + public void updateStatus() { + interactor.execute(); + } +} diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java b/src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java new file mode 100644 index 00000000..d82c508a --- /dev/null +++ b/src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java @@ -0,0 +1,59 @@ +package interface_adapter.MarketStatus; + +import use_case.MarketStatus.MarketStatusOutputBoundary; +import use_case.MarketStatus.MarketStatusResponseModel; +import entity.MarketStatus; + +public class MarketStatusPresenter implements MarketStatusOutputBoundary { + private final MarketStatusViewModel viewModel; + + public MarketStatusPresenter(MarketStatusViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void prepareSuccessView(MarketStatusResponseModel responseModel) { + MarketStatus status = responseModel.getMarketStatus(); + + boolean open = status.isOpen(); + String session = status.getSession(); + String holiday = status.getHoliday(); + + // Status message + String text; + if (open) { + if (session != null && !session.isBlank()) { + text = "US market is OPEN (" + session + ")"; + } else { + text = "US market is OPEN"; + } + } else { + if (holiday != null && !holiday.isBlank()) { + text = "US market is CLOSED (Holiday: " + holiday + ")"; + } else { + text = "US market is CLOSED"; + } + } + + viewModel.setStatusText(text); + viewModel.setOpen(open); + viewModel.setSession(session); + viewModel.setHoliday(holiday); + viewModel.setTimezone(status.getTimezone()); + viewModel.setErrorMessage(null); + + viewModel.firePropertyChanged(); + } + + @Override + public void prepareFailView(String errorMessage) { + viewModel.setStatusText("Market status unavailable"); + viewModel.setOpen(false); + viewModel.setSession(null); + viewModel.setHoliday(null); + viewModel.setTimezone(null); + viewModel.setErrorMessage(errorMessage); + + viewModel.firePropertyChanged(); + } +} diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java b/src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java new file mode 100644 index 00000000..ef8981f1 --- /dev/null +++ b/src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java @@ -0,0 +1,88 @@ +package interface_adapter.MarketStatus; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + +public class MarketStatusViewModel { + private final PropertyChangeSupport support = new PropertyChangeSupport(this); + + // What is shown in UI + private String statusText; + + // Whether the market is open + private boolean open; + + // Extra info, optional + private String session; // "pre", "regular", "post", etc. + private String exchange; // e.g. "US" + private String holiday; // holiday name, or "" + private String timezone; // e.g. "America/New_York"; + + private String errorMessage; + + // --- observer methods --- + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + public void firePropertyChanged() { + support.firePropertyChange("state", null, this); + } + + // getters + public String getStatusText() { + return statusText; + } + + public void setStatusText(String statusText) { + this.statusText = statusText; + } + + public boolean isOpen() { + return open; + } + + public void setOpen(boolean open) { + this.open = open; + } + + public String getSession() { + return session; + } + + public void setSession(String session) { + this.session = session; + } + + public String getExchange() { + return exchange; + } + + public void setExchange(String exchange) { + this.exchange = exchange; + } + + public String getHoliday() { + return holiday; + } + + public void setHoliday(String holiday) { + this.holiday = holiday; + } + + public String getTimezone() { + return timezone; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java b/src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java new file mode 100644 index 00000000..0f66b62d --- /dev/null +++ b/src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java @@ -0,0 +1,7 @@ +package use_case.MarketStatus; + +import entity.MarketStatus; + +public interface MarketStatusDataAccessInterface { + MarketStatus loadStatus() throws Exception; +} diff --git a/src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java b/src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java new file mode 100644 index 00000000..af989f09 --- /dev/null +++ b/src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java @@ -0,0 +1,6 @@ +package use_case.MarketStatus; + +public interface MarketStatusInputBoundary { + void execute(); +} + diff --git a/src/main/java/use_case/MarketStatus/MarketStatusInteractor.java b/src/main/java/use_case/MarketStatus/MarketStatusInteractor.java new file mode 100644 index 00000000..53e05935 --- /dev/null +++ b/src/main/java/use_case/MarketStatus/MarketStatusInteractor.java @@ -0,0 +1,32 @@ +package use_case.MarketStatus; + +import entity.MarketStatus; + +public class MarketStatusInteractor implements MarketStatusInputBoundary{ + private final MarketStatusDataAccessInterface dataAccess; + private final MarketStatusOutputBoundary presenter; + + public MarketStatusInteractor(MarketStatusDataAccessInterface dataAccess, + MarketStatusOutputBoundary presenter) { + this.dataAccess = dataAccess; + this.presenter = presenter; + } + + @Override + public void execute() { + try { + MarketStatus status = dataAccess.loadStatus(); + + if (status == null) { + presenter.prepareFailView("No market status available."); + return; + } + + MarketStatusResponseModel response = new MarketStatusResponseModel(status); + presenter.prepareSuccessView(response); + + } catch (Exception e) { + presenter.prepareFailView("Unable to load market status: " + e.getMessage()); + } + } +} diff --git a/src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java b/src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java new file mode 100644 index 00000000..46ceaa33 --- /dev/null +++ b/src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java @@ -0,0 +1,8 @@ +package use_case.MarketStatus; + +public interface MarketStatusOutputBoundary { + + void prepareSuccessView(MarketStatusResponseModel responseModel); + + void prepareFailView(String errorMessage); +} \ No newline at end of file diff --git a/src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java b/src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java new file mode 100644 index 00000000..0c460f1e --- /dev/null +++ b/src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java @@ -0,0 +1,14 @@ +package use_case.MarketStatus; + +import entity.MarketStatus; + +public class MarketStatusResponseModel { + + private final MarketStatus marketStatus; + + public MarketStatusResponseModel(MarketStatus marketStatus) { + this.marketStatus = marketStatus; + } + + public MarketStatus getMarketStatus() {return marketStatus;} +} \ No newline at end of file From f0a42a04a36099d19f241dbadbaf6a24f94876bd Mon Sep 17 00:00:00 2001 From: samario Date: Mon, 24 Nov 2025 17:31:16 -0500 Subject: [PATCH 007/103] Test for News Interactor --- .../use_case/News/NewsInteractorTest.java | 201 ++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/test/java/use_case/News/NewsInteractorTest.java diff --git a/src/test/java/use_case/News/NewsInteractorTest.java b/src/test/java/use_case/News/NewsInteractorTest.java new file mode 100644 index 00000000..e8e1563a --- /dev/null +++ b/src/test/java/use_case/News/NewsInteractorTest.java @@ -0,0 +1,201 @@ +package use_case.News; + +import entity.NewsArticle; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for NewsInteractor. + */ +public class NewsInteractorTest { + + /** + * Simple in-memory stub for NewsDataAccessInterface. + * You configure its fields before running each test. + */ + private static class InMemoryNewsDao implements NewsDataAccessInterface { + + List marketNewsToReturn = new ArrayList<>(); + List companyNewsToReturn = new ArrayList<>(); + + boolean throwOnMarket = false; + boolean throwOnCompany = false; + + // capture last call arguments + String lastCompanySymbol; + String lastCompanyFrom; + String lastCompanyTo; + + @Override + public List loadMarketNews() throws Exception { + if (throwOnMarket) { + throw new Exception("Market error"); + } + return marketNewsToReturn; + } + + @Override + public List loadCompanyNews(String symbol, String fromDate, String toDate) throws Exception { + lastCompanySymbol = symbol; + lastCompanyFrom = fromDate; + lastCompanyTo = toDate; + + if (throwOnCompany) { + throw new Exception("Company error"); + } + return companyNewsToReturn; + } + } + + /** + * Recording presenter that just stores what the interactor sends. + */ + private static class RecordingPresenter implements NewsOutputBoundary { + + NewsResponseModel lastSuccess; + String lastError; + + @Override + public void prepareSuccessView(NewsResponseModel responseModel) { + lastSuccess = responseModel; + } + + @Override + public void prepareFailView(String errorMessage) { + lastError = errorMessage; + } + } + + private NewsArticle sampleArticle(String symbol) { + return new NewsArticle( + 1L, + "general", + 1_700_000_000L, + "Test headline for " + symbol, + "", + symbol, + "TestSource", + "Test summary", + "https://example.com/article" + ); + } + + @Test + void executeMarketNews_success_callsPresenterWithArticles() { + // Arrange + InMemoryNewsDao dao = new InMemoryNewsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + dao.marketNewsToReturn = List.of(sampleArticle("AAPL"), sampleArticle("MSFT")); + + NewsInteractor interactor = new NewsInteractor(dao, presenter); + + NewsRequestModel request = new NewsRequestModel(); // market news + + // Act + interactor.executeMarketNews(request); + + // Assert + assertNull(presenter.lastError, "No error should be reported"); + assertNotNull(presenter.lastSuccess, "Success response should be sent"); + List articles = presenter.lastSuccess.getArticles(); + assertEquals(2, articles.size()); + assertEquals("AAPL", articles.get(0).getRelated()); + assertEquals("MSFT", articles.get(1).getRelated()); + } + + @Test + void executeMarketNews_error_callsFailView() { + // Arrange + InMemoryNewsDao dao = new InMemoryNewsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + dao.throwOnMarket = true; + + NewsInteractor interactor = new NewsInteractor(dao, presenter); + NewsRequestModel request = new NewsRequestModel(); + + // Act + interactor.executeMarketNews(request); + + // Assert + assertNull(presenter.lastSuccess, "No success response expected"); + assertNotNull(presenter.lastError, "Error should be reported"); + assertTrue(presenter.lastError.contains("Unable to load market news"), + "Error message should mention market news"); + } + + @Test + void executeCompanyNews_emptySymbol_reportsError() { + // Arrange + InMemoryNewsDao dao = new InMemoryNewsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + NewsInteractor interactor = new NewsInteractor(dao, presenter); + + // symbol is blank → invalid + NewsRequestModel request = new NewsRequestModel(" ", "2024-01-01", "2024-01-31"); + + // Act + interactor.executeCompanyNews(request); + + // Assert + assertNull(presenter.lastSuccess, "No success response expected"); + assertEquals("Symbol is required for company news.", presenter.lastError); + // Also check DAO was never called + assertNull(dao.lastCompanySymbol, "DAO should not be called on invalid symbol"); + } + + @Test + void executeCompanyNews_success_passesArticlesAndArgs() { + // Arrange + InMemoryNewsDao dao = new InMemoryNewsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.companyNewsToReturn = List.of(sampleArticle("AAPL")); + + NewsInteractor interactor = new NewsInteractor(dao, presenter); + + NewsRequestModel request = + new NewsRequestModel("AAPL", "2024-01-01", "2024-01-31"); + + // Act + interactor.executeCompanyNews(request); + + // Assert DAO was called with correct arguments + assertEquals("AAPL", dao.lastCompanySymbol); + assertEquals("2024-01-01", dao.lastCompanyFrom); + assertEquals("2024-01-31", dao.lastCompanyTo); + + // Assert presenter got success + assertNull(presenter.lastError); + assertNotNull(presenter.lastSuccess); + List articles = presenter.lastSuccess.getArticles(); + assertEquals(1, articles.size()); + assertEquals("AAPL", articles.get(0).getRelated()); + } + + @Test + void executeCompanyNews_noArticles_triggersFailView() { + // Arrange + InMemoryNewsDao dao = new InMemoryNewsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.companyNewsToReturn = List.of(); // empty list + + NewsInteractor interactor = new NewsInteractor(dao, presenter); + + NewsRequestModel request = + new NewsRequestModel("AAPL", "2024-01-01", "2024-01-31"); + + // Act + interactor.executeCompanyNews(request); + + // Assert + assertNull(presenter.lastSuccess); + assertNotNull(presenter.lastError); + assertTrue(presenter.lastError.contains("No news found for symbol"), + "Should report no news found"); + } +} From 2fbab253761c8a42eec0dc929de461c40b7905ed Mon Sep 17 00:00:00 2001 From: samario Date: Mon, 24 Nov 2025 17:32:54 -0500 Subject: [PATCH 008/103] Test for MarketStatus Interactor --- .../MarketStatusInteractorTest.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java diff --git a/src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java b/src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java new file mode 100644 index 00000000..2f52a0d0 --- /dev/null +++ b/src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java @@ -0,0 +1,160 @@ +package use_case.MarketStatus; + +import entity.MarketStatus; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MarketStatusInteractor. + */ +public class MarketStatusInteractorTest { + + /** + * Simple in-memory stub for MarketStatusDataAccessInterface. + */ + private static class InMemoryMarketStatusDao implements MarketStatusDataAccessInterface { + + MarketStatus statusToReturn; + boolean throwOnLoad = false; + int loadCalls = 0; + + @Override + public MarketStatus loadStatus() throws Exception { + loadCalls++; + if (throwOnLoad) { + throw new Exception("Data access error"); + } + return statusToReturn; + } + } + + /** + * Recording presenter that just stores what the interactor sends. + */ + private static class RecordingPresenter implements MarketStatusOutputBoundary { + + MarketStatusResponseModel lastSuccess; + String lastError; + + @Override + public void prepareSuccessView(MarketStatusResponseModel responseModel) { + lastSuccess = responseModel; + } + + @Override + public void prepareFailView(String errorMessage) { + lastError = errorMessage; + } + } + + @Test + void execute_success_openMarket() { + // Arrange + InMemoryMarketStatusDao dao = new InMemoryMarketStatusDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.statusToReturn = new MarketStatus( + "US", + true, // open + "regular", // session + "", // holiday + 1_700_000_000L, // timestamp + "America/New_York" // timezone + ); + + MarketStatusInteractor interactor = + new MarketStatusInteractor(dao, presenter); + + // Act + interactor.execute(); + + // Assert DAO called + assertEquals(1, dao.loadCalls, "DAO should be called exactly once"); + + // Assert presenter got success, not error + assertNull(presenter.lastError, "No error should be reported"); + assertNotNull(presenter.lastSuccess, "Success response should be sent"); + + MarketStatus status = presenter.lastSuccess.getMarketStatus(); + assertNotNull(status); + assertEquals("US", status.getExchange()); + assertTrue(status.isOpen()); + assertEquals("regular", status.getSession()); + } + + @Test + void execute_success_closedHoliday() { + // Arrange + InMemoryMarketStatusDao dao = new InMemoryMarketStatusDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.statusToReturn = new MarketStatus( + "US", + false, // closed + "", // session + "Memorial Day", // holiday + 1_700_000_000L, + "America/New_York" + ); + + MarketStatusInteractor interactor = + new MarketStatusInteractor(dao, presenter); + + // Act + interactor.execute(); + + // Assert + assertEquals(1, dao.loadCalls); + assertNull(presenter.lastError); + assertNotNull(presenter.lastSuccess); + + MarketStatus status = presenter.lastSuccess.getMarketStatus(); + assertFalse(status.isOpen()); + assertEquals("Memorial Day", status.getHoliday()); + } + + @Test + void execute_nullStatus_triggersFailView() { + // Arrange + InMemoryMarketStatusDao dao = new InMemoryMarketStatusDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.statusToReturn = null; // simulate no data + + MarketStatusInteractor interactor = + new MarketStatusInteractor(dao, presenter); + + // Act + interactor.execute(); + + // Assert + assertEquals(1, dao.loadCalls); + assertNull(presenter.lastSuccess, "No success should be sent"); + assertNotNull(presenter.lastError, "Error should be reported"); + assertTrue(presenter.lastError.contains("No market status available"), + "Error message should mention missing status"); + } + + @Test + void execute_daoThrows_triggersFailView() { + // Arrange + InMemoryMarketStatusDao dao = new InMemoryMarketStatusDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.throwOnLoad = true; + + MarketStatusInteractor interactor = + new MarketStatusInteractor(dao, presenter); + + // Act + interactor.execute(); + + // Assert + assertEquals(1, dao.loadCalls); + assertNull(presenter.lastSuccess); + assertNotNull(presenter.lastError); + assertTrue(presenter.lastError.contains("Unable to load market status"), + "Error message should mention inability to load status"); + } +} \ No newline at end of file From 00b9295f22d9034a56d123f0631fba3579006c51 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Mon, 24 Nov 2025 18:39:24 -0500 Subject: [PATCH 009/103] dash with stockpage sample --- src/main/java/Service/LoginService.java | 77 +++++++ src/main/java/app/Dashboard.java | 179 ++++++++++++++++ src/main/java/app/Main.java | 264 +++++++++++++++++++++-- src/main/java/app/StockPage.java | 272 ++++++++++++++++++++++++ src/main/java/app/User.java | 29 +++ 5 files changed, 805 insertions(+), 16 deletions(-) create mode 100644 src/main/java/Service/LoginService.java create mode 100644 src/main/java/app/Dashboard.java create mode 100644 src/main/java/app/StockPage.java create mode 100644 src/main/java/app/User.java diff --git a/src/main/java/Service/LoginService.java b/src/main/java/Service/LoginService.java new file mode 100644 index 00000000..71676353 --- /dev/null +++ b/src/main/java/Service/LoginService.java @@ -0,0 +1,77 @@ +package Service; + +import App.User; +import java.util.HashMap; +import java.util.Map; + +public class LoginService { + private Map users; + private User currentUser; + + public LoginService() { + this.users = new HashMap<>(); + // Add some test users (in a real app, this would be in a database) + users.put("user1", new User("user1", "password1")); + users.put("user2", new User("user2", "password2")); + } + + /** + * Attempts to log in a user with the given credentials + * @param username The username to log in with + * @param password The password to verify + * @return true if login is successful, false otherwise + */ + public boolean login(String username, String password) { + User user = users.get(username); + if (user != null && user.checkPassword(password)) { + user.setLoggedIn(true); + currentUser = user; + return true; + } + return false; + } + + /** + * Logs out the current user + */ + public void logout() { + if (currentUser != null) { + currentUser.setLoggedIn(false); + currentUser = null; + } + } + + /** + * Creates a new user account + * @param username The desired username + * @param password The desired password + * @return true if account was created successfully, false if username already exists + */ + public boolean signUp(String username, String password) { + if (username == null || username.trim().isEmpty() || password == null || password.isEmpty()) { + return false; + } + + if (users.containsKey(username)) { + return false; // User already exists + } + users.put(username, new User(username, password)); + return true; + } + + /** + * Checks if a user is currently logged in + * @return true if a user is logged in, false otherwise + */ + public boolean isLoggedIn() { + return currentUser != null && currentUser.isLoggedIn(); + } + + /** + * Gets the username of the currently logged-in user + * @return The username or null if no user is logged in + */ + public String getCurrentUser() { + return currentUser != null ? currentUser.getUsername() : null; + } +} \ No newline at end of file diff --git a/src/main/java/app/Dashboard.java b/src/main/java/app/Dashboard.java new file mode 100644 index 00000000..feaecea8 --- /dev/null +++ b/src/main/java/app/Dashboard.java @@ -0,0 +1,179 @@ +package app; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.logging.Logger; + +/** + * The main application window displayed after successful login. + * Manages the navigation between different dashboard views using CardLayout. + */ +public class Dashboard extends JFrame { + + private static final Logger LOGGER = Logger.getLogger(Dashboard.class.getName()); + + private final StockPage homePanel; + private final CardLayout cardLayout = new CardLayout(); + private final JPanel mainContentPanel; + private final String username; + + public Dashboard(String username) { + this.username = username; + setTitle("Stock App Dashboard - Logged in as: " + username); + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + setSize(1000, 600); // Slightly larger frame for better dashboard layout + setLocationRelativeTo(null); + + this.homePanel = new StockPage(); // The real-time trade view + this.mainContentPanel = new JPanel(cardLayout); + + setLayout(new BorderLayout()); + + // 1. Create the Navigation Bar (North Panel) + JPanel navBar = createNavigationBar(); + add(navBar, BorderLayout.NORTH); + + // 2. Setup the Card Layout for main content (Center Panel) + setupMainContent(); + add(mainContentPanel, BorderLayout.CENTER); + + // Add a listener to ensure WebSocket is closed on application exit + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + homePanel.closeWebSocket(); // Ensure WebSocket connection is closed + dispose(); // Close the window + System.exit(0); // Terminate the application + } + }); + } + + /** + * Creates the top navigation bar containing the four main buttons and the Logout button. + * Uses a GridBagLayout for flexible spacing. + */ + private JPanel createNavigationBar() { + JPanel navBar = new JPanel(new GridBagLayout()); + navBar.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + navBar.setBackground(new Color(240, 240, 240)); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(0, 5, 0, 5); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.anchor = GridBagConstraints.CENTER; + + String[] buttonNames = {"Home (Real-Time Trade)", "Portfolio", "Watchlist", "Settings"}; + + // Create and add the four main navigation buttons + for (int i = 0; i < buttonNames.length; i++) { + JButton button = new JButton(buttonNames[i]); + button.setFont(new Font("Arial", Font.BOLD, 14)); + String cardName = buttonNames[i].split(" ")[0]; // Use first word as card key + + button.addActionListener(e -> { + cardLayout.show(mainContentPanel, cardName); + if (!cardName.equals("Home")) { + homePanel.closeWebSocket(); // Stop real-time data when navigating away + } + updateButtonStyles(navBar, button); + }); + + gbc.gridx = i; + gbc.weightx = 1.0; // Distribute space evenly + navBar.add(button, gbc); + } + + // Add a flexible spacer to push the Logout button to the far right + gbc.gridx = buttonNames.length; + gbc.weightx = 10.0; + navBar.add(Box.createHorizontalGlue(), gbc); + + // Add Logout button + gbc.gridx = buttonNames.length + 1; + gbc.weightx = 0.0; // Don't take extra space + JButton logoutButton = new JButton("Logout (" + username + ")"); + logoutButton.setBackground(new Color(200, 50, 50)); + logoutButton.setForeground(Color.WHITE); + logoutButton.addActionListener(e -> handleLogout()); + navBar.add(logoutButton, gbc); + + // Initially set "Home" button as selected (assuming it's the first button) + SwingUtilities.invokeLater(() -> { + if (navBar.getComponentCount() > 0) { + updateButtonStyles(navBar, (JButton) navBar.getComponent(0)); + } + }); + + return navBar; + } + + /** + * Sets up the main content area with different panels for the CardLayout. + */ + private void setupMainContent() { + // Add the real-time trade panel (Home) + mainContentPanel.add(homePanel, "Home"); + + // Add placeholder panels for other views + mainContentPanel.add(createPlaceholderPanel("Portfolio View", Color.LIGHT_GRAY), "Portfolio"); + mainContentPanel.add(createPlaceholderPanel("Watchlist Management", Color.CYAN), "Watchlist"); + mainContentPanel.add(createPlaceholderPanel("User Settings", Color.YELLOW), "Settings"); + + // Show the Home view by default + cardLayout.show(mainContentPanel, "Home"); + } + + /** + * Helper to create a simple placeholder panel for non-implemented views. + */ + private JPanel createPlaceholderPanel(String title, Color bgColor) { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBackground(bgColor); + JLabel label = new JLabel("--- " + title + " ---", SwingConstants.CENTER); + label.setFont(new Font("Arial", Font.ITALIC, 24)); + panel.add(label, BorderLayout.CENTER); + return panel; + } + + /** + * Updates the styling of the navigation buttons to indicate the active view. + */ + private void updateButtonStyles(JPanel navBar, JButton active) { + for (Component comp : navBar.getComponents()) { + if (comp instanceof JButton) { + JButton button = (JButton) comp; + if (button.getText().startsWith("Logout")) continue; // Skip Logout button + + if (button == active) { + button.setBackground(new Color(50, 150, 250)); // Active color (Blue) + button.setForeground(Color.WHITE); + } else { + button.setBackground(UIManager.getColor("Button.background")); // Default color + button.setForeground(UIManager.getColor("Button.foreground")); + } + } + } + } + + /** + * Handles the user logging out. + */ + private void handleLogout() { + int confirm = JOptionPane.showConfirmDialog(this, + "Are you sure you want to log out?", "Confirm Logout", + JOptionPane.YES_NO_OPTION); + + if (confirm == JOptionPane.YES_OPTION) { + homePanel.closeWebSocket(); // Close connection before logging out + + // Close dashboard and open the login screen + this.dispose(); + SwingUtilities.invokeLater(() -> { + // Relaunch the Main (Login) window + new Main().setVisible(true); + }); + } + } +} \ No newline at end of file diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 424404fb..596d5414 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -1,21 +1,253 @@ package app; +import Service.LoginService; import javax.swing.*; +import java.awt.*; +import java.awt.event.*; + +public class Main extends JFrame { + private static LoginService loginService = new LoginService(); + private JPanel cardPanel; + private CardLayout cardLayout; + private JTextField usernameField, signupUsernameField; + private JPasswordField passwordField, signupPasswordField; + private JLabel statusLabel; + + public Main() { + setTitle("Stock App - Login"); + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + setSize(400, 300); + setLocationRelativeTo(null); + + // Create card layout for switching between login and signup panels + cardLayout = new CardLayout(); + cardPanel = new JPanel(cardLayout); + + // Create login panel + JPanel loginPanel = createLoginPanel(); + + // Create signup panel + JPanel signupPanel = createSignupPanel(); + + // Add panels to card layout + cardPanel.add(loginPanel, "login"); + cardPanel.add(signupPanel, "signup"); + + add(cardPanel, BorderLayout.CENTER); + + // Status label at the bottom + statusLabel = new JLabel(" ", JLabel.CENTER); + add(statusLabel, BorderLayout.SOUTH); + + // Show login panel by default + cardLayout.show(cardPanel, "login"); + } + + private JPanel createLoginPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 5, 5, 5); + gbc.fill = GridBagConstraints.HORIZONTAL; + + // Title + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 2; + JLabel titleLabel = new JLabel("Login to Stock App", JLabel.CENTER); + titleLabel.setFont(new Font("Arial", Font.BOLD, 18)); + panel.add(titleLabel, gbc); + + // Username + gbc.gridy++; + gbc.gridwidth = 1; + panel.add(new JLabel("Username:"), gbc); + gbc.gridx = 1; + usernameField = new JTextField(15); + panel.add(usernameField, gbc); + + // Password + gbc.gridy++; + gbc.gridx = 0; + panel.add(new JLabel("Password:"), gbc); + gbc.gridx = 1; + passwordField = new JPasswordField(15); + panel.add(passwordField, gbc); + + // Login button + gbc.gridy++; + gbc.gridx = 0; + gbc.gridwidth = 2; + JButton loginButton = new JButton("Login"); + loginButton.addActionListener(e -> handleLogin()); + panel.add(loginButton, gbc); + + // Switch to signup + gbc.gridy++; + JLabel signupPrompt = new JLabel("Don't have an account? "); + JButton switchToSignup = new JButton("Sign Up"); + switchToSignup.setBorderPainted(false); + switchToSignup.setContentAreaFilled(false); + switchToSignup.setForeground(Color.BLUE); + switchToSignup.setCursor(new Cursor(Cursor.HAND_CURSOR)); + switchToSignup.addActionListener(e -> cardLayout.show(cardPanel, "signup")); + + JPanel switchPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); + switchPanel.add(signupPrompt); + switchPanel.add(switchToSignup); + panel.add(switchPanel, gbc); + + // Add enter key listener for login + passwordField.addActionListener(e -> handleLogin()); + + return panel; + } + + private JPanel createSignupPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.insets = new Insets(5, 5, 5, 5); + gbc.fill = GridBagConstraints.HORIZONTAL; + + // Title + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 2; + JLabel titleLabel = new JLabel("Create Account", JLabel.CENTER); + titleLabel.setFont(new Font("Arial", Font.BOLD, 18)); + panel.add(titleLabel, gbc); + + // Username + gbc.gridy++; + gbc.gridwidth = 1; + panel.add(new JLabel("Choose a username:"), gbc); + gbc.gridx = 1; + signupUsernameField = new JTextField(15); + panel.add(signupUsernameField, gbc); + + // Password + gbc.gridy++; + gbc.gridx = 0; + panel.add(new JLabel("Choose a password:"), gbc); + gbc.gridx = 1; + signupPasswordField = new JPasswordField(15); + panel.add(signupPasswordField, gbc); + + // Sign Up button + gbc.gridy++; + gbc.gridx = 0; + gbc.gridwidth = 2; + JButton signupButton = new JButton("Sign Up"); + signupButton.addActionListener(e -> handleSignup()); + panel.add(signupButton, gbc); + + // Switch to login + gbc.gridy++; + JLabel loginPrompt = new JLabel("Already have an account? "); + JButton switchToLogin = new JButton("Login"); + switchToLogin.setBorderPainted(false); + switchToLogin.setContentAreaFilled(false); + switchToLogin.setForeground(Color.BLUE); + switchToLogin.setCursor(new Cursor(Cursor.HAND_CURSOR)); + switchToLogin.addActionListener(e -> { + cardLayout.show(cardPanel, "login"); + clearFields(); + }); + + JPanel switchPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); + switchPanel.add(loginPrompt); + switchPanel.add(switchToLogin); + panel.add(switchPanel, gbc); + + return panel; + } + + private void handleLogin() { + String username = usernameField.getText().trim(); + String password = new String(passwordField.getPassword()); + + if (username.isEmpty() || password.isEmpty()) { + showStatus("Please fill in all fields", Color.RED); + return; + } + + if (loginService.login(username, password)) { + showStatus("Login successful! Opening dashboard...", new Color(0, 150, 0)); + + // --- NEW DASHBOARD LOGIC --- + new Thread(() -> { + try { + Thread.sleep(500); // Small delay for status message to register + SwingUtilities.invokeLater(() -> { + // 1. Dispose of the current login window + this.dispose(); + // 2. Open the main application dashboard + Dashboard dashboard = new Dashboard(username); + dashboard.setVisible(true); + }); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + }).start(); + // --- END NEW DASHBOARD LOGIC --- + + } else { + showStatus("Invalid username or password", Color.RED); + } + } + + private void handleSignup() { + String username = signupUsernameField.getText().trim(); + String password = new String(signupPasswordField.getPassword()); + + if (username.isEmpty() || password.isEmpty()) { + showStatus("Please fill in all fields", Color.RED); + return; + } + + if (loginService.signUp(username, password)) { + showStatus("Account created successfully!", new Color(0, 150, 0)); + // Switch back to login after a short delay + new Thread(() -> { + try { + Thread.sleep(1000); + SwingUtilities.invokeLater(() -> { + cardLayout.show(cardPanel, "login"); + clearFields(); + }); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + }).start(); + } else { + showStatus("Username already exists", Color.RED); + } + } + + private void showStatus(String message, Color color) { + statusLabel.setForeground(color); + statusLabel.setText(message); + } + + private void clearFields() { + usernameField.setText(""); + passwordField.setText(""); + signupUsernameField.setText(""); + signupPasswordField.setText(""); + statusLabel.setText(" "); + } -public class Main { public static void main(String[] args) { - AppBuilder appBuilder = new AppBuilder(); - JFrame application = appBuilder - .addLoginView() - .addSignupView() - .addLoggedInView() - .addSignupUseCase() - .addLoginUseCase() - .addChangePasswordUseCase() - .build(); - - application.pack(); - application.setLocationRelativeTo(null); - application.setVisible(true); - } -} + // Set look and feel for better appearance + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + e.printStackTrace(); + } + + // Create and show the login window + SwingUtilities.invokeLater(() -> { + Main loginWindow = new Main(); + loginWindow.setVisible(true); + }); + } +} \ No newline at end of file diff --git a/src/main/java/app/StockPage.java b/src/main/java/app/StockPage.java new file mode 100644 index 00000000..3e3b37e0 --- /dev/null +++ b/src/main/java/app/StockPage.java @@ -0,0 +1,272 @@ +package app; + +import okhttp3.*; + +import javax.swing.*; +import java.awt.*; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A JPanel containing the real-time trade dashboard UI and WebSocket logic. + * This component is embedded within the main Dashboard JFrame. + * NOTE: This version uses manual string parsing instead of a JSON library. + */ +public class StockPage extends JPanel { // Class name changed from DashboardContent + + private static final Logger LOGGER = Logger.getLogger(StockPage.class.getName()); // Logger updated + + // Replace with your actual Finnhub API Key + private static final String API_KEY = "d4977ehr01qshn3kvpt0d4977ehr01qshn3kvptg"; + private static final String WEB_SOCKET_URL = "wss://ws.finnhub.io?token=" + API_KEY; + private static final String DEFAULT_SYMBOL = "BINANCE:BTCUSDT"; // Example symbol + + // GUI Elements for display + private final JLabel symbolLabel = new JLabel("---"); + private final JLabel priceLabel = new JLabel("---"); + private final JLabel volumeLabel = new JLabel("---"); + private final JLabel timestampLabel = new JLabel("---"); + private final JLabel statusLabel = new JLabel("Status: Disconnected", SwingConstants.CENTER); + + private WebSocket webSocket; + private final JButton connectButton = new JButton("Connect"); + + /** + * Initializes the GUI components. + */ + public StockPage() { // Constructor name changed + setLayout(new BorderLayout()); + setBorder(BorderFactory.createTitledBorder("Real-Time Trade Data")); + + // --- Header (Status) --- + statusLabel.setFont(new Font("Arial", Font.BOLD, 14)); + statusLabel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + add(statusLabel, BorderLayout.NORTH); + + // --- Main Data Panel --- + JPanel dataPanel = new JPanel(new GridLayout(4, 2, 10, 10)); + dataPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + + // Helper method to style the value labels + styleLabel(symbolLabel); + styleLabel(priceLabel); + styleLabel(volumeLabel); + styleLabel(timestampLabel); + + dataPanel.add(new JLabel("Symbol:")); + dataPanel.add(symbolLabel); + + dataPanel.add(new JLabel("Last Price (P):")); + dataPanel.add(priceLabel); + + dataPanel.add(new JLabel("Volume (V):")); + dataPanel.add(volumeLabel); + + dataPanel.add(new JLabel("Timestamp (T):")); + dataPanel.add(timestampLabel); + + add(dataPanel, BorderLayout.CENTER); + + // --- Control Panel (Connect/Disconnect) --- + JPanel controlPanel = new JPanel(); + connectButton.addActionListener(e -> toggleConnection()); + controlPanel.add(connectButton); + add(controlPanel, BorderLayout.SOUTH); + } + + /** + * Helper to apply basic styling to the data display labels. + */ + private void styleLabel(JLabel label) { + label.setFont(new Font("Monospaced", Font.BOLD, 18)); + label.setForeground(new Color(0, 128, 0)); // Green color for data + } + + /** + * Toggles the WebSocket connection status. + */ + private void toggleConnection() { + if (webSocket != null) { + closeWebSocket(); + } else { + connectWebSocket(); + } + } + + /** + * Gracefully closes the WebSocket connection. + */ + public void closeWebSocket() { + if (webSocket != null) { + LOGGER.info("Closing existing connection..."); + webSocket.close(1000, "User initiated disconnect"); + webSocket = null; + // Also update GUI elements + SwingUtilities.invokeLater(() -> { + statusLabel.setText("Status: Disconnected"); + statusLabel.setForeground(Color.BLACK); + connectButton.setText("Connect"); + connectButton.setEnabled(true); + }); + } + } + + /** + * Connects to the WebSocket and sends subscription messages. + */ + private void connectWebSocket() { + OkHttpClient client = new OkHttpClient.Builder() + .readTimeout(0, TimeUnit.MILLISECONDS) + .build(); + + Request request = new Request.Builder().url(WEB_SOCKET_URL).build(); + + statusLabel.setText("Status: Connecting..."); + statusLabel.setForeground(Color.ORANGE); + connectButton.setText("Connecting..."); + connectButton.setEnabled(false); + + webSocket = client.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket ws, Response response) { + LOGGER.info("WebSocket Opened. Subscribing to " + DEFAULT_SYMBOL); + + String subscribeMsg = String.format("{\"type\":\"subscribe\",\"symbol\":\"%s\"}", DEFAULT_SYMBOL); + ws.send(subscribeMsg); + + SwingUtilities.invokeLater(() -> { + statusLabel.setText("Status: Connected to " + DEFAULT_SYMBOL); + statusLabel.setForeground(new Color(0, 128, 0)); + connectButton.setText("Disconnect"); + connectButton.setEnabled(true); + }); + } + + @Override + public void onMessage(WebSocket ws, String text) { + processMessage(text); + } + + @Override + public void onClosing(WebSocket ws, int code, String reason) { + LOGGER.info("WebSocket Closing: Code " + code + ", Reason: " + reason); + } + + @Override + public void onFailure(WebSocket ws, Throwable t, Response response) { + LOGGER.log(Level.SEVERE, "WebSocket Failure: " + t.getMessage(), t); + closeWebSocket(); // Attempt to clean up resources + SwingUtilities.invokeLater(() -> { + statusLabel.setText("Status: Failure! Check API Key/Console."); + statusLabel.setForeground(Color.RED); + connectButton.setText("Connect"); + connectButton.setEnabled(true); + }); + } + + @Override + public void onClosed(WebSocket ws, int code, String reason) { + LOGGER.info("WebSocket Closed."); + webSocket = null; + SwingUtilities.invokeLater(() -> { + statusLabel.setText("Status: Disconnected"); + statusLabel.setForeground(Color.BLACK); + connectButton.setText("Connect"); + connectButton.setEnabled(true); + }); + } + }); + } + + /** + * Extracts a value from a JSON string using basic String methods. + */ + private String extractValue(String json, String key) { + String keySearch = "\"" + key + "\":"; + int keyIndex = json.indexOf(keySearch); + + if (keyIndex == -1) { + return null; + } + + int valueStart = keyIndex + keySearch.length(); + + // Check for string value (starts with double quote) + char firstChar = json.charAt(valueStart); + if (firstChar == '"') { + valueStart++; + int valueEnd = json.indexOf('"', valueStart); + if (valueEnd != -1) { + return json.substring(valueStart, valueEnd); + } + } + + // Logic for numerical/boolean values + else { + // Find the index of the immediate next JSON delimiter: ',', '}', or ']' + int indexComma = json.indexOf(',', valueStart); + int indexCurly = json.indexOf('}', valueStart); + int indexBracket = json.indexOf(']', valueStart); + + // Start with a large index to find the minimum + int terminationIndex = Integer.MAX_VALUE; + + // Find the minimum positive index among the delimiters + if (indexComma != -1 && indexComma > valueStart) terminationIndex = Math.min(terminationIndex, indexComma); + if (indexCurly != -1 && indexCurly > valueStart) terminationIndex = Math.min(terminationIndex, indexCurly); + if (indexBracket != -1 && indexBracket > valueStart) terminationIndex = Math.min(terminationIndex, indexBracket); + + // If a valid termination character was found + if (terminationIndex != Integer.MAX_VALUE) { + // substring is exclusive of the end index, correctly extracting the value + return json.substring(valueStart, terminationIndex).trim(); + } + } + return null; + } + + /** + * Parses the incoming JSON message using manual string manipulation and updates the GUI. + */ + private void processMessage(String jsonMessage) { + try { + if (jsonMessage.contains("\"type\":\"trade\"")) { + + String symbol = extractValue(jsonMessage, "s"); + String priceStr = extractValue(jsonMessage, "p"); + double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; + + String volumeStr = extractValue(jsonMessage, "v"); + double volume = (volumeStr != null) ? Double.parseDouble(volumeStr) : 0.0; + + String timestampStr = extractValue(jsonMessage, "t"); + long timestamp = (timestampStr != null) ? Long.parseLong(timestampStr) : 0L; + + SwingUtilities.invokeLater(() -> { + symbolLabel.setText(symbol != null ? symbol : "N/A"); + priceLabel.setText(String.format("$%,.2f", price)); + volumeLabel.setText(String.format("%,.4f", volume)); + + if (timestamp > 0) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + String time = Instant.ofEpochMilli(timestamp) + .atZone(ZoneId.systemDefault()) + .toLocalTime() + .format(formatter); + timestampLabel.setText(time); + } else { + timestampLabel.setText("N/A"); + } + }); + } + } catch (NumberFormatException e) { + LOGGER.log(Level.WARNING, "Error processing JSON message: " + e.getMessage()); + } catch (Exception e) { + LOGGER.log(Level.WARNING, "General error in processMessage: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/app/User.java b/src/main/java/app/User.java new file mode 100644 index 00000000..03cef6f7 --- /dev/null +++ b/src/main/java/app/User.java @@ -0,0 +1,29 @@ +package App; + +public class User { + private final String username; + private String password; // In a real application, this should be hashed + private boolean isLoggedIn; + + public User(String username, String password) { + this.username = username; + this.password = password; + this.isLoggedIn = false; + } + + public String getUsername() { + return username; + } + + public boolean checkPassword(String password) { + return this.password.equals(password); + } + + public boolean isLoggedIn() { + return isLoggedIn; + } + + public void setLoggedIn(boolean loggedIn) { + isLoggedIn = loggedIn; + } +} \ No newline at end of file From 01f500dda3f5a064d0b68de87c18cfc476d10db0 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Mon, 24 Nov 2025 18:41:33 -0500 Subject: [PATCH 010/103] incomplete filter search data access, complete Stock entity --- src/main/java/app/AppBuilder.java | 2 +- src/main/java/app/Main.java | 2 + .../FilterSearchDataAccessObject.java | 140 ++++++++++++++++++ src/main/java/entity/Stock.java | 49 ++++++ .../filter_search/FilterSearchController.java | 2 +- .../FilterSearchDataAccessInterface.java | 10 ++ .../filter_search/FilterSearchRequest.java | 32 ++++ .../filter_search/FilterSearchResponse.java | 17 +++ src/main/java/view/FilterSearchView.java | 49 +++++- 9 files changed, 294 insertions(+), 9 deletions(-) create mode 100644 src/main/java/data_access/FilterSearchDataAccessObject.java create mode 100644 src/main/java/entity/Stock.java create mode 100644 src/main/java/use_case/filter_search/FilterSearchDataAccessInterface.java create mode 100644 src/main/java/use_case/filter_search/FilterSearchRequest.java create mode 100644 src/main/java/use_case/filter_search/FilterSearchResponse.java diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 84cfdba7..7497f260 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -161,7 +161,7 @@ public JFrame build() { application.add(cardPanel); - viewManagerModel.setState(signupView.getViewName()); + viewManagerModel.setState(filterSearchView.getViewName()); viewManagerModel.firePropertyChange(); return application; diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 424404fb..4747b9b8 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -9,9 +9,11 @@ public static void main(String[] args) { .addLoginView() .addSignupView() .addLoggedInView() + .addFilterSearchView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() + .addFilterSearchUseCase() .build(); application.pack(); diff --git a/src/main/java/data_access/FilterSearchDataAccessObject.java b/src/main/java/data_access/FilterSearchDataAccessObject.java new file mode 100644 index 00000000..43a8c21b --- /dev/null +++ b/src/main/java/data_access/FilterSearchDataAccessObject.java @@ -0,0 +1,140 @@ +package data_access; + +import entity.Stock; +import use_case.filter_search.FilterSearchDataAccessInterface; +import org.json.JSONObject; +import org.json.JSONArray; +import org.json.JSONTokener; + + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import java.io.IOException; +import java.util.List; + +public class FilterSearchDataAccessObject implements FilterSearchDataAccessInterface { + private static final String BASE_URL = "https://finnhub.io/api/v1"; + + private final String apiKey; + + public FilterSearchDataAccessObject(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public List loadStocks(String exchange, String mic, String securityType, String currency) throws Exception { + if (exchange == null || exchange.isBlank()) { + throw new IllegalArgumentException("Exchange must not be empty."); + } + StringBuilder urlBuilder = new StringBuilder(BASE_URL) + .append("/stock") + .append("/symbol?exchange=").append(exchange); + + if (mic != null && !mic.isBlank()) { + urlBuilder.append("&mic=").append(mic); + } + + if (securityType != null && !securityType.isBlank()) { + urlBuilder.append("&type=").append(securityType); + } + + if (currency != null && !currency.isBlank()) { + urlBuilder.append("¤cy=").append(currency); + } + + urlBuilder.append("&token=").append(apiKey); + + JSONArray array = fetchJsonArray(urlBuilder.toString()); + return parseStockArray(array); + } + + private JSONArray fetchJsonArray(String urlString) throws IOException { + HttpURLConnection connection = null; + InputStream inputStream = null; + + try { + URL url = new URL(urlString); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + // Possible timeout + connection.setConnectTimeout(10_000); + connection.setReadTimeout(10_000); + + int status = connection.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + // Read errors + String errorBody = readStream(connection.getErrorStream()); + throw new IOException("HTTP error " + status + " from Finnhub: " + errorBody); + } + + inputStream = connection.getInputStream(); + JSONTokener tokener = new JSONTokener(inputStream); + return new JSONArray(tokener); + + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ignored) {} + } + if (connection != null) { + connection.disconnect(); + } + } + } + + private String readStream(InputStream stream) throws IOException { + if (stream == null) return ""; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(stream, StandardCharsets.UTF_8))) { + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString(); + } + } + + private List parseStockArray(JSONArray array) { + List result = new ArrayList<>(); + if (array == null) { + return result; + } + + for (int i = 0; i < array.length(); i++) { + JSONObject obj = array.optJSONObject(i); + if (obj == null) continue; + + String currency = obj.optString("currency", "US"); + String description = obj.optString("description", ""); + String displaySymbol = obj.optString("displaySymbol", ""); + String figi = obj.optString("figi", ""); + String symbol = obj.optString("symbol", ""); + String mic = obj.optString("mic", ""); + String type = obj.optString("type", ""); + + Stock stock = new Stock( + currency, + description, + displaySymbol, + figi, + symbol, + mic, + type + ); + result.add(stock); + } +} + diff --git a/src/main/java/entity/Stock.java b/src/main/java/entity/Stock.java new file mode 100644 index 00000000..587dffa7 --- /dev/null +++ b/src/main/java/entity/Stock.java @@ -0,0 +1,49 @@ +package entity; + +import use_case.filter_search.FilterSearchInputData; + +public class Stock { + private final String mic; + private final String type; + private final String currency; + private final String description; + private final String displaySymbol; + private final String figi; + private final String symbol; + + public Stock(String currency, String description, String displaySymbol, + String figi, String mic, String symbol, String type) { + this.currency = currency; + this.description = description; + this.displaySymbol = displaySymbol; + this.mic = mic; + this.type = type; + this.symbol = symbol; + this.figi = figi; + + } + + public String getMic() { + return mic; + } + + public String getType() { + return type; + } + + public String getCurrency() { + return currency; + } + + public String getDescription() { + return description; + } + + public String getDisplaySymbol() { + return displaySymbol; + } + + public String getFigi() { + return figi; + } +} diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchController.java b/src/main/java/interface_adapter/filter_search/FilterSearchController.java index 76693514..1793d530 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchController.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchController.java @@ -17,7 +17,7 @@ public FilterSearchController(FilterSearchInputBoundary filterSearchInputBoundar * @param securityType the security type * @param currency the currency */ - public void execute(String exchange, String mic, String securityType, String currency) { + public void loadFilterSearch(String exchange, String mic, String securityType, String currency) { final FilterSearchInputData filterSearchInputData = new FilterSearchInputData(exchange, mic, securityType, currency); filterSearchInputBoundary.execute(filterSearchInputData); diff --git a/src/main/java/use_case/filter_search/FilterSearchDataAccessInterface.java b/src/main/java/use_case/filter_search/FilterSearchDataAccessInterface.java new file mode 100644 index 00000000..e0b2eb2b --- /dev/null +++ b/src/main/java/use_case/filter_search/FilterSearchDataAccessInterface.java @@ -0,0 +1,10 @@ +package use_case.filter_search; + +import entity.Stock; + +import java.util.List; + +public interface FilterSearchDataAccessInterface { + List loadStocks(String exchange, String mic, String securitytype, String currency) throws Exception; +} + diff --git a/src/main/java/use_case/filter_search/FilterSearchRequest.java b/src/main/java/use_case/filter_search/FilterSearchRequest.java new file mode 100644 index 00000000..61b68f0b --- /dev/null +++ b/src/main/java/use_case/filter_search/FilterSearchRequest.java @@ -0,0 +1,32 @@ +package use_case.filter_search; + +public class FilterSearchRequest { + private final String exchange; + private final String mic; + private final String securityType; + private final String currency; + + public FilterSearchRequest(String exchange) { + this.exchange = exchange; + this.mic = null; + this.securityType = null; + this.currency = null; + } + + public FilterSearchRequest(String exchange, String mic, String securityType, String currency) { + this.exchange = exchange; + this.mic = mic; + this.securityType = securityType; + this.currency = currency; + + } + + public String getExchange() {return this.exchange;} + + public String getMic() {return this.mic;} + + public String getSecurityType() {return this.securityType;} + + public String getCurrency() {return this.currency;} + +} diff --git a/src/main/java/use_case/filter_search/FilterSearchResponse.java b/src/main/java/use_case/filter_search/FilterSearchResponse.java new file mode 100644 index 00000000..9e3a2e6b --- /dev/null +++ b/src/main/java/use_case/filter_search/FilterSearchResponse.java @@ -0,0 +1,17 @@ +package use_case.filter_search; + +import entity.Stock; + +import java.util.List; + +public class FilterSearchResponse { + private final List result; + + public FilterSearchResponse(List result) { + this.result = result; + } + + public List getResults() { + return this.result; + } +} diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java index dd3ce2bd..e1e5c6be 100644 --- a/src/main/java/view/FilterSearchView.java +++ b/src/main/java/view/FilterSearchView.java @@ -29,7 +29,8 @@ public class FilterSearchView extends JPanel implements ActionListener, Property private final JLabel mic = new JLabel("MIC:"); private final JLabel securityType = new JLabel("Security Type:"); private final JLabel currency = new JLabel("Currency:"); - private final Button search; + private final JButton search; + private final JButton backToHomeButton; private final String[] exchangeOptions = {"AD" , "AS" , "AT" , "AX" , "BA" , "BC" , "BD" , "BE" , "BH" , "BK" , "BO" , "BR" , @@ -159,7 +160,12 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { final JComboBox micDrop = new JComboBox(micOptions); final JComboBox securityDrop = new JComboBox(securityOptions); final JComboBox currencyDrop = new JComboBox(currencyOptions); - this.search = new Button("Search"); + this.search = new JButton("Search"); + + this.setLayout(new BorderLayout()); + + JPanel topPanel = new JPanel(); + this.add(topPanel, BorderLayout.NORTH); JPanel panel_e = new JPanel(); panel_e.add(exchange); @@ -177,11 +183,39 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { panel_c.add(currency); panel_c.add(currencyDrop); - this.add(panel_e, BorderLayout.PAGE_START); - this.add(panel_m, BorderLayout.PAGE_START); - this.add(panel_s, BorderLayout.PAGE_START); - this.add(panel_c, BorderLayout.PAGE_START); - this.add(search, BorderLayout.PAGE_START); + JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + backToHomeButton = new JButton("Back"); + leftPanel.add(backToHomeButton); + topPanel.add(leftPanel, BorderLayout.WEST); + + topPanel.add(panel_e, BorderLayout.PAGE_START); + topPanel.add(panel_m, BorderLayout.PAGE_START); + topPanel.add(panel_s, BorderLayout.PAGE_START); + topPanel.add(panel_c, BorderLayout.PAGE_START); + topPanel.add(search, BorderLayout.PAGE_START); + + JPanel mainPanel = new JPanel(); + + + + + backToHomeButton.addActionListener(e -> { + // TODO: implement navigation back to the main/home view. + // For now, you can leave this empty or show a message: + JOptionPane.showMessageDialog( + this, + "Back to Home is not implemented yet.", + "Info", + JOptionPane.INFORMATION_MESSAGE + ); + }); + + search.addActionListener(e -> { + filterSearchController.execute(exchangeDrop.getSelectedItem().toString(), + micDrop.getSelectedItem().toString(), securityDrop.getSelectedItem().toString(), + currencyDrop.getSelectedItem().toString()); + + }); @@ -207,4 +241,5 @@ public void propertyChange(PropertyChangeEvent evt) { } + } From 6673cb8ea54bacdcea9cdbb6632e6f30751bd009 Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Tue, 25 Nov 2025 12:03:00 -0500 Subject: [PATCH 011/103] earning --- src/main/java/app/Dashboard.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/app/Dashboard.java b/src/main/java/app/Dashboard.java index feaecea8..70594beb 100644 --- a/src/main/java/app/Dashboard.java +++ b/src/main/java/app/Dashboard.java @@ -64,7 +64,7 @@ private JPanel createNavigationBar() { gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.CENTER; - String[] buttonNames = {"Home (Real-Time Trade)", "Portfolio", "Watchlist", "Settings"}; + String[] buttonNames = {"Home (Real-Time Trade)", "Portfolio", "Watchlist", "Company Earnings History", "Settings"}; // Create and add the four main navigation buttons for (int i = 0; i < buttonNames.length; i++) { @@ -120,6 +120,7 @@ private void setupMainContent() { mainContentPanel.add(createPlaceholderPanel("Portfolio View", Color.LIGHT_GRAY), "Portfolio"); mainContentPanel.add(createPlaceholderPanel("Watchlist Management", Color.CYAN), "Watchlist"); mainContentPanel.add(createPlaceholderPanel("User Settings", Color.YELLOW), "Settings"); + mainContentPanel.add(createPlaceholderPanel("Company Earning History", Color.YELLOW), "Earnings"); // Show the Home view by default cardLayout.show(mainContentPanel, "Home"); From d71800be3877e99e090f1c56612943590530e132 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Thu, 27 Nov 2025 17:16:24 -0500 Subject: [PATCH 012/103] incomplete filter search data access, complete Stock entity --- .../FilterSearchDataAccessObject.java | 2 ++ .../filter_search/FilterSearchController.java | 9 ++++--- .../FilterSearchInputBoundary.java | 6 ++--- src/main/java/view/FilterSearchView.java | 26 ++++++++++++++----- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/main/java/data_access/FilterSearchDataAccessObject.java b/src/main/java/data_access/FilterSearchDataAccessObject.java index 43a8c21b..c151e436 100644 --- a/src/main/java/data_access/FilterSearchDataAccessObject.java +++ b/src/main/java/data_access/FilterSearchDataAccessObject.java @@ -136,5 +136,7 @@ private List parseStockArray(JSONArray array) { ); result.add(stock); } + return result; + } } diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchController.java b/src/main/java/interface_adapter/filter_search/FilterSearchController.java index 1793d530..4bd8ce40 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchController.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchController.java @@ -2,12 +2,13 @@ import use_case.filter_search.FilterSearchInputBoundary; import use_case.filter_search.FilterSearchInputData; +import use_case.filter_search.FilterSearchRequest; public class FilterSearchController { - private final FilterSearchInputBoundary filterSearchInputBoundary; + private final FilterSearchInputBoundary interactor; public FilterSearchController(FilterSearchInputBoundary filterSearchInputBoundary) { - this.filterSearchInputBoundary = filterSearchInputBoundary; + this.interactor = filterSearchInputBoundary; } /** @@ -18,8 +19,8 @@ public FilterSearchController(FilterSearchInputBoundary filterSearchInputBoundar * @param currency the currency */ public void loadFilterSearch(String exchange, String mic, String securityType, String currency) { - final FilterSearchInputData filterSearchInputData = new FilterSearchInputData(exchange, mic, securityType, currency); + final FilterSearchRequest request = new FilterSearchRequest(exchange, mic, securityType, currency); - filterSearchInputBoundary.execute(filterSearchInputData); + interactor.execute(request); } } diff --git a/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java b/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java index 1016ab44..dea54206 100644 --- a/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java +++ b/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java @@ -1,7 +1,5 @@ package use_case.filter_search; -import use_case.login.LoginInputData; - /** * Input Boundary for the Filter Search Use Case. */ @@ -9,7 +7,7 @@ public interface FilterSearchInputBoundary { /** * Executes the filter search use case. - * @param filterSearchInputData the input data + * @param request the input data */ - void execute(FilterSearchInputData filterSearchInputData); + void execute(FilterSearchRequest request); } diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java index e1e5c6be..23c01bf1 100644 --- a/src/main/java/view/FilterSearchView.java +++ b/src/main/java/view/FilterSearchView.java @@ -40,7 +40,7 @@ public class FilterSearchView extends JPanel implements ActionListener, Property "OL" , "OM" , "PA" , "PM" , "PR" , "QA" , "RO" , "RG" , "SA" , "SG" , "SI" , "SN" , "SR" , "SS" , "ST" , "SW" , "SZ" , "T" , "TA" , "TL" , "TO" , "TW" , "TWO" , "US" , "V" , "VI" , "VN" , "VS" , "WA" , "HA" , "SX" , "TG" , "SC"}; - private final String[] micOptions = {"XADS", "XAMS", "ASEX", "XASX", "XBUE", "XBOG", "XBUD", "XBER", "XBAH", "XBKK", + private final String[] micOptions = {null, "XADS", "XAMS", "ASEX", "XASX", "XBUE", "XBOG", "XBUD", "XBER", "XBAH", "XBKK", "XBOM", "XBRU", "XCAI", "XCNQ", "XCSE", "BVCA", "XCAS", "XDFM", "XETR", "XDUS", "XFRA", "XHEL", "XHKG", "XHAM", "XICE", "XDUB", "XIST", "XIDX", "XJSE", "XKLS", "XKOS", "XKRX", "XKUW", "XLON", "XLIS", "XMAD", "MISX", "XMIL", "XMAL", "XMUN", "XMEX", "NEOE", "XNSA", "XNSE", "XNZE", "XOSL", "XMUS", "XPAR", "XPHS", @@ -48,7 +48,7 @@ public class FilterSearchView extends JPanel implements ActionListener, Property "WJPX", "XTAE", "XTAL", "XTSE", "XTAI", "FNSE", "ROCO", "XTSX", "XWBO", "HSTC", "XLIT", "XWAR", "XHAN", "XNYS", "XGAT", "XASE", "BATS", "ARCX", "XNMS", "XNCM", "XNGS", "IEXG", "XNAS", "OTCM", "OOTC", "XSTC", "XHNX", "FNIS", "FNDK", "NFI"}; - private final String[] securityOptions = {"ABS Auto", "ABS Card", "ABS Home", "ABS Other", "ACCEPT BANCARIA", + private final String[] securityOptions = {null, "ABS Auto", "ABS Card", "ABS Home", "ABS Other", "ACCEPT BANCARIA", "ADJ CONV. TO FIXED", "ADJ CONV. TO FIXED, OID", "ADJUSTABLE", "ADJUSTABLE, OID", "ADR", "Agncy ABS Home", "Agncy ABS Other", "Agncy CMBS", "Agncy CMO FLT", "Agncy CMO INV", "Agncy CMO IO", "Agncy CMO Other", "Agncy CMO PO", "Agncy CMO Z", "Asset-Based", "ASSET-BASED BRIDGE", "ASSET-BASED BRIDGE REV", @@ -123,7 +123,7 @@ public class FilterSearchView extends JPanel implements ActionListener, Property "UNITRANCHE REV" , "UNITRANCHE TERM" , "US DOMESTIC" , "US GOVERNMENT" , "US NON-DOLLAR" , "VAR RATE DEM OBL" , "VAT-TRNCH" , "VENEZUELAN CP" , "VIETNAMESE CD" , "VOLATILITY DERIVATIVE" , "Warrant", "YANKEE" , "YANKEE CD" , "YEN CD" , "YEN CP" , "Yield Curve" , "ZERO COUPON" , "ZERO COUPON, OID"}; - private final String[] currencyOptions = {"ADP", "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "ATS", "AUD", + private final String[] currencyOptions = {null, "ADP", "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "ATS", "AUD", "AUd", "AWG", "AZM", "AZN", "BAM", "BBD", "BDT", "BEF", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BRl", "BSD", "BTN", "BWP", "BWp", "BYN", "BYR", "BYS", "BZD", "CAD", "CAd", "CDF", "CER", "CHF", "CHf", "CLF", "CLP", "CNH", "CNT", "CNY", "COP", "COU", "CRC", "CRS", "CUP", "CVE", "CYP", "CZK", "DEM", "DJF", @@ -211,9 +211,23 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { }); search.addActionListener(e -> { - filterSearchController.execute(exchangeDrop.getSelectedItem().toString(), - micDrop.getSelectedItem().toString(), securityDrop.getSelectedItem().toString(), - currencyDrop.getSelectedItem().toString()); + String ex = exchangeDrop.getSelectedItem().toString(); + String mi = null; + String sec = null; + String curr = null; + if (micDrop.getSelectedItem() != null) { + mi = micDrop.getSelectedItem().toString(); + } + + if (securityDrop.getSelectedItem() != null) { + sec = securityDrop.getSelectedItem().toString(); + } + + if (currencyDrop.getSelectedItem() != null) { + curr = currencyDrop.getSelectedItem().toString(); + } + + filterSearchController.loadFilterSearch(ex , mi, sec, curr); }); From 2394f4b5a67388d41d751ef72289e3d74d43b563 Mon Sep 17 00:00:00 2001 From: samario Date: Sat, 29 Nov 2025 15:46:54 -0500 Subject: [PATCH 013/103] Standardize the naming of packages --- src/main/java/data_access/NewsDataAccessObject.java | 2 +- .../{NewsPage => news}/NewsController.java | 6 +++--- .../{NewsPage => news}/NewsPresenter.java | 7 +++---- .../{NewsPage => news}/NewsViewModel.java | 2 +- .../use_case/{News => news}/NewsDataAccessInterface.java | 2 +- .../java/use_case/{News => news}/NewsInputBoundary.java | 2 +- src/main/java/use_case/{News => news}/NewsInteractor.java | 2 +- .../java/use_case/{News => news}/NewsOutputBoundary.java | 2 +- .../java/use_case/{News => news}/NewsRequestModel.java | 2 +- .../java/use_case/{News => news}/NewsResponseModel.java | 2 +- src/main/java/view/NewsView.java | 4 ++-- .../java/use_case/{News => news}/NewsInteractorTest.java | 2 +- 12 files changed, 17 insertions(+), 18 deletions(-) rename src/main/java/interface_adapter/{NewsPage => news}/NewsController.java (87%) rename src/main/java/interface_adapter/{NewsPage => news}/NewsPresenter.java (82%) rename src/main/java/interface_adapter/{NewsPage => news}/NewsViewModel.java (97%) rename src/main/java/use_case/{News => news}/NewsDataAccessInterface.java (95%) rename src/main/java/use_case/{News => news}/NewsInputBoundary.java (93%) rename src/main/java/use_case/{News => news}/NewsInteractor.java (98%) rename src/main/java/use_case/{News => news}/NewsOutputBoundary.java (89%) rename src/main/java/use_case/{News => news}/NewsRequestModel.java (98%) rename src/main/java/use_case/{News => news}/NewsResponseModel.java (94%) rename src/test/java/use_case/{News => news}/NewsInteractorTest.java (99%) diff --git a/src/main/java/data_access/NewsDataAccessObject.java b/src/main/java/data_access/NewsDataAccessObject.java index 3673427b..7c79a53d 100644 --- a/src/main/java/data_access/NewsDataAccessObject.java +++ b/src/main/java/data_access/NewsDataAccessObject.java @@ -1,7 +1,7 @@ package data_access; import entity.NewsArticle; -import use_case.News.NewsDataAccessInterface; +import use_case.news.NewsDataAccessInterface; import org.json.JSONObject; import org.json.JSONArray; diff --git a/src/main/java/interface_adapter/NewsPage/NewsController.java b/src/main/java/interface_adapter/news/NewsController.java similarity index 87% rename from src/main/java/interface_adapter/NewsPage/NewsController.java rename to src/main/java/interface_adapter/news/NewsController.java index cd370962..c77052f4 100644 --- a/src/main/java/interface_adapter/NewsPage/NewsController.java +++ b/src/main/java/interface_adapter/news/NewsController.java @@ -1,7 +1,7 @@ -package interface_adapter.NewsPage; +package interface_adapter.news; -import use_case.News.NewsInputBoundary; -import use_case.News.NewsRequestModel; +import use_case.news.NewsInputBoundary; +import use_case.news.NewsRequestModel; /** * read from UI diff --git a/src/main/java/interface_adapter/NewsPage/NewsPresenter.java b/src/main/java/interface_adapter/news/NewsPresenter.java similarity index 82% rename from src/main/java/interface_adapter/NewsPage/NewsPresenter.java rename to src/main/java/interface_adapter/news/NewsPresenter.java index db5237aa..add0a3c5 100644 --- a/src/main/java/interface_adapter/NewsPage/NewsPresenter.java +++ b/src/main/java/interface_adapter/news/NewsPresenter.java @@ -1,8 +1,7 @@ -package interface_adapter.NewsPage; +package interface_adapter.news; -import use_case.News.NewsInputBoundary; -import use_case.News.NewsOutputBoundary; -import use_case.News.NewsResponseModel; +import use_case.news.NewsOutputBoundary; +import use_case.news.NewsResponseModel; /** * receives output data from interactor diff --git a/src/main/java/interface_adapter/NewsPage/NewsViewModel.java b/src/main/java/interface_adapter/news/NewsViewModel.java similarity index 97% rename from src/main/java/interface_adapter/NewsPage/NewsViewModel.java rename to src/main/java/interface_adapter/news/NewsViewModel.java index ce0d1358..794b56c8 100644 --- a/src/main/java/interface_adapter/NewsPage/NewsViewModel.java +++ b/src/main/java/interface_adapter/news/NewsViewModel.java @@ -1,4 +1,4 @@ -package interface_adapter.NewsPage; +package interface_adapter.news; import entity.NewsArticle; diff --git a/src/main/java/use_case/News/NewsDataAccessInterface.java b/src/main/java/use_case/news/NewsDataAccessInterface.java similarity index 95% rename from src/main/java/use_case/News/NewsDataAccessInterface.java rename to src/main/java/use_case/news/NewsDataAccessInterface.java index 9416c245..70b49552 100644 --- a/src/main/java/use_case/News/NewsDataAccessInterface.java +++ b/src/main/java/use_case/news/NewsDataAccessInterface.java @@ -1,4 +1,4 @@ -package use_case.News; +package use_case.news; import java.util.List; import entity.NewsArticle; diff --git a/src/main/java/use_case/News/NewsInputBoundary.java b/src/main/java/use_case/news/NewsInputBoundary.java similarity index 93% rename from src/main/java/use_case/News/NewsInputBoundary.java rename to src/main/java/use_case/news/NewsInputBoundary.java index 18522fb7..50bc751f 100644 --- a/src/main/java/use_case/News/NewsInputBoundary.java +++ b/src/main/java/use_case/news/NewsInputBoundary.java @@ -1,4 +1,4 @@ -package use_case.News; +package use_case.news; /** * Define what actions this user case supports diff --git a/src/main/java/use_case/News/NewsInteractor.java b/src/main/java/use_case/news/NewsInteractor.java similarity index 98% rename from src/main/java/use_case/News/NewsInteractor.java rename to src/main/java/use_case/news/NewsInteractor.java index 644ddd96..ad767ac5 100644 --- a/src/main/java/use_case/News/NewsInteractor.java +++ b/src/main/java/use_case/news/NewsInteractor.java @@ -1,4 +1,4 @@ -package use_case.News; +package use_case.news; import entity.NewsArticle; import java.util.List; diff --git a/src/main/java/use_case/News/NewsOutputBoundary.java b/src/main/java/use_case/news/NewsOutputBoundary.java similarity index 89% rename from src/main/java/use_case/News/NewsOutputBoundary.java rename to src/main/java/use_case/news/NewsOutputBoundary.java index 91fc34f2..97023df9 100644 --- a/src/main/java/use_case/News/NewsOutputBoundary.java +++ b/src/main/java/use_case/news/NewsOutputBoundary.java @@ -1,4 +1,4 @@ -package use_case.News; +package use_case.news; /** * Interface implemented by the presenter diff --git a/src/main/java/use_case/News/NewsRequestModel.java b/src/main/java/use_case/news/NewsRequestModel.java similarity index 98% rename from src/main/java/use_case/News/NewsRequestModel.java rename to src/main/java/use_case/news/NewsRequestModel.java index d097e50e..9e6e6226 100644 --- a/src/main/java/use_case/News/NewsRequestModel.java +++ b/src/main/java/use_case/news/NewsRequestModel.java @@ -1,4 +1,4 @@ -package use_case.News; +package use_case.news; /** * Request model for news. diff --git a/src/main/java/use_case/News/NewsResponseModel.java b/src/main/java/use_case/news/NewsResponseModel.java similarity index 94% rename from src/main/java/use_case/News/NewsResponseModel.java rename to src/main/java/use_case/news/NewsResponseModel.java index ec9eb102..5463c7c8 100644 --- a/src/main/java/use_case/News/NewsResponseModel.java +++ b/src/main/java/use_case/news/NewsResponseModel.java @@ -1,4 +1,4 @@ -package use_case.News; +package use_case.news; import entity.NewsArticle; import java.util.List; diff --git a/src/main/java/view/NewsView.java b/src/main/java/view/NewsView.java index 295d5e40..ba5165c1 100644 --- a/src/main/java/view/NewsView.java +++ b/src/main/java/view/NewsView.java @@ -1,7 +1,7 @@ package view; -import interface_adapter.NewsPage.NewsController; -import interface_adapter.NewsPage.NewsViewModel; +import interface_adapter.news.NewsController; +import interface_adapter.news.NewsViewModel; import entity.NewsArticle; import javax.swing.*; diff --git a/src/test/java/use_case/News/NewsInteractorTest.java b/src/test/java/use_case/news/NewsInteractorTest.java similarity index 99% rename from src/test/java/use_case/News/NewsInteractorTest.java rename to src/test/java/use_case/news/NewsInteractorTest.java index e8e1563a..afbe96f5 100644 --- a/src/test/java/use_case/News/NewsInteractorTest.java +++ b/src/test/java/use_case/news/NewsInteractorTest.java @@ -1,4 +1,4 @@ -package use_case.News; +package use_case.news; import entity.NewsArticle; import org.junit.jupiter.api.Test; From b5f1b45d8e9e1d052bf5d9a072b3ae82c1f36c4e Mon Sep 17 00:00:00 2001 From: samario Date: Sat, 29 Nov 2025 15:48:50 -0500 Subject: [PATCH 014/103] Standardize the naming of packages --- src/main/java/data_access/MarketStatusDataAccessObject.java | 2 +- .../MarketStatusController.java | 4 ++-- .../MarketStatusPresenter.java | 6 +++--- .../MarketStatusViewModel.java | 2 +- .../MarketStatusDataAccessInterface.java | 2 +- .../MarketStatusInputBoundary.java | 2 +- .../MarketStatusInteractor.java | 2 +- .../MarketStatusOutputBoundary.java | 2 +- .../MarketStatusResponseModel.java | 2 +- .../MarketStatusInteractorTest.java | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) rename src/main/java/interface_adapter/{MarketStatus => market_status}/MarketStatusController.java (74%) rename src/main/java/interface_adapter/{MarketStatus => market_status}/MarketStatusPresenter.java (91%) rename src/main/java/interface_adapter/{MarketStatus => market_status}/MarketStatusViewModel.java (97%) rename src/main/java/use_case/{MarketStatus => market_status}/MarketStatusDataAccessInterface.java (80%) rename src/main/java/use_case/{MarketStatus => market_status}/MarketStatusInputBoundary.java (68%) rename src/main/java/use_case/{MarketStatus => market_status}/MarketStatusInteractor.java (96%) rename src/main/java/use_case/{MarketStatus => market_status}/MarketStatusOutputBoundary.java (83%) rename src/main/java/use_case/{MarketStatus => market_status}/MarketStatusResponseModel.java (90%) rename src/test/java/use_case/{MarketStatus => market_status}/MarketStatusInteractorTest.java (99%) diff --git a/src/main/java/data_access/MarketStatusDataAccessObject.java b/src/main/java/data_access/MarketStatusDataAccessObject.java index 37375ddb..2c3863af 100644 --- a/src/main/java/data_access/MarketStatusDataAccessObject.java +++ b/src/main/java/data_access/MarketStatusDataAccessObject.java @@ -3,7 +3,7 @@ import entity.MarketStatus; import org.json.JSONObject; import org.json.JSONTokener; -import use_case.MarketStatus.MarketStatusDataAccessInterface; +import use_case.market_status.MarketStatusDataAccessInterface; import java.io.BufferedReader; import java.io.IOException; diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusController.java b/src/main/java/interface_adapter/market_status/MarketStatusController.java similarity index 74% rename from src/main/java/interface_adapter/MarketStatus/MarketStatusController.java rename to src/main/java/interface_adapter/market_status/MarketStatusController.java index f6c18f77..eb116db7 100644 --- a/src/main/java/interface_adapter/MarketStatus/MarketStatusController.java +++ b/src/main/java/interface_adapter/market_status/MarketStatusController.java @@ -1,6 +1,6 @@ -package interface_adapter.MarketStatus; +package interface_adapter.market_status; -import use_case.MarketStatus.MarketStatusInputBoundary; +import use_case.market_status.MarketStatusInputBoundary; public class MarketStatusController { diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java b/src/main/java/interface_adapter/market_status/MarketStatusPresenter.java similarity index 91% rename from src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java rename to src/main/java/interface_adapter/market_status/MarketStatusPresenter.java index d82c508a..1b4db2dc 100644 --- a/src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java +++ b/src/main/java/interface_adapter/market_status/MarketStatusPresenter.java @@ -1,7 +1,7 @@ -package interface_adapter.MarketStatus; +package interface_adapter.market_status; -import use_case.MarketStatus.MarketStatusOutputBoundary; -import use_case.MarketStatus.MarketStatusResponseModel; +import use_case.market_status.MarketStatusOutputBoundary; +import use_case.market_status.MarketStatusResponseModel; import entity.MarketStatus; public class MarketStatusPresenter implements MarketStatusOutputBoundary { diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java b/src/main/java/interface_adapter/market_status/MarketStatusViewModel.java similarity index 97% rename from src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java rename to src/main/java/interface_adapter/market_status/MarketStatusViewModel.java index ef8981f1..c42a874b 100644 --- a/src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java +++ b/src/main/java/interface_adapter/market_status/MarketStatusViewModel.java @@ -1,4 +1,4 @@ -package interface_adapter.MarketStatus; +package interface_adapter.market_status; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; diff --git a/src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java b/src/main/java/use_case/market_status/MarketStatusDataAccessInterface.java similarity index 80% rename from src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java rename to src/main/java/use_case/market_status/MarketStatusDataAccessInterface.java index 0f66b62d..9e382973 100644 --- a/src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java +++ b/src/main/java/use_case/market_status/MarketStatusDataAccessInterface.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; import entity.MarketStatus; diff --git a/src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java b/src/main/java/use_case/market_status/MarketStatusInputBoundary.java similarity index 68% rename from src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java rename to src/main/java/use_case/market_status/MarketStatusInputBoundary.java index af989f09..8f5fbe90 100644 --- a/src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java +++ b/src/main/java/use_case/market_status/MarketStatusInputBoundary.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; public interface MarketStatusInputBoundary { void execute(); diff --git a/src/main/java/use_case/MarketStatus/MarketStatusInteractor.java b/src/main/java/use_case/market_status/MarketStatusInteractor.java similarity index 96% rename from src/main/java/use_case/MarketStatus/MarketStatusInteractor.java rename to src/main/java/use_case/market_status/MarketStatusInteractor.java index 53e05935..fb1d71b4 100644 --- a/src/main/java/use_case/MarketStatus/MarketStatusInteractor.java +++ b/src/main/java/use_case/market_status/MarketStatusInteractor.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; import entity.MarketStatus; diff --git a/src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java b/src/main/java/use_case/market_status/MarketStatusOutputBoundary.java similarity index 83% rename from src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java rename to src/main/java/use_case/market_status/MarketStatusOutputBoundary.java index 46ceaa33..0b50a594 100644 --- a/src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java +++ b/src/main/java/use_case/market_status/MarketStatusOutputBoundary.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; public interface MarketStatusOutputBoundary { diff --git a/src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java b/src/main/java/use_case/market_status/MarketStatusResponseModel.java similarity index 90% rename from src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java rename to src/main/java/use_case/market_status/MarketStatusResponseModel.java index 0c460f1e..75f938de 100644 --- a/src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java +++ b/src/main/java/use_case/market_status/MarketStatusResponseModel.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; import entity.MarketStatus; diff --git a/src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java b/src/test/java/use_case/market_status/MarketStatusInteractorTest.java similarity index 99% rename from src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java rename to src/test/java/use_case/market_status/MarketStatusInteractorTest.java index 2f52a0d0..233f763c 100644 --- a/src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java +++ b/src/test/java/use_case/market_status/MarketStatusInteractorTest.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; import entity.MarketStatus; import org.junit.jupiter.api.Test; From 7d9ab9525b8d473b0894fff8d86141be13b4425b Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sat, 29 Nov 2025 19:52:47 -0500 Subject: [PATCH 015/103] semi complete missing display --- src/main/java/app/AppBuilder.java | 8 +- src/main/java/entity/Stock.java | 2 - .../filter_search/FilterSearchController.java | 5 +- .../filter_search/FilterSearchPresenter.java | 19 +- .../filter_search/FilterSearchViewModel.java | 31 +++ .../FilterSearchDataAccessInterface.java | 2 +- .../FilterSearchInputBoundary.java | 119 ++++++++++++ .../filter_search/FilterSearchInputData.java | 29 --- .../filter_search/FilterSearchInteractor.java | 33 +++- .../FilterSearchOutputBoundary.java | 8 +- .../filter_search/FilterSearchOutputData.java | 8 - .../filter_search/FilterSearchResponse.java | 2 +- src/main/java/view/FilterSearchView.java | 176 ++++-------------- 13 files changed, 232 insertions(+), 210 deletions(-) delete mode 100644 src/main/java/use_case/filter_search/FilterSearchInputData.java delete mode 100644 src/main/java/use_case/filter_search/FilterSearchOutputData.java diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 7497f260..46a27030 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -1,6 +1,7 @@ package app; import data_access.FileUserDataAccessObject; +import data_access.FilterSearchDataAccessObject; import entity.UserFactory; import interface_adapter.ViewManagerModel; import interface_adapter.filter_search.FilterSearchController; @@ -20,6 +21,7 @@ import use_case.change_password.ChangePasswordInputBoundary; import use_case.change_password.ChangePasswordInteractor; import use_case.change_password.ChangePasswordOutputBoundary; +import use_case.filter_search.FilterSearchDataAccessInterface; import use_case.filter_search.FilterSearchInputBoundary; import use_case.filter_search.FilterSearchInteractor; import use_case.filter_search.FilterSearchOutputBoundary; @@ -131,8 +133,10 @@ public AppBuilder addChangePasswordUseCase() { public AppBuilder addFilterSearchUseCase() { final FilterSearchOutputBoundary filterSearchOutputBoundary = new FilterSearchPresenter(filterSearchViewModel, viewManagerModel); - final FilterSearchInputBoundary filterSearchInteractor = new FilterSearchInteractor( - filterSearchOutputBoundary); + String key = "REPLACE"; + FilterSearchDataAccessInterface filterObject = new FilterSearchDataAccessObject(key); + final FilterSearchInputBoundary filterSearchInteractor = + new FilterSearchInteractor( filterObject, filterSearchOutputBoundary); FilterSearchController filterSearchController = new FilterSearchController(filterSearchInteractor); filterSearchView.setFilterSearchController(filterSearchController); diff --git a/src/main/java/entity/Stock.java b/src/main/java/entity/Stock.java index 587dffa7..afc9f5df 100644 --- a/src/main/java/entity/Stock.java +++ b/src/main/java/entity/Stock.java @@ -1,7 +1,5 @@ package entity; -import use_case.filter_search.FilterSearchInputData; - public class Stock { private final String mic; private final String type; diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchController.java b/src/main/java/interface_adapter/filter_search/FilterSearchController.java index 4bd8ce40..0bb2e658 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchController.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchController.java @@ -1,10 +1,10 @@ package interface_adapter.filter_search; import use_case.filter_search.FilterSearchInputBoundary; -import use_case.filter_search.FilterSearchInputData; import use_case.filter_search.FilterSearchRequest; public class FilterSearchController { + private final FilterSearchInputBoundary interactor; public FilterSearchController(FilterSearchInputBoundary filterSearchInputBoundary) { @@ -18,9 +18,8 @@ public FilterSearchController(FilterSearchInputBoundary filterSearchInputBoundar * @param securityType the security type * @param currency the currency */ - public void loadFilterSearch(String exchange, String mic, String securityType, String currency) { + public void loadStocks(String exchange, String mic, String securityType, String currency) { final FilterSearchRequest request = new FilterSearchRequest(exchange, mic, securityType, currency); - interactor.execute(request); } } diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java b/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java index b0856cc6..3583c2e9 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java @@ -2,7 +2,7 @@ import interface_adapter.ViewManagerModel; import use_case.filter_search.FilterSearchOutputBoundary; -import use_case.filter_search.FilterSearchOutputData; +import use_case.filter_search.FilterSearchResponse; /** * The Presenter for the Filter Search Use Case. @@ -10,21 +10,26 @@ public class FilterSearchPresenter implements FilterSearchOutputBoundary{ private final FilterSearchViewModel filterSearchViewModel; - private final ViewManagerModel viewManagerModel; public FilterSearchPresenter(FilterSearchViewModel filterSearchViewModel, ViewManagerModel viewManagerModel) { - this.viewManagerModel = viewManagerModel; this.filterSearchViewModel = filterSearchViewModel; } @Override - public void prepareSuccessView(FilterSearchOutputData filterSearchOutputData) { - this.filterSearchViewModel.firePropertyChange(); + public void prepareSuccessView(FilterSearchResponse response) { + + filterSearchViewModel.setStocks(response.getStocks()); filterSearchViewModel.setState(new FilterSearchState()); + filterSearchViewModel.firePropertyChange(); - this.viewManagerModel.setState(filterSearchViewModel.getViewName()); - this.viewManagerModel.firePropertyChange(); + } + @Override + public void prepareFailView(String message) { + filterSearchViewModel.setStocks(null); + filterSearchViewModel.setErrorMessage(message); + filterSearchViewModel.firePropertyChange(); } + } diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java b/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java index d655ed6e..14fdb4e8 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java @@ -1,15 +1,46 @@ package interface_adapter.filter_search; +import entity.Stock; import interface_adapter.ViewModel; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.List; + /** * The View Model for the Filter Search View. */ public class FilterSearchViewModel extends ViewModel { + private final PropertyChangeSupport support = new PropertyChangeSupport(this); + + private List stocks; + private String errorMessage; + + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + public FilterSearchViewModel() { super("Filter Search"); setState(new FilterSearchState()); + + } + + public List getStocks() { + return stocks; + } + + public void setStocks(List stocks) { + this.stocks = stocks; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; } } diff --git a/src/main/java/use_case/filter_search/FilterSearchDataAccessInterface.java b/src/main/java/use_case/filter_search/FilterSearchDataAccessInterface.java index e0b2eb2b..fc9246b6 100644 --- a/src/main/java/use_case/filter_search/FilterSearchDataAccessInterface.java +++ b/src/main/java/use_case/filter_search/FilterSearchDataAccessInterface.java @@ -5,6 +5,6 @@ import java.util.List; public interface FilterSearchDataAccessInterface { - List loadStocks(String exchange, String mic, String securitytype, String currency) throws Exception; + List loadStocks(String exchange, String mic, String securityType, String currency) throws Exception; } diff --git a/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java b/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java index dea54206..278fb3a0 100644 --- a/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java +++ b/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java @@ -9,5 +9,124 @@ public interface FilterSearchInputBoundary { * Executes the filter search use case. * @param request the input data */ + public final String[] EXCHANGE_OPTIONS = {"AD", + "AS", "AT", "AX", "BA", "BC", "BD", "BE", "BH", "BK", "BO", "BR", + "CA", "CN", "CO", "CR", "CS", "DB", "DE", "DU", "F", "HE", "HK", + "HM", "IC", "IR", "IS", "JK", "JO", "KL", "KQ", "KS", "KW", "L", + "LS", "MC", "ME", "MI", "MT", "MU", "MX", "NE", "NL", "NS", "NZ", + "OL", "OM", "PA", "PM", "PR", "QA", "RO", "RG", "SA", "SG", "SI", + "SN", "SR", "SS", "ST", "SW", "SZ", "T", "TA", "TL", "TO", "TW", + "TWO", "US", "V", "VI", "VN", "VS", "WA", "HA", "SX", "TG", "SC"}; + + public final String[] MIC_OPTIONS = {null, "XADS", "XAMS", "ASEX", "XASX", "XBUE", "XBOG", "XBUD", "XBER", "XBAH", "XBKK", + "XBOM", "XBRU", "XCAI", "XCNQ", "XCSE", "BVCA", "XCAS", "XDFM", "XETR", "XDUS", "XFRA", "XHEL", "XHKG", + "XHAM", "XICE", "XDUB", "XIST", "XIDX", "XJSE", "XKLS", "XKOS", "XKRX", "XKUW", "XLON", "XLIS", "XMAD", + "MISX", "XMIL", "XMAL", "XMUN", "XMEX", "NEOE", "XNSA", "XNSE", "XNZE", "XOSL", "XMUS", "XPAR", "XPHS", + "XPRA", "DSMD", "XBSE", "XRIS", "BVMF", "XSTU", "XSES", "XSGO", "XSAU", "XSHG", "XSTO", "WSWX", "XSHE", + "WJPX", "XTAE", "XTAL", "XTSE", "XTAI", "FNSE", "ROCO", "XTSX", "XWBO", "HSTC", "XLIT", "XWAR", "XHAN", + "XNYS", "XGAT", "XASE", "BATS", "ARCX", "XNMS", "XNCM", "XNGS", "IEXG", "XNAS", "OTCM", "OOTC", "XSTC", + "XHNX", "FNIS", "FNDK", "NFI"}; + + final String[] SECURITY_OPTIONS = {null, "ABS Auto", "ABS Card", "ABS Home", "ABS Other", "ACCEPT BANCARIA", + "ADJ CONV. TO FIXED", "ADJ CONV. TO FIXED, OID", "ADJUSTABLE", "ADJUSTABLE, OID", "ADR", "Agncy ABS Home", + "Agncy ABS Other", "Agncy CMBS", "Agncy CMO FLT", "Agncy CMO INV", "Agncy CMO IO", "Agncy CMO Other", + "Agncy CMO PO", "Agncy CMO Z", "Asset-Based", "ASSET-BASED BRIDGE", "ASSET-BASED BRIDGE REV", + "ASSET-BASED BRIDGE TERM", "ASSET-BASED DELAY-DRAW TERM", "ASSET-BASED DIP", "ASSET-BASED DIP DELAY-DRAW", + "ASSET-BASED DIP REV", "ASSET-BASED DIP TERM", "ASSET-BASED LOC", "ASSET-BASED PIK REV", + "ASSET-BASED PIK TERM", "ASSET-BASED REV", "ASSET-BASED TERM", "AUSTRALIAN", "AUSTRALIAN CD", + "AUSTRALIAN CP", "Austrian Crt", "BANK ACCEPT BILL", "BANK BILL", "BANK NOTE", "BANKERS ACCEPT", + "BANKERS ACCEPTANCE", "BASIS SWAP", "BASIS TRADE ON CLOSE", "Basket WRT", "BDR", "BEARER DEP NOTE", + "Belgium Cert", "BELGIUM CP", "BILL OF EXCHANGE", "BILLET A ORDRE", "Bond", "BRAZIL GENERIC", + "BRAZILIAN CDI", "BRIDGE", "BRIDGE DELAY-DRAW", "BRIDGE DELAY-DRAW TERM", "BRIDGE DIP TERM", + "BRIDGE GUARANTEE FAC", "BRIDGE ISLAMIC", "BRIDGE ISLAMIC TERM", "BRIDGE PIK", "BRIDGE PIK REV", + "BRIDGE PIK TERM", "BRIDGE REV", "BRIDGE REV GUARANTEE FAC", "BRIDGE STANDBY TERM", "BRIDGE TERM", + "BRIDGE TERM GUARANTEE FAC", "BRIDGE TERM VAT-TRNCH", "BRIDGE VAT-TRNCH", "BULLDOG", "BUTTERFLY SWAP", + "CAD INT BEAR CP", "CALC_INSTRUMENT", "Calendar Spread Option", "CALL LOANS", "CALLABLE CP", "CANADIAN", + "Canadian", "CANADIAN CD", "CANADIAN CP", "Canadian DR", "CAPS & FLOORS", "Car Forward", "CASH", + "CASH FLOW", "CASH FLOW, OID", "CASH RATE", "CBLO", "CD", "CDI", "CDR", "CEDEAR", "CF", "CHILEAN CD", + "CHILEAN DN", "Closed-End Fund", "CMBS", "Cmdt Fut WRT", "Cmdt Idx WRT", "COLLAT CALL NOTE", "COLOMBIAN CD", + "COMMERCIAL NOTE", "COMMERCIAL PAPER", "Commodity Index", "Common Stock", "CONTRACT FOR DIFFERENCE", + "CONTRACT FRA", "Conv Bond", "Conv Prfd", "Corp Bnd WRT", "Cover Pool", "CP-LIKE EXT NOTE", "CPI LINKED", + "CROSS", "Crypto", "Currency future.", "Currency option.", "Currency spot.", "Currency WRT", "CURVE_ROLL", + "DELAY-DRAW", "DELAY-DRAW ISLAMIC", "DELAY-DRAW ISLAMIC LOC", "DELAY-DRAW ISLAMIC TERM", "DELAY-DRAW LOC", + "DELAY-DRAW PIK TERM", "DELAY-DRAW STANDBY TERM", "DELAY-DRAW TERM", "DELAY-DRAW TERM GUARANTEE F", + "DELAY-DRAW TERM VAT-TRNCH", "DEPOSIT", "DEPOSIT NOTE", "DIM SUM BRIDGE TERM", "DIM SUM DELAY-DRAW TERM", + "DIM SUM REV", "DIM SUM TERM", "DIP", "DIP DELAY-DRAW ISLAMIC TERM", "DIP DELAY-DRAW PIK TERM", + "DIP DELAY-DRAW TERM", "DIP LOC", "DIP PIK TERM", "DIP REV", "DIP STANDBY LOC", "DIP SYNTH LOC", + "DIP TERM", "DISCOUNT FIXBIS", "DISCOUNT NOTES", "DIVIDEND NEUTRAL STOCK FUTURE", "DOMESTC TIME DEP", + "DOMESTIC", "DOMESTIC MTN", "Dutch Cert", "DUTCH CP", "EDR", "Equity Index", "Equity Option", "Equity WRT", + "ETP", "EURO CD", "EURO CP", "EURO MTN", "EURO NON-DOLLAR", "EURO STRUCTRD LN", "EURO TIME DEPST", + "EURO-DOLLAR", "EURO-ZONE", "EXTEND COMM NOTE", "EXTEND. NOTE MTN", "FDIC", "FED FUNDS", "FIDC", + "Financial commodity future.", "Financial commodity generic.", "Financial commodity option.", + "Financial commodity spot.", "Financial index future.", "Financial index generic.", + "Financial index option.", "FINNISH CD", "FINNISH CP", "FIXED", "Fixed Income Index", "FIXED, OID", + "FIXING RATE", "FLOATING", "FLOATING CP", "FLOATING, OID", "FNMA FHAVA", "Foreign Sh.", "FORWARD", + "FORWARD CROSS", "FORWARD CURVE", "FRA", "FRENCH CD", "French Cert", "FRENCH CP", "Fund of Funds", + "Futures Monthly Ticker", "FWD SWAP", "FX Curve", "FX DISCOUNT NOTE", "GDR", "Generic currency future.", + "Generic index future.", "German Cert", "GERMAN CP", "GLOBAL", "GUARANTEE FAC", "HB", "HDR", "HONG KONG CD", + "I.R. Fut WRT", "I.R. Swp WRT", "IDR", "IMM FORWARD", "IMM SWAP", "Index", "Index Option", "Index WRT", + "INDIAN CD", "INDIAN CP", "INDONESIAN CP", "Indx Fut WRT", "INFLATION SWAP", "INT BEAR FIXBIS", + "Int. Rt. WRT", "INTER. APPRECIATION", "INTER. APPRECIATION, OID", "ISLAMIC", "ISLAMIC BA", "ISLAMIC CP", + "ISLAMIC GUARANTEE FAC", "ISLAMIC LOC", "ISLAMIC REV", "ISLAMIC STANDBY", "ISLAMIC STANDBY REV", + "ISLAMIC STANDBY TERM", "ISLAMIC TERM", "ISLAMIC TERM GUARANTEE FAC", "ISLAMIC TERM VAT-TRNCH", "JUMBO CD", + "KOREAN CD", "KOREAN CP", "LEBANESE CP", "LIQUIDITY NOTE", "LOC", "LOC GUARANTEE FAC", "LOC TERM", + "Ltd Part", "MALAYSIAN CP", "Managed Account", "MARGIN TERM DEP", "MASTER NOTES", "MBS 10yr", "MBS 15yr", + "MBS 20yr", "MBS 30yr", "MBS 35yr", "MBS 40yr", "MBS 50yr", "MBS 5yr", "MBS 7yr", "MBS ARM", "MBS balloon", + "MBS Other", "MED TERM NOTE", "MEDIUM TERM CD", "MEDIUM TERM ECD", "MEXICAN CP", "MEXICAN PAGARE", "Misc.", + "MLP", "MONETARY BILLS", "MONEY MARKET CALL", "MUNI CP", "MUNI INT BEAR CP", "MUNI SWAP", "MURABAHA", + "Mutual Fund", "MV", "MX CERT BURSATIL", "NDF SWAP", "NEG EURO CP", "NEG INST DEPOSIT", "NEGOTIABLE CD", + "NEW ZEALAND CD", "NEW ZEALAND CP", "NON-DELIVERABLE FORWARD", "NON-DELIVERABLE IRS SWAP", "NVDR", + "NY Reg Shrs", "OID", "ONSHORE FORWARD", "ONSHORE SWAP", "Open-End Fund", "OPTION", + "Option on Equity Future", "OPTION VOLATILITY", "OTHER", "OVER/NIGHT", "OVERDRAFT", + "OVERNIGHT INDEXED SWAP", "PANAMANIAN CP", "Participate Cert", "PHILIPPINE CP", + "Physical commodity forward.", "Physical commodity future.", "Physical commodity generic.", + "Physical commodity option.", "Physical commodity spot.", "Physical index future.", + "Physical index option.", "PIK", "PIK LOC", "PIK REV", "PIK SYNTH LOC", "PIK TERM", "PLAZOS FIJOS", + "PORTUGUESE CP", "Preference", "Preferred", "PRES", "Prfd WRT", "PRIV PLACEMENT", "PRIVATE", "Private Comp", + "Private-equity backed", "PROMISSORY NOTE", "PROV T-BILL", "Prvt CMBS", "Prvt CMO FLT", "Prvt CMO INV", + "Prvt CMO IO", "Prvt CMO Other", "Prvt CMO PO", "Prvt CMO Z", "PUBLIC", "Pvt Eqty Fund", "RDC", "Receipt", + "REIT", "REPO", "RESERVE-BASED DIP REV", "RESERVE-BASED LOC", "RESERVE-BASED REV", "RESERVE-BASED TERM", + "RESTRUCTURD DEBT", "RETAIL CD", "RETURN IDX", "REV", "REV GUARANTEE FAC", "REV VAT-TRNCH", "Revolver", + "Right", "Royalty Trst", "S.TERM LOAN NOTE", "SAMURAI", "Savings Plan", "Savings Share", "SBA Pool", "SDR", + "Sec Lending", "SHOGUN", "SHORT TERM BN", "SHORT TERM DN", "SINGAPORE CP", "Singapore DR", + "SINGLE STOCK DIVIDEND FUTURE", "SINGLE STOCK FORWARD", "SINGLE STOCK FUTURE", "SINGLE STOCK FUTURE SPREAD", + "SN", "SPANISH CP", "SPECIAL LMMK PGM", "SPOT", "Spot index.", "STANDBY", "STANDBY LOC", + "STANDBY LOC GUARANTEE FAC", "STANDBY REV", "STANDBY TERM", "Stapled Security", "STERLING CD", + "STERLING CP", "Strategy Trade.", "SWAP" , "SWAP SPREAD" , "SWAPTION VOLATILITY" , "SWEDISH CP" , + "SWINGLINE" , "Swiss Cert" , "SYNTH LOC" , "SYNTH REV" , "SYNTH TERM" , "Synthetic Term" , "TAIWAN CP" , + "TAIWAN CP GUAR" , "TAIWAN NEGO CD" , "TAIWAN TIME DEPO" , "TAX CREDIT" , "TAX CREDIT, OID" , "TDR" , + "TERM" , "TERM DEPOSITS" , "TERM GUARANTEE FAC" , "TERM REV" , "TERM VAT-TRNCH" , "TEST" , "THAILAND CP" , + "TLTRO TERM" , "Tracking Stk" , "TREASURY BILL" , "U.S. CD" , "U.S. CP" , "U.S. INT BEAR CP" , "UIT" , + "UK GILT STOCK" , "UMBS MBS Other" , "Unit" , "Unit Inv Tst" , "UNITRANCHE" , "UNITRANCHE ASSET-BASED REV", + "UNITRANCHE DELAY-DRAW PIK T" , "UNITRANCHE DELAY-DRAW TERM" , "UNITRANCHE PIK REV" , "UNITRANCHE PIK TERM", + "UNITRANCHE REV" , "UNITRANCHE TERM" , "US DOMESTIC" , "US GOVERNMENT" , "US NON-DOLLAR" , + "VAR RATE DEM OBL" , "VAT-TRNCH" , "VENEZUELAN CP" , "VIETNAMESE CD" , "VOLATILITY DERIVATIVE" , "Warrant", + "YANKEE" , "YANKEE CD" , "YEN CD" , "YEN CP" , "Yield Curve" , "ZERO COUPON" , "ZERO COUPON, OID"}; + + public final String[] CURRENCY_OPTIONS = {null, "ADP", "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "ATS", "AUD", + "AUd", "AWG", "AZM", "AZN", "BAM", "BBD", "BDT", "BEF", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", + "BRl", "BSD", "BTN", "BWP", "BWp", "BYN", "BYR", "BYS", "BZD", "CAD", "CAd", "CDF", "CER", "CHF", "CHf", + "CLF", "CLP", "CNH", "CNT", "CNY", "COP", "COU", "CRC", "CRS", "CUP", "CVE", "CYP", "CZK", "DEM", "DJF", + "DKK", "DOP", "DZD", "ECS", "EEK", "EES", "EGD", "EGP", "ERN", "ESP", "ETB", "EUA", "EUR", "EUr", "FIM", + "FJD", "FKP", "FRF", "GBP", "GBp", "GEL", "GHC", "GHS", "GIP", "GLD", "GMD", "GNF", "GRD", "GTQ", "GWP", + "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "IEP", "ILS", "ILs", "INR", "IQD", "IRR", "ISK", "ITL", + "JEP", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KWd", "KYD", "KZT", "LAK", + "LBP", "LKR", "LRD", "LSL", "LTL", "LUF", "LVL", "LYD", "MAD", "MDL", "MGA", "MGF", "MKD", "MLF", "MMK", + "MNT", "MOP", "MRO", "MRU", "MTL", "MULTI", "MUR", "MVR", "MWK", "MWk", "MXN", "MYR", "MYr", "MZM", "MZN", + "NAD", "NAd", "NGN", "NIC", "NID", "NIO", "NLG", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", + "PKR", "PLD", "PLN", "PLT", "PTE", "PYG", "QAR", "ROL", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", + "SDD", "SDG", "SDP", "SDR", "SEK", "SGD", "SGd", "SHP", "SIT", "SKK", "SLE", "SLL", "SLV", "SOS", "SPL", + "SRD", "SRG", "SSP", "STD", "STN", "SVC", "SYP", "SZL", "SZl", "THB", "THO", "TJS", "TMM", "TMT", "TND", + "TOP", "TPE", "TRL", "TRY", "TTD", "TVD", "TWD", "TZS", "UAH", "UDI", "UGX", "US", "USD", "USd", "UVR", + "UYI", "UYU", "UYW", "UZS", "VEB", "VEE", "VEF", "VES", "VND", "VUV", "WST", "X0S", "X1S", "X2S", "X3S", + "X4S", "X5S", "X6S", "X7S", "X8S", "X9S", "XAD", "XAF", "XAG", "XAL", "XAO", "XAS", "XAU", "XAV", "XBA", + "XBI", "XBN", "XBS", "XBT", "XBW", "XCD", "XCG", "XCR", "XCS", "XCU", "XDG", "XDH", "XDI", "XDO", "XDR", + "XDT", "XEG", "XEN", "XEO", "XET", "XEU", "XFI", "XFL", "XFM", "XFT", "XGZ", "XHB", "XIC", "XIN", "XIO", + "XLC", "XLI", "XLM", "XLU", "XMA", "XMK", "XMN", "XMR", "XNI", "XOF", "XPB", "XPD", "XPF", "XPT", "XRA", + "XRH", "XRI", "XRP", "XRU", "XSA", "XSN", "XSO", "XST", "XSU", "XSZ", "XTH", "XTK", "XTR", "XUC", "XUN", + "XUT", "XVC", "XVV", "XXT", "XZC", "XZI", "YER", "ZAR", "ZAr", "ZMK", "ZMW", "ZWD", "ZWd", "ZWF", "ZWG", + "ZWg", "ZWL", "ZWN", "ZWR"}; + + void execute(FilterSearchRequest request); } diff --git a/src/main/java/use_case/filter_search/FilterSearchInputData.java b/src/main/java/use_case/filter_search/FilterSearchInputData.java deleted file mode 100644 index dacea92b..00000000 --- a/src/main/java/use_case/filter_search/FilterSearchInputData.java +++ /dev/null @@ -1,29 +0,0 @@ -package use_case.filter_search; - -/** - * The Input Data for the Filter Search Use Case. - */ - -public class FilterSearchInputData { - - private final String exchange; - private final String mic; - private final String securityType; - private final String currency; - - public FilterSearchInputData(String exchange, String mic, String securityType, String currency) { - this.exchange = exchange; - this.mic = mic; - this.securityType = securityType; - this.currency = currency; - } - - String getExchange() { return this.exchange; } - - String getMic() { return this.mic; } - - String getSecurityType() { return this.securityType; } - - String getCurrency() { return this.currency; } - -} diff --git a/src/main/java/use_case/filter_search/FilterSearchInteractor.java b/src/main/java/use_case/filter_search/FilterSearchInteractor.java index cde81075..bec5de89 100644 --- a/src/main/java/use_case/filter_search/FilterSearchInteractor.java +++ b/src/main/java/use_case/filter_search/FilterSearchInteractor.java @@ -1,26 +1,39 @@ package use_case.filter_search; -import use_case.login.LoginInputBoundary; +import entity.Stock; + +import java.util.List; /** * The Filter Search Interactor. */ public class FilterSearchInteractor implements FilterSearchInputBoundary { + private final FilterSearchDataAccessInterface filterSearchDataAccess; private final FilterSearchOutputBoundary filterSearchPresenter; - public FilterSearchInteractor(FilterSearchOutputBoundary filterSearchOutputBoundary) { + public FilterSearchInteractor(FilterSearchDataAccessInterface data, FilterSearchOutputBoundary filterSearchOutputBoundary) { + this.filterSearchDataAccess = data; this.filterSearchPresenter = filterSearchOutputBoundary; } @Override - public void execute(FilterSearchInputData filterSearchInputData) { - final String exchange = filterSearchInputData.getExchange(); - final String mic = filterSearchInputData.getMic(); - final String securityType = filterSearchInputData.getSecurityType(); - final String currency = filterSearchInputData.getCurrency(); - - final FilterSearchOutputData filterSearchOutputData = new FilterSearchOutputData(); - filterSearchPresenter.prepareSuccessView(filterSearchOutputData); + public void execute(FilterSearchRequest filterSearchRequest) { + + try { + final String exchange = filterSearchRequest.getExchange(); + final String mic = filterSearchRequest.getMic(); + final String securityType = filterSearchRequest.getSecurityType(); + final String currency = filterSearchRequest.getCurrency(); + + List stocks = filterSearchDataAccess.loadStocks(exchange, mic, securityType, currency); + + FilterSearchResponse response = new FilterSearchResponse(stocks); + filterSearchPresenter.prepareSuccessView(response); + } + + catch (Exception e) { + filterSearchPresenter.prepareFailView("Unable to load filtered stocks."); + } } } diff --git a/src/main/java/use_case/filter_search/FilterSearchOutputBoundary.java b/src/main/java/use_case/filter_search/FilterSearchOutputBoundary.java index ab9b2fc2..6d2c7fac 100644 --- a/src/main/java/use_case/filter_search/FilterSearchOutputBoundary.java +++ b/src/main/java/use_case/filter_search/FilterSearchOutputBoundary.java @@ -1,7 +1,5 @@ package use_case.filter_search; -import use_case.filter_search.FilterSearchOutputData; - /** * The output boundary for the Filter Search Use Case. */ @@ -9,7 +7,9 @@ public interface FilterSearchOutputBoundary { /** * Prepares the success view for the Filter Search Use Case. - * @param outputData the output data + * @param filterSearchResponse the output data */ - void prepareSuccessView(FilterSearchOutputData outputData); + void prepareSuccessView(FilterSearchResponse filterSearchResponse); + + void prepareFailView(String s); } diff --git a/src/main/java/use_case/filter_search/FilterSearchOutputData.java b/src/main/java/use_case/filter_search/FilterSearchOutputData.java deleted file mode 100644 index 21d25f52..00000000 --- a/src/main/java/use_case/filter_search/FilterSearchOutputData.java +++ /dev/null @@ -1,8 +0,0 @@ -package use_case.filter_search; - -/** - * Output Data for the Filter Search Use Case. - */ - -public class FilterSearchOutputData { -} diff --git a/src/main/java/use_case/filter_search/FilterSearchResponse.java b/src/main/java/use_case/filter_search/FilterSearchResponse.java index 9e3a2e6b..b52d60ac 100644 --- a/src/main/java/use_case/filter_search/FilterSearchResponse.java +++ b/src/main/java/use_case/filter_search/FilterSearchResponse.java @@ -11,7 +11,7 @@ public FilterSearchResponse(List result) { this.result = result; } - public List getResults() { + public List getStocks() { return this.result; } } diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java index 23c01bf1..89e04637 100644 --- a/src/main/java/view/FilterSearchView.java +++ b/src/main/java/view/FilterSearchView.java @@ -1,15 +1,12 @@ package view; +import entity.Stock; import interface_adapter.filter_search.FilterSearchController; import interface_adapter.filter_search.FilterSearchState; import interface_adapter.filter_search.FilterSearchViewModel; -import interface_adapter.logged_in.ChangePasswordController; -import interface_adapter.logout.LogoutController; -import interface_adapter.signup.SignupState; +import use_case.filter_search.FilterSearchInputBoundary; import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -20,138 +17,32 @@ * The View for when the user is in the Filter Search Page. */ -public class FilterSearchView extends JPanel implements ActionListener, PropertyChangeListener { - private final String viewName = "Filter Search"; - private final FilterSearchViewModel filterSearchViewModel; - private FilterSearchController filterSearchController = null; - - private final JLabel exchange = new JLabel("Exchange"); - private final JLabel mic = new JLabel("MIC:"); - private final JLabel securityType = new JLabel("Security Type:"); - private final JLabel currency = new JLabel("Currency:"); - private final JButton search; - private final JButton backToHomeButton; - - private final String[] exchangeOptions = {"AD" , - "AS" , "AT" , "AX" , "BA" , "BC" , "BD" , "BE" , "BH" , "BK" , "BO" , "BR" , - "CA" , "CN" , "CO" , "CR" , "CS" , "DB" , "DE" , "DU" , "F" , "HE" , "HK" , - "HM" , "IC" , "IR" , "IS" , "JK" , "JO" , "KL" , "KQ" , "KS" , "KW" , "L" , - "LS" , "MC" , "ME" , "MI" , "MT" , "MU" , "MX" , "NE" , "NL" , "NS" , "NZ" , - "OL" , "OM" , "PA" , "PM" , "PR" , "QA" , "RO" , "RG" , "SA" , "SG" , "SI" , - "SN" , "SR" , "SS" , "ST" , "SW" , "SZ" , "T" , "TA" , "TL" , "TO" , "TW" , - "TWO" , "US" , "V" , "VI" , "VN" , "VS" , "WA" , "HA" , "SX" , "TG" , "SC"}; - private final String[] micOptions = {null, "XADS", "XAMS", "ASEX", "XASX", "XBUE", "XBOG", "XBUD", "XBER", "XBAH", "XBKK", - "XBOM", "XBRU", "XCAI", "XCNQ", "XCSE", "BVCA", "XCAS", "XDFM", "XETR", "XDUS", "XFRA", "XHEL", "XHKG", - "XHAM", "XICE", "XDUB", "XIST", "XIDX", "XJSE", "XKLS", "XKOS", "XKRX", "XKUW", "XLON", "XLIS", "XMAD", - "MISX", "XMIL", "XMAL", "XMUN", "XMEX", "NEOE", "XNSA", "XNSE", "XNZE", "XOSL", "XMUS", "XPAR", "XPHS", - "XPRA", "DSMD", "XBSE", "XRIS", "BVMF", "XSTU", "XSES", "XSGO", "XSAU", "XSHG", "XSTO", "WSWX", "XSHE", - "WJPX", "XTAE", "XTAL", "XTSE", "XTAI", "FNSE", "ROCO", "XTSX", "XWBO", "HSTC", "XLIT", "XWAR", "XHAN", - "XNYS", "XGAT", "XASE", "BATS", "ARCX", "XNMS", "XNCM", "XNGS", "IEXG", "XNAS", "OTCM", "OOTC", "XSTC", - "XHNX", "FNIS", "FNDK", "NFI"}; - private final String[] securityOptions = {null, "ABS Auto", "ABS Card", "ABS Home", "ABS Other", "ACCEPT BANCARIA", - "ADJ CONV. TO FIXED", "ADJ CONV. TO FIXED, OID", "ADJUSTABLE", "ADJUSTABLE, OID", "ADR", "Agncy ABS Home", - "Agncy ABS Other", "Agncy CMBS", "Agncy CMO FLT", "Agncy CMO INV", "Agncy CMO IO", "Agncy CMO Other", - "Agncy CMO PO", "Agncy CMO Z", "Asset-Based", "ASSET-BASED BRIDGE", "ASSET-BASED BRIDGE REV", - "ASSET-BASED BRIDGE TERM", "ASSET-BASED DELAY-DRAW TERM", "ASSET-BASED DIP", "ASSET-BASED DIP DELAY-DRAW", - "ASSET-BASED DIP REV", "ASSET-BASED DIP TERM", "ASSET-BASED LOC", "ASSET-BASED PIK REV", - "ASSET-BASED PIK TERM", "ASSET-BASED REV", "ASSET-BASED TERM", "AUSTRALIAN", "AUSTRALIAN CD", - "AUSTRALIAN CP", "Austrian Crt", "BANK ACCEPT BILL", "BANK BILL", "BANK NOTE", "BANKERS ACCEPT", - "BANKERS ACCEPTANCE", "BASIS SWAP", "BASIS TRADE ON CLOSE", "Basket WRT", "BDR", "BEARER DEP NOTE", - "Belgium Cert", "BELGIUM CP", "BILL OF EXCHANGE", "BILLET A ORDRE", "Bond", "BRAZIL GENERIC", - "BRAZILIAN CDI", "BRIDGE", "BRIDGE DELAY-DRAW", "BRIDGE DELAY-DRAW TERM", "BRIDGE DIP TERM", - "BRIDGE GUARANTEE FAC", "BRIDGE ISLAMIC", "BRIDGE ISLAMIC TERM", "BRIDGE PIK", "BRIDGE PIK REV", - "BRIDGE PIK TERM", "BRIDGE REV", "BRIDGE REV GUARANTEE FAC", "BRIDGE STANDBY TERM", "BRIDGE TERM", - "BRIDGE TERM GUARANTEE FAC", "BRIDGE TERM VAT-TRNCH", "BRIDGE VAT-TRNCH", "BULLDOG", "BUTTERFLY SWAP", - "CAD INT BEAR CP", "CALC_INSTRUMENT", "Calendar Spread Option", "CALL LOANS", "CALLABLE CP", "CANADIAN", - "Canadian", "CANADIAN CD", "CANADIAN CP", "Canadian DR", "CAPS & FLOORS", "Car Forward", "CASH", - "CASH FLOW", "CASH FLOW, OID", "CASH RATE", "CBLO", "CD", "CDI", "CDR", "CEDEAR", "CF", "CHILEAN CD", - "CHILEAN DN", "Closed-End Fund", "CMBS", "Cmdt Fut WRT", "Cmdt Idx WRT", "COLLAT CALL NOTE", "COLOMBIAN CD", - "COMMERCIAL NOTE", "COMMERCIAL PAPER", "Commodity Index", "Common Stock", "CONTRACT FOR DIFFERENCE", - "CONTRACT FRA", "Conv Bond", "Conv Prfd", "Corp Bnd WRT", "Cover Pool", "CP-LIKE EXT NOTE", "CPI LINKED", - "CROSS", "Crypto", "Currency future.", "Currency option.", "Currency spot.", "Currency WRT", "CURVE_ROLL", - "DELAY-DRAW", "DELAY-DRAW ISLAMIC", "DELAY-DRAW ISLAMIC LOC", "DELAY-DRAW ISLAMIC TERM", "DELAY-DRAW LOC", - "DELAY-DRAW PIK TERM", "DELAY-DRAW STANDBY TERM", "DELAY-DRAW TERM", "DELAY-DRAW TERM GUARANTEE F", - "DELAY-DRAW TERM VAT-TRNCH", "DEPOSIT", "DEPOSIT NOTE", "DIM SUM BRIDGE TERM", "DIM SUM DELAY-DRAW TERM", - "DIM SUM REV", "DIM SUM TERM", "DIP", "DIP DELAY-DRAW ISLAMIC TERM", "DIP DELAY-DRAW PIK TERM", - "DIP DELAY-DRAW TERM", "DIP LOC", "DIP PIK TERM", "DIP REV", "DIP STANDBY LOC", "DIP SYNTH LOC", - "DIP TERM", "DISCOUNT FIXBIS", "DISCOUNT NOTES", "DIVIDEND NEUTRAL STOCK FUTURE", "DOMESTC TIME DEP", - "DOMESTIC", "DOMESTIC MTN", "Dutch Cert", "DUTCH CP", "EDR", "Equity Index", "Equity Option", "Equity WRT", - "ETP", "EURO CD", "EURO CP", "EURO MTN", "EURO NON-DOLLAR", "EURO STRUCTRD LN", "EURO TIME DEPST", - "EURO-DOLLAR", "EURO-ZONE", "EXTEND COMM NOTE", "EXTEND. NOTE MTN", "FDIC", "FED FUNDS", "FIDC", - "Financial commodity future.", "Financial commodity generic.", "Financial commodity option.", - "Financial commodity spot.", "Financial index future.", "Financial index generic.", - "Financial index option.", "FINNISH CD", "FINNISH CP", "FIXED", "Fixed Income Index", "FIXED, OID", - "FIXING RATE", "FLOATING", "FLOATING CP", "FLOATING, OID", "FNMA FHAVA", "Foreign Sh.", "FORWARD", - "FORWARD CROSS", "FORWARD CURVE", "FRA", "FRENCH CD", "French Cert", "FRENCH CP", "Fund of Funds", - "Futures Monthly Ticker", "FWD SWAP", "FX Curve", "FX DISCOUNT NOTE", "GDR", "Generic currency future.", - "Generic index future.", "German Cert", "GERMAN CP", "GLOBAL", "GUARANTEE FAC", "HB", "HDR", "HONG KONG CD", - "I.R. Fut WRT", "I.R. Swp WRT", "IDR", "IMM FORWARD", "IMM SWAP", "Index", "Index Option", "Index WRT", - "INDIAN CD", "INDIAN CP", "INDONESIAN CP", "Indx Fut WRT", "INFLATION SWAP", "INT BEAR FIXBIS", - "Int. Rt. WRT", "INTER. APPRECIATION", "INTER. APPRECIATION, OID", "ISLAMIC", "ISLAMIC BA", "ISLAMIC CP", - "ISLAMIC GUARANTEE FAC", "ISLAMIC LOC", "ISLAMIC REV", "ISLAMIC STANDBY", "ISLAMIC STANDBY REV", - "ISLAMIC STANDBY TERM", "ISLAMIC TERM", "ISLAMIC TERM GUARANTEE FAC", "ISLAMIC TERM VAT-TRNCH", "JUMBO CD", - "KOREAN CD", "KOREAN CP", "LEBANESE CP", "LIQUIDITY NOTE", "LOC", "LOC GUARANTEE FAC", "LOC TERM", - "Ltd Part", "MALAYSIAN CP", "Managed Account", "MARGIN TERM DEP", "MASTER NOTES", "MBS 10yr", "MBS 15yr", - "MBS 20yr", "MBS 30yr", "MBS 35yr", "MBS 40yr", "MBS 50yr", "MBS 5yr", "MBS 7yr", "MBS ARM", "MBS balloon", - "MBS Other", "MED TERM NOTE", "MEDIUM TERM CD", "MEDIUM TERM ECD", "MEXICAN CP", "MEXICAN PAGARE", "Misc.", - "MLP", "MONETARY BILLS", "MONEY MARKET CALL", "MUNI CP", "MUNI INT BEAR CP", "MUNI SWAP", "MURABAHA", - "Mutual Fund", "MV", "MX CERT BURSATIL", "NDF SWAP", "NEG EURO CP", "NEG INST DEPOSIT", "NEGOTIABLE CD", - "NEW ZEALAND CD", "NEW ZEALAND CP", "NON-DELIVERABLE FORWARD", "NON-DELIVERABLE IRS SWAP", "NVDR", - "NY Reg Shrs", "OID", "ONSHORE FORWARD", "ONSHORE SWAP", "Open-End Fund", "OPTION", - "Option on Equity Future", "OPTION VOLATILITY", "OTHER", "OVER/NIGHT", "OVERDRAFT", - "OVERNIGHT INDEXED SWAP", "PANAMANIAN CP", "Participate Cert", "PHILIPPINE CP", - "Physical commodity forward.", "Physical commodity future.", "Physical commodity generic.", - "Physical commodity option.", "Physical commodity spot.", "Physical index future.", - "Physical index option.", "PIK", "PIK LOC", "PIK REV", "PIK SYNTH LOC", "PIK TERM", "PLAZOS FIJOS", - "PORTUGUESE CP", "Preference", "Preferred", "PRES", "Prfd WRT", "PRIV PLACEMENT", "PRIVATE", "Private Comp", - "Private-equity backed", "PROMISSORY NOTE", "PROV T-BILL", "Prvt CMBS", "Prvt CMO FLT", "Prvt CMO INV", - "Prvt CMO IO", "Prvt CMO Other", "Prvt CMO PO", "Prvt CMO Z", "PUBLIC", "Pvt Eqty Fund", "RDC", "Receipt", - "REIT", "REPO", "RESERVE-BASED DIP REV", "RESERVE-BASED LOC", "RESERVE-BASED REV", "RESERVE-BASED TERM", - "RESTRUCTURD DEBT", "RETAIL CD", "RETURN IDX", "REV", "REV GUARANTEE FAC", "REV VAT-TRNCH", "Revolver", - "Right", "Royalty Trst", "S.TERM LOAN NOTE", "SAMURAI", "Savings Plan", "Savings Share", "SBA Pool", "SDR", - "Sec Lending", "SHOGUN", "SHORT TERM BN", "SHORT TERM DN", "SINGAPORE CP", "Singapore DR", - "SINGLE STOCK DIVIDEND FUTURE", "SINGLE STOCK FORWARD", "SINGLE STOCK FUTURE", "SINGLE STOCK FUTURE SPREAD", - "SN", "SPANISH CP", "SPECIAL LMMK PGM", "SPOT", "Spot index.", "STANDBY", "STANDBY LOC", - "STANDBY LOC GUARANTEE FAC", "STANDBY REV", "STANDBY TERM", "Stapled Security", "STERLING CD", - "STERLING CP", "Strategy Trade.", "SWAP" , "SWAP SPREAD" , "SWAPTION VOLATILITY" , "SWEDISH CP" , - "SWINGLINE" , "Swiss Cert" , "SYNTH LOC" , "SYNTH REV" , "SYNTH TERM" , "Synthetic Term" , "TAIWAN CP" , - "TAIWAN CP GUAR" , "TAIWAN NEGO CD" , "TAIWAN TIME DEPO" , "TAX CREDIT" , "TAX CREDIT, OID" , "TDR" , - "TERM" , "TERM DEPOSITS" , "TERM GUARANTEE FAC" , "TERM REV" , "TERM VAT-TRNCH" , "TEST" , "THAILAND CP" , - "TLTRO TERM" , "Tracking Stk" , "TREASURY BILL" , "U.S. CD" , "U.S. CP" , "U.S. INT BEAR CP" , "UIT" , - "UK GILT STOCK" , "UMBS MBS Other" , "Unit" , "Unit Inv Tst" , "UNITRANCHE" , "UNITRANCHE ASSET-BASED REV", - "UNITRANCHE DELAY-DRAW PIK T" , "UNITRANCHE DELAY-DRAW TERM" , "UNITRANCHE PIK REV" , "UNITRANCHE PIK TERM", - "UNITRANCHE REV" , "UNITRANCHE TERM" , "US DOMESTIC" , "US GOVERNMENT" , "US NON-DOLLAR" , - "VAR RATE DEM OBL" , "VAT-TRNCH" , "VENEZUELAN CP" , "VIETNAMESE CD" , "VOLATILITY DERIVATIVE" , "Warrant", - "YANKEE" , "YANKEE CD" , "YEN CD" , "YEN CP" , "Yield Curve" , "ZERO COUPON" , "ZERO COUPON, OID"}; - private final String[] currencyOptions = {null, "ADP", "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "ATS", "AUD", - "AUd", "AWG", "AZM", "AZN", "BAM", "BBD", "BDT", "BEF", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", - "BRl", "BSD", "BTN", "BWP", "BWp", "BYN", "BYR", "BYS", "BZD", "CAD", "CAd", "CDF", "CER", "CHF", "CHf", - "CLF", "CLP", "CNH", "CNT", "CNY", "COP", "COU", "CRC", "CRS", "CUP", "CVE", "CYP", "CZK", "DEM", "DJF", - "DKK", "DOP", "DZD", "ECS", "EEK", "EES", "EGD", "EGP", "ERN", "ESP", "ETB", "EUA", "EUR", "EUr", "FIM", - "FJD", "FKP", "FRF", "GBP", "GBp", "GEL", "GHC", "GHS", "GIP", "GLD", "GMD", "GNF", "GRD", "GTQ", "GWP", - "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", "IDR", "IEP", "ILS", "ILs", "INR", "IQD", "IRR", "ISK", "ITL", - "JEP", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KWd", "KYD", "KZT", "LAK", - "LBP", "LKR", "LRD", "LSL", "LTL", "LUF", "LVL", "LYD", "MAD", "MDL", "MGA", "MGF", "MKD", "MLF", "MMK", - "MNT", "MOP", "MRO", "MRU", "MTL", "MULTI", "MUR", "MVR", "MWK", "MWk", "MXN", "MYR", "MYr", "MZM", "MZN", - "NAD", "NAd", "NGN", "NIC", "NID", "NIO", "NLG", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", - "PKR", "PLD", "PLN", "PLT", "PTE", "PYG", "QAR", "ROL", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", - "SDD", "SDG", "SDP", "SDR", "SEK", "SGD", "SGd", "SHP", "SIT", "SKK", "SLE", "SLL", "SLV", "SOS", "SPL", - "SRD", "SRG", "SSP", "STD", "STN", "SVC", "SYP", "SZL", "SZl", "THB", "THO", "TJS", "TMM", "TMT", "TND", - "TOP", "TPE", "TRL", "TRY", "TTD", "TVD", "TWD", "TZS", "UAH", "UDI", "UGX", "US", "USD", "USd", "UVR", - "UYI", "UYU", "UYW", "UZS", "VEB", "VEE", "VEF", "VES", "VND", "VUV", "WST", "X0S", "X1S", "X2S", "X3S", - "X4S", "X5S", "X6S", "X7S", "X8S", "X9S", "XAD", "XAF", "XAG", "XAL", "XAO", "XAS", "XAU", "XAV", "XBA", - "XBI", "XBN", "XBS", "XBT", "XBW", "XCD", "XCG", "XCR", "XCS", "XCU", "XDG", "XDH", "XDI", "XDO", "XDR", - "XDT", "XEG", "XEN", "XEO", "XET", "XEU", "XFI", "XFL", "XFM", "XFT", "XGZ", "XHB", "XIC", "XIN", "XIO", - "XLC", "XLI", "XLM", "XLU", "XMA", "XMK", "XMN", "XMR", "XNI", "XOF", "XPB", "XPD", "XPF", "XPT", "XRA", - "XRH", "XRI", "XRP", "XRU", "XSA", "XSN", "XSO", "XST", "XSU", "XSZ", "XTH", "XTK", "XTR", "XUC", "XUN", - "XUT", "XVC", "XVV", "XXT", "XZC", "XZI", "YER", "ZAR", "ZAr", "ZMK", "ZMW", "ZWD", "ZWd", "ZWF", "ZWG", - "ZWg", "ZWL", "ZWN", "ZWR"}; +public class FilterSearchView extends JPanel implements PropertyChangeListener { + private FilterSearchController filterSearchController; + + private String ex; + private String mi; + private String sec; + private String curr; + + private static final int COL_SYMBOL = 0; + private static final int COL_DESCRIPTION = 1; + private static final int COL_CURRENCY = 2; + private static final int COL_DISPLAY = 3; + private static final int COL_FIGI = 4; + private static final int COL_MIC = 5; + private static final int COL_TYPE = 6; public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { - this.filterSearchViewModel = filterSearchViewModel; - this.filterSearchViewModel.addPropertyChangeListener(this); + filterSearchViewModel.addPropertyChangeListener(this); + + final String[] exchangeOptions = FilterSearchInputBoundary.EXCHANGE_OPTIONS; + final String[] micOptions = FilterSearchInputBoundary.MIC_OPTIONS; + final String[] securityOptions = FilterSearchInputBoundary.SECURITY_OPTIONS; + final String[] currencyOptions = FilterSearchInputBoundary.CURRENCY_OPTIONS; + final JLabel title = new JLabel("Filter Search"); title.setAlignmentX(Component.CENTER_ALIGNMENT); @@ -160,7 +51,7 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { final JComboBox micDrop = new JComboBox(micOptions); final JComboBox securityDrop = new JComboBox(securityOptions); final JComboBox currencyDrop = new JComboBox(currencyOptions); - this.search = new JButton("Search"); + JButton search = new JButton("Search"); this.setLayout(new BorderLayout()); @@ -168,23 +59,27 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { this.add(topPanel, BorderLayout.NORTH); JPanel panel_e = new JPanel(); + JLabel exchange = new JLabel("Exchange"); panel_e.add(exchange); panel_e.add(exchangeDrop); JPanel panel_m = new JPanel(); + JLabel mic = new JLabel("MIC:"); panel_m.add(mic); panel_m.add(micDrop); JPanel panel_s = new JPanel(); + JLabel securityType = new JLabel("Security Type:"); panel_s.add(securityType); panel_s.add(securityDrop); JPanel panel_c = new JPanel(); + JLabel currency = new JLabel("Currency:"); panel_c.add(currency); panel_c.add(currencyDrop); JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - backToHomeButton = new JButton("Back"); + JButton backToHomeButton = new JButton("Back"); leftPanel.add(backToHomeButton); topPanel.add(leftPanel, BorderLayout.WEST); @@ -196,7 +91,7 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { JPanel mainPanel = new JPanel(); - + String[] columnNames = {"Symbol", "Description", "Currency", "Display Symbol", "FIGI", "MIC", "Security Type"}; backToHomeButton.addActionListener(e -> { @@ -227,21 +122,17 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { curr = currencyDrop.getSelectedItem().toString(); } - filterSearchController.loadFilterSearch(ex , mi, sec, curr); + filterSearchController.loadStocks(ex , mi, sec, curr); }); - } - @Override - public void actionPerformed(ActionEvent evt) { - /* TODO add search results */ - } public String getViewName() { + String viewName = "Filter Search"; return viewName; } @@ -255,5 +146,4 @@ public void propertyChange(PropertyChangeEvent evt) { } - } From 7e1d95ffb30a8eba69f8ff7950a6475097385fc2 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sat, 29 Nov 2025 20:02:54 -0500 Subject: [PATCH 016/103] added apikey --- src/main/java/app/AppBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 46a27030..d2080f3a 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -133,7 +133,7 @@ public AppBuilder addChangePasswordUseCase() { public AppBuilder addFilterSearchUseCase() { final FilterSearchOutputBoundary filterSearchOutputBoundary = new FilterSearchPresenter(filterSearchViewModel, viewManagerModel); - String key = "REPLACE"; + String key = "d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"; FilterSearchDataAccessInterface filterObject = new FilterSearchDataAccessObject(key); final FilterSearchInputBoundary filterSearchInteractor = new FilterSearchInteractor( filterObject, filterSearchOutputBoundary); From ced0e9696556a0c5d305d4306c8cded8dbda6f72 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sat, 29 Nov 2025 20:50:12 -0500 Subject: [PATCH 017/103] added apikey --- src/main/java/entity/Stock.java | 4 ++ .../FilterSearchInputBoundary.java | 9 +--- src/main/java/view/FilterSearchView.java | 45 +++++++++++++++++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/src/main/java/entity/Stock.java b/src/main/java/entity/Stock.java index afc9f5df..5b80a6fa 100644 --- a/src/main/java/entity/Stock.java +++ b/src/main/java/entity/Stock.java @@ -44,4 +44,8 @@ public String getDisplaySymbol() { public String getFigi() { return figi; } + + public String getSymbol() { + return symbol; + } } diff --git a/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java b/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java index 278fb3a0..5964f2cd 100644 --- a/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java +++ b/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java @@ -9,14 +9,7 @@ public interface FilterSearchInputBoundary { * Executes the filter search use case. * @param request the input data */ - public final String[] EXCHANGE_OPTIONS = {"AD", - "AS", "AT", "AX", "BA", "BC", "BD", "BE", "BH", "BK", "BO", "BR", - "CA", "CN", "CO", "CR", "CS", "DB", "DE", "DU", "F", "HE", "HK", - "HM", "IC", "IR", "IS", "JK", "JO", "KL", "KQ", "KS", "KW", "L", - "LS", "MC", "ME", "MI", "MT", "MU", "MX", "NE", "NL", "NS", "NZ", - "OL", "OM", "PA", "PM", "PR", "QA", "RO", "RG", "SA", "SG", "SI", - "SN", "SR", "SS", "ST", "SW", "SZ", "T", "TA", "TL", "TO", "TW", - "TWO", "US", "V", "VI", "VN", "VS", "WA", "HA", "SX", "TG", "SC"}; + public final String[] EXCHANGE_OPTIONS = {"US"}; public final String[] MIC_OPTIONS = {null, "XADS", "XAMS", "ASEX", "XASX", "XBUE", "XBOG", "XBUD", "XBER", "XBAH", "XBKK", "XBOM", "XBRU", "XCAI", "XCNQ", "XCSE", "BVCA", "XCAS", "XDFM", "XETR", "XDUS", "XFRA", "XHEL", "XHKG", diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java index 89e04637..328a4336 100644 --- a/src/main/java/view/FilterSearchView.java +++ b/src/main/java/view/FilterSearchView.java @@ -1,5 +1,6 @@ package view; +import data_access.FilterSearchDataAccessObject; import entity.Stock; import interface_adapter.filter_search.FilterSearchController; import interface_adapter.filter_search.FilterSearchState; @@ -12,6 +13,8 @@ import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.List; /** * The View for when the user is in the Filter Search Page. @@ -93,7 +96,7 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { String[] columnNames = {"Symbol", "Description", "Currency", "Display Symbol", "FIGI", "MIC", "Security Type"}; - + // Home Button backToHomeButton.addActionListener(e -> { // TODO: implement navigation back to the main/home view. // For now, you can leave this empty or show a message: @@ -105,6 +108,8 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { ); }); + + // Search Button search.addActionListener(e -> { String ex = exchangeDrop.getSelectedItem().toString(); String mi = null; @@ -119,10 +124,44 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { } if (currencyDrop.getSelectedItem() != null) { - curr = currencyDrop.getSelectedItem().toString(); + curr = currencyDrop.getSelectedItem().toString(); + } + + filterSearchController.loadStocks(ex, mi, sec, curr); + data_access.FilterSearchDataAccessObject obj = new + FilterSearchDataAccessObject("d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"); + List res; + try { + res = obj.loadStocks(ex, mi, sec, curr); + } catch (Exception exc) { + throw new RuntimeException(exc); + } + + Object[][] data = new Object[res.size()][7]; + + for (int i = 0; i < res.size(); i++) { + Object[] temp = new Object[7]; + temp[0] = res.get(i).getSymbol(); + temp[1] = res.get(i).getDescription(); + temp[2] = res.get(i).getCurrency(); + temp[3] = res.get(i).getDisplaySymbol(); + temp[4] = res.get(i).getFigi(); + temp[5] = res.get(i).getMic(); + temp[6] = res.get(i).getType(); + + + data[i] = temp; } - filterSearchController.loadStocks(ex , mi, sec, curr); + JTable table = new JTable(data, columnNames); + table.setPreferredScrollableViewportSize(new Dimension(100, 100)); + table.setFillsViewportHeight(true); + + JScrollPane scrollPane = new JScrollPane(table); + + mainPanel.add(scrollPane, BorderLayout.CENTER); + + this.add(mainPanel, BorderLayout.CENTER); }); From d1de7a267696ec09705d88f0b9582144b2d34058 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sat, 29 Nov 2025 21:11:24 -0500 Subject: [PATCH 018/103] Completed view minus link to dash --- src/main/java/view/FilterSearchView.java | 44 ++++++++++--------- .../FilterSearchInteractorTest.java | 6 +++ 2 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 src/test/java/use_case/filter_search/FilterSearchInteractorTest.java diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java index 328a4336..ff1f97d6 100644 --- a/src/main/java/view/FilterSearchView.java +++ b/src/main/java/view/FilterSearchView.java @@ -133,35 +133,37 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { List res; try { res = obj.loadStocks(ex, mi, sec, curr); - } catch (Exception exc) { - throw new RuntimeException(exc); - } - Object[][] data = new Object[res.size()][7]; + Object[][] data = new Object[res.size()][7]; - for (int i = 0; i < res.size(); i++) { - Object[] temp = new Object[7]; - temp[0] = res.get(i).getSymbol(); - temp[1] = res.get(i).getDescription(); - temp[2] = res.get(i).getCurrency(); - temp[3] = res.get(i).getDisplaySymbol(); - temp[4] = res.get(i).getFigi(); - temp[5] = res.get(i).getMic(); - temp[6] = res.get(i).getType(); + for (int i = 0; i < res.size(); i++) { + Object[] temp = new Object[7]; + temp[0] = res.get(i).getSymbol(); + temp[1] = res.get(i).getDescription(); + temp[2] = res.get(i).getCurrency(); + temp[3] = res.get(i).getDisplaySymbol(); + temp[4] = res.get(i).getFigi(); + temp[5] = res.get(i).getMic(); + temp[6] = res.get(i).getType(); - data[i] = temp; - } + data[i] = temp; + } + + JTable table = new JTable(data, columnNames); + table.setPreferredScrollableViewportSize(new Dimension(800, 800)); + table.setFillsViewportHeight(true); - JTable table = new JTable(data, columnNames); - table.setPreferredScrollableViewportSize(new Dimension(100, 100)); - table.setFillsViewportHeight(true); + JScrollPane scrollPane = new JScrollPane(table); - JScrollPane scrollPane = new JScrollPane(table); + mainPanel.add(scrollPane, BorderLayout.CENTER); + + this.add(mainPanel, BorderLayout.CENTER); + } catch (Exception exc) { + this.add(new JLabel("Error while loading stocks!")); + } - mainPanel.add(scrollPane, BorderLayout.CENTER); - this.add(mainPanel, BorderLayout.CENTER); }); diff --git a/src/test/java/use_case/filter_search/FilterSearchInteractorTest.java b/src/test/java/use_case/filter_search/FilterSearchInteractorTest.java new file mode 100644 index 00000000..e1059f07 --- /dev/null +++ b/src/test/java/use_case/filter_search/FilterSearchInteractorTest.java @@ -0,0 +1,6 @@ +package use_case.filter_search; + +public class FilterSearchInteractorTest { + + +} From 3f1b27a436da1539b3e15c391a7805c41dd9a3f2 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sat, 29 Nov 2025 21:32:58 -0500 Subject: [PATCH 019/103] adding dashboard by changing loggedinview and the appbuilder to add the jframe --- src/main/java/app/AppBuilder.java | 2 +- src/main/java/view/LoggedInView.java | 67 ++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 20 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 3c654cad..2850af25 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -133,7 +133,7 @@ public AppBuilder addLogoutUseCase() { } public JFrame build() { - final JFrame application = new JFrame("User Login Example"); + final JFrame application = new JFrame("Stock Application"); application.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); application.add(cardPanel); diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 1eb2cbd8..0ef0d59d 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -32,29 +32,65 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JTextField passwordInputField = new JTextField(15); private final JButton changePassword; + private final JTextField searchStockField = new JTextField(25); + private final JButton searchButton = new JButton("Search"); + private final JTextArea stockInfoArea = new JTextArea(); + private final JButton addToWatchlistButton = new JButton("Add to Watchlist"); + public LoggedInView(LoggedInViewModel loggedInViewModel) { this.loggedInViewModel = loggedInViewModel; this.loggedInViewModel.addPropertyChangeListener(this); - final JLabel title = new JLabel("Logged In Screen"); - title.setAlignmentX(Component.CENTER_ALIGNMENT); - - final LabelTextPanel passwordInfo = new LabelTextPanel( - new JLabel("Password"), passwordInputField); - - final JLabel usernameInfo = new JLabel("Currently logged in: "); username = new JLabel(); - final JPanel buttons = new JPanel(); logOut = new JButton("Log Out"); - buttons.add(logOut); - changePassword = new JButton("Change Password"); - buttons.add(changePassword); logOut.addActionListener(this); - this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + this.setLayout(new BorderLayout()); + + final JPanel topToolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 10)); + final JButton newsButton = new JButton("News"); + final JButton filterSearchButton = new JButton("Filter Search"); + final JButton historyButton = new JButton("History"); + final JButton marketOpenButton = new JButton("Market Open"); + final JButton accountButton = new JButton("Account"); + + topToolbar.add(newsButton); + topToolbar.add(filterSearchButton); + topToolbar.add(historyButton); + topToolbar.add(marketOpenButton); + topToolbar.add(accountButton); + + this.add(topToolbar, BorderLayout.NORTH); + + final JPanel centerPanel = new JPanel(); + centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS)); + + final JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); + final JLabel searchLabel = new JLabel("Search Stock:"); + searchPanel.add(searchLabel); + searchPanel.add(searchStockField); + searchPanel.add(searchButton); + + centerPanel.add(searchPanel); + + stockInfoArea.setLineWrap(true); + stockInfoArea.setWrapStyleWord(true); + + final JPanel stockInfoPanel = new JPanel(new BorderLayout()); + stockInfoPanel.setBorder(BorderFactory.createTitledBorder("Stock Information")); + stockInfoPanel.add(new JScrollPane(stockInfoArea), BorderLayout.CENTER); + + centerPanel.add(stockInfoPanel); + + this.add(centerPanel, BorderLayout.CENTER); + + final JPanel bottomPanel = new JPanel(new BorderLayout()); + bottomPanel.add(addToWatchlistButton, BorderLayout.CENTER); + + this.add(bottomPanel, BorderLayout.SOUTH); passwordInputField.getDocument().addDocumentListener(new DocumentListener() { @@ -94,13 +130,6 @@ public void changedUpdate(DocumentEvent e) { } ); - this.add(title); - this.add(usernameInfo); - this.add(username); - - this.add(passwordInfo); - this.add(passwordErrorField); - this.add(buttons); } /** From d811fb28ffed6f97e0ea2a6a2afd44e9dcf669ef Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sat, 29 Nov 2025 22:12:22 -0500 Subject: [PATCH 020/103] Completed view minus link to dash --- src/main/java/app/AppBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index d2080f3a..2cd61904 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -165,7 +165,7 @@ public JFrame build() { application.add(cardPanel); - viewManagerModel.setState(filterSearchView.getViewName()); + viewManagerModel.setState(signupView.getViewName()); viewManagerModel.firePropertyChange(); return application; From 6bc5b2c17e313ae78ced4666aedaf2292ce80130 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sat, 29 Nov 2025 22:47:26 -0500 Subject: [PATCH 021/103] Added corrected Error message --- src/main/java/view/FilterSearchView.java | 7 ++++++- .../use_case/filter_search/FilterSearchInteractorTest.java | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java index ff1f97d6..1092e287 100644 --- a/src/main/java/view/FilterSearchView.java +++ b/src/main/java/view/FilterSearchView.java @@ -160,7 +160,12 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { this.add(mainPanel, BorderLayout.CENTER); } catch (Exception exc) { - this.add(new JLabel("Error while loading stocks!")); + JOptionPane.showMessageDialog( + this, + "Error while loading stocks: " + exc.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE + ); } diff --git a/src/test/java/use_case/filter_search/FilterSearchInteractorTest.java b/src/test/java/use_case/filter_search/FilterSearchInteractorTest.java index e1059f07..cc7ac07e 100644 --- a/src/test/java/use_case/filter_search/FilterSearchInteractorTest.java +++ b/src/test/java/use_case/filter_search/FilterSearchInteractorTest.java @@ -1,6 +1,11 @@ package use_case.filter_search; +import org.junit.jupiter.api.Test; + public class FilterSearchInteractorTest { + @Test + void successTest() { + } } From 8502aff4dfc929d05d4bfa3a1d740d9a073ceeb9 Mon Sep 17 00:00:00 2001 From: samario Date: Sat, 29 Nov 2025 22:51:50 -0500 Subject: [PATCH 022/103] Use the main implemented by Shivam --- src/main/java/app/AppBuilder.java | 4 +- src/main/java/view/LoggedInView.java | 69 ++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 3c654cad..ebf704f1 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -133,7 +133,7 @@ public AppBuilder addLogoutUseCase() { } public JFrame build() { - final JFrame application = new JFrame("User Login Example"); + final JFrame application = new JFrame("Stock Application"); application.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); application.add(cardPanel); @@ -145,4 +145,4 @@ public JFrame build() { } -} +} \ No newline at end of file diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 1eb2cbd8..d27e8236 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -32,29 +32,65 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JTextField passwordInputField = new JTextField(15); private final JButton changePassword; + private final JTextField searchStockField = new JTextField(25); + private final JButton searchButton = new JButton("Search"); + private final JTextArea stockInfoArea = new JTextArea(); + private final JButton addToWatchlistButton = new JButton("Add to Watchlist"); + public LoggedInView(LoggedInViewModel loggedInViewModel) { this.loggedInViewModel = loggedInViewModel; this.loggedInViewModel.addPropertyChangeListener(this); - final JLabel title = new JLabel("Logged In Screen"); - title.setAlignmentX(Component.CENTER_ALIGNMENT); - - final LabelTextPanel passwordInfo = new LabelTextPanel( - new JLabel("Password"), passwordInputField); - - final JLabel usernameInfo = new JLabel("Currently logged in: "); username = new JLabel(); - final JPanel buttons = new JPanel(); logOut = new JButton("Log Out"); - buttons.add(logOut); - changePassword = new JButton("Change Password"); - buttons.add(changePassword); logOut.addActionListener(this); - this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + this.setLayout(new BorderLayout()); + + final JPanel topToolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 10)); + final JButton newsButton = new JButton("News"); + final JButton filterSearchButton = new JButton("Filter Search"); + final JButton historyButton = new JButton("History"); + final JButton marketOpenButton = new JButton("Market Open"); + final JButton accountButton = new JButton("Account"); + + topToolbar.add(newsButton); + topToolbar.add(filterSearchButton); + topToolbar.add(historyButton); + topToolbar.add(marketOpenButton); + topToolbar.add(accountButton); + + this.add(topToolbar, BorderLayout.NORTH); + + final JPanel centerPanel = new JPanel(); + centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS)); + + final JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); + final JLabel searchLabel = new JLabel("Search Stock:"); + searchPanel.add(searchLabel); + searchPanel.add(searchStockField); + searchPanel.add(searchButton); + + centerPanel.add(searchPanel); + + stockInfoArea.setLineWrap(true); + stockInfoArea.setWrapStyleWord(true); + + final JPanel stockInfoPanel = new JPanel(new BorderLayout()); + stockInfoPanel.setBorder(BorderFactory.createTitledBorder("Stock Information")); + stockInfoPanel.add(new JScrollPane(stockInfoArea), BorderLayout.CENTER); + + centerPanel.add(stockInfoPanel); + + this.add(centerPanel, BorderLayout.CENTER); + + final JPanel bottomPanel = new JPanel(new BorderLayout()); + bottomPanel.add(addToWatchlistButton, BorderLayout.CENTER); + + this.add(bottomPanel, BorderLayout.SOUTH); passwordInputField.getDocument().addDocumentListener(new DocumentListener() { @@ -94,13 +130,6 @@ public void changedUpdate(DocumentEvent e) { } ); - this.add(title); - this.add(usernameInfo); - this.add(username); - - this.add(passwordInfo); - this.add(passwordErrorField); - this.add(buttons); } /** @@ -142,4 +171,4 @@ public void setChangePasswordController(ChangePasswordController changePasswordC public void setLogoutController(LogoutController logoutController) { // TODO: save the logout controller in the instance variable. } -} +} \ No newline at end of file From 0eeebf150f706787ddb2e2e8cb768b8ea3acb871 Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Sat, 29 Nov 2025 23:03:07 -0500 Subject: [PATCH 023/103] history --- .../FinnhubEarningsDataAccessObject.java | 149 ++++++++++++++++++ src/main/java/entity/EarningsRecord.java | 34 ++++ .../EarningsHistoryController.java | 32 ++++ .../EarningsHistoryPresenter.java | 61 +++++++ .../EarningsHistoryState.java | 65 ++++++++ .../EarningsHistoryViewModel.java | 34 ++++ .../EarningsDataAccessInterface.java | 19 +++ .../GetEarningsHistoryInputBoundary.java | 5 + .../GetEarningsHistoryInputData.java | 14 ++ .../GetEarningsHistoryInteractor.java | 50 ++++++ .../GetEarningsHistoryOutputBoundary.java | 13 ++ .../GetEarningsHistoryOutputData.java | 24 +++ src/main/java/view/EarningsHistoryView.java | 101 ++++++++++++ src/test/java/app_LH/AppBuilder.java | 62 ++++++++ src/test/java/app_LH/Main.java | 12 ++ 15 files changed, 675 insertions(+) create mode 100644 src/main/java/data_access/FinnhubEarningsDataAccessObject.java create mode 100644 src/main/java/entity/EarningsRecord.java create mode 100644 src/main/java/interface_adapter/earnings_history/EarningsHistoryController.java create mode 100644 src/main/java/interface_adapter/earnings_history/EarningsHistoryPresenter.java create mode 100644 src/main/java/interface_adapter/earnings_history/EarningsHistoryState.java create mode 100644 src/main/java/interface_adapter/earnings_history/EarningsHistoryViewModel.java create mode 100644 src/main/java/use_case/earnings_history/EarningsDataAccessInterface.java create mode 100644 src/main/java/use_case/earnings_history/GetEarningsHistoryInputBoundary.java create mode 100644 src/main/java/use_case/earnings_history/GetEarningsHistoryInputData.java create mode 100644 src/main/java/use_case/earnings_history/GetEarningsHistoryInteractor.java create mode 100644 src/main/java/use_case/earnings_history/GetEarningsHistoryOutputBoundary.java create mode 100644 src/main/java/use_case/earnings_history/GetEarningsHistoryOutputData.java create mode 100644 src/main/java/view/EarningsHistoryView.java create mode 100644 src/test/java/app_LH/AppBuilder.java create mode 100644 src/test/java/app_LH/Main.java diff --git a/src/main/java/data_access/FinnhubEarningsDataAccessObject.java b/src/main/java/data_access/FinnhubEarningsDataAccessObject.java new file mode 100644 index 00000000..1524405d --- /dev/null +++ b/src/main/java/data_access/FinnhubEarningsDataAccessObject.java @@ -0,0 +1,149 @@ +package data_access; + +import entity.EarningsRecord; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import use_case.earnings_history.EarningsDataAccessInterface; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Data access object that calls Finnhub's earnings endpoint and returns + * a list of EarningsRecord entities. + * + * Contract with EarningsDataAccessInterface: + * - return null -> symbol not found / unsupported (API returns "error") + * - return empty -> symbol valid but no earnings data + * - throw IOException -> network / HTTP issue + */ +public class FinnhubEarningsDataAccessObject implements EarningsDataAccessInterface { + + private static final String API_KEY = "d4977ehr01qshn3kvpt0d4977ehr01qshn3kvptg"; + private static final String BASE_URL = "https://finnhub.io/api/v1/stock/earnings"; + + private final OkHttpClient client = new OkHttpClient.Builder() + .callTimeout(10, TimeUnit.SECONDS) + .build(); + + @Override + public List getEarningsFor(String symbol) throws IOException { + if (symbol == null || symbol.trim().isEmpty()) { + return new ArrayList<>(); + } + + symbol = symbol.trim().toUpperCase(); + + HttpUrl url = HttpUrl.parse(BASE_URL).newBuilder() + .addQueryParameter("symbol", symbol) + .addQueryParameter("limit", "10") + .addQueryParameter("token", API_KEY) + .build(); + + Request request = new Request.Builder() + .url(url) + .build(); + + try (Response response = client.newCall(request).execute()) { + + if (!response.isSuccessful()) { + throw new IOException("Unexpected HTTP code " + response); + } + + String body = response.body() != null ? response.body().string() : ""; + + // Finnhub returns { "error": "..." } for bad symbols + if (body.contains("\"error\"")) { + return null; + } + + return parseEarningsJson(body); + } + } + + // ---------------- JSON parsing helpers ---------------- + + /** + * Expected JSON: + * [ { "period": "...", "actual": ..., "estimate": ..., "surprise": ... }, ... ] + */ + private List parseEarningsJson(String json) { + List rows = new ArrayList<>(); + if (json == null) return rows; + + String trimmed = json.trim(); + if (trimmed.length() < 2 || "[]".equals(trimmed)) return rows; + + // Remove [ and ] + if (trimmed.startsWith("[")) { + trimmed = trimmed.substring(1); + } + if (trimmed.endsWith("]")) { + trimmed = trimmed.substring(0, trimmed.length() - 1); + } + + // Split by "},{" between objects + String[] objects = trimmed.split("\\},\\s*\\{"); + + for (String obj : objects) { + obj = obj.trim(); + if (!obj.startsWith("{")) obj = "{" + obj; + if (!obj.endsWith("}")) obj = obj + "}"; + + String period = extractValue(obj, "period"); + String actualStr = extractValue(obj, "actual"); + String estimateStr = extractValue(obj, "estimate"); + String surpriseStr = extractValue(obj, "surprise"); + + Double actual = parseDoubleOrNull(actualStr); + Double estimate = parseDoubleOrNull(estimateStr); + Double surprise = parseDoubleOrNull(surpriseStr); + + rows.add(new EarningsRecord(period, actual, estimate, surprise)); + } + return rows; + } + + private Double parseDoubleOrNull(String s) { + if (s == null || s.isEmpty() || "null".equals(s)) return null; + try { + return Double.parseDouble(s); + } catch (NumberFormatException e) { + return null; + } + } + + private String extractValue(String json, String key) { + String keySearch = "\"" + key + "\":"; + int keyIndex = json.indexOf(keySearch); + if (keyIndex == -1) return null; + + int valueStart = keyIndex + keySearch.length(); + if (valueStart >= json.length()) return null; + + char firstChar = json.charAt(valueStart); + + // String value in quotes + if (firstChar == '"') { + valueStart++; + int valueEnd = json.indexOf('"', valueStart); + if (valueEnd != -1) { + return json.substring(valueStart, valueEnd); + } + } else { // number / null / boolean + int indexComma = json.indexOf(',', valueStart); + int indexCurly = json.indexOf('}', valueStart); + + int terminationIndex = json.length(); + if (indexComma != -1) terminationIndex = Math.min(terminationIndex, indexComma); + if (indexCurly != -1) terminationIndex = Math.min(terminationIndex, indexCurly); + + return json.substring(valueStart, terminationIndex).trim(); + } + return null; + } +} diff --git a/src/main/java/entity/EarningsRecord.java b/src/main/java/entity/EarningsRecord.java new file mode 100644 index 00000000..2cf3864f --- /dev/null +++ b/src/main/java/entity/EarningsRecord.java @@ -0,0 +1,34 @@ +package entity; + +/** + * Entity representing one earnings record returned by the API. + */ +public class EarningsRecord { + private final String period; + private final Double actual; + private final Double estimate; + private final Double surprise; + + public EarningsRecord(String period, Double actual, Double estimate, Double surprise) { + this.period = period; + this.actual = actual; + this.estimate = estimate; + this.surprise = surprise; + } + + public String getPeriod() { + return period; + } + + public Double getActual() { + return actual; + } + + public Double getEstimate() { + return estimate; + } + + public Double getSurprise() { + return surprise; + } +} diff --git a/src/main/java/interface_adapter/earnings_history/EarningsHistoryController.java b/src/main/java/interface_adapter/earnings_history/EarningsHistoryController.java new file mode 100644 index 00000000..318894e4 --- /dev/null +++ b/src/main/java/interface_adapter/earnings_history/EarningsHistoryController.java @@ -0,0 +1,32 @@ +package interface_adapter.earnings_history; + +import use_case.earnings_history.GetEarningsHistoryInputBoundary; +import use_case.earnings_history.GetEarningsHistoryInputData; + +public class EarningsHistoryController { + + private final GetEarningsHistoryInputBoundary interactor; + private final EarningsHistoryViewModel viewModel; + + public EarningsHistoryController(GetEarningsHistoryInputBoundary interactor, + EarningsHistoryViewModel viewModel) { + this.interactor = interactor; + this.viewModel = viewModel; + } + + public void onLoadButtonClicked(String symbol) { + EarningsHistoryState state = new EarningsHistoryState(viewModel.getState()); + state.setSymbol(symbol); + state.setLoading(true); + state.setMessage("", EarningsHistoryState.MessageType.NONE); + viewModel.setState(state); + viewModel.firePropertyChanged(); + + // background thread + new Thread(() -> { + GetEarningsHistoryInputData inputData = + new GetEarningsHistoryInputData(symbol); + interactor.execute(inputData); + }).start(); + } +} diff --git a/src/main/java/interface_adapter/earnings_history/EarningsHistoryPresenter.java b/src/main/java/interface_adapter/earnings_history/EarningsHistoryPresenter.java new file mode 100644 index 00000000..bcfc1e78 --- /dev/null +++ b/src/main/java/interface_adapter/earnings_history/EarningsHistoryPresenter.java @@ -0,0 +1,61 @@ +package interface_adapter.earnings_history; + +import entity.EarningsRecord; +import use_case.earnings_history.GetEarningsHistoryOutputBoundary; +import use_case.earnings_history.GetEarningsHistoryOutputData; + +import java.util.List; + +public class EarningsHistoryPresenter implements GetEarningsHistoryOutputBoundary { + + private final EarningsHistoryViewModel viewModel; + + public EarningsHistoryPresenter(EarningsHistoryViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void prepareSuccessView(GetEarningsHistoryOutputData outputData) { + EarningsHistoryState state = new EarningsHistoryState(viewModel.getState()); + List records = outputData.getRecords(); + + state.setRecords(records); + state.setMessage("", EarningsHistoryState.MessageType.NONE); + state.setLoading(false); + + viewModel.setState(state); + viewModel.firePropertyChanged(); + } + + @Override + public void prepareSymbolErrorView(String errorMessage) { + EarningsHistoryState state = new EarningsHistoryState(viewModel.getState()); + state.setRecords(List.of()); + state.setMessage(errorMessage, EarningsHistoryState.MessageType.ERROR); + state.setLoading(false); + + viewModel.setState(state); + viewModel.firePropertyChanged(); + } + + @Override + public void prepareNoDataView(String message) { + EarningsHistoryState state = new EarningsHistoryState(viewModel.getState()); + state.setRecords(List.of()); + state.setMessage(message, EarningsHistoryState.MessageType.WARNING); + state.setLoading(false); + + viewModel.setState(state); + viewModel.firePropertyChanged(); + } + + @Override + public void prepareConnectionErrorView(String message) { + EarningsHistoryState state = new EarningsHistoryState(viewModel.getState()); + state.setMessage(message, EarningsHistoryState.MessageType.ERROR); + state.setLoading(false); + + viewModel.setState(state); + viewModel.firePropertyChanged(); + } +} diff --git a/src/main/java/interface_adapter/earnings_history/EarningsHistoryState.java b/src/main/java/interface_adapter/earnings_history/EarningsHistoryState.java new file mode 100644 index 00000000..1b91b540 --- /dev/null +++ b/src/main/java/interface_adapter/earnings_history/EarningsHistoryState.java @@ -0,0 +1,65 @@ +package interface_adapter.earnings_history; + +import entity.EarningsRecord; + +import java.util.ArrayList; +import java.util.List; + +public class EarningsHistoryState { + + public enum MessageType { NONE, INFO, WARNING, ERROR } + + private String symbol = ""; + private List records = new ArrayList<>(); + private String message = ""; + private MessageType messageType = MessageType.NONE; + private boolean loading = false; + + public EarningsHistoryState() { + } + + public EarningsHistoryState(EarningsHistoryState copy) { + this.symbol = copy.symbol; + this.records = new ArrayList<>(copy.records); + this.message = copy.message; + this.messageType = copy.messageType; + this.loading = copy.loading; + } + + public String getSymbol() { + return symbol; + } + + public void setSymbol(String symbol) { + this.symbol = symbol; + } + + public List getRecords() { + return records; + } + + public void setRecords(List records) { + this.records = records; + } + + public String getMessage() { + return message; + } + + public MessageType getMessageType() { + return messageType; + } + + public void setMessage(String message, MessageType type) { + this.message = message; + this.messageType = type; + } + + public boolean isLoading() { + return loading; + } + + public void setLoading(boolean loading) { + this.loading = loading; + } +} diff --git a/src/main/java/interface_adapter/earnings_history/EarningsHistoryViewModel.java b/src/main/java/interface_adapter/earnings_history/EarningsHistoryViewModel.java new file mode 100644 index 00000000..d9fc8fa5 --- /dev/null +++ b/src/main/java/interface_adapter/earnings_history/EarningsHistoryViewModel.java @@ -0,0 +1,34 @@ +package interface_adapter.earnings_history; + +import interface_adapter.ViewModel; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + +public class EarningsHistoryViewModel extends ViewModel { + + public static final String TITLE_LABEL = "Company Earnings"; + + private final PropertyChangeSupport support = new PropertyChangeSupport(this); + private EarningsHistoryState state = new EarningsHistoryState(); + + public EarningsHistoryViewModel() { + super("earnings history"); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + public EarningsHistoryState getState() { + return state; + } + + public void setState(EarningsHistoryState state) { + this.state = state; + } + + public void firePropertyChanged() { + support.firePropertyChange("state", null, this.state); + } +} diff --git a/src/main/java/use_case/earnings_history/EarningsDataAccessInterface.java b/src/main/java/use_case/earnings_history/EarningsDataAccessInterface.java new file mode 100644 index 00000000..de5ed727 --- /dev/null +++ b/src/main/java/use_case/earnings_history/EarningsDataAccessInterface.java @@ -0,0 +1,19 @@ +package use_case.earnings_history; + +import entity.EarningsRecord; + +import java.io.IOException; +import java.util.List; + +/** + * Gateway used by the interactor to fetch earnings data. + * + * Contract: + * - return null -> symbol not found / unsupported (API returns "error") + * - return empty -> symbol valid but no earnings data + * - throw IOException -> network / HTTP issue + */ +public interface EarningsDataAccessInterface { + + List getEarningsFor(String symbol) throws IOException; +} diff --git a/src/main/java/use_case/earnings_history/GetEarningsHistoryInputBoundary.java b/src/main/java/use_case/earnings_history/GetEarningsHistoryInputBoundary.java new file mode 100644 index 00000000..4ca84dc6 --- /dev/null +++ b/src/main/java/use_case/earnings_history/GetEarningsHistoryInputBoundary.java @@ -0,0 +1,5 @@ +package use_case.earnings_history; + +public interface GetEarningsHistoryInputBoundary { + void execute(GetEarningsHistoryInputData inputData); +} diff --git a/src/main/java/use_case/earnings_history/GetEarningsHistoryInputData.java b/src/main/java/use_case/earnings_history/GetEarningsHistoryInputData.java new file mode 100644 index 00000000..33beecfc --- /dev/null +++ b/src/main/java/use_case/earnings_history/GetEarningsHistoryInputData.java @@ -0,0 +1,14 @@ +package use_case.earnings_history; + +public class GetEarningsHistoryInputData { + + private final String symbol; + + public GetEarningsHistoryInputData(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } +} diff --git a/src/main/java/use_case/earnings_history/GetEarningsHistoryInteractor.java b/src/main/java/use_case/earnings_history/GetEarningsHistoryInteractor.java new file mode 100644 index 00000000..1242a8a3 --- /dev/null +++ b/src/main/java/use_case/earnings_history/GetEarningsHistoryInteractor.java @@ -0,0 +1,50 @@ +package use_case.earnings_history; + +import entity.EarningsRecord; + +import java.io.IOException; +import java.util.List; + +public class GetEarningsHistoryInteractor implements GetEarningsHistoryInputBoundary { + + private final EarningsDataAccessInterface dataAccess; + private final GetEarningsHistoryOutputBoundary presenter; + + public GetEarningsHistoryInteractor(EarningsDataAccessInterface dataAccess, + GetEarningsHistoryOutputBoundary presenter) { + this.dataAccess = dataAccess; + this.presenter = presenter; + } + + @Override + public void execute(GetEarningsHistoryInputData inputData) { + String symbol = inputData.getSymbol(); + + if (symbol == null || symbol.trim().isEmpty()) { + presenter.prepareSymbolErrorView("Please enter a symbol."); + return; + } + + symbol = symbol.trim().toUpperCase(); + + try { + // IMPORTANT: method name matches the interface + List records = dataAccess.getEarningsFor(symbol); + + if (records == null) { + presenter.prepareSymbolErrorView( + "Company not found or symbol not supported.\nPlease check the symbol."); + } else if (records.isEmpty()) { + presenter.prepareNoDataView( + "No earnings data found for this company."); + } else { + GetEarningsHistoryOutputData outputData = + new GetEarningsHistoryOutputData(symbol, records); + presenter.prepareSuccessView(outputData); + } + } catch (IOException e) { + presenter.prepareConnectionErrorView( + "Network error or API issue.\nCheck your internet or API key."); + } + } +} diff --git a/src/main/java/use_case/earnings_history/GetEarningsHistoryOutputBoundary.java b/src/main/java/use_case/earnings_history/GetEarningsHistoryOutputBoundary.java new file mode 100644 index 00000000..9749170f --- /dev/null +++ b/src/main/java/use_case/earnings_history/GetEarningsHistoryOutputBoundary.java @@ -0,0 +1,13 @@ +package use_case.earnings_history; + +public interface GetEarningsHistoryOutputBoundary { + + void prepareSuccessView(GetEarningsHistoryOutputData outputData); + + // These three correspond to the three different JOptionPane cases + void prepareSymbolErrorView(String errorMessage); + + void prepareNoDataView(String message); + + void prepareConnectionErrorView(String message); +} diff --git a/src/main/java/use_case/earnings_history/GetEarningsHistoryOutputData.java b/src/main/java/use_case/earnings_history/GetEarningsHistoryOutputData.java new file mode 100644 index 00000000..1c5fd704 --- /dev/null +++ b/src/main/java/use_case/earnings_history/GetEarningsHistoryOutputData.java @@ -0,0 +1,24 @@ +package use_case.earnings_history; + +import entity.EarningsRecord; + +import java.util.List; + +public class GetEarningsHistoryOutputData { + + private final String symbol; + private final List records; + + public GetEarningsHistoryOutputData(String symbol, List records) { + this.symbol = symbol; + this.records = records; + } + + public String getSymbol() { + return symbol; + } + + public List getRecords() { + return records; + } +} diff --git a/src/main/java/view/EarningsHistoryView.java b/src/main/java/view/EarningsHistoryView.java new file mode 100644 index 00000000..f77f1c4d --- /dev/null +++ b/src/main/java/view/EarningsHistoryView.java @@ -0,0 +1,101 @@ +package view; + +import entity.EarningsRecord; +import interface_adapter.earnings_history.EarningsHistoryController; +import interface_adapter.earnings_history.EarningsHistoryState; +import interface_adapter.earnings_history.EarningsHistoryViewModel; + +import javax.swing.*; +import javax.swing.table.DefaultTableModel; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.List; + +public class EarningsHistoryView extends JPanel implements PropertyChangeListener { + + private final EarningsHistoryController controller; + private final EarningsHistoryViewModel viewModel; + + private final JTextField symbolField = new JTextField("", 25); + private final JButton loadButton = new JButton("Load Earnings"); + + private final DefaultTableModel tableModel = new DefaultTableModel( + new Object[]{"Period", "Actual", "Estimate", "Surprise"}, 0 + ); + private final JTable table = new JTable(tableModel); + + public EarningsHistoryView(EarningsHistoryController controller, + EarningsHistoryViewModel viewModel) { + this.controller = controller; + this.viewModel = viewModel; + + viewModel.addPropertyChangeListener(this); + + setLayout(new BorderLayout()); + setBorder(BorderFactory.createTitledBorder("Company Earnings")); + + JPanel topPanel = new JPanel(); + topPanel.add(new JLabel("Company Symbol:")); + topPanel.add(symbolField); + topPanel.add(loadButton); + add(topPanel, BorderLayout.NORTH); + + table.setFillsViewportHeight(true); + add(new JScrollPane(table), BorderLayout.CENTER); + + loadButton.addActionListener(e -> + controller.onLoadButtonClicked(symbolField.getText())); + + loadButton.setFont(new Font("Segoe UI", Font.BOLD, 12)); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + EarningsHistoryState state = (EarningsHistoryState) evt.getNewValue(); + + loadButton.setEnabled(!state.isLoading()); + updateTable(state.getRecords()); + showMessageIfAny(state); + } + + private void updateTable(List records) { + tableModel.setRowCount(0); + for (EarningsRecord r : records) { + tableModel.addRow(new Object[]{ + r.getPeriod(), + r.getActual() != null ? String.format("%.3f", r.getActual()) : "N/A", + r.getEstimate() != null ? String.format("%.3f", r.getEstimate()) : "N/A", + r.getSurprise() != null ? String.format("%.3f", r.getSurprise()) : "N/A" + }); + } + } + + private void showMessageIfAny(EarningsHistoryState state) { + String msg = state.getMessage(); + if (msg == null || msg.isEmpty()) return; + + int type; + String title; + switch (state.getMessageType()) { + case WARNING -> { + type = JOptionPane.WARNING_MESSAGE; + title = "No Data"; + } + case ERROR -> { + type = JOptionPane.ERROR_MESSAGE; + title = "Error"; + } + case INFO -> { + type = JOptionPane.INFORMATION_MESSAGE; + title = "Info"; + } + default -> { + type = JOptionPane.PLAIN_MESSAGE; + title = "Info"; + } + } + + JOptionPane.showMessageDialog(this, msg, title, type); + } +} diff --git a/src/test/java/app_LH/AppBuilder.java b/src/test/java/app_LH/AppBuilder.java new file mode 100644 index 00000000..32956d58 --- /dev/null +++ b/src/test/java/app_LH/AppBuilder.java @@ -0,0 +1,62 @@ +//package app_LH; +// +//import data_access.FinnhubEarningsDataAccessObject; +//import interface_adapter.earnings_history.EarningsHistoryController; +//import interface_adapter.earnings_history.EarningsHistoryPresenter; +//import interface_adapter.earnings_history.EarningsHistoryViewModel; +//import use_case.earnings_history.EarningsDataAccessInterface; +//import use_case.earnings_history.GetEarningsHistoryInputBoundary; +//import use_case.earnings_history.GetEarningsHistoryInteractor; +//import use_case.earnings_history.GetEarningsHistoryOutputBoundary; +//import view.EarningsHistoryView; +// +//import javax.swing.*; +//import java.awt.*; +// +///** +// * Builds a JFrame with the EarningsHistoryView wired to its use case. +// */ +//public class AppBuilder { +// +// private final JFrame frame; +// private final CardLayout cardLayout; +// private final JPanel viewsPanel; +// private static final String EARNINGS_HISTORY_VIEW = "earnings history"; +// +// public AppBuilder() { +// frame = new JFrame("Earnings History – Test App"); +// frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); +// +// cardLayout = new CardLayout(); +// viewsPanel = new JPanel(cardLayout); +// frame.setContentPane(viewsPanel); +// } +// +// /** +// * Add the EarningsHistory view, wires controller,interactor, and DAO. +// */ +// public AppBuilder addEarningsHistoryView() { +// EarningsHistoryViewModel earningsVM = new EarningsHistoryViewModel(); +// EarningsDataAccessInterface earningsDAO = new FinnhubEarningsDataAccessObject(); +// GetEarningsHistoryOutputBoundary presenter = new EarningsHistoryPresenter(earningsVM); +// GetEarningsHistoryInputBoundary interactor = new GetEarningsHistoryInteractor(earningsDAO, presenter); +// EarningsHistoryController controller = new EarningsHistoryController(interactor, earningsVM); +// EarningsHistoryView earningsView = new EarningsHistoryView(controller, earningsVM); +// +// viewsPanel.add(earningsView, EARNINGS_HISTORY_VIEW); +// +// return this; +// } +// +// /** +// * Finalizes the window and returns the JFrame. +// */ +// public JFrame build() { +// frame.pack(); +// frame.setLocationRelativeTo(null); +// frame.setVisible(true); +// cardLayout.show(viewsPanel, EARNINGS_HISTORY_VIEW); //show the earnings view by default +// +// return frame; +// } +//} diff --git a/src/test/java/app_LH/Main.java b/src/test/java/app_LH/Main.java new file mode 100644 index 00000000..12eb7b62 --- /dev/null +++ b/src/test/java/app_LH/Main.java @@ -0,0 +1,12 @@ +//package app_LH; +// +//import javax.swing.*; +// +//public class Main { +// public static void main(String[] args) { +// SwingUtilities.invokeLater(() -> { +// AppBuilder builder = new AppBuilder(); +// builder.addEarningsHistoryView().build(); +// }); +// } +//} From 63b2efdf6cf070db60e4dccf85c8e91cbf3c60d1 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sat, 29 Nov 2025 23:17:55 -0500 Subject: [PATCH 024/103] Added addNewsView, addNewsUseCase and getViewName method for AppBuilder and NewsView --- src/main/java/app/AppBuilder.java | 33 ++++++++++++++++++++++++---- src/main/java/view/LoggedInView.java | 7 ++++++ src/main/java/view/NewsView.java | 6 +++++ 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 3c654cad..044c5388 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -1,6 +1,7 @@ package app; import data_access.FileUserDataAccessObject; +import data_access.NewsDataAccessObject; import entity.UserFactory; import interface_adapter.ViewManagerModel; import interface_adapter.logged_in.ChangePasswordController; @@ -11,6 +12,9 @@ import interface_adapter.login.LoginViewModel; import interface_adapter.logout.LogoutController; import interface_adapter.logout.LogoutPresenter; +import interface_adapter.news.NewsController; +import interface_adapter.news.NewsPresenter; +import interface_adapter.news.NewsViewModel; import interface_adapter.signup.SignupController; import interface_adapter.signup.SignupPresenter; import interface_adapter.signup.SignupViewModel; @@ -23,13 +27,13 @@ import use_case.logout.LogoutInputBoundary; import use_case.logout.LogoutInteractor; import use_case.logout.LogoutOutputBoundary; +import use_case.news.NewsInputBoundary; +import use_case.news.NewsInteractor; +import use_case.news.NewsOutputBoundary; import use_case.signup.SignupInputBoundary; import use_case.signup.SignupInteractor; import use_case.signup.SignupOutputBoundary; -import view.LoggedInView; -import view.LoginView; -import view.SignupView; -import view.ViewManager; +import view.*; import javax.swing.*; import java.awt.*; @@ -45,6 +49,7 @@ public class AppBuilder { // of the classes from the data_access package // DAO version using local file storage + final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject("d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"); final FileUserDataAccessObject userDataAccessObject = new FileUserDataAccessObject("users.csv", userFactory); // DAO version using a shared external database @@ -82,6 +87,16 @@ public AppBuilder addLoggedInView() { return this; } + public AppBuilder addNewsView() { + NewsViewModel newsViewModel = new NewsViewModel(); + NewsOutputBoundary newsOutputBoundary = new NewsPresenter(newsViewModel); + NewsInputBoundary newsInputBoundary = new NewsInteractor(newsDataAccessObject, newsOutputBoundary); + NewsController newsController = new NewsController(newsInputBoundary); + NewsView newsView = new NewsView(newsController, newsViewModel); + cardPanel.add(newsView, newsView.getViewName()); + return this; + } + public AppBuilder addSignupUseCase() { final SignupOutputBoundary signupOutputBoundary = new SignupPresenter(viewManagerModel, signupViewModel, loginViewModel); @@ -116,6 +131,16 @@ public AppBuilder addChangePasswordUseCase() { return this; } + public AppBuilder addNewsUsecase() { + NewsViewModel newsViewModel = new NewsViewModel(); + NewsOutputBoundary newsOutputBoundary = new NewsPresenter(newsViewModel); + + NewsInputBoundary newsInputBoundary = new NewsInteractor(newsDataAccessObject, newsOutputBoundary); + NewsController newsController = new NewsController(newsInputBoundary); + loggedInView.setNewsController(newsController); + return this; + } + /** * Adds the Logout Use Case to the application. * @return this builder diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 1eb2cbd8..aee1d53d 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -4,6 +4,7 @@ import interface_adapter.logged_in.LoggedInState; import interface_adapter.logged_in.LoggedInViewModel; import interface_adapter.logout.LogoutController; +import interface_adapter.news.NewsController; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -24,6 +25,7 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JLabel passwordErrorField = new JLabel(); private ChangePasswordController changePasswordController = null; private LogoutController logoutController; + private NewsController newsController; private final JLabel username; @@ -142,4 +144,9 @@ public void setChangePasswordController(ChangePasswordController changePasswordC public void setLogoutController(LogoutController logoutController) { // TODO: save the logout controller in the instance variable. } + + public void setNewsController(NewsController newsController) { + this.newsController = newsController; + } + } diff --git a/src/main/java/view/NewsView.java b/src/main/java/view/NewsView.java index ba5165c1..4d658b61 100644 --- a/src/main/java/view/NewsView.java +++ b/src/main/java/view/NewsView.java @@ -19,6 +19,8 @@ public class NewsView extends JPanel implements PropertyChangeListener { + private final String viewName = "news"; + private final NewsController controller; private final NewsViewModel viewModel; @@ -269,4 +271,8 @@ public java.awt.Component getTableCellRendererComponent( return this; } } + + public String getViewName(){ + return this.viewName; + } } From 047f28c22c163c1668ad60e306422487510ce65a Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sat, 29 Nov 2025 23:22:20 -0500 Subject: [PATCH 025/103] Added method calls to Main --- src/main/java/app/Main.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 424404fb..38f49ab8 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -9,9 +9,11 @@ public static void main(String[] args) { .addLoginView() .addSignupView() .addLoggedInView() + .addNewsView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() + .addNewsUsecase() .build(); application.pack(); From f975ed0de1f14bb9a0b1c2b9fce12ed606bfb3e6 Mon Sep 17 00:00:00 2001 From: samario Date: Sun, 30 Nov 2025 01:40:14 -0500 Subject: [PATCH 026/103] Modify the code in FilterSearchView to achieve a cleaner architecture. Then improve the display of the page. --- src/main/java/app/AppBuilder.java | 5 +- .../FilterSearchDataAccessObject.java | 14 +- .../filter_search/FilterSearchPresenter.java | 17 +- .../filter_search/FilterSearchState.java | 33 +++ .../filter_search/FilterSearchViewModel.java | 32 ++- .../filter_search/FilterSearchInteractor.java | 22 +- .../filter_search/FilterSearchResponse.java | 10 +- src/main/java/view/FilterSearchView.java | 196 +++++++++--------- 8 files changed, 185 insertions(+), 144 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 2cd61904..001b0a70 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -92,7 +92,7 @@ public AppBuilder addLoggedInView() { public AppBuilder addFilterSearchView() { filterSearchViewModel = new FilterSearchViewModel(); filterSearchView = new FilterSearchView(filterSearchViewModel); - cardPanel.add(filterSearchView, filterSearchViewModel.getViewName()); + cardPanel.add(filterSearchView, filterSearchView.getViewName()); return this; } @@ -131,8 +131,7 @@ public AppBuilder addChangePasswordUseCase() { } public AppBuilder addFilterSearchUseCase() { - final FilterSearchOutputBoundary filterSearchOutputBoundary = new FilterSearchPresenter(filterSearchViewModel, - viewManagerModel); + final FilterSearchOutputBoundary filterSearchOutputBoundary = new FilterSearchPresenter(filterSearchViewModel); String key = "d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"; FilterSearchDataAccessInterface filterObject = new FilterSearchDataAccessObject(key); final FilterSearchInputBoundary filterSearchInteractor = diff --git a/src/main/java/data_access/FilterSearchDataAccessObject.java b/src/main/java/data_access/FilterSearchDataAccessObject.java index c151e436..c149552f 100644 --- a/src/main/java/data_access/FilterSearchDataAccessObject.java +++ b/src/main/java/data_access/FilterSearchDataAccessObject.java @@ -37,21 +37,25 @@ public List loadStocks(String exchange, String mic, String securityType, } StringBuilder urlBuilder = new StringBuilder(BASE_URL) .append("/stock") - .append("/symbol?exchange=").append(exchange); + .append("/symbol?exchange=") + .append(URLEncoder.encode(exchange, StandardCharsets.UTF_8)); if (mic != null && !mic.isBlank()) { - urlBuilder.append("&mic=").append(mic); + urlBuilder.append("&mic=") + .append(URLEncoder.encode(mic, StandardCharsets.UTF_8)); } if (securityType != null && !securityType.isBlank()) { - urlBuilder.append("&type=").append(securityType); + urlBuilder.append("&securityType=") + .append(URLEncoder.encode(securityType, StandardCharsets.UTF_8)); } if (currency != null && !currency.isBlank()) { - urlBuilder.append("¤cy=").append(currency); + urlBuilder.append("¤cy=") + .append(URLEncoder.encode(currency, StandardCharsets.UTF_8)); } - urlBuilder.append("&token=").append(apiKey); + urlBuilder.append("&token=").append(URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); JSONArray array = fetchJsonArray(urlBuilder.toString()); return parseStockArray(array); diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java b/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java index 3583c2e9..bb669463 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchPresenter.java @@ -1,9 +1,9 @@ package interface_adapter.filter_search; -import interface_adapter.ViewManagerModel; +import entity.Stock; import use_case.filter_search.FilterSearchOutputBoundary; import use_case.filter_search.FilterSearchResponse; - +import java.util.List; /** * The Presenter for the Filter Search Use Case. */ @@ -11,22 +11,25 @@ public class FilterSearchPresenter implements FilterSearchOutputBoundary{ private final FilterSearchViewModel filterSearchViewModel; - public FilterSearchPresenter(FilterSearchViewModel filterSearchViewModel, ViewManagerModel viewManagerModel) { - + public FilterSearchPresenter(FilterSearchViewModel filterSearchViewModel) { this.filterSearchViewModel = filterSearchViewModel; } @Override public void prepareSuccessView(FilterSearchResponse response) { + List stocks = response.getStocks(); + System.out.println("PRESENTER: response stocks = " + + (stocks == null ? "null" : stocks.size())); - filterSearchViewModel.setStocks(response.getStocks()); - filterSearchViewModel.setState(new FilterSearchState()); - filterSearchViewModel.firePropertyChange(); + filterSearchViewModel.setStocks(stocks); + filterSearchViewModel.setErrorMessage(null); + filterSearchViewModel.firePropertyChange(); } @Override public void prepareFailView(String message) { + System.out.println("PRESENTER: fail view, message = " + message); filterSearchViewModel.setStocks(null); filterSearchViewModel.setErrorMessage(message); filterSearchViewModel.firePropertyChange(); diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchState.java b/src/main/java/interface_adapter/filter_search/FilterSearchState.java index 4c972ac4..123c39c1 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchState.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchState.java @@ -1,4 +1,37 @@ package interface_adapter.filter_search; +import entity.Stock; +import java.util.List; + public class FilterSearchState { + // List of stocks to display in the table + private List stocks; + + // Error message to show in a dialog (null or empty if no error) + private String errorMessage; + + public FilterSearchState() { + } + + // Optional copy constructor (useful if your ViewModel copies state) + public FilterSearchState(FilterSearchState copy) { + this.stocks = copy.stocks; + this.errorMessage = copy.errorMessage; + } + + public List getStocks() { + return stocks; + } + + public void setStocks(List stocks) { + this.stocks = stocks; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } } diff --git a/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java b/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java index 14fdb4e8..0120d4c1 100644 --- a/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java +++ b/src/main/java/interface_adapter/filter_search/FilterSearchViewModel.java @@ -1,7 +1,6 @@ package interface_adapter.filter_search; import entity.Stock; -import interface_adapter.ViewModel; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; @@ -11,36 +10,29 @@ * The View Model for the Filter Search View. */ -public class FilterSearchViewModel extends ViewModel { - +public class FilterSearchViewModel { private final PropertyChangeSupport support = new PropertyChangeSupport(this); + private final String viewName = "Filter Search"; + private List stocks; private String errorMessage; - public void addPropertyChangeListener(PropertyChangeListener listener) { - support.addPropertyChangeListener(listener); - } + public String getViewName() { return viewName; } - public FilterSearchViewModel() { - super("Filter Search"); - setState(new FilterSearchState()); + public List getStocks() { return stocks; } - } + public void setStocks(List stocks) { this.stocks = stocks; } - public List getStocks() { - return stocks; - } + public String getErrorMessage() { return errorMessage; } - public void setStocks(List stocks) { - this.stocks = stocks; - } + public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } - public String getErrorMessage() { - return errorMessage; + public void firePropertyChange() { + support.firePropertyChange("filterSearch", null, null); } - public void setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; + public void addPropertyChangeListener(PropertyChangeListener l) { + support.addPropertyChangeListener(l); } } diff --git a/src/main/java/use_case/filter_search/FilterSearchInteractor.java b/src/main/java/use_case/filter_search/FilterSearchInteractor.java index bec5de89..57980d39 100644 --- a/src/main/java/use_case/filter_search/FilterSearchInteractor.java +++ b/src/main/java/use_case/filter_search/FilterSearchInteractor.java @@ -12,28 +12,36 @@ public class FilterSearchInteractor implements FilterSearchInputBoundary { private final FilterSearchDataAccessInterface filterSearchDataAccess; private final FilterSearchOutputBoundary filterSearchPresenter; - public FilterSearchInteractor(FilterSearchDataAccessInterface data, FilterSearchOutputBoundary filterSearchOutputBoundary) { + public FilterSearchInteractor(FilterSearchDataAccessInterface data, + FilterSearchOutputBoundary filterSearchOutputBoundary) { this.filterSearchDataAccess = data; this.filterSearchPresenter = filterSearchOutputBoundary; } @Override public void execute(FilterSearchRequest filterSearchRequest) { + final String exchange = filterSearchRequest.getExchange(); + final String mic = filterSearchRequest.getMic(); + final String securityType = filterSearchRequest.getSecurityType(); + final String currency = filterSearchRequest.getCurrency(); try { - final String exchange = filterSearchRequest.getExchange(); - final String mic = filterSearchRequest.getMic(); - final String securityType = filterSearchRequest.getSecurityType(); - final String currency = filterSearchRequest.getCurrency(); + List stocks = + filterSearchDataAccess.loadStocks(exchange, mic, securityType, currency); - List stocks = filterSearchDataAccess.loadStocks(exchange, mic, securityType, currency); + System.out.println("INTERACTOR: stocks from DAO = " + + (stocks == null ? "null" : stocks.size())); FilterSearchResponse response = new FilterSearchResponse(stocks); filterSearchPresenter.prepareSuccessView(response); } catch (Exception e) { - filterSearchPresenter.prepareFailView("Unable to load filtered stocks."); + // Debug output in console: + e.printStackTrace(); + + filterSearchPresenter.prepareFailView("Unable to load filtered stocks." + + e.getMessage()); } } } diff --git a/src/main/java/use_case/filter_search/FilterSearchResponse.java b/src/main/java/use_case/filter_search/FilterSearchResponse.java index b52d60ac..2063001e 100644 --- a/src/main/java/use_case/filter_search/FilterSearchResponse.java +++ b/src/main/java/use_case/filter_search/FilterSearchResponse.java @@ -5,13 +5,15 @@ import java.util.List; public class FilterSearchResponse { - private final List result; + private final List stocks; - public FilterSearchResponse(List result) { - this.result = result; + public FilterSearchResponse(List stocks) { + System.out.println("RESPONSE ctor: incoming stocks = " + + (stocks == null ? "null" : stocks.size())); + this.stocks = stocks; } public List getStocks() { - return this.result; + return stocks; } } diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java index 1092e287..74f32822 100644 --- a/src/main/java/view/FilterSearchView.java +++ b/src/main/java/view/FilterSearchView.java @@ -8,6 +8,7 @@ import use_case.filter_search.FilterSearchInputBoundary; import javax.swing.*; +import javax.swing.table.DefaultTableModel; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -22,11 +23,11 @@ public class FilterSearchView extends JPanel implements PropertyChangeListener { private FilterSearchController filterSearchController; + private final FilterSearchViewModel filterSearchViewModel; - private String ex; - private String mi; - private String sec; - private String curr; + // Table-related fields + private JTable table; + private DefaultTableModel tableModel; private static final int COL_SYMBOL = 0; private static final int COL_DESCRIPTION = 1; @@ -39,62 +40,71 @@ public class FilterSearchView extends JPanel implements PropertyChangeListener { public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { - filterSearchViewModel.addPropertyChangeListener(this); + this.filterSearchViewModel = filterSearchViewModel; + this.filterSearchViewModel.addPropertyChangeListener(this); final String[] exchangeOptions = FilterSearchInputBoundary.EXCHANGE_OPTIONS; final String[] micOptions = FilterSearchInputBoundary.MIC_OPTIONS; final String[] securityOptions = FilterSearchInputBoundary.SECURITY_OPTIONS; final String[] currencyOptions = FilterSearchInputBoundary.CURRENCY_OPTIONS; - - final JLabel title = new JLabel("Filter Search"); - title.setAlignmentX(Component.CENTER_ALIGNMENT); - - final JComboBox exchangeDrop = new JComboBox(exchangeOptions); - final JComboBox micDrop = new JComboBox(micOptions); - final JComboBox securityDrop = new JComboBox(securityOptions); - final JComboBox currencyDrop = new JComboBox(currencyOptions); + final JComboBox exchangeDrop = new JComboBox<>(exchangeOptions); + final JComboBox micDrop = new JComboBox<>(micOptions); + final JComboBox securityDrop = new JComboBox<>(securityOptions); + final JComboBox currencyDrop = new JComboBox<>(currencyOptions); JButton search = new JButton("Search"); - this.setLayout(new BorderLayout()); + setLayout(new BorderLayout()); - JPanel topPanel = new JPanel(); + JPanel topPanel = new JPanel(new BorderLayout()); this.add(topPanel, BorderLayout.NORTH); + JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton backToHomeButton = new JButton("Back"); + leftPanel.add(backToHomeButton); + topPanel.add(leftPanel, BorderLayout.WEST); + + JPanel filtersPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JPanel panel_e = new JPanel(); - JLabel exchange = new JLabel("Exchange"); - panel_e.add(exchange); + panel_e.add(new JLabel("Exchange")); panel_e.add(exchangeDrop); JPanel panel_m = new JPanel(); - JLabel mic = new JLabel("MIC:"); - panel_m.add(mic); + panel_m.add(new JLabel("MIC:")); panel_m.add(micDrop); JPanel panel_s = new JPanel(); - JLabel securityType = new JLabel("Security Type:"); - panel_s.add(securityType); + panel_s.add(new JLabel("Security Type:")); panel_s.add(securityDrop); JPanel panel_c = new JPanel(); - JLabel currency = new JLabel("Currency:"); - panel_c.add(currency); + panel_c.add(new JLabel("Currency:")); panel_c.add(currencyDrop); - JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - JButton backToHomeButton = new JButton("Back"); - leftPanel.add(backToHomeButton); - topPanel.add(leftPanel, BorderLayout.WEST); + filtersPanel.add(panel_e); + filtersPanel.add(panel_m); + filtersPanel.add(panel_s); + filtersPanel.add(panel_c); + filtersPanel.add(search); - topPanel.add(panel_e, BorderLayout.PAGE_START); - topPanel.add(panel_m, BorderLayout.PAGE_START); - topPanel.add(panel_s, BorderLayout.PAGE_START); - topPanel.add(panel_c, BorderLayout.PAGE_START); - topPanel.add(search, BorderLayout.PAGE_START); + topPanel.add(filtersPanel, BorderLayout.CENTER); - JPanel mainPanel = new JPanel(); + String[] columnNames = {"Symbol", "Description", "Currency", + "Display Symbol", "FIGI", "MIC", "Security Type"}; - String[] columnNames = {"Symbol", "Description", "Currency", "Display Symbol", "FIGI", "MIC", "Security Type"}; + tableModel = new DefaultTableModel(columnNames, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return false; // read-only table + } + }; + table = new JTable(tableModel); + table.setFillsViewportHeight(true); + table.setPreferredScrollableViewportSize(new Dimension(800, 400)); + + JScrollPane scrollPane = new JScrollPane(table); + add(scrollPane, BorderLayout.CENTER); // Home Button backToHomeButton.addActionListener(e -> { @@ -108,88 +118,78 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { ); }); - // Search Button search.addActionListener(e -> { - String ex = exchangeDrop.getSelectedItem().toString(); - String mi = null; - String sec = null; - String curr = null; - if (micDrop.getSelectedItem() != null) { - mi = micDrop.getSelectedItem().toString(); - } - - if (securityDrop.getSelectedItem() != null) { - sec = securityDrop.getSelectedItem().toString(); - } - - if (currencyDrop.getSelectedItem() != null) { - curr = currencyDrop.getSelectedItem().toString(); - } - - filterSearchController.loadStocks(ex, mi, sec, curr); - data_access.FilterSearchDataAccessObject obj = new - FilterSearchDataAccessObject("d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"); - List res; - try { - res = obj.loadStocks(ex, mi, sec, curr); - - Object[][] data = new Object[res.size()][7]; - - for (int i = 0; i < res.size(); i++) { - Object[] temp = new Object[7]; - temp[0] = res.get(i).getSymbol(); - temp[1] = res.get(i).getDescription(); - temp[2] = res.get(i).getCurrency(); - temp[3] = res.get(i).getDisplaySymbol(); - temp[4] = res.get(i).getFigi(); - temp[5] = res.get(i).getMic(); - temp[6] = res.get(i).getType(); - - data[i] = temp; - } - - JTable table = new JTable(data, columnNames); - table.setPreferredScrollableViewportSize(new Dimension(800, 800)); - table.setFillsViewportHeight(true); - - JScrollPane scrollPane = new JScrollPane(table); - - mainPanel.add(scrollPane, BorderLayout.CENTER); - - this.add(mainPanel, BorderLayout.CENTER); - } catch (Exception exc) { + String ex = exchangeDrop.getSelectedItem() != null + ? exchangeDrop.getSelectedItem().toString() : ""; + String mi = micDrop.getSelectedItem() != null + ? micDrop.getSelectedItem().toString() : ""; + String sec = securityDrop.getSelectedItem() != null + ? securityDrop.getSelectedItem().toString() : ""; + String curr = currencyDrop.getSelectedItem() != null + ? currencyDrop.getSelectedItem().toString() : ""; + + if (filterSearchController != null) { + filterSearchController.loadStocks(ex, mi, sec, curr); + } else { JOptionPane.showMessageDialog( this, - "Error while loading stocks: " + exc.getMessage(), - "Error", - JOptionPane.ERROR_MESSAGE + "FilterSearchController is not set.", + "Warning", + JOptionPane.WARNING_MESSAGE ); } - - - }); - - - - } - public String getViewName() { - String viewName = "Filter Search"; - return viewName; + public String getViewName() {; + return "Filter Search"; } public void setFilterSearchController(FilterSearchController filterSearchController) { this.filterSearchController = filterSearchController; } + @Override public void propertyChange(PropertyChangeEvent evt) { - final FilterSearchState state = (FilterSearchState) evt.getNewValue(); - } + List stocks = filterSearchViewModel.getStocks(); + System.out.println("VIEW propertyChange: stocks = " + + (stocks == null ? "null" : stocks.size())); + String error = filterSearchViewModel.getErrorMessage(); + if (error != null && !error.isEmpty()) { + JOptionPane.showMessageDialog( + this, + error, + "Search Error", + JOptionPane.ERROR_MESSAGE + ); + return; + } + + // Update table model with new stocks + tableModel.setRowCount(0); // clear old rows + + if (stocks != null) { + for (Stock s : stocks) { + Object[] row = new Object[7]; + row[COL_SYMBOL] = s.getSymbol(); + row[COL_DESCRIPTION] = s.getDescription(); + row[COL_CURRENCY] = s.getCurrency(); + row[COL_DISPLAY] = s.getDisplaySymbol(); + row[COL_FIGI] = s.getFigi(); + row[COL_MIC] = s.getMic(); + row[COL_TYPE] = s.getType(); + tableModel.addRow(row); + } + } + System.out.println("TABLE rowCount = " + tableModel.getRowCount()); + } } + + + + From 32fc4c591110bbc74010cc8512e8d64f4b603ec8 Mon Sep 17 00:00:00 2001 From: SamarioLiu Date: Sun, 30 Nov 2025 02:01:36 -0500 Subject: [PATCH 027/103] Revert "2 news page" --- src/main/java/app/AppBuilder.java | 33 +-- src/main/java/app/Main.java | 2 - .../data_access/NewsDataAccessObject.java | 179 ----------- src/main/java/entity/NewsArticle.java | 53 ---- .../news/NewsController.java | 31 -- .../interface_adapter/news/NewsPresenter.java | 32 -- .../interface_adapter/news/NewsViewModel.java | 47 --- .../news/NewsDataAccessInterface.java | 17 -- .../java/use_case/news/NewsInputBoundary.java | 12 - .../java/use_case/news/NewsInteractor.java | 70 ----- .../use_case/news/NewsOutputBoundary.java | 11 - .../java/use_case/news/NewsRequestModel.java | 46 --- .../java/use_case/news/NewsResponseModel.java | 21 -- src/main/java/view/LoggedInView.java | 7 - src/main/java/view/NewsView.java | 278 ------------------ .../use_case/news/NewsInteractorTest.java | 201 ------------- 16 files changed, 4 insertions(+), 1036 deletions(-) delete mode 100644 src/main/java/data_access/NewsDataAccessObject.java delete mode 100644 src/main/java/entity/NewsArticle.java delete mode 100644 src/main/java/interface_adapter/news/NewsController.java delete mode 100644 src/main/java/interface_adapter/news/NewsPresenter.java delete mode 100644 src/main/java/interface_adapter/news/NewsViewModel.java delete mode 100644 src/main/java/use_case/news/NewsDataAccessInterface.java delete mode 100644 src/main/java/use_case/news/NewsInputBoundary.java delete mode 100644 src/main/java/use_case/news/NewsInteractor.java delete mode 100644 src/main/java/use_case/news/NewsOutputBoundary.java delete mode 100644 src/main/java/use_case/news/NewsRequestModel.java delete mode 100644 src/main/java/use_case/news/NewsResponseModel.java delete mode 100644 src/main/java/view/NewsView.java delete mode 100644 src/test/java/use_case/news/NewsInteractorTest.java diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 4867220f..2850af25 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -1,7 +1,6 @@ package app; import data_access.FileUserDataAccessObject; -import data_access.NewsDataAccessObject; import entity.UserFactory; import interface_adapter.ViewManagerModel; import interface_adapter.logged_in.ChangePasswordController; @@ -12,9 +11,6 @@ import interface_adapter.login.LoginViewModel; import interface_adapter.logout.LogoutController; import interface_adapter.logout.LogoutPresenter; -import interface_adapter.news.NewsController; -import interface_adapter.news.NewsPresenter; -import interface_adapter.news.NewsViewModel; import interface_adapter.signup.SignupController; import interface_adapter.signup.SignupPresenter; import interface_adapter.signup.SignupViewModel; @@ -27,13 +23,13 @@ import use_case.logout.LogoutInputBoundary; import use_case.logout.LogoutInteractor; import use_case.logout.LogoutOutputBoundary; -import use_case.news.NewsInputBoundary; -import use_case.news.NewsInteractor; -import use_case.news.NewsOutputBoundary; import use_case.signup.SignupInputBoundary; import use_case.signup.SignupInteractor; import use_case.signup.SignupOutputBoundary; -import view.*; +import view.LoggedInView; +import view.LoginView; +import view.SignupView; +import view.ViewManager; import javax.swing.*; import java.awt.*; @@ -49,7 +45,6 @@ public class AppBuilder { // of the classes from the data_access package // DAO version using local file storage - final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject("d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"); final FileUserDataAccessObject userDataAccessObject = new FileUserDataAccessObject("users.csv", userFactory); // DAO version using a shared external database @@ -87,16 +82,6 @@ public AppBuilder addLoggedInView() { return this; } - public AppBuilder addNewsView() { - NewsViewModel newsViewModel = new NewsViewModel(); - NewsOutputBoundary newsOutputBoundary = new NewsPresenter(newsViewModel); - NewsInputBoundary newsInputBoundary = new NewsInteractor(newsDataAccessObject, newsOutputBoundary); - NewsController newsController = new NewsController(newsInputBoundary); - NewsView newsView = new NewsView(newsController, newsViewModel); - cardPanel.add(newsView, newsView.getViewName()); - return this; - } - public AppBuilder addSignupUseCase() { final SignupOutputBoundary signupOutputBoundary = new SignupPresenter(viewManagerModel, signupViewModel, loginViewModel); @@ -131,16 +116,6 @@ public AppBuilder addChangePasswordUseCase() { return this; } - public AppBuilder addNewsUsecase() { - NewsViewModel newsViewModel = new NewsViewModel(); - NewsOutputBoundary newsOutputBoundary = new NewsPresenter(newsViewModel); - - NewsInputBoundary newsInputBoundary = new NewsInteractor(newsDataAccessObject, newsOutputBoundary); - NewsController newsController = new NewsController(newsInputBoundary); - loggedInView.setNewsController(newsController); - return this; - } - /** * Adds the Logout Use Case to the application. * @return this builder diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 38f49ab8..424404fb 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -9,11 +9,9 @@ public static void main(String[] args) { .addLoginView() .addSignupView() .addLoggedInView() - .addNewsView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() - .addNewsUsecase() .build(); application.pack(); diff --git a/src/main/java/data_access/NewsDataAccessObject.java b/src/main/java/data_access/NewsDataAccessObject.java deleted file mode 100644 index 7c79a53d..00000000 --- a/src/main/java/data_access/NewsDataAccessObject.java +++ /dev/null @@ -1,179 +0,0 @@ -package data_access; - -import entity.NewsArticle; -import use_case.news.NewsDataAccessInterface; - -import org.json.JSONObject; -import org.json.JSONArray; -import org.json.JSONTokener; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -/** - * Data access implementation for Finnhub news endpoints using org.json. - *

- * Endpoints: - * - General market news: - * - Company news: - */ -public class NewsDataAccessObject implements NewsDataAccessInterface { - - private static final String BASE_URL = "https://finnhub.io/api/v1"; - - private final String apiKey; - - public NewsDataAccessObject(String apiKey) { - this.apiKey = apiKey; - } - - @Override - public List loadMarketNews() throws Exception { - String endpoint = BASE_URL + "/news?category=general&token=" + - URLEncoder.encode(apiKey, StandardCharsets.UTF_8); - JSONArray array = fetchJsonArray(endpoint); - return parseNewsArray(array); - } - - @Override - public List loadCompanyNews(String symbol, - String fromDate, - String toDate) throws Exception { - if (symbol == null || symbol.isBlank()) { - throw new IllegalArgumentException("Symbol must not be empty for company news."); - } - - // It is okay if fromDate or toDate are null/blank: Finnhub may require them, - // so we can set defaults here (for example: last 7 days) - StringBuilder urlBuilder = new StringBuilder(BASE_URL) - .append("/company-news") - .append("?symbol=").append(URLEncoder.encode(symbol, StandardCharsets.UTF_8)); - - if (fromDate != null && !fromDate.isBlank()) { - urlBuilder.append("&from=").append(URLEncoder.encode(fromDate, StandardCharsets.UTF_8)); - } - if (toDate != null && !toDate.isBlank()) { - urlBuilder.append("&to=").append(URLEncoder.encode(toDate, StandardCharsets.UTF_8)); - } - - urlBuilder.append("&token=").append(URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); - - JSONArray array = fetchJsonArray(urlBuilder.toString()); - return parseNewsArray(array); - } - - /** - * Helper: performs HTTP GET and returns the response as a JSONArray. - */ - private JSONArray fetchJsonArray(String urlString) throws IOException { - HttpURLConnection connection = null; - InputStream inputStream = null; - - try { - URL url = new URL(urlString); - connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - - // Possible timeout - connection.setConnectTimeout(10_000); - connection.setReadTimeout(10_000); - - int status = connection.getResponseCode(); - if (status != HttpURLConnection.HTTP_OK) { - // Read errors - String errorBody = readStream(connection.getErrorStream()); - throw new IOException("HTTP error " + status + " from Finnhub: " + errorBody); - } - - inputStream = connection.getInputStream(); - JSONTokener tokener = new JSONTokener(inputStream); - return new JSONArray(tokener); - - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException ignored) {} - } - if (connection != null) { - connection.disconnect(); - } - } - } - - /** - * Helper: read an InputStream fully into a String (for error messages). - */ - private String readStream(InputStream stream) throws IOException { - if (stream == null) return ""; - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(stream, StandardCharsets.UTF_8))) { - - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append('\n'); - } - return sb.toString(); - } - } - - /** - * Parse JSON array into a list of NewsArticle entities. - *

- * Each object typically has fields: - * - id (long) - * - category (String) - * - datetime (long) - * - headline (String) - * - image (String) - * - related (String) // comma-separated symbols - * - source (String) - * - summary (String) - * - url (String) - */ - private List parseNewsArray(JSONArray array) { - List result = new ArrayList<>(); - if (array == null) { - return result; - } - - for (int i = 0; i < array.length(); i++) { - JSONObject obj = array.optJSONObject(i); - if (obj == null) continue; - - long id = obj.optLong("id", 0L); - String category = obj.optString("category", ""); - long datetime = obj.optLong("datetime", 0L); - String headline = obj.optString("headline", ""); - String image = obj.optString("image", ""); - String related = obj.optString("related", ""); - String source = obj.optString("source", ""); - String summary = obj.optString("summary", ""); - String url = obj.optString("url", ""); - - NewsArticle article = new NewsArticle( - id, - category, - datetime, - headline, - image, - related, - source, - summary, - url - ); - result.add(article); - } - - return result; - } -} diff --git a/src/main/java/entity/NewsArticle.java b/src/main/java/entity/NewsArticle.java deleted file mode 100644 index c8214f79..00000000 --- a/src/main/java/entity/NewsArticle.java +++ /dev/null @@ -1,53 +0,0 @@ -package entity; - -public class NewsArticle { - - // Fields from Finnhub API - private final long id; // unique article id - private final String category; // e.g. "general", "crypto", etc. - private final long datetime; // UNIX timestamp (seconds) - private final String headline; // article title - private final String image; // image URL - private final String related; // related stock and companies, e.g. "AAPL,Apple.Inc" - private final String source; // e.g., "Bloomberg" - private final String summary; // short description - private final String url; // link to full article - - public NewsArticle(long id, - String category, - long datetime, - String headline, - String image, - String related, - String source, - String summary, - String url) { - this.id = id; - this.category = category; - this.datetime = datetime; - this.headline = headline; - this.image = image; - this.related = related; - this.source = source; - this.summary = summary; - this.url = url; - } - - public long getId() {return id;} - - public String getCategory() {return category;} - - public long getDatetime() {return datetime;} - - public String getHeadline() {return headline;} - - public String getImage() {return image;} - - public String getRelated() {return related;} - - public String getSource() {return source;} - - public String getSummary() {return summary;} - - public String getUrl() {return url;} -} \ No newline at end of file diff --git a/src/main/java/interface_adapter/news/NewsController.java b/src/main/java/interface_adapter/news/NewsController.java deleted file mode 100644 index c77052f4..00000000 --- a/src/main/java/interface_adapter/news/NewsController.java +++ /dev/null @@ -1,31 +0,0 @@ -package interface_adapter.news; - -import use_case.news.NewsInputBoundary; -import use_case.news.NewsRequestModel; - -/** - * read from UI - * build a NewsRequestModel - * "send" the request through input boundary - */ -public class NewsController { - - private final NewsInputBoundary interactor; - - public NewsController(NewsInputBoundary interactor) { - this.interactor = interactor; - } - - // Load general market news (category=general) - public void loadMarketNews() { - NewsRequestModel request = new NewsRequestModel(); // market news ctor - interactor.executeMarketNews(request); - } - - // Load company-specific news (for a symbol like "AAPL") - public void loadCompanyNews(String symbol, String fromDate, String toDate) { - NewsRequestModel request = new NewsRequestModel(symbol, fromDate, toDate); - interactor.executeCompanyNews(request); - } -} - diff --git a/src/main/java/interface_adapter/news/NewsPresenter.java b/src/main/java/interface_adapter/news/NewsPresenter.java deleted file mode 100644 index add0a3c5..00000000 --- a/src/main/java/interface_adapter/news/NewsPresenter.java +++ /dev/null @@ -1,32 +0,0 @@ -package interface_adapter.news; - -import use_case.news.NewsOutputBoundary; -import use_case.news.NewsResponseModel; - -/** - * receives output data from interactor - * writes the data through ViewModel nad then update the state - */ -public class NewsPresenter implements NewsOutputBoundary { - - private final NewsViewModel viewModel; - - public NewsPresenter(NewsViewModel viewModel) { - this.viewModel = viewModel; - } - - @Override - public void prepareSuccessView(NewsResponseModel responseModel) { - viewModel.setArticles(responseModel.getArticles()); - viewModel.setErrorMessage(null); - viewModel.firePropertyChanged(); - } - - @Override - public void prepareFailView(String errorMessage) { - viewModel.setArticles(null); - viewModel.setErrorMessage(errorMessage); - viewModel.firePropertyChanged(); - } -} - diff --git a/src/main/java/interface_adapter/news/NewsViewModel.java b/src/main/java/interface_adapter/news/NewsViewModel.java deleted file mode 100644 index 794b56c8..00000000 --- a/src/main/java/interface_adapter/news/NewsViewModel.java +++ /dev/null @@ -1,47 +0,0 @@ -package interface_adapter.news; - -import entity.NewsArticle; - -import java.beans.PropertyChangeListener; -import java.beans.PropertyChangeSupport; -import java.util.List; - -/** - * The Presenter can update the states, so the View will changer correspondingly - */ -public class NewsViewModel { - - private final PropertyChangeSupport support = new PropertyChangeSupport(this); - - // UI state fields (were in NewsState before) - private List articles; - private String errorMessage; - - // listeners - public void addPropertyChangeListener(PropertyChangeListener listener) { - support.addPropertyChangeListener(listener); - } - - public void firePropertyChanged() { - // property name can be anything, keep "state" to avoid changing other code too much - support.firePropertyChange("state", null, this); - } - - - public List getArticles() { - return articles; - } - - public void setArticles(List articles) { - this.articles = articles; - } - - public String getErrorMessage() { - return errorMessage; - } - - public void setErrorMessage(String errorMessage) { - this.errorMessage = errorMessage; - } -} - diff --git a/src/main/java/use_case/news/NewsDataAccessInterface.java b/src/main/java/use_case/news/NewsDataAccessInterface.java deleted file mode 100644 index 70b49552..00000000 --- a/src/main/java/use_case/news/NewsDataAccessInterface.java +++ /dev/null @@ -1,17 +0,0 @@ -package use_case.news; - -import java.util.List; -import entity.NewsArticle; - -public interface NewsDataAccessInterface { - - // Maps to Finnhub endpoint: /news?category=general - List loadMarketNews() throws Exception; - - //Maps to Finnhub endpoint: - // /company-news?symbol={symbol}&from={fromDate}&to={toDate} - // Dates are "YYYY-MM-DD". - List loadCompanyNews(String symbol, - String fromDate, - String toDate) throws Exception; -} \ No newline at end of file diff --git a/src/main/java/use_case/news/NewsInputBoundary.java b/src/main/java/use_case/news/NewsInputBoundary.java deleted file mode 100644 index 50bc751f..00000000 --- a/src/main/java/use_case/news/NewsInputBoundary.java +++ /dev/null @@ -1,12 +0,0 @@ -package use_case.news; - -/** - * Define what actions this user case supports - */ -public interface NewsInputBoundary { - // Load general market news (category=general). - void executeMarketNews(NewsRequestModel requestModel); - - // Load company-specific news for the given symbol. - void executeCompanyNews(NewsRequestModel requestModel); -} diff --git a/src/main/java/use_case/news/NewsInteractor.java b/src/main/java/use_case/news/NewsInteractor.java deleted file mode 100644 index ad767ac5..00000000 --- a/src/main/java/use_case/news/NewsInteractor.java +++ /dev/null @@ -1,70 +0,0 @@ -package use_case.news; - -import entity.NewsArticle; -import java.util.List; - -/** - * Implement "what happens when user asks for news" - *

- * 1. Validate the NewsRequestModel - * 2. Call NewsDataAccessInterface - * 3. Build a NewsResponse Model - * 4. Call the presenter -- NewsOutputBoundary - */ -public class NewsInteractor implements NewsInputBoundary{ - private final NewsDataAccessInterface newsDataAccess; - private final NewsOutputBoundary newsPresenter; - - public NewsInteractor(NewsDataAccessInterface newsDataAccess, - NewsOutputBoundary newsPresenter) { - this.newsDataAccess = newsDataAccess; - this.newsPresenter = newsPresenter; - } - - @Override - public void executeMarketNews(NewsRequestModel requestModel) { - try { - List articles = newsDataAccess.loadMarketNews(); - - if (articles == null || articles.isEmpty()) { - newsPresenter.prepareFailView("No market news available."); - return; - } - - NewsResponseModel response = new NewsResponseModel(articles); - newsPresenter.prepareSuccessView(response); - - } catch (Exception e) { - newsPresenter.prepareFailView("Unable to load market news: " + e.getMessage()); - } - } - - @Override - public void executeCompanyNews(NewsRequestModel requestModel) { - String symbol = requestModel.getSymbol(); - - if (symbol == null || symbol.isBlank()) { - newsPresenter.prepareFailView("Symbol is required for company news."); - return; - } - - try { - List articles = newsDataAccess.loadCompanyNews( - symbol, - requestModel.getFromDate(), - requestModel.getToDate() - ); - - if (articles == null || articles.isEmpty()) { - newsPresenter.prepareFailView("No news found for symbol: " + symbol); - return; - } - - NewsResponseModel response = new NewsResponseModel(articles); - newsPresenter.prepareSuccessView(response); - - } catch (Exception e) { - newsPresenter.prepareFailView("Unable to load company news: " + e.getMessage()); - } - } -} diff --git a/src/main/java/use_case/news/NewsOutputBoundary.java b/src/main/java/use_case/news/NewsOutputBoundary.java deleted file mode 100644 index 97023df9..00000000 --- a/src/main/java/use_case/news/NewsOutputBoundary.java +++ /dev/null @@ -1,11 +0,0 @@ -package use_case.news; - -/** - * Interface implemented by the presenter - */ -public interface NewsOutputBoundary { - - void prepareSuccessView(NewsResponseModel responseModel); - - void prepareFailView(String errorMessage); -} diff --git a/src/main/java/use_case/news/NewsRequestModel.java b/src/main/java/use_case/news/NewsRequestModel.java deleted file mode 100644 index 9e6e6226..00000000 --- a/src/main/java/use_case/news/NewsRequestModel.java +++ /dev/null @@ -1,46 +0,0 @@ -package use_case.news; - -/** - * Request model for news. - * For market news, symbol/fromDate/toDate can all be null. - * For company news, symbol must be non-null; dates may be null (use default range). - * Input Data holder, the data comes from the UI - */ -public class NewsRequestModel { - - private final String symbol; // e.g. "AAPL" or null for market news - private final String fromDate; // "YYYY-MM-DD" or null - private final String toDate; // "YYYY-MM-DD" or null - - // Constructor for general market news - public NewsRequestModel() { - this.symbol = null; - this.fromDate = null; - this.toDate = null; - } - - // Constructor for company news - public NewsRequestModel(String symbol, String fromDate, String toDate) { - this.symbol = symbol; - this.fromDate = fromDate; - this.toDate = toDate; - } - - public String getSymbol() { - return symbol; - } - - public String getFromDate() { - return fromDate; - } - - public String getToDate() { - return toDate; - } - - // Market news don't have a symbol - public boolean isMarketNewsRequest() { - return symbol == null || symbol.isBlank(); - } -} - diff --git a/src/main/java/use_case/news/NewsResponseModel.java b/src/main/java/use_case/news/NewsResponseModel.java deleted file mode 100644 index 5463c7c8..00000000 --- a/src/main/java/use_case/news/NewsResponseModel.java +++ /dev/null @@ -1,21 +0,0 @@ -package use_case.news; - -import entity.NewsArticle; -import java.util.List; - -/** - * Data holder for the presenter to build the View - * Created by the Interactor and passed to OutputBoundary - */ -public class NewsResponseModel { - - private final List articles; - - public NewsResponseModel(List articles) { - this.articles = articles; - } - - public List getArticles() { - return articles; - } -} diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 2cb286fb..0ef0d59d 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -4,7 +4,6 @@ import interface_adapter.logged_in.LoggedInState; import interface_adapter.logged_in.LoggedInViewModel; import interface_adapter.logout.LogoutController; -import interface_adapter.news.NewsController; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -25,7 +24,6 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JLabel passwordErrorField = new JLabel(); private ChangePasswordController changePasswordController = null; private LogoutController logoutController; - private NewsController newsController; private final JLabel username; @@ -173,9 +171,4 @@ public void setChangePasswordController(ChangePasswordController changePasswordC public void setLogoutController(LogoutController logoutController) { // TODO: save the logout controller in the instance variable. } - - public void setNewsController(NewsController newsController) { - this.newsController = newsController; - } - } diff --git a/src/main/java/view/NewsView.java b/src/main/java/view/NewsView.java deleted file mode 100644 index 4d658b61..00000000 --- a/src/main/java/view/NewsView.java +++ /dev/null @@ -1,278 +0,0 @@ -package view; - -import interface_adapter.news.NewsController; -import interface_adapter.news.NewsViewModel; -import entity.NewsArticle; - -import javax.swing.*; -import javax.swing.table.DefaultTableModel; -import java.awt.*; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.List; -import javax.swing.JTextArea; - - -public class NewsView extends JPanel implements PropertyChangeListener { - - private final String viewName = "news"; - - private final NewsController controller; - private final NewsViewModel viewModel; - - private final JButton backToHomeButton; - private final JButton marketNewsButton; - private final JButton companyNewsButton; - private final JTextField symbolField; - private final JTextField fromDateField; - private final JTextField toDateField; - private final JTable newsTable; - private final DefaultTableModel tableModel; - private final JLabel errorLabel; - - private static final DateTimeFormatter DATE_FORMATTER = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - - // column indices for later use - private static final int COL_DATE = 0; - private static final int COL_SYMBOL = 1; - private static final int COL_TITLE = 2; - private static final int COL_SUMMARY = 3; - private static final int COL_SOURCE = 4; - private static final int COL_LINK = 5; - private static final int COL_IMAGE = 6; - - public NewsView(NewsController controller, NewsViewModel viewModel) { - this.controller = controller; - this.viewModel = viewModel; - - this.viewModel.addPropertyChangeListener(this); - - setLayout(new BorderLayout()); - - // ===== TOP PANEL ===== - JPanel topPanel = new JPanel(new BorderLayout()); - - // Left: Back to Home button - JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - backToHomeButton = new JButton("Back to Home"); - leftPanel.add(backToHomeButton); - topPanel.add(leftPanel, BorderLayout.WEST); - - // Right: symbol/date filters and news buttons - JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - - symbolField = new JTextField(8); - fromDateField = new JTextField(10); - toDateField = new JTextField(10); - companyNewsButton = new JButton("Company News"); - marketNewsButton = new JButton("Market News"); - - rightPanel.add(new JLabel("Symbol:")); - rightPanel.add(symbolField); - rightPanel.add(new JLabel("From (YYYY-MM-DD):")); - rightPanel.add(fromDateField); - rightPanel.add(new JLabel("To (YYYY-MM-DD):")); - rightPanel.add(toDateField); - rightPanel.add(companyNewsButton); - rightPanel.add(marketNewsButton); - - topPanel.add(rightPanel, BorderLayout.EAST); - - add(topPanel, BorderLayout.NORTH); - - // ===== CENTRE TABLE ===== - String[] columnNames = {"Date", "Symbol", "Title", "Summary", "Source", "Link", "Image"}; - tableModel = new DefaultTableModel(columnNames, 0) { - @Override - public boolean isCellEditable(int row, int column) { - return false; - } - }; - - // Create the NewsTable right here - newsTable = new JTable(tableModel); - - // set renderer for headline column to enable wrapping - newsTable.getColumnModel() - .getColumn(COL_SUMMARY) - .setCellRenderer(new TextAreaRenderer()); - - newsTable.addMouseListener(new java.awt.event.MouseAdapter() { - @Override - public void mouseClicked(java.awt.event.MouseEvent e) { - int row = newsTable.rowAtPoint(e.getPoint()); - int col = newsTable.columnAtPoint(e.getPoint()); - - if (row < 0 || currentArticles == null || row >= currentArticles.size()) { - return; - } - - NewsArticle article = currentArticles.get(row); - - try { - if (col == COL_LINK) { // column index for "Link" - String url = article.getUrl(); - if (url != null && !url.isBlank()) { - java.awt.Desktop.getDesktop().browse(new java.net.URI(url)); - } - } else if (col == COL_IMAGE) { // column index for "Image" - String imgUrl = article.getImage(); - if (imgUrl != null && !imgUrl.isBlank()) { - java.awt.Desktop.getDesktop().browse(new java.net.URI(imgUrl)); - } - } - } catch (Exception ex) { - JOptionPane.showMessageDialog( - NewsView.this, - "Unable to open link: " + ex.getMessage(), - "Error", - JOptionPane.ERROR_MESSAGE - ); - } - } - }); - - // turn off auto-resize of chart - newsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); - - //set preferred width - newsTable.getColumnModel().getColumn(COL_DATE).setPreferredWidth(130); - newsTable.getColumnModel().getColumn(COL_SYMBOL).setPreferredWidth(80); - newsTable.getColumnModel().getColumn(COL_TITLE).setPreferredWidth(160); - newsTable.getColumnModel().getColumn(COL_SUMMARY).setPreferredWidth(360); - newsTable.getColumnModel().getColumn(COL_SOURCE).setPreferredWidth(100); - newsTable.getColumnModel().getColumn(COL_LINK).setPreferredWidth(60); - newsTable.getColumnModel().getColumn(COL_IMAGE).setPreferredWidth(60); - - JScrollPane scrollPane = new JScrollPane(newsTable); - add(scrollPane, BorderLayout.CENTER); - - // ===== BOTTOM ERROR LABEL ============================================== - errorLabel = new JLabel(" "); - errorLabel.setForeground(Color.RED); - add(errorLabel, BorderLayout.SOUTH); - - // ===== BUTTON ACTIONS ================================================== - - // Back to Home – placeholder for now - backToHomeButton.addActionListener(e -> { - // TODO: implement navigation back to the main/home view. - // For now, you can leave this empty or show a message: - JOptionPane.showMessageDialog( - this, - "Back to Home is not implemented yet.", - "Info", - JOptionPane.INFORMATION_MESSAGE - ); - }); - - // Market news – load top market news into centre table - marketNewsButton.addActionListener(e -> controller.loadMarketNews()); - - // Company news – use filters and show results in centre table - companyNewsButton.addActionListener(e -> { - String symbol = symbolField.getText().trim(); - String from = fromDateField.getText().trim(); - String to = toDateField.getText().trim(); - controller.loadCompanyNews(symbol, from, to); - }); - - // automatically show top market news when this view is created - controller.loadMarketNews(); - } - - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (!"state".equals(evt.getPropertyName())) { - return; - } - - updateTable(viewModel.getArticles()); - updateError(viewModel.getErrorMessage()); - } - - private java.util.List currentArticles; - - private void updateTable(List articles) { - tableModel.setRowCount(0); // clear - currentArticles = articles; - - if (articles == null) { - return; - } - - for (NewsArticle article : articles) { - String dateStr = formatDate(article.getDatetime()); - String symbol = article.getRelated(); - String title = article.getHeadline(); - String summary = article.getSummary(); - String source = article.getSource(); - - String linkLabel = (article.getUrl() == null || article.getUrl().isBlank()) ? "" : "Open"; - String imageLabel = (article.getImage() == null || article.getImage().isBlank()) ? "" : "View"; - - tableModel.addRow(new Object[]{dateStr, symbol, title, summary, source, linkLabel, imageLabel}); - } - } - - private void updateError(String errorMessage) { - if (errorMessage == null || errorMessage.isBlank()) { - errorLabel.setText(" "); - } else { - errorLabel.setText(errorMessage); - } - } - - private String formatDate(long unixSeconds) { - if (unixSeconds <= 0) { - return ""; - } - LocalDateTime dt = LocalDateTime.ofInstant( - Instant.ofEpochSecond(unixSeconds), - ZoneId.systemDefault() - ); - return dt.format(DATE_FORMATTER); - } - - private static class TextAreaRenderer extends JTextArea implements javax.swing.table.TableCellRenderer { - - public TextAreaRenderer() { - setLineWrap(true); - setWrapStyleWord(true); - setOpaque(true); - } - - @Override - public java.awt.Component getTableCellRendererComponent( - JTable table, Object value, boolean isSelected, boolean hasFocus, - int row, int column) { - - setText(value == null ? "" : value.toString()); - - if (isSelected) { - setBackground(table.getSelectionBackground()); - setForeground(table.getSelectionForeground()); - } else { - setBackground(table.getBackground()); - setForeground(table.getForeground()); - } - - setSize(table.getColumnModel().getColumn(column).getWidth(), Short.MAX_VALUE); - int preferredHeight = getPreferredSize().height; - if (table.getRowHeight(row) != preferredHeight) { - table.setRowHeight(row, preferredHeight); - } - - return this; - } - } - - public String getViewName(){ - return this.viewName; - } -} diff --git a/src/test/java/use_case/news/NewsInteractorTest.java b/src/test/java/use_case/news/NewsInteractorTest.java deleted file mode 100644 index afbe96f5..00000000 --- a/src/test/java/use_case/news/NewsInteractorTest.java +++ /dev/null @@ -1,201 +0,0 @@ -package use_case.news; - -import entity.NewsArticle; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Unit tests for NewsInteractor. - */ -public class NewsInteractorTest { - - /** - * Simple in-memory stub for NewsDataAccessInterface. - * You configure its fields before running each test. - */ - private static class InMemoryNewsDao implements NewsDataAccessInterface { - - List marketNewsToReturn = new ArrayList<>(); - List companyNewsToReturn = new ArrayList<>(); - - boolean throwOnMarket = false; - boolean throwOnCompany = false; - - // capture last call arguments - String lastCompanySymbol; - String lastCompanyFrom; - String lastCompanyTo; - - @Override - public List loadMarketNews() throws Exception { - if (throwOnMarket) { - throw new Exception("Market error"); - } - return marketNewsToReturn; - } - - @Override - public List loadCompanyNews(String symbol, String fromDate, String toDate) throws Exception { - lastCompanySymbol = symbol; - lastCompanyFrom = fromDate; - lastCompanyTo = toDate; - - if (throwOnCompany) { - throw new Exception("Company error"); - } - return companyNewsToReturn; - } - } - - /** - * Recording presenter that just stores what the interactor sends. - */ - private static class RecordingPresenter implements NewsOutputBoundary { - - NewsResponseModel lastSuccess; - String lastError; - - @Override - public void prepareSuccessView(NewsResponseModel responseModel) { - lastSuccess = responseModel; - } - - @Override - public void prepareFailView(String errorMessage) { - lastError = errorMessage; - } - } - - private NewsArticle sampleArticle(String symbol) { - return new NewsArticle( - 1L, - "general", - 1_700_000_000L, - "Test headline for " + symbol, - "", - symbol, - "TestSource", - "Test summary", - "https://example.com/article" - ); - } - - @Test - void executeMarketNews_success_callsPresenterWithArticles() { - // Arrange - InMemoryNewsDao dao = new InMemoryNewsDao(); - RecordingPresenter presenter = new RecordingPresenter(); - dao.marketNewsToReturn = List.of(sampleArticle("AAPL"), sampleArticle("MSFT")); - - NewsInteractor interactor = new NewsInteractor(dao, presenter); - - NewsRequestModel request = new NewsRequestModel(); // market news - - // Act - interactor.executeMarketNews(request); - - // Assert - assertNull(presenter.lastError, "No error should be reported"); - assertNotNull(presenter.lastSuccess, "Success response should be sent"); - List articles = presenter.lastSuccess.getArticles(); - assertEquals(2, articles.size()); - assertEquals("AAPL", articles.get(0).getRelated()); - assertEquals("MSFT", articles.get(1).getRelated()); - } - - @Test - void executeMarketNews_error_callsFailView() { - // Arrange - InMemoryNewsDao dao = new InMemoryNewsDao(); - RecordingPresenter presenter = new RecordingPresenter(); - dao.throwOnMarket = true; - - NewsInteractor interactor = new NewsInteractor(dao, presenter); - NewsRequestModel request = new NewsRequestModel(); - - // Act - interactor.executeMarketNews(request); - - // Assert - assertNull(presenter.lastSuccess, "No success response expected"); - assertNotNull(presenter.lastError, "Error should be reported"); - assertTrue(presenter.lastError.contains("Unable to load market news"), - "Error message should mention market news"); - } - - @Test - void executeCompanyNews_emptySymbol_reportsError() { - // Arrange - InMemoryNewsDao dao = new InMemoryNewsDao(); - RecordingPresenter presenter = new RecordingPresenter(); - NewsInteractor interactor = new NewsInteractor(dao, presenter); - - // symbol is blank → invalid - NewsRequestModel request = new NewsRequestModel(" ", "2024-01-01", "2024-01-31"); - - // Act - interactor.executeCompanyNews(request); - - // Assert - assertNull(presenter.lastSuccess, "No success response expected"); - assertEquals("Symbol is required for company news.", presenter.lastError); - // Also check DAO was never called - assertNull(dao.lastCompanySymbol, "DAO should not be called on invalid symbol"); - } - - @Test - void executeCompanyNews_success_passesArticlesAndArgs() { - // Arrange - InMemoryNewsDao dao = new InMemoryNewsDao(); - RecordingPresenter presenter = new RecordingPresenter(); - - dao.companyNewsToReturn = List.of(sampleArticle("AAPL")); - - NewsInteractor interactor = new NewsInteractor(dao, presenter); - - NewsRequestModel request = - new NewsRequestModel("AAPL", "2024-01-01", "2024-01-31"); - - // Act - interactor.executeCompanyNews(request); - - // Assert DAO was called with correct arguments - assertEquals("AAPL", dao.lastCompanySymbol); - assertEquals("2024-01-01", dao.lastCompanyFrom); - assertEquals("2024-01-31", dao.lastCompanyTo); - - // Assert presenter got success - assertNull(presenter.lastError); - assertNotNull(presenter.lastSuccess); - List articles = presenter.lastSuccess.getArticles(); - assertEquals(1, articles.size()); - assertEquals("AAPL", articles.get(0).getRelated()); - } - - @Test - void executeCompanyNews_noArticles_triggersFailView() { - // Arrange - InMemoryNewsDao dao = new InMemoryNewsDao(); - RecordingPresenter presenter = new RecordingPresenter(); - - dao.companyNewsToReturn = List.of(); // empty list - - NewsInteractor interactor = new NewsInteractor(dao, presenter); - - NewsRequestModel request = - new NewsRequestModel("AAPL", "2024-01-01", "2024-01-31"); - - // Act - interactor.executeCompanyNews(request); - - // Assert - assertNull(presenter.lastSuccess); - assertNotNull(presenter.lastError); - assertTrue(presenter.lastError.contains("No news found for symbol"), - "Should report no news found"); - } -} From 88ce7716a9873f4eeaf8b2ec860fcd1e1813ecab Mon Sep 17 00:00:00 2001 From: samario Date: Sun, 30 Nov 2025 02:14:55 -0500 Subject: [PATCH 028/103] Synchronized with Main branch --- src/main/java/app/Main.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 38f49ab8..3453fd32 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -9,15 +9,13 @@ public static void main(String[] args) { .addLoginView() .addSignupView() .addLoggedInView() - .addNewsView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() - .addNewsUsecase() .build(); application.pack(); application.setLocationRelativeTo(null); application.setVisible(true); } -} +} \ No newline at end of file From c523673ecea8f44d060b43df206faf1cc89b9231 Mon Sep 17 00:00:00 2001 From: samario Date: Sun, 30 Nov 2025 02:32:36 -0500 Subject: [PATCH 029/103] Synchronized with Main branch --- src/main/java/app/AppBuilder.java | 3 +- src/main/java/view/LoggedInView.java | 76 +++++++++++++++++++--------- 2 files changed, 54 insertions(+), 25 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 3d9bada1..090efa35 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -49,8 +49,9 @@ public class AppBuilder { // of the classes from the data_access package // DAO version using local file storage - final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject("d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"); final FileUserDataAccessObject userDataAccessObject = new FileUserDataAccessObject("users.csv", userFactory); + final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject("d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"); + // DAO version using a shared external database // final DBUserDataAccessObject userDataAccessObject = new DBUserDataAccessObject(userFactory); diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index aee1d53d..a35ec17d 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -34,29 +34,65 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JTextField passwordInputField = new JTextField(15); private final JButton changePassword; + private final JTextField searchStockField = new JTextField(25); + private final JButton searchButton = new JButton("Search"); + private final JTextArea stockInfoArea = new JTextArea(); + private final JButton addToWatchlistButton = new JButton("Add to Watchlist"); + public LoggedInView(LoggedInViewModel loggedInViewModel) { this.loggedInViewModel = loggedInViewModel; this.loggedInViewModel.addPropertyChangeListener(this); - final JLabel title = new JLabel("Logged In Screen"); - title.setAlignmentX(Component.CENTER_ALIGNMENT); - - final LabelTextPanel passwordInfo = new LabelTextPanel( - new JLabel("Password"), passwordInputField); - - final JLabel usernameInfo = new JLabel("Currently logged in: "); username = new JLabel(); - final JPanel buttons = new JPanel(); logOut = new JButton("Log Out"); - buttons.add(logOut); - changePassword = new JButton("Change Password"); - buttons.add(changePassword); logOut.addActionListener(this); - this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + this.setLayout(new BorderLayout()); + + final JPanel topToolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 10)); + final JButton newsButton = new JButton("News"); + final JButton filterSearchButton = new JButton("Filter Search"); + final JButton historyButton = new JButton("History"); + final JButton marketOpenButton = new JButton("Market Open"); + final JButton accountButton = new JButton("Account"); + + topToolbar.add(newsButton); + topToolbar.add(filterSearchButton); + topToolbar.add(historyButton); + topToolbar.add(marketOpenButton); + topToolbar.add(accountButton); + + this.add(topToolbar, BorderLayout.NORTH); + + final JPanel centerPanel = new JPanel(); + centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS)); + + final JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); + final JLabel searchLabel = new JLabel("Search Stock:"); + searchPanel.add(searchLabel); + searchPanel.add(searchStockField); + searchPanel.add(searchButton); + + centerPanel.add(searchPanel); + + stockInfoArea.setLineWrap(true); + stockInfoArea.setWrapStyleWord(true); + + final JPanel stockInfoPanel = new JPanel(new BorderLayout()); + stockInfoPanel.setBorder(BorderFactory.createTitledBorder("Stock Information")); + stockInfoPanel.add(new JScrollPane(stockInfoArea), BorderLayout.CENTER); + + centerPanel.add(stockInfoPanel); + + this.add(centerPanel, BorderLayout.CENTER); + + final JPanel bottomPanel = new JPanel(new BorderLayout()); + bottomPanel.add(addToWatchlistButton, BorderLayout.CENTER); + + this.add(bottomPanel, BorderLayout.SOUTH); passwordInputField.getDocument().addDocumentListener(new DocumentListener() { @@ -96,13 +132,6 @@ public void changedUpdate(DocumentEvent e) { } ); - this.add(title); - this.add(usernameInfo); - this.add(username); - - this.add(passwordInfo); - this.add(passwordErrorField); - this.add(buttons); } /** @@ -141,12 +170,11 @@ public void setChangePasswordController(ChangePasswordController changePasswordC this.changePasswordController = changePasswordController; } - public void setLogoutController(LogoutController logoutController) { - // TODO: save the logout controller in the instance variable. - } - public void setNewsController(NewsController newsController) { this.newsController = newsController; } -} + public void setLogoutController(LogoutController logoutController) { + // TODO: save the logout controller in the instance variable. + } +} \ No newline at end of file From 9b5c0b991d6f93d3da2f3d68b5f77a3cc7d0de11 Mon Sep 17 00:00:00 2001 From: SamarioLiu Date: Sun, 30 Nov 2025 02:40:03 -0500 Subject: [PATCH 030/103] Revert "Revert "2 news page"" --- src/main/java/app/AppBuilder.java | 33 ++- src/main/java/app/Main.java | 2 + .../data_access/NewsDataAccessObject.java | 179 +++++++++++ src/main/java/entity/NewsArticle.java | 53 ++++ .../news/NewsController.java | 31 ++ .../interface_adapter/news/NewsPresenter.java | 32 ++ .../interface_adapter/news/NewsViewModel.java | 47 +++ .../news/NewsDataAccessInterface.java | 17 ++ .../java/use_case/news/NewsInputBoundary.java | 12 + .../java/use_case/news/NewsInteractor.java | 70 +++++ .../use_case/news/NewsOutputBoundary.java | 11 + .../java/use_case/news/NewsRequestModel.java | 46 +++ .../java/use_case/news/NewsResponseModel.java | 21 ++ src/main/java/view/LoggedInView.java | 7 + src/main/java/view/NewsView.java | 278 ++++++++++++++++++ .../use_case/news/NewsInteractorTest.java | 201 +++++++++++++ 16 files changed, 1036 insertions(+), 4 deletions(-) create mode 100644 src/main/java/data_access/NewsDataAccessObject.java create mode 100644 src/main/java/entity/NewsArticle.java create mode 100644 src/main/java/interface_adapter/news/NewsController.java create mode 100644 src/main/java/interface_adapter/news/NewsPresenter.java create mode 100644 src/main/java/interface_adapter/news/NewsViewModel.java create mode 100644 src/main/java/use_case/news/NewsDataAccessInterface.java create mode 100644 src/main/java/use_case/news/NewsInputBoundary.java create mode 100644 src/main/java/use_case/news/NewsInteractor.java create mode 100644 src/main/java/use_case/news/NewsOutputBoundary.java create mode 100644 src/main/java/use_case/news/NewsRequestModel.java create mode 100644 src/main/java/use_case/news/NewsResponseModel.java create mode 100644 src/main/java/view/NewsView.java create mode 100644 src/test/java/use_case/news/NewsInteractorTest.java diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 2850af25..4867220f 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -1,6 +1,7 @@ package app; import data_access.FileUserDataAccessObject; +import data_access.NewsDataAccessObject; import entity.UserFactory; import interface_adapter.ViewManagerModel; import interface_adapter.logged_in.ChangePasswordController; @@ -11,6 +12,9 @@ import interface_adapter.login.LoginViewModel; import interface_adapter.logout.LogoutController; import interface_adapter.logout.LogoutPresenter; +import interface_adapter.news.NewsController; +import interface_adapter.news.NewsPresenter; +import interface_adapter.news.NewsViewModel; import interface_adapter.signup.SignupController; import interface_adapter.signup.SignupPresenter; import interface_adapter.signup.SignupViewModel; @@ -23,13 +27,13 @@ import use_case.logout.LogoutInputBoundary; import use_case.logout.LogoutInteractor; import use_case.logout.LogoutOutputBoundary; +import use_case.news.NewsInputBoundary; +import use_case.news.NewsInteractor; +import use_case.news.NewsOutputBoundary; import use_case.signup.SignupInputBoundary; import use_case.signup.SignupInteractor; import use_case.signup.SignupOutputBoundary; -import view.LoggedInView; -import view.LoginView; -import view.SignupView; -import view.ViewManager; +import view.*; import javax.swing.*; import java.awt.*; @@ -45,6 +49,7 @@ public class AppBuilder { // of the classes from the data_access package // DAO version using local file storage + final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject("d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"); final FileUserDataAccessObject userDataAccessObject = new FileUserDataAccessObject("users.csv", userFactory); // DAO version using a shared external database @@ -82,6 +87,16 @@ public AppBuilder addLoggedInView() { return this; } + public AppBuilder addNewsView() { + NewsViewModel newsViewModel = new NewsViewModel(); + NewsOutputBoundary newsOutputBoundary = new NewsPresenter(newsViewModel); + NewsInputBoundary newsInputBoundary = new NewsInteractor(newsDataAccessObject, newsOutputBoundary); + NewsController newsController = new NewsController(newsInputBoundary); + NewsView newsView = new NewsView(newsController, newsViewModel); + cardPanel.add(newsView, newsView.getViewName()); + return this; + } + public AppBuilder addSignupUseCase() { final SignupOutputBoundary signupOutputBoundary = new SignupPresenter(viewManagerModel, signupViewModel, loginViewModel); @@ -116,6 +131,16 @@ public AppBuilder addChangePasswordUseCase() { return this; } + public AppBuilder addNewsUsecase() { + NewsViewModel newsViewModel = new NewsViewModel(); + NewsOutputBoundary newsOutputBoundary = new NewsPresenter(newsViewModel); + + NewsInputBoundary newsInputBoundary = new NewsInteractor(newsDataAccessObject, newsOutputBoundary); + NewsController newsController = new NewsController(newsInputBoundary); + loggedInView.setNewsController(newsController); + return this; + } + /** * Adds the Logout Use Case to the application. * @return this builder diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 424404fb..38f49ab8 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -9,9 +9,11 @@ public static void main(String[] args) { .addLoginView() .addSignupView() .addLoggedInView() + .addNewsView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() + .addNewsUsecase() .build(); application.pack(); diff --git a/src/main/java/data_access/NewsDataAccessObject.java b/src/main/java/data_access/NewsDataAccessObject.java new file mode 100644 index 00000000..7c79a53d --- /dev/null +++ b/src/main/java/data_access/NewsDataAccessObject.java @@ -0,0 +1,179 @@ +package data_access; + +import entity.NewsArticle; +import use_case.news.NewsDataAccessInterface; + +import org.json.JSONObject; +import org.json.JSONArray; +import org.json.JSONTokener; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Data access implementation for Finnhub news endpoints using org.json. + *

+ * Endpoints: + * - General market news: + * - Company news: + */ +public class NewsDataAccessObject implements NewsDataAccessInterface { + + private static final String BASE_URL = "https://finnhub.io/api/v1"; + + private final String apiKey; + + public NewsDataAccessObject(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public List loadMarketNews() throws Exception { + String endpoint = BASE_URL + "/news?category=general&token=" + + URLEncoder.encode(apiKey, StandardCharsets.UTF_8); + JSONArray array = fetchJsonArray(endpoint); + return parseNewsArray(array); + } + + @Override + public List loadCompanyNews(String symbol, + String fromDate, + String toDate) throws Exception { + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol must not be empty for company news."); + } + + // It is okay if fromDate or toDate are null/blank: Finnhub may require them, + // so we can set defaults here (for example: last 7 days) + StringBuilder urlBuilder = new StringBuilder(BASE_URL) + .append("/company-news") + .append("?symbol=").append(URLEncoder.encode(symbol, StandardCharsets.UTF_8)); + + if (fromDate != null && !fromDate.isBlank()) { + urlBuilder.append("&from=").append(URLEncoder.encode(fromDate, StandardCharsets.UTF_8)); + } + if (toDate != null && !toDate.isBlank()) { + urlBuilder.append("&to=").append(URLEncoder.encode(toDate, StandardCharsets.UTF_8)); + } + + urlBuilder.append("&token=").append(URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); + + JSONArray array = fetchJsonArray(urlBuilder.toString()); + return parseNewsArray(array); + } + + /** + * Helper: performs HTTP GET and returns the response as a JSONArray. + */ + private JSONArray fetchJsonArray(String urlString) throws IOException { + HttpURLConnection connection = null; + InputStream inputStream = null; + + try { + URL url = new URL(urlString); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + // Possible timeout + connection.setConnectTimeout(10_000); + connection.setReadTimeout(10_000); + + int status = connection.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + // Read errors + String errorBody = readStream(connection.getErrorStream()); + throw new IOException("HTTP error " + status + " from Finnhub: " + errorBody); + } + + inputStream = connection.getInputStream(); + JSONTokener tokener = new JSONTokener(inputStream); + return new JSONArray(tokener); + + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ignored) {} + } + if (connection != null) { + connection.disconnect(); + } + } + } + + /** + * Helper: read an InputStream fully into a String (for error messages). + */ + private String readStream(InputStream stream) throws IOException { + if (stream == null) return ""; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(stream, StandardCharsets.UTF_8))) { + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString(); + } + } + + /** + * Parse JSON array into a list of NewsArticle entities. + *

+ * Each object typically has fields: + * - id (long) + * - category (String) + * - datetime (long) + * - headline (String) + * - image (String) + * - related (String) // comma-separated symbols + * - source (String) + * - summary (String) + * - url (String) + */ + private List parseNewsArray(JSONArray array) { + List result = new ArrayList<>(); + if (array == null) { + return result; + } + + for (int i = 0; i < array.length(); i++) { + JSONObject obj = array.optJSONObject(i); + if (obj == null) continue; + + long id = obj.optLong("id", 0L); + String category = obj.optString("category", ""); + long datetime = obj.optLong("datetime", 0L); + String headline = obj.optString("headline", ""); + String image = obj.optString("image", ""); + String related = obj.optString("related", ""); + String source = obj.optString("source", ""); + String summary = obj.optString("summary", ""); + String url = obj.optString("url", ""); + + NewsArticle article = new NewsArticle( + id, + category, + datetime, + headline, + image, + related, + source, + summary, + url + ); + result.add(article); + } + + return result; + } +} diff --git a/src/main/java/entity/NewsArticle.java b/src/main/java/entity/NewsArticle.java new file mode 100644 index 00000000..c8214f79 --- /dev/null +++ b/src/main/java/entity/NewsArticle.java @@ -0,0 +1,53 @@ +package entity; + +public class NewsArticle { + + // Fields from Finnhub API + private final long id; // unique article id + private final String category; // e.g. "general", "crypto", etc. + private final long datetime; // UNIX timestamp (seconds) + private final String headline; // article title + private final String image; // image URL + private final String related; // related stock and companies, e.g. "AAPL,Apple.Inc" + private final String source; // e.g., "Bloomberg" + private final String summary; // short description + private final String url; // link to full article + + public NewsArticle(long id, + String category, + long datetime, + String headline, + String image, + String related, + String source, + String summary, + String url) { + this.id = id; + this.category = category; + this.datetime = datetime; + this.headline = headline; + this.image = image; + this.related = related; + this.source = source; + this.summary = summary; + this.url = url; + } + + public long getId() {return id;} + + public String getCategory() {return category;} + + public long getDatetime() {return datetime;} + + public String getHeadline() {return headline;} + + public String getImage() {return image;} + + public String getRelated() {return related;} + + public String getSource() {return source;} + + public String getSummary() {return summary;} + + public String getUrl() {return url;} +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/news/NewsController.java b/src/main/java/interface_adapter/news/NewsController.java new file mode 100644 index 00000000..c77052f4 --- /dev/null +++ b/src/main/java/interface_adapter/news/NewsController.java @@ -0,0 +1,31 @@ +package interface_adapter.news; + +import use_case.news.NewsInputBoundary; +import use_case.news.NewsRequestModel; + +/** + * read from UI + * build a NewsRequestModel + * "send" the request through input boundary + */ +public class NewsController { + + private final NewsInputBoundary interactor; + + public NewsController(NewsInputBoundary interactor) { + this.interactor = interactor; + } + + // Load general market news (category=general) + public void loadMarketNews() { + NewsRequestModel request = new NewsRequestModel(); // market news ctor + interactor.executeMarketNews(request); + } + + // Load company-specific news (for a symbol like "AAPL") + public void loadCompanyNews(String symbol, String fromDate, String toDate) { + NewsRequestModel request = new NewsRequestModel(symbol, fromDate, toDate); + interactor.executeCompanyNews(request); + } +} + diff --git a/src/main/java/interface_adapter/news/NewsPresenter.java b/src/main/java/interface_adapter/news/NewsPresenter.java new file mode 100644 index 00000000..add0a3c5 --- /dev/null +++ b/src/main/java/interface_adapter/news/NewsPresenter.java @@ -0,0 +1,32 @@ +package interface_adapter.news; + +import use_case.news.NewsOutputBoundary; +import use_case.news.NewsResponseModel; + +/** + * receives output data from interactor + * writes the data through ViewModel nad then update the state + */ +public class NewsPresenter implements NewsOutputBoundary { + + private final NewsViewModel viewModel; + + public NewsPresenter(NewsViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void prepareSuccessView(NewsResponseModel responseModel) { + viewModel.setArticles(responseModel.getArticles()); + viewModel.setErrorMessage(null); + viewModel.firePropertyChanged(); + } + + @Override + public void prepareFailView(String errorMessage) { + viewModel.setArticles(null); + viewModel.setErrorMessage(errorMessage); + viewModel.firePropertyChanged(); + } +} + diff --git a/src/main/java/interface_adapter/news/NewsViewModel.java b/src/main/java/interface_adapter/news/NewsViewModel.java new file mode 100644 index 00000000..794b56c8 --- /dev/null +++ b/src/main/java/interface_adapter/news/NewsViewModel.java @@ -0,0 +1,47 @@ +package interface_adapter.news; + +import entity.NewsArticle; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.List; + +/** + * The Presenter can update the states, so the View will changer correspondingly + */ +public class NewsViewModel { + + private final PropertyChangeSupport support = new PropertyChangeSupport(this); + + // UI state fields (were in NewsState before) + private List articles; + private String errorMessage; + + // listeners + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + public void firePropertyChanged() { + // property name can be anything, keep "state" to avoid changing other code too much + support.firePropertyChange("state", null, this); + } + + + public List getArticles() { + return articles; + } + + public void setArticles(List articles) { + this.articles = articles; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} + diff --git a/src/main/java/use_case/news/NewsDataAccessInterface.java b/src/main/java/use_case/news/NewsDataAccessInterface.java new file mode 100644 index 00000000..70b49552 --- /dev/null +++ b/src/main/java/use_case/news/NewsDataAccessInterface.java @@ -0,0 +1,17 @@ +package use_case.news; + +import java.util.List; +import entity.NewsArticle; + +public interface NewsDataAccessInterface { + + // Maps to Finnhub endpoint: /news?category=general + List loadMarketNews() throws Exception; + + //Maps to Finnhub endpoint: + // /company-news?symbol={symbol}&from={fromDate}&to={toDate} + // Dates are "YYYY-MM-DD". + List loadCompanyNews(String symbol, + String fromDate, + String toDate) throws Exception; +} \ No newline at end of file diff --git a/src/main/java/use_case/news/NewsInputBoundary.java b/src/main/java/use_case/news/NewsInputBoundary.java new file mode 100644 index 00000000..50bc751f --- /dev/null +++ b/src/main/java/use_case/news/NewsInputBoundary.java @@ -0,0 +1,12 @@ +package use_case.news; + +/** + * Define what actions this user case supports + */ +public interface NewsInputBoundary { + // Load general market news (category=general). + void executeMarketNews(NewsRequestModel requestModel); + + // Load company-specific news for the given symbol. + void executeCompanyNews(NewsRequestModel requestModel); +} diff --git a/src/main/java/use_case/news/NewsInteractor.java b/src/main/java/use_case/news/NewsInteractor.java new file mode 100644 index 00000000..ad767ac5 --- /dev/null +++ b/src/main/java/use_case/news/NewsInteractor.java @@ -0,0 +1,70 @@ +package use_case.news; + +import entity.NewsArticle; +import java.util.List; + +/** + * Implement "what happens when user asks for news" + *

+ * 1. Validate the NewsRequestModel + * 2. Call NewsDataAccessInterface + * 3. Build a NewsResponse Model + * 4. Call the presenter -- NewsOutputBoundary + */ +public class NewsInteractor implements NewsInputBoundary{ + private final NewsDataAccessInterface newsDataAccess; + private final NewsOutputBoundary newsPresenter; + + public NewsInteractor(NewsDataAccessInterface newsDataAccess, + NewsOutputBoundary newsPresenter) { + this.newsDataAccess = newsDataAccess; + this.newsPresenter = newsPresenter; + } + + @Override + public void executeMarketNews(NewsRequestModel requestModel) { + try { + List articles = newsDataAccess.loadMarketNews(); + + if (articles == null || articles.isEmpty()) { + newsPresenter.prepareFailView("No market news available."); + return; + } + + NewsResponseModel response = new NewsResponseModel(articles); + newsPresenter.prepareSuccessView(response); + + } catch (Exception e) { + newsPresenter.prepareFailView("Unable to load market news: " + e.getMessage()); + } + } + + @Override + public void executeCompanyNews(NewsRequestModel requestModel) { + String symbol = requestModel.getSymbol(); + + if (symbol == null || symbol.isBlank()) { + newsPresenter.prepareFailView("Symbol is required for company news."); + return; + } + + try { + List articles = newsDataAccess.loadCompanyNews( + symbol, + requestModel.getFromDate(), + requestModel.getToDate() + ); + + if (articles == null || articles.isEmpty()) { + newsPresenter.prepareFailView("No news found for symbol: " + symbol); + return; + } + + NewsResponseModel response = new NewsResponseModel(articles); + newsPresenter.prepareSuccessView(response); + + } catch (Exception e) { + newsPresenter.prepareFailView("Unable to load company news: " + e.getMessage()); + } + } +} diff --git a/src/main/java/use_case/news/NewsOutputBoundary.java b/src/main/java/use_case/news/NewsOutputBoundary.java new file mode 100644 index 00000000..97023df9 --- /dev/null +++ b/src/main/java/use_case/news/NewsOutputBoundary.java @@ -0,0 +1,11 @@ +package use_case.news; + +/** + * Interface implemented by the presenter + */ +public interface NewsOutputBoundary { + + void prepareSuccessView(NewsResponseModel responseModel); + + void prepareFailView(String errorMessage); +} diff --git a/src/main/java/use_case/news/NewsRequestModel.java b/src/main/java/use_case/news/NewsRequestModel.java new file mode 100644 index 00000000..9e6e6226 --- /dev/null +++ b/src/main/java/use_case/news/NewsRequestModel.java @@ -0,0 +1,46 @@ +package use_case.news; + +/** + * Request model for news. + * For market news, symbol/fromDate/toDate can all be null. + * For company news, symbol must be non-null; dates may be null (use default range). + * Input Data holder, the data comes from the UI + */ +public class NewsRequestModel { + + private final String symbol; // e.g. "AAPL" or null for market news + private final String fromDate; // "YYYY-MM-DD" or null + private final String toDate; // "YYYY-MM-DD" or null + + // Constructor for general market news + public NewsRequestModel() { + this.symbol = null; + this.fromDate = null; + this.toDate = null; + } + + // Constructor for company news + public NewsRequestModel(String symbol, String fromDate, String toDate) { + this.symbol = symbol; + this.fromDate = fromDate; + this.toDate = toDate; + } + + public String getSymbol() { + return symbol; + } + + public String getFromDate() { + return fromDate; + } + + public String getToDate() { + return toDate; + } + + // Market news don't have a symbol + public boolean isMarketNewsRequest() { + return symbol == null || symbol.isBlank(); + } +} + diff --git a/src/main/java/use_case/news/NewsResponseModel.java b/src/main/java/use_case/news/NewsResponseModel.java new file mode 100644 index 00000000..5463c7c8 --- /dev/null +++ b/src/main/java/use_case/news/NewsResponseModel.java @@ -0,0 +1,21 @@ +package use_case.news; + +import entity.NewsArticle; +import java.util.List; + +/** + * Data holder for the presenter to build the View + * Created by the Interactor and passed to OutputBoundary + */ +public class NewsResponseModel { + + private final List articles; + + public NewsResponseModel(List articles) { + this.articles = articles; + } + + public List getArticles() { + return articles; + } +} diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 0ef0d59d..2cb286fb 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -4,6 +4,7 @@ import interface_adapter.logged_in.LoggedInState; import interface_adapter.logged_in.LoggedInViewModel; import interface_adapter.logout.LogoutController; +import interface_adapter.news.NewsController; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -24,6 +25,7 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JLabel passwordErrorField = new JLabel(); private ChangePasswordController changePasswordController = null; private LogoutController logoutController; + private NewsController newsController; private final JLabel username; @@ -171,4 +173,9 @@ public void setChangePasswordController(ChangePasswordController changePasswordC public void setLogoutController(LogoutController logoutController) { // TODO: save the logout controller in the instance variable. } + + public void setNewsController(NewsController newsController) { + this.newsController = newsController; + } + } diff --git a/src/main/java/view/NewsView.java b/src/main/java/view/NewsView.java new file mode 100644 index 00000000..4d658b61 --- /dev/null +++ b/src/main/java/view/NewsView.java @@ -0,0 +1,278 @@ +package view; + +import interface_adapter.news.NewsController; +import interface_adapter.news.NewsViewModel; +import entity.NewsArticle; + +import javax.swing.*; +import javax.swing.table.DefaultTableModel; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; +import javax.swing.JTextArea; + + +public class NewsView extends JPanel implements PropertyChangeListener { + + private final String viewName = "news"; + + private final NewsController controller; + private final NewsViewModel viewModel; + + private final JButton backToHomeButton; + private final JButton marketNewsButton; + private final JButton companyNewsButton; + private final JTextField symbolField; + private final JTextField fromDateField; + private final JTextField toDateField; + private final JTable newsTable; + private final DefaultTableModel tableModel; + private final JLabel errorLabel; + + private static final DateTimeFormatter DATE_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + // column indices for later use + private static final int COL_DATE = 0; + private static final int COL_SYMBOL = 1; + private static final int COL_TITLE = 2; + private static final int COL_SUMMARY = 3; + private static final int COL_SOURCE = 4; + private static final int COL_LINK = 5; + private static final int COL_IMAGE = 6; + + public NewsView(NewsController controller, NewsViewModel viewModel) { + this.controller = controller; + this.viewModel = viewModel; + + this.viewModel.addPropertyChangeListener(this); + + setLayout(new BorderLayout()); + + // ===== TOP PANEL ===== + JPanel topPanel = new JPanel(new BorderLayout()); + + // Left: Back to Home button + JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + backToHomeButton = new JButton("Back to Home"); + leftPanel.add(backToHomeButton); + topPanel.add(leftPanel, BorderLayout.WEST); + + // Right: symbol/date filters and news buttons + JPanel rightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + + symbolField = new JTextField(8); + fromDateField = new JTextField(10); + toDateField = new JTextField(10); + companyNewsButton = new JButton("Company News"); + marketNewsButton = new JButton("Market News"); + + rightPanel.add(new JLabel("Symbol:")); + rightPanel.add(symbolField); + rightPanel.add(new JLabel("From (YYYY-MM-DD):")); + rightPanel.add(fromDateField); + rightPanel.add(new JLabel("To (YYYY-MM-DD):")); + rightPanel.add(toDateField); + rightPanel.add(companyNewsButton); + rightPanel.add(marketNewsButton); + + topPanel.add(rightPanel, BorderLayout.EAST); + + add(topPanel, BorderLayout.NORTH); + + // ===== CENTRE TABLE ===== + String[] columnNames = {"Date", "Symbol", "Title", "Summary", "Source", "Link", "Image"}; + tableModel = new DefaultTableModel(columnNames, 0) { + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + + // Create the NewsTable right here + newsTable = new JTable(tableModel); + + // set renderer for headline column to enable wrapping + newsTable.getColumnModel() + .getColumn(COL_SUMMARY) + .setCellRenderer(new TextAreaRenderer()); + + newsTable.addMouseListener(new java.awt.event.MouseAdapter() { + @Override + public void mouseClicked(java.awt.event.MouseEvent e) { + int row = newsTable.rowAtPoint(e.getPoint()); + int col = newsTable.columnAtPoint(e.getPoint()); + + if (row < 0 || currentArticles == null || row >= currentArticles.size()) { + return; + } + + NewsArticle article = currentArticles.get(row); + + try { + if (col == COL_LINK) { // column index for "Link" + String url = article.getUrl(); + if (url != null && !url.isBlank()) { + java.awt.Desktop.getDesktop().browse(new java.net.URI(url)); + } + } else if (col == COL_IMAGE) { // column index for "Image" + String imgUrl = article.getImage(); + if (imgUrl != null && !imgUrl.isBlank()) { + java.awt.Desktop.getDesktop().browse(new java.net.URI(imgUrl)); + } + } + } catch (Exception ex) { + JOptionPane.showMessageDialog( + NewsView.this, + "Unable to open link: " + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE + ); + } + } + }); + + // turn off auto-resize of chart + newsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + + //set preferred width + newsTable.getColumnModel().getColumn(COL_DATE).setPreferredWidth(130); + newsTable.getColumnModel().getColumn(COL_SYMBOL).setPreferredWidth(80); + newsTable.getColumnModel().getColumn(COL_TITLE).setPreferredWidth(160); + newsTable.getColumnModel().getColumn(COL_SUMMARY).setPreferredWidth(360); + newsTable.getColumnModel().getColumn(COL_SOURCE).setPreferredWidth(100); + newsTable.getColumnModel().getColumn(COL_LINK).setPreferredWidth(60); + newsTable.getColumnModel().getColumn(COL_IMAGE).setPreferredWidth(60); + + JScrollPane scrollPane = new JScrollPane(newsTable); + add(scrollPane, BorderLayout.CENTER); + + // ===== BOTTOM ERROR LABEL ============================================== + errorLabel = new JLabel(" "); + errorLabel.setForeground(Color.RED); + add(errorLabel, BorderLayout.SOUTH); + + // ===== BUTTON ACTIONS ================================================== + + // Back to Home – placeholder for now + backToHomeButton.addActionListener(e -> { + // TODO: implement navigation back to the main/home view. + // For now, you can leave this empty or show a message: + JOptionPane.showMessageDialog( + this, + "Back to Home is not implemented yet.", + "Info", + JOptionPane.INFORMATION_MESSAGE + ); + }); + + // Market news – load top market news into centre table + marketNewsButton.addActionListener(e -> controller.loadMarketNews()); + + // Company news – use filters and show results in centre table + companyNewsButton.addActionListener(e -> { + String symbol = symbolField.getText().trim(); + String from = fromDateField.getText().trim(); + String to = toDateField.getText().trim(); + controller.loadCompanyNews(symbol, from, to); + }); + + // automatically show top market news when this view is created + controller.loadMarketNews(); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (!"state".equals(evt.getPropertyName())) { + return; + } + + updateTable(viewModel.getArticles()); + updateError(viewModel.getErrorMessage()); + } + + private java.util.List currentArticles; + + private void updateTable(List articles) { + tableModel.setRowCount(0); // clear + currentArticles = articles; + + if (articles == null) { + return; + } + + for (NewsArticle article : articles) { + String dateStr = formatDate(article.getDatetime()); + String symbol = article.getRelated(); + String title = article.getHeadline(); + String summary = article.getSummary(); + String source = article.getSource(); + + String linkLabel = (article.getUrl() == null || article.getUrl().isBlank()) ? "" : "Open"; + String imageLabel = (article.getImage() == null || article.getImage().isBlank()) ? "" : "View"; + + tableModel.addRow(new Object[]{dateStr, symbol, title, summary, source, linkLabel, imageLabel}); + } + } + + private void updateError(String errorMessage) { + if (errorMessage == null || errorMessage.isBlank()) { + errorLabel.setText(" "); + } else { + errorLabel.setText(errorMessage); + } + } + + private String formatDate(long unixSeconds) { + if (unixSeconds <= 0) { + return ""; + } + LocalDateTime dt = LocalDateTime.ofInstant( + Instant.ofEpochSecond(unixSeconds), + ZoneId.systemDefault() + ); + return dt.format(DATE_FORMATTER); + } + + private static class TextAreaRenderer extends JTextArea implements javax.swing.table.TableCellRenderer { + + public TextAreaRenderer() { + setLineWrap(true); + setWrapStyleWord(true); + setOpaque(true); + } + + @Override + public java.awt.Component getTableCellRendererComponent( + JTable table, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { + + setText(value == null ? "" : value.toString()); + + if (isSelected) { + setBackground(table.getSelectionBackground()); + setForeground(table.getSelectionForeground()); + } else { + setBackground(table.getBackground()); + setForeground(table.getForeground()); + } + + setSize(table.getColumnModel().getColumn(column).getWidth(), Short.MAX_VALUE); + int preferredHeight = getPreferredSize().height; + if (table.getRowHeight(row) != preferredHeight) { + table.setRowHeight(row, preferredHeight); + } + + return this; + } + } + + public String getViewName(){ + return this.viewName; + } +} diff --git a/src/test/java/use_case/news/NewsInteractorTest.java b/src/test/java/use_case/news/NewsInteractorTest.java new file mode 100644 index 00000000..afbe96f5 --- /dev/null +++ b/src/test/java/use_case/news/NewsInteractorTest.java @@ -0,0 +1,201 @@ +package use_case.news; + +import entity.NewsArticle; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for NewsInteractor. + */ +public class NewsInteractorTest { + + /** + * Simple in-memory stub for NewsDataAccessInterface. + * You configure its fields before running each test. + */ + private static class InMemoryNewsDao implements NewsDataAccessInterface { + + List marketNewsToReturn = new ArrayList<>(); + List companyNewsToReturn = new ArrayList<>(); + + boolean throwOnMarket = false; + boolean throwOnCompany = false; + + // capture last call arguments + String lastCompanySymbol; + String lastCompanyFrom; + String lastCompanyTo; + + @Override + public List loadMarketNews() throws Exception { + if (throwOnMarket) { + throw new Exception("Market error"); + } + return marketNewsToReturn; + } + + @Override + public List loadCompanyNews(String symbol, String fromDate, String toDate) throws Exception { + lastCompanySymbol = symbol; + lastCompanyFrom = fromDate; + lastCompanyTo = toDate; + + if (throwOnCompany) { + throw new Exception("Company error"); + } + return companyNewsToReturn; + } + } + + /** + * Recording presenter that just stores what the interactor sends. + */ + private static class RecordingPresenter implements NewsOutputBoundary { + + NewsResponseModel lastSuccess; + String lastError; + + @Override + public void prepareSuccessView(NewsResponseModel responseModel) { + lastSuccess = responseModel; + } + + @Override + public void prepareFailView(String errorMessage) { + lastError = errorMessage; + } + } + + private NewsArticle sampleArticle(String symbol) { + return new NewsArticle( + 1L, + "general", + 1_700_000_000L, + "Test headline for " + symbol, + "", + symbol, + "TestSource", + "Test summary", + "https://example.com/article" + ); + } + + @Test + void executeMarketNews_success_callsPresenterWithArticles() { + // Arrange + InMemoryNewsDao dao = new InMemoryNewsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + dao.marketNewsToReturn = List.of(sampleArticle("AAPL"), sampleArticle("MSFT")); + + NewsInteractor interactor = new NewsInteractor(dao, presenter); + + NewsRequestModel request = new NewsRequestModel(); // market news + + // Act + interactor.executeMarketNews(request); + + // Assert + assertNull(presenter.lastError, "No error should be reported"); + assertNotNull(presenter.lastSuccess, "Success response should be sent"); + List articles = presenter.lastSuccess.getArticles(); + assertEquals(2, articles.size()); + assertEquals("AAPL", articles.get(0).getRelated()); + assertEquals("MSFT", articles.get(1).getRelated()); + } + + @Test + void executeMarketNews_error_callsFailView() { + // Arrange + InMemoryNewsDao dao = new InMemoryNewsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + dao.throwOnMarket = true; + + NewsInteractor interactor = new NewsInteractor(dao, presenter); + NewsRequestModel request = new NewsRequestModel(); + + // Act + interactor.executeMarketNews(request); + + // Assert + assertNull(presenter.lastSuccess, "No success response expected"); + assertNotNull(presenter.lastError, "Error should be reported"); + assertTrue(presenter.lastError.contains("Unable to load market news"), + "Error message should mention market news"); + } + + @Test + void executeCompanyNews_emptySymbol_reportsError() { + // Arrange + InMemoryNewsDao dao = new InMemoryNewsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + NewsInteractor interactor = new NewsInteractor(dao, presenter); + + // symbol is blank → invalid + NewsRequestModel request = new NewsRequestModel(" ", "2024-01-01", "2024-01-31"); + + // Act + interactor.executeCompanyNews(request); + + // Assert + assertNull(presenter.lastSuccess, "No success response expected"); + assertEquals("Symbol is required for company news.", presenter.lastError); + // Also check DAO was never called + assertNull(dao.lastCompanySymbol, "DAO should not be called on invalid symbol"); + } + + @Test + void executeCompanyNews_success_passesArticlesAndArgs() { + // Arrange + InMemoryNewsDao dao = new InMemoryNewsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.companyNewsToReturn = List.of(sampleArticle("AAPL")); + + NewsInteractor interactor = new NewsInteractor(dao, presenter); + + NewsRequestModel request = + new NewsRequestModel("AAPL", "2024-01-01", "2024-01-31"); + + // Act + interactor.executeCompanyNews(request); + + // Assert DAO was called with correct arguments + assertEquals("AAPL", dao.lastCompanySymbol); + assertEquals("2024-01-01", dao.lastCompanyFrom); + assertEquals("2024-01-31", dao.lastCompanyTo); + + // Assert presenter got success + assertNull(presenter.lastError); + assertNotNull(presenter.lastSuccess); + List articles = presenter.lastSuccess.getArticles(); + assertEquals(1, articles.size()); + assertEquals("AAPL", articles.get(0).getRelated()); + } + + @Test + void executeCompanyNews_noArticles_triggersFailView() { + // Arrange + InMemoryNewsDao dao = new InMemoryNewsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.companyNewsToReturn = List.of(); // empty list + + NewsInteractor interactor = new NewsInteractor(dao, presenter); + + NewsRequestModel request = + new NewsRequestModel("AAPL", "2024-01-01", "2024-01-31"); + + // Act + interactor.executeCompanyNews(request); + + // Assert + assertNull(presenter.lastSuccess); + assertNotNull(presenter.lastError); + assertTrue(presenter.lastError.contains("No news found for symbol"), + "Should report no news found"); + } +} From 03a24be813ad1e04e2cb952d2353aa754ae53abd Mon Sep 17 00:00:00 2001 From: samario Date: Sun, 30 Nov 2025 03:35:18 -0500 Subject: [PATCH 031/103] Add the News Feature into the app --- src/main/java/app/AppBuilder.java | 20 ++++++++++++-------- src/main/java/view/LoggedInView.java | 21 +++++++++++++++++---- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 090efa35..5d7580ae 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -62,6 +62,8 @@ public class AppBuilder { private LoggedInViewModel loggedInViewModel; private LoggedInView loggedInView; private LoginView loginView; + private NewsViewModel newsViewModel; + private NewsView newsView; public AppBuilder() { cardPanel.setLayout(cardLayout); @@ -89,11 +91,13 @@ public AppBuilder addLoggedInView() { } public AppBuilder addNewsView() { - NewsViewModel newsViewModel = new NewsViewModel(); + newsViewModel = new NewsViewModel(); NewsOutputBoundary newsOutputBoundary = new NewsPresenter(newsViewModel); - NewsInputBoundary newsInputBoundary = new NewsInteractor(newsDataAccessObject, newsOutputBoundary); + NewsInputBoundary newsInputBoundary = + new NewsInteractor(newsDataAccessObject, newsOutputBoundary); NewsController newsController = new NewsController(newsInputBoundary); - NewsView newsView = new NewsView(newsController, newsViewModel); + + newsView = new NewsView(newsController, newsViewModel); cardPanel.add(newsView, newsView.getViewName()); return this; } @@ -133,12 +137,12 @@ public AppBuilder addChangePasswordUseCase() { } public AppBuilder addNewsUsecase() { - NewsViewModel newsViewModel = new NewsViewModel(); - NewsOutputBoundary newsOutputBoundary = new NewsPresenter(newsViewModel); + // Logged-in page: News button → News view + loggedInView.setNewsNavigation(viewManagerModel, newsView.getViewName()); + + // News page: Back button → Logged-in view + newsView.setBackNavigation(viewManagerModel, loggedInView.getViewName()); - NewsInputBoundary newsInputBoundary = new NewsInteractor(newsDataAccessObject, newsOutputBoundary); - NewsController newsController = new NewsController(newsInputBoundary); - loggedInView.setNewsController(newsController); return this; } diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index a35ec17d..7c95cb1c 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -5,6 +5,7 @@ import interface_adapter.logged_in.LoggedInViewModel; import interface_adapter.logout.LogoutController; import interface_adapter.news.NewsController; +import interface_adapter.ViewManagerModel; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -26,6 +27,8 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private ChangePasswordController changePasswordController = null; private LogoutController logoutController; private NewsController newsController; + private ViewManagerModel viewManagerModel; + private String newsViewName; private final JLabel username; @@ -53,7 +56,16 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { this.setLayout(new BorderLayout()); final JPanel topToolbar = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 10)); + final JButton newsButton = new JButton("News"); + newsButton.addActionListener(e -> { + System.out.println("News button clicked"); // debug + if (viewManagerModel != null && newsViewName != null) { + viewManagerModel.setState(newsViewName); + viewManagerModel.firePropertyChange(); + } + }); + final JButton filterSearchButton = new JButton("Filter Search"); final JButton historyButton = new JButton("History"); final JButton marketOpenButton = new JButton("Market Open"); @@ -170,11 +182,12 @@ public void setChangePasswordController(ChangePasswordController changePasswordC this.changePasswordController = changePasswordController; } - public void setNewsController(NewsController newsController) { - this.newsController = newsController; - } - public void setLogoutController(LogoutController logoutController) { // TODO: save the logout controller in the instance variable. } + + public void setNewsNavigation(ViewManagerModel viewManagerModel, String newsViewName) { + this.viewManagerModel = viewManagerModel; + this.newsViewName = newsViewName; + } } \ No newline at end of file From ed77aba5614ebb13e9df4092127136727a676036 Mon Sep 17 00:00:00 2001 From: samario Date: Sun, 30 Nov 2025 03:36:10 -0500 Subject: [PATCH 032/103] Add the News Feature into the app. Finish implementation of Back-to-home button in NewsView --- src/main/java/app/Main.java | 2 ++ src/main/java/view/NewsView.java | 21 +++++++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 3453fd32..9fd275f7 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -9,9 +9,11 @@ public static void main(String[] args) { .addLoginView() .addSignupView() .addLoggedInView() + .addNewsView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() + .addNewsUsecase() .build(); application.pack(); diff --git a/src/main/java/view/NewsView.java b/src/main/java/view/NewsView.java index 4d658b61..7684b8f8 100644 --- a/src/main/java/view/NewsView.java +++ b/src/main/java/view/NewsView.java @@ -2,6 +2,7 @@ import interface_adapter.news.NewsController; import interface_adapter.news.NewsViewModel; +import interface_adapter.ViewManagerModel; import entity.NewsArticle; import javax.swing.*; @@ -19,6 +20,9 @@ public class NewsView extends JPanel implements PropertyChangeListener { + private ViewManagerModel viewManagerModel; + private String homeViewName; + private final String viewName = "news"; private final NewsController controller; @@ -161,14 +165,10 @@ public void mouseClicked(java.awt.event.MouseEvent e) { // Back to Home – placeholder for now backToHomeButton.addActionListener(e -> { - // TODO: implement navigation back to the main/home view. - // For now, you can leave this empty or show a message: - JOptionPane.showMessageDialog( - this, - "Back to Home is not implemented yet.", - "Info", - JOptionPane.INFORMATION_MESSAGE - ); + if (viewManagerModel != null && homeViewName != null) { + viewManagerModel.setState(homeViewName); + viewManagerModel.firePropertyChange(); + } }); // Market news – load top market news into centre table @@ -275,4 +275,9 @@ public java.awt.Component getTableCellRendererComponent( public String getViewName(){ return this.viewName; } + + public void setBackNavigation(ViewManagerModel viewManagerModel, String homeViewName) { + this.viewManagerModel = viewManagerModel; + this.homeViewName = homeViewName; + } } From d124e95df5f703fc910ca1acb0c8bc285a20f120 Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Sun, 30 Nov 2025 11:04:05 -0500 Subject: [PATCH 033/103] cleanup --- src/main/java/app/Main.java | 262 +++--------------------------------- 1 file changed, 16 insertions(+), 246 deletions(-) diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 596d5414..9fd275f7 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -1,253 +1,23 @@ package app; -import Service.LoginService; import javax.swing.*; -import java.awt.*; -import java.awt.event.*; - -public class Main extends JFrame { - private static LoginService loginService = new LoginService(); - private JPanel cardPanel; - private CardLayout cardLayout; - private JTextField usernameField, signupUsernameField; - private JPasswordField passwordField, signupPasswordField; - private JLabel statusLabel; - - public Main() { - setTitle("Stock App - Login"); - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - setSize(400, 300); - setLocationRelativeTo(null); - - // Create card layout for switching between login and signup panels - cardLayout = new CardLayout(); - cardPanel = new JPanel(cardLayout); - - // Create login panel - JPanel loginPanel = createLoginPanel(); - - // Create signup panel - JPanel signupPanel = createSignupPanel(); - - // Add panels to card layout - cardPanel.add(loginPanel, "login"); - cardPanel.add(signupPanel, "signup"); - - add(cardPanel, BorderLayout.CENTER); - - // Status label at the bottom - statusLabel = new JLabel(" ", JLabel.CENTER); - add(statusLabel, BorderLayout.SOUTH); - - // Show login panel by default - cardLayout.show(cardPanel, "login"); - } - - private JPanel createLoginPanel() { - JPanel panel = new JPanel(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(5, 5, 5, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - - // Title - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 2; - JLabel titleLabel = new JLabel("Login to Stock App", JLabel.CENTER); - titleLabel.setFont(new Font("Arial", Font.BOLD, 18)); - panel.add(titleLabel, gbc); - - // Username - gbc.gridy++; - gbc.gridwidth = 1; - panel.add(new JLabel("Username:"), gbc); - gbc.gridx = 1; - usernameField = new JTextField(15); - panel.add(usernameField, gbc); - - // Password - gbc.gridy++; - gbc.gridx = 0; - panel.add(new JLabel("Password:"), gbc); - gbc.gridx = 1; - passwordField = new JPasswordField(15); - panel.add(passwordField, gbc); - - // Login button - gbc.gridy++; - gbc.gridx = 0; - gbc.gridwidth = 2; - JButton loginButton = new JButton("Login"); - loginButton.addActionListener(e -> handleLogin()); - panel.add(loginButton, gbc); - - // Switch to signup - gbc.gridy++; - JLabel signupPrompt = new JLabel("Don't have an account? "); - JButton switchToSignup = new JButton("Sign Up"); - switchToSignup.setBorderPainted(false); - switchToSignup.setContentAreaFilled(false); - switchToSignup.setForeground(Color.BLUE); - switchToSignup.setCursor(new Cursor(Cursor.HAND_CURSOR)); - switchToSignup.addActionListener(e -> cardLayout.show(cardPanel, "signup")); - - JPanel switchPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); - switchPanel.add(signupPrompt); - switchPanel.add(switchToSignup); - panel.add(switchPanel, gbc); - - // Add enter key listener for login - passwordField.addActionListener(e -> handleLogin()); - - return panel; - } - - private JPanel createSignupPanel() { - JPanel panel = new JPanel(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(5, 5, 5, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - - // Title - gbc.gridx = 0; - gbc.gridy = 0; - gbc.gridwidth = 2; - JLabel titleLabel = new JLabel("Create Account", JLabel.CENTER); - titleLabel.setFont(new Font("Arial", Font.BOLD, 18)); - panel.add(titleLabel, gbc); - - // Username - gbc.gridy++; - gbc.gridwidth = 1; - panel.add(new JLabel("Choose a username:"), gbc); - gbc.gridx = 1; - signupUsernameField = new JTextField(15); - panel.add(signupUsernameField, gbc); - - // Password - gbc.gridy++; - gbc.gridx = 0; - panel.add(new JLabel("Choose a password:"), gbc); - gbc.gridx = 1; - signupPasswordField = new JPasswordField(15); - panel.add(signupPasswordField, gbc); - - // Sign Up button - gbc.gridy++; - gbc.gridx = 0; - gbc.gridwidth = 2; - JButton signupButton = new JButton("Sign Up"); - signupButton.addActionListener(e -> handleSignup()); - panel.add(signupButton, gbc); - - // Switch to login - gbc.gridy++; - JLabel loginPrompt = new JLabel("Already have an account? "); - JButton switchToLogin = new JButton("Login"); - switchToLogin.setBorderPainted(false); - switchToLogin.setContentAreaFilled(false); - switchToLogin.setForeground(Color.BLUE); - switchToLogin.setCursor(new Cursor(Cursor.HAND_CURSOR)); - switchToLogin.addActionListener(e -> { - cardLayout.show(cardPanel, "login"); - clearFields(); - }); - - JPanel switchPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0)); - switchPanel.add(loginPrompt); - switchPanel.add(switchToLogin); - panel.add(switchPanel, gbc); - - return panel; - } - - private void handleLogin() { - String username = usernameField.getText().trim(); - String password = new String(passwordField.getPassword()); - - if (username.isEmpty() || password.isEmpty()) { - showStatus("Please fill in all fields", Color.RED); - return; - } - - if (loginService.login(username, password)) { - showStatus("Login successful! Opening dashboard...", new Color(0, 150, 0)); - - // --- NEW DASHBOARD LOGIC --- - new Thread(() -> { - try { - Thread.sleep(500); // Small delay for status message to register - SwingUtilities.invokeLater(() -> { - // 1. Dispose of the current login window - this.dispose(); - // 2. Open the main application dashboard - Dashboard dashboard = new Dashboard(username); - dashboard.setVisible(true); - }); - } catch (InterruptedException ex) { - ex.printStackTrace(); - } - }).start(); - // --- END NEW DASHBOARD LOGIC --- - - } else { - showStatus("Invalid username or password", Color.RED); - } - } - - private void handleSignup() { - String username = signupUsernameField.getText().trim(); - String password = new String(signupPasswordField.getPassword()); - - if (username.isEmpty() || password.isEmpty()) { - showStatus("Please fill in all fields", Color.RED); - return; - } - - if (loginService.signUp(username, password)) { - showStatus("Account created successfully!", new Color(0, 150, 0)); - // Switch back to login after a short delay - new Thread(() -> { - try { - Thread.sleep(1000); - SwingUtilities.invokeLater(() -> { - cardLayout.show(cardPanel, "login"); - clearFields(); - }); - } catch (InterruptedException ex) { - ex.printStackTrace(); - } - }).start(); - } else { - showStatus("Username already exists", Color.RED); - } - } - - private void showStatus(String message, Color color) { - statusLabel.setForeground(color); - statusLabel.setText(message); - } - - private void clearFields() { - usernameField.setText(""); - passwordField.setText(""); - signupUsernameField.setText(""); - signupPasswordField.setText(""); - statusLabel.setText(" "); - } +public class Main { public static void main(String[] args) { - // Set look and feel for better appearance - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (Exception e) { - e.printStackTrace(); - } - - // Create and show the login window - SwingUtilities.invokeLater(() -> { - Main loginWindow = new Main(); - loginWindow.setVisible(true); - }); + AppBuilder appBuilder = new AppBuilder(); + JFrame application = appBuilder + .addLoginView() + .addSignupView() + .addLoggedInView() + .addNewsView() + .addSignupUseCase() + .addLoginUseCase() + .addChangePasswordUseCase() + .addNewsUsecase() + .build(); + + application.pack(); + application.setLocationRelativeTo(null); + application.setVisible(true); } } \ No newline at end of file From f52667e9469315b65f16e427d4841c54c285a8a3 Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Sun, 30 Nov 2025 11:07:31 -0500 Subject: [PATCH 034/103] cleanup1 --- src/main/java/app/Dashboard.java | 180 -------------------- src/main/java/app/Main.java | 2 - src/main/java/app/StockPage.java | 272 ------------------------------- 3 files changed, 454 deletions(-) delete mode 100644 src/main/java/app/Dashboard.java delete mode 100644 src/main/java/app/StockPage.java diff --git a/src/main/java/app/Dashboard.java b/src/main/java/app/Dashboard.java deleted file mode 100644 index 70594beb..00000000 --- a/src/main/java/app/Dashboard.java +++ /dev/null @@ -1,180 +0,0 @@ -package app; - -import javax.swing.*; -import java.awt.*; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.util.logging.Logger; - -/** - * The main application window displayed after successful login. - * Manages the navigation between different dashboard views using CardLayout. - */ -public class Dashboard extends JFrame { - - private static final Logger LOGGER = Logger.getLogger(Dashboard.class.getName()); - - private final StockPage homePanel; - private final CardLayout cardLayout = new CardLayout(); - private final JPanel mainContentPanel; - private final String username; - - public Dashboard(String username) { - this.username = username; - setTitle("Stock App Dashboard - Logged in as: " + username); - setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - setSize(1000, 600); // Slightly larger frame for better dashboard layout - setLocationRelativeTo(null); - - this.homePanel = new StockPage(); // The real-time trade view - this.mainContentPanel = new JPanel(cardLayout); - - setLayout(new BorderLayout()); - - // 1. Create the Navigation Bar (North Panel) - JPanel navBar = createNavigationBar(); - add(navBar, BorderLayout.NORTH); - - // 2. Setup the Card Layout for main content (Center Panel) - setupMainContent(); - add(mainContentPanel, BorderLayout.CENTER); - - // Add a listener to ensure WebSocket is closed on application exit - addWindowListener(new WindowAdapter() { - @Override - public void windowClosing(WindowEvent e) { - homePanel.closeWebSocket(); // Ensure WebSocket connection is closed - dispose(); // Close the window - System.exit(0); // Terminate the application - } - }); - } - - /** - * Creates the top navigation bar containing the four main buttons and the Logout button. - * Uses a GridBagLayout for flexible spacing. - */ - private JPanel createNavigationBar() { - JPanel navBar = new JPanel(new GridBagLayout()); - navBar.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - navBar.setBackground(new Color(240, 240, 240)); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.insets = new Insets(0, 5, 0, 5); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.anchor = GridBagConstraints.CENTER; - - String[] buttonNames = {"Home (Real-Time Trade)", "Portfolio", "Watchlist", "Company Earnings History", "Settings"}; - - // Create and add the four main navigation buttons - for (int i = 0; i < buttonNames.length; i++) { - JButton button = new JButton(buttonNames[i]); - button.setFont(new Font("Arial", Font.BOLD, 14)); - String cardName = buttonNames[i].split(" ")[0]; // Use first word as card key - - button.addActionListener(e -> { - cardLayout.show(mainContentPanel, cardName); - if (!cardName.equals("Home")) { - homePanel.closeWebSocket(); // Stop real-time data when navigating away - } - updateButtonStyles(navBar, button); - }); - - gbc.gridx = i; - gbc.weightx = 1.0; // Distribute space evenly - navBar.add(button, gbc); - } - - // Add a flexible spacer to push the Logout button to the far right - gbc.gridx = buttonNames.length; - gbc.weightx = 10.0; - navBar.add(Box.createHorizontalGlue(), gbc); - - // Add Logout button - gbc.gridx = buttonNames.length + 1; - gbc.weightx = 0.0; // Don't take extra space - JButton logoutButton = new JButton("Logout (" + username + ")"); - logoutButton.setBackground(new Color(200, 50, 50)); - logoutButton.setForeground(Color.WHITE); - logoutButton.addActionListener(e -> handleLogout()); - navBar.add(logoutButton, gbc); - - // Initially set "Home" button as selected (assuming it's the first button) - SwingUtilities.invokeLater(() -> { - if (navBar.getComponentCount() > 0) { - updateButtonStyles(navBar, (JButton) navBar.getComponent(0)); - } - }); - - return navBar; - } - - /** - * Sets up the main content area with different panels for the CardLayout. - */ - private void setupMainContent() { - // Add the real-time trade panel (Home) - mainContentPanel.add(homePanel, "Home"); - - // Add placeholder panels for other views - mainContentPanel.add(createPlaceholderPanel("Portfolio View", Color.LIGHT_GRAY), "Portfolio"); - mainContentPanel.add(createPlaceholderPanel("Watchlist Management", Color.CYAN), "Watchlist"); - mainContentPanel.add(createPlaceholderPanel("User Settings", Color.YELLOW), "Settings"); - mainContentPanel.add(createPlaceholderPanel("Company Earning History", Color.YELLOW), "Earnings"); - - // Show the Home view by default - cardLayout.show(mainContentPanel, "Home"); - } - - /** - * Helper to create a simple placeholder panel for non-implemented views. - */ - private JPanel createPlaceholderPanel(String title, Color bgColor) { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBackground(bgColor); - JLabel label = new JLabel("--- " + title + " ---", SwingConstants.CENTER); - label.setFont(new Font("Arial", Font.ITALIC, 24)); - panel.add(label, BorderLayout.CENTER); - return panel; - } - - /** - * Updates the styling of the navigation buttons to indicate the active view. - */ - private void updateButtonStyles(JPanel navBar, JButton active) { - for (Component comp : navBar.getComponents()) { - if (comp instanceof JButton) { - JButton button = (JButton) comp; - if (button.getText().startsWith("Logout")) continue; // Skip Logout button - - if (button == active) { - button.setBackground(new Color(50, 150, 250)); // Active color (Blue) - button.setForeground(Color.WHITE); - } else { - button.setBackground(UIManager.getColor("Button.background")); // Default color - button.setForeground(UIManager.getColor("Button.foreground")); - } - } - } - } - - /** - * Handles the user logging out. - */ - private void handleLogout() { - int confirm = JOptionPane.showConfirmDialog(this, - "Are you sure you want to log out?", "Confirm Logout", - JOptionPane.YES_NO_OPTION); - - if (confirm == JOptionPane.YES_OPTION) { - homePanel.closeWebSocket(); // Close connection before logging out - - // Close dashboard and open the login screen - this.dispose(); - SwingUtilities.invokeLater(() -> { - // Relaunch the Main (Login) window - new Main().setVisible(true); - }); - } - } -} \ No newline at end of file diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 9fd275f7..3453fd32 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -9,11 +9,9 @@ public static void main(String[] args) { .addLoginView() .addSignupView() .addLoggedInView() - .addNewsView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() - .addNewsUsecase() .build(); application.pack(); diff --git a/src/main/java/app/StockPage.java b/src/main/java/app/StockPage.java deleted file mode 100644 index 3e3b37e0..00000000 --- a/src/main/java/app/StockPage.java +++ /dev/null @@ -1,272 +0,0 @@ -package app; - -import okhttp3.*; - -import javax.swing.*; -import java.awt.*; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A JPanel containing the real-time trade dashboard UI and WebSocket logic. - * This component is embedded within the main Dashboard JFrame. - * NOTE: This version uses manual string parsing instead of a JSON library. - */ -public class StockPage extends JPanel { // Class name changed from DashboardContent - - private static final Logger LOGGER = Logger.getLogger(StockPage.class.getName()); // Logger updated - - // Replace with your actual Finnhub API Key - private static final String API_KEY = "d4977ehr01qshn3kvpt0d4977ehr01qshn3kvptg"; - private static final String WEB_SOCKET_URL = "wss://ws.finnhub.io?token=" + API_KEY; - private static final String DEFAULT_SYMBOL = "BINANCE:BTCUSDT"; // Example symbol - - // GUI Elements for display - private final JLabel symbolLabel = new JLabel("---"); - private final JLabel priceLabel = new JLabel("---"); - private final JLabel volumeLabel = new JLabel("---"); - private final JLabel timestampLabel = new JLabel("---"); - private final JLabel statusLabel = new JLabel("Status: Disconnected", SwingConstants.CENTER); - - private WebSocket webSocket; - private final JButton connectButton = new JButton("Connect"); - - /** - * Initializes the GUI components. - */ - public StockPage() { // Constructor name changed - setLayout(new BorderLayout()); - setBorder(BorderFactory.createTitledBorder("Real-Time Trade Data")); - - // --- Header (Status) --- - statusLabel.setFont(new Font("Arial", Font.BOLD, 14)); - statusLabel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - add(statusLabel, BorderLayout.NORTH); - - // --- Main Data Panel --- - JPanel dataPanel = new JPanel(new GridLayout(4, 2, 10, 10)); - dataPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); - - // Helper method to style the value labels - styleLabel(symbolLabel); - styleLabel(priceLabel); - styleLabel(volumeLabel); - styleLabel(timestampLabel); - - dataPanel.add(new JLabel("Symbol:")); - dataPanel.add(symbolLabel); - - dataPanel.add(new JLabel("Last Price (P):")); - dataPanel.add(priceLabel); - - dataPanel.add(new JLabel("Volume (V):")); - dataPanel.add(volumeLabel); - - dataPanel.add(new JLabel("Timestamp (T):")); - dataPanel.add(timestampLabel); - - add(dataPanel, BorderLayout.CENTER); - - // --- Control Panel (Connect/Disconnect) --- - JPanel controlPanel = new JPanel(); - connectButton.addActionListener(e -> toggleConnection()); - controlPanel.add(connectButton); - add(controlPanel, BorderLayout.SOUTH); - } - - /** - * Helper to apply basic styling to the data display labels. - */ - private void styleLabel(JLabel label) { - label.setFont(new Font("Monospaced", Font.BOLD, 18)); - label.setForeground(new Color(0, 128, 0)); // Green color for data - } - - /** - * Toggles the WebSocket connection status. - */ - private void toggleConnection() { - if (webSocket != null) { - closeWebSocket(); - } else { - connectWebSocket(); - } - } - - /** - * Gracefully closes the WebSocket connection. - */ - public void closeWebSocket() { - if (webSocket != null) { - LOGGER.info("Closing existing connection..."); - webSocket.close(1000, "User initiated disconnect"); - webSocket = null; - // Also update GUI elements - SwingUtilities.invokeLater(() -> { - statusLabel.setText("Status: Disconnected"); - statusLabel.setForeground(Color.BLACK); - connectButton.setText("Connect"); - connectButton.setEnabled(true); - }); - } - } - - /** - * Connects to the WebSocket and sends subscription messages. - */ - private void connectWebSocket() { - OkHttpClient client = new OkHttpClient.Builder() - .readTimeout(0, TimeUnit.MILLISECONDS) - .build(); - - Request request = new Request.Builder().url(WEB_SOCKET_URL).build(); - - statusLabel.setText("Status: Connecting..."); - statusLabel.setForeground(Color.ORANGE); - connectButton.setText("Connecting..."); - connectButton.setEnabled(false); - - webSocket = client.newWebSocket(request, new WebSocketListener() { - @Override - public void onOpen(WebSocket ws, Response response) { - LOGGER.info("WebSocket Opened. Subscribing to " + DEFAULT_SYMBOL); - - String subscribeMsg = String.format("{\"type\":\"subscribe\",\"symbol\":\"%s\"}", DEFAULT_SYMBOL); - ws.send(subscribeMsg); - - SwingUtilities.invokeLater(() -> { - statusLabel.setText("Status: Connected to " + DEFAULT_SYMBOL); - statusLabel.setForeground(new Color(0, 128, 0)); - connectButton.setText("Disconnect"); - connectButton.setEnabled(true); - }); - } - - @Override - public void onMessage(WebSocket ws, String text) { - processMessage(text); - } - - @Override - public void onClosing(WebSocket ws, int code, String reason) { - LOGGER.info("WebSocket Closing: Code " + code + ", Reason: " + reason); - } - - @Override - public void onFailure(WebSocket ws, Throwable t, Response response) { - LOGGER.log(Level.SEVERE, "WebSocket Failure: " + t.getMessage(), t); - closeWebSocket(); // Attempt to clean up resources - SwingUtilities.invokeLater(() -> { - statusLabel.setText("Status: Failure! Check API Key/Console."); - statusLabel.setForeground(Color.RED); - connectButton.setText("Connect"); - connectButton.setEnabled(true); - }); - } - - @Override - public void onClosed(WebSocket ws, int code, String reason) { - LOGGER.info("WebSocket Closed."); - webSocket = null; - SwingUtilities.invokeLater(() -> { - statusLabel.setText("Status: Disconnected"); - statusLabel.setForeground(Color.BLACK); - connectButton.setText("Connect"); - connectButton.setEnabled(true); - }); - } - }); - } - - /** - * Extracts a value from a JSON string using basic String methods. - */ - private String extractValue(String json, String key) { - String keySearch = "\"" + key + "\":"; - int keyIndex = json.indexOf(keySearch); - - if (keyIndex == -1) { - return null; - } - - int valueStart = keyIndex + keySearch.length(); - - // Check for string value (starts with double quote) - char firstChar = json.charAt(valueStart); - if (firstChar == '"') { - valueStart++; - int valueEnd = json.indexOf('"', valueStart); - if (valueEnd != -1) { - return json.substring(valueStart, valueEnd); - } - } - - // Logic for numerical/boolean values - else { - // Find the index of the immediate next JSON delimiter: ',', '}', or ']' - int indexComma = json.indexOf(',', valueStart); - int indexCurly = json.indexOf('}', valueStart); - int indexBracket = json.indexOf(']', valueStart); - - // Start with a large index to find the minimum - int terminationIndex = Integer.MAX_VALUE; - - // Find the minimum positive index among the delimiters - if (indexComma != -1 && indexComma > valueStart) terminationIndex = Math.min(terminationIndex, indexComma); - if (indexCurly != -1 && indexCurly > valueStart) terminationIndex = Math.min(terminationIndex, indexCurly); - if (indexBracket != -1 && indexBracket > valueStart) terminationIndex = Math.min(terminationIndex, indexBracket); - - // If a valid termination character was found - if (terminationIndex != Integer.MAX_VALUE) { - // substring is exclusive of the end index, correctly extracting the value - return json.substring(valueStart, terminationIndex).trim(); - } - } - return null; - } - - /** - * Parses the incoming JSON message using manual string manipulation and updates the GUI. - */ - private void processMessage(String jsonMessage) { - try { - if (jsonMessage.contains("\"type\":\"trade\"")) { - - String symbol = extractValue(jsonMessage, "s"); - String priceStr = extractValue(jsonMessage, "p"); - double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; - - String volumeStr = extractValue(jsonMessage, "v"); - double volume = (volumeStr != null) ? Double.parseDouble(volumeStr) : 0.0; - - String timestampStr = extractValue(jsonMessage, "t"); - long timestamp = (timestampStr != null) ? Long.parseLong(timestampStr) : 0L; - - SwingUtilities.invokeLater(() -> { - symbolLabel.setText(symbol != null ? symbol : "N/A"); - priceLabel.setText(String.format("$%,.2f", price)); - volumeLabel.setText(String.format("%,.4f", volume)); - - if (timestamp > 0) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); - String time = Instant.ofEpochMilli(timestamp) - .atZone(ZoneId.systemDefault()) - .toLocalTime() - .format(formatter); - timestampLabel.setText(time); - } else { - timestampLabel.setText("N/A"); - } - }); - } - } catch (NumberFormatException e) { - LOGGER.log(Level.WARNING, "Error processing JSON message: " + e.getMessage()); - } catch (Exception e) { - LOGGER.log(Level.WARNING, "General error in processMessage: " + e.getMessage()); - } - } -} \ No newline at end of file From 16ffc92d8235c1ce7ab55e8ec0f53cc5320c2a17 Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Sun, 30 Nov 2025 11:11:44 -0500 Subject: [PATCH 035/103] cleaning --- src/main/java/Service/LoginService.java | 1 - src/main/java/app/User.java | 29 ------------------------- 2 files changed, 30 deletions(-) delete mode 100644 src/main/java/app/User.java diff --git a/src/main/java/Service/LoginService.java b/src/main/java/Service/LoginService.java index 71676353..fe5fcde4 100644 --- a/src/main/java/Service/LoginService.java +++ b/src/main/java/Service/LoginService.java @@ -1,6 +1,5 @@ package Service; -import App.User; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/app/User.java b/src/main/java/app/User.java deleted file mode 100644 index 03cef6f7..00000000 --- a/src/main/java/app/User.java +++ /dev/null @@ -1,29 +0,0 @@ -package App; - -public class User { - private final String username; - private String password; // In a real application, this should be hashed - private boolean isLoggedIn; - - public User(String username, String password) { - this.username = username; - this.password = password; - this.isLoggedIn = false; - } - - public String getUsername() { - return username; - } - - public boolean checkPassword(String password) { - return this.password.equals(password); - } - - public boolean isLoggedIn() { - return isLoggedIn; - } - - public void setLoggedIn(boolean loggedIn) { - isLoggedIn = loggedIn; - } -} \ No newline at end of file From f4f0c4eb6c7ccf3b56a2830801967ce476eafa5c Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Sun, 30 Nov 2025 11:20:24 -0500 Subject: [PATCH 036/103] update --- src/main/java/Service/LoginService.java | 76 ------------------------- 1 file changed, 76 deletions(-) delete mode 100644 src/main/java/Service/LoginService.java diff --git a/src/main/java/Service/LoginService.java b/src/main/java/Service/LoginService.java deleted file mode 100644 index fe5fcde4..00000000 --- a/src/main/java/Service/LoginService.java +++ /dev/null @@ -1,76 +0,0 @@ -package Service; - -import java.util.HashMap; -import java.util.Map; - -public class LoginService { - private Map users; - private User currentUser; - - public LoginService() { - this.users = new HashMap<>(); - // Add some test users (in a real app, this would be in a database) - users.put("user1", new User("user1", "password1")); - users.put("user2", new User("user2", "password2")); - } - - /** - * Attempts to log in a user with the given credentials - * @param username The username to log in with - * @param password The password to verify - * @return true if login is successful, false otherwise - */ - public boolean login(String username, String password) { - User user = users.get(username); - if (user != null && user.checkPassword(password)) { - user.setLoggedIn(true); - currentUser = user; - return true; - } - return false; - } - - /** - * Logs out the current user - */ - public void logout() { - if (currentUser != null) { - currentUser.setLoggedIn(false); - currentUser = null; - } - } - - /** - * Creates a new user account - * @param username The desired username - * @param password The desired password - * @return true if account was created successfully, false if username already exists - */ - public boolean signUp(String username, String password) { - if (username == null || username.trim().isEmpty() || password == null || password.isEmpty()) { - return false; - } - - if (users.containsKey(username)) { - return false; // User already exists - } - users.put(username, new User(username, password)); - return true; - } - - /** - * Checks if a user is currently logged in - * @return true if a user is logged in, false otherwise - */ - public boolean isLoggedIn() { - return currentUser != null && currentUser.isLoggedIn(); - } - - /** - * Gets the username of the currently logged-in user - * @return The username or null if no user is logged in - */ - public String getCurrentUser() { - return currentUser != null ? currentUser.getUsername() : null; - } -} \ No newline at end of file From 4c6f8c7968f6c57c4e5b7755906053c694fc81bf Mon Sep 17 00:00:00 2001 From: samario Date: Mon, 24 Nov 2025 17:21:17 -0500 Subject: [PATCH 037/103] Copy and Paste from old repo --- .../MarketStatusDataAccessObject.java | 108 ++++++++++++++++++ src/main/java/entity/MarketStatus.java | 36 ++++++ .../MarketStatus/MarketStatusController.java | 16 +++ .../MarketStatus/MarketStatusPresenter.java | 59 ++++++++++ .../MarketStatus/MarketStatusViewModel.java | 88 ++++++++++++++ .../MarketStatusDataAccessInterface.java | 7 ++ .../MarketStatusInputBoundary.java | 6 + .../MarketStatus/MarketStatusInteractor.java | 32 ++++++ .../MarketStatusOutputBoundary.java | 8 ++ .../MarketStatusResponseModel.java | 14 +++ 10 files changed, 374 insertions(+) create mode 100644 src/main/java/data_access/MarketStatusDataAccessObject.java create mode 100644 src/main/java/entity/MarketStatus.java create mode 100644 src/main/java/interface_adapter/MarketStatus/MarketStatusController.java create mode 100644 src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java create mode 100644 src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java create mode 100644 src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java create mode 100644 src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java create mode 100644 src/main/java/use_case/MarketStatus/MarketStatusInteractor.java create mode 100644 src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java create mode 100644 src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java diff --git a/src/main/java/data_access/MarketStatusDataAccessObject.java b/src/main/java/data_access/MarketStatusDataAccessObject.java new file mode 100644 index 00000000..37375ddb --- /dev/null +++ b/src/main/java/data_access/MarketStatusDataAccessObject.java @@ -0,0 +1,108 @@ +package data_access; + +import entity.MarketStatus; +import org.json.JSONObject; +import org.json.JSONTokener; +import use_case.MarketStatus.MarketStatusDataAccessInterface; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +public class MarketStatusDataAccessObject implements MarketStatusDataAccessInterface { + private static final String BASE_URL = "https://finnhub.io/api/v1/stock/market-status"; + + private final String apiKey; + + public MarketStatusDataAccessObject(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public MarketStatus loadStatus() throws Exception { + String urlString = BASE_URL + + "?exchange=US" + + "&token=" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8); + + JSONObject json = fetchJsonObject(urlString); + return parseMarketStatus(json); + } + + // Perform HTTP GET and return the response as a JSONObject. + private JSONObject fetchJsonObject(String urlString) throws IOException { + HttpURLConnection connection = null; + InputStream inputStream = null; + + try { + URL url = new URL(urlString); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + + connection.setConnectTimeout(10_000); + connection.setReadTimeout(10_000); + + int status = connection.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + String errorBody = readStream(connection.getErrorStream()); + throw new IOException("HTTP error " + status + " from Finnhub: " + errorBody); + } + + inputStream = connection.getInputStream(); + JSONTokener tokener = new JSONTokener(inputStream); + return new JSONObject(tokener); + + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ignored) {} + } + if (connection != null) { + connection.disconnect(); + } + } + } + + // Convert errorMessage + private String readStream(InputStream stream) throws IOException { + if (stream == null) return ""; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(stream, StandardCharsets.UTF_8))) { + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString(); + } + } + + + private MarketStatus parseMarketStatus(JSONObject obj) { + if (obj == null) { + return null; + } + + String exchange = obj.optString("exchange", "US"); + boolean isOpen = obj.optBoolean("isOpen", false); + String session = obj.optString("session", ""); + String holiday = obj.optString("holiday", ""); + long timestamp = obj.optLong("t", 0L); + String timezone = obj.optString("timezone", ""); + + return new MarketStatus( + exchange, + isOpen, + session, + holiday, + timestamp, + timezone + ); + } +} diff --git a/src/main/java/entity/MarketStatus.java b/src/main/java/entity/MarketStatus.java new file mode 100644 index 00000000..9f24c44a --- /dev/null +++ b/src/main/java/entity/MarketStatus.java @@ -0,0 +1,36 @@ +package entity; + +public class MarketStatus { + private final String exchange; // e.g. "US" + private final boolean open; // true if market is open + private final String session; // "pre", "regular", "post" + private final String holiday; // holiday name if closed, else "" + private final long timestamp; // UNIX seconds + private final String timezone; // e.g. "America/New_York" + + public MarketStatus(String exchange, + boolean open, + String session, + String holiday, + long timestamp, + String timezone) { + this.exchange = exchange; + this.open = open; + this.session = session; + this.holiday = holiday; + this.timestamp = timestamp; + this.timezone = timezone; + } + + public String getExchange() {return exchange;} + + public boolean isOpen() {return open;} + + public String getSession() {return session;} + + public String getHoliday() {return holiday;} + + public long getTimestamp() {return timestamp;} + + public String getTimezone() {return timezone;} +} diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusController.java b/src/main/java/interface_adapter/MarketStatus/MarketStatusController.java new file mode 100644 index 00000000..f6c18f77 --- /dev/null +++ b/src/main/java/interface_adapter/MarketStatus/MarketStatusController.java @@ -0,0 +1,16 @@ +package interface_adapter.MarketStatus; + +import use_case.MarketStatus.MarketStatusInputBoundary; + +public class MarketStatusController { + + private final MarketStatusInputBoundary interactor; + + public MarketStatusController(MarketStatusInputBoundary interactor) { + this.interactor = interactor; + } + + public void updateStatus() { + interactor.execute(); + } +} diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java b/src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java new file mode 100644 index 00000000..d82c508a --- /dev/null +++ b/src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java @@ -0,0 +1,59 @@ +package interface_adapter.MarketStatus; + +import use_case.MarketStatus.MarketStatusOutputBoundary; +import use_case.MarketStatus.MarketStatusResponseModel; +import entity.MarketStatus; + +public class MarketStatusPresenter implements MarketStatusOutputBoundary { + private final MarketStatusViewModel viewModel; + + public MarketStatusPresenter(MarketStatusViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void prepareSuccessView(MarketStatusResponseModel responseModel) { + MarketStatus status = responseModel.getMarketStatus(); + + boolean open = status.isOpen(); + String session = status.getSession(); + String holiday = status.getHoliday(); + + // Status message + String text; + if (open) { + if (session != null && !session.isBlank()) { + text = "US market is OPEN (" + session + ")"; + } else { + text = "US market is OPEN"; + } + } else { + if (holiday != null && !holiday.isBlank()) { + text = "US market is CLOSED (Holiday: " + holiday + ")"; + } else { + text = "US market is CLOSED"; + } + } + + viewModel.setStatusText(text); + viewModel.setOpen(open); + viewModel.setSession(session); + viewModel.setHoliday(holiday); + viewModel.setTimezone(status.getTimezone()); + viewModel.setErrorMessage(null); + + viewModel.firePropertyChanged(); + } + + @Override + public void prepareFailView(String errorMessage) { + viewModel.setStatusText("Market status unavailable"); + viewModel.setOpen(false); + viewModel.setSession(null); + viewModel.setHoliday(null); + viewModel.setTimezone(null); + viewModel.setErrorMessage(errorMessage); + + viewModel.firePropertyChanged(); + } +} diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java b/src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java new file mode 100644 index 00000000..ef8981f1 --- /dev/null +++ b/src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java @@ -0,0 +1,88 @@ +package interface_adapter.MarketStatus; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + +public class MarketStatusViewModel { + private final PropertyChangeSupport support = new PropertyChangeSupport(this); + + // What is shown in UI + private String statusText; + + // Whether the market is open + private boolean open; + + // Extra info, optional + private String session; // "pre", "regular", "post", etc. + private String exchange; // e.g. "US" + private String holiday; // holiday name, or "" + private String timezone; // e.g. "America/New_York"; + + private String errorMessage; + + // --- observer methods --- + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + public void firePropertyChanged() { + support.firePropertyChange("state", null, this); + } + + // getters + public String getStatusText() { + return statusText; + } + + public void setStatusText(String statusText) { + this.statusText = statusText; + } + + public boolean isOpen() { + return open; + } + + public void setOpen(boolean open) { + this.open = open; + } + + public String getSession() { + return session; + } + + public void setSession(String session) { + this.session = session; + } + + public String getExchange() { + return exchange; + } + + public void setExchange(String exchange) { + this.exchange = exchange; + } + + public String getHoliday() { + return holiday; + } + + public void setHoliday(String holiday) { + this.holiday = holiday; + } + + public String getTimezone() { + return timezone; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} diff --git a/src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java b/src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java new file mode 100644 index 00000000..0f66b62d --- /dev/null +++ b/src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java @@ -0,0 +1,7 @@ +package use_case.MarketStatus; + +import entity.MarketStatus; + +public interface MarketStatusDataAccessInterface { + MarketStatus loadStatus() throws Exception; +} diff --git a/src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java b/src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java new file mode 100644 index 00000000..af989f09 --- /dev/null +++ b/src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java @@ -0,0 +1,6 @@ +package use_case.MarketStatus; + +public interface MarketStatusInputBoundary { + void execute(); +} + diff --git a/src/main/java/use_case/MarketStatus/MarketStatusInteractor.java b/src/main/java/use_case/MarketStatus/MarketStatusInteractor.java new file mode 100644 index 00000000..53e05935 --- /dev/null +++ b/src/main/java/use_case/MarketStatus/MarketStatusInteractor.java @@ -0,0 +1,32 @@ +package use_case.MarketStatus; + +import entity.MarketStatus; + +public class MarketStatusInteractor implements MarketStatusInputBoundary{ + private final MarketStatusDataAccessInterface dataAccess; + private final MarketStatusOutputBoundary presenter; + + public MarketStatusInteractor(MarketStatusDataAccessInterface dataAccess, + MarketStatusOutputBoundary presenter) { + this.dataAccess = dataAccess; + this.presenter = presenter; + } + + @Override + public void execute() { + try { + MarketStatus status = dataAccess.loadStatus(); + + if (status == null) { + presenter.prepareFailView("No market status available."); + return; + } + + MarketStatusResponseModel response = new MarketStatusResponseModel(status); + presenter.prepareSuccessView(response); + + } catch (Exception e) { + presenter.prepareFailView("Unable to load market status: " + e.getMessage()); + } + } +} diff --git a/src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java b/src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java new file mode 100644 index 00000000..46ceaa33 --- /dev/null +++ b/src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java @@ -0,0 +1,8 @@ +package use_case.MarketStatus; + +public interface MarketStatusOutputBoundary { + + void prepareSuccessView(MarketStatusResponseModel responseModel); + + void prepareFailView(String errorMessage); +} \ No newline at end of file diff --git a/src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java b/src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java new file mode 100644 index 00000000..0c460f1e --- /dev/null +++ b/src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java @@ -0,0 +1,14 @@ +package use_case.MarketStatus; + +import entity.MarketStatus; + +public class MarketStatusResponseModel { + + private final MarketStatus marketStatus; + + public MarketStatusResponseModel(MarketStatus marketStatus) { + this.marketStatus = marketStatus; + } + + public MarketStatus getMarketStatus() {return marketStatus;} +} \ No newline at end of file From 03d206c7bde73ed5dfa286adc36f09bad04305ce Mon Sep 17 00:00:00 2001 From: samario Date: Mon, 24 Nov 2025 17:32:54 -0500 Subject: [PATCH 038/103] Test for MarketStatus Interactor --- .../MarketStatusInteractorTest.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java diff --git a/src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java b/src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java new file mode 100644 index 00000000..2f52a0d0 --- /dev/null +++ b/src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java @@ -0,0 +1,160 @@ +package use_case.MarketStatus; + +import entity.MarketStatus; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for MarketStatusInteractor. + */ +public class MarketStatusInteractorTest { + + /** + * Simple in-memory stub for MarketStatusDataAccessInterface. + */ + private static class InMemoryMarketStatusDao implements MarketStatusDataAccessInterface { + + MarketStatus statusToReturn; + boolean throwOnLoad = false; + int loadCalls = 0; + + @Override + public MarketStatus loadStatus() throws Exception { + loadCalls++; + if (throwOnLoad) { + throw new Exception("Data access error"); + } + return statusToReturn; + } + } + + /** + * Recording presenter that just stores what the interactor sends. + */ + private static class RecordingPresenter implements MarketStatusOutputBoundary { + + MarketStatusResponseModel lastSuccess; + String lastError; + + @Override + public void prepareSuccessView(MarketStatusResponseModel responseModel) { + lastSuccess = responseModel; + } + + @Override + public void prepareFailView(String errorMessage) { + lastError = errorMessage; + } + } + + @Test + void execute_success_openMarket() { + // Arrange + InMemoryMarketStatusDao dao = new InMemoryMarketStatusDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.statusToReturn = new MarketStatus( + "US", + true, // open + "regular", // session + "", // holiday + 1_700_000_000L, // timestamp + "America/New_York" // timezone + ); + + MarketStatusInteractor interactor = + new MarketStatusInteractor(dao, presenter); + + // Act + interactor.execute(); + + // Assert DAO called + assertEquals(1, dao.loadCalls, "DAO should be called exactly once"); + + // Assert presenter got success, not error + assertNull(presenter.lastError, "No error should be reported"); + assertNotNull(presenter.lastSuccess, "Success response should be sent"); + + MarketStatus status = presenter.lastSuccess.getMarketStatus(); + assertNotNull(status); + assertEquals("US", status.getExchange()); + assertTrue(status.isOpen()); + assertEquals("regular", status.getSession()); + } + + @Test + void execute_success_closedHoliday() { + // Arrange + InMemoryMarketStatusDao dao = new InMemoryMarketStatusDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.statusToReturn = new MarketStatus( + "US", + false, // closed + "", // session + "Memorial Day", // holiday + 1_700_000_000L, + "America/New_York" + ); + + MarketStatusInteractor interactor = + new MarketStatusInteractor(dao, presenter); + + // Act + interactor.execute(); + + // Assert + assertEquals(1, dao.loadCalls); + assertNull(presenter.lastError); + assertNotNull(presenter.lastSuccess); + + MarketStatus status = presenter.lastSuccess.getMarketStatus(); + assertFalse(status.isOpen()); + assertEquals("Memorial Day", status.getHoliday()); + } + + @Test + void execute_nullStatus_triggersFailView() { + // Arrange + InMemoryMarketStatusDao dao = new InMemoryMarketStatusDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.statusToReturn = null; // simulate no data + + MarketStatusInteractor interactor = + new MarketStatusInteractor(dao, presenter); + + // Act + interactor.execute(); + + // Assert + assertEquals(1, dao.loadCalls); + assertNull(presenter.lastSuccess, "No success should be sent"); + assertNotNull(presenter.lastError, "Error should be reported"); + assertTrue(presenter.lastError.contains("No market status available"), + "Error message should mention missing status"); + } + + @Test + void execute_daoThrows_triggersFailView() { + // Arrange + InMemoryMarketStatusDao dao = new InMemoryMarketStatusDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + dao.throwOnLoad = true; + + MarketStatusInteractor interactor = + new MarketStatusInteractor(dao, presenter); + + // Act + interactor.execute(); + + // Assert + assertEquals(1, dao.loadCalls); + assertNull(presenter.lastSuccess); + assertNotNull(presenter.lastError); + assertTrue(presenter.lastError.contains("Unable to load market status"), + "Error message should mention inability to load status"); + } +} \ No newline at end of file From f7e10975616a1f62e40336772ae23c77a778f16f Mon Sep 17 00:00:00 2001 From: samario Date: Sat, 29 Nov 2025 15:48:50 -0500 Subject: [PATCH 039/103] Standardize the naming of packages --- src/main/java/data_access/MarketStatusDataAccessObject.java | 2 +- .../MarketStatusController.java | 4 ++-- .../MarketStatusPresenter.java | 6 +++--- .../MarketStatusViewModel.java | 2 +- .../MarketStatusDataAccessInterface.java | 2 +- .../MarketStatusInputBoundary.java | 2 +- .../MarketStatusInteractor.java | 2 +- .../MarketStatusOutputBoundary.java | 2 +- .../MarketStatusResponseModel.java | 2 +- .../MarketStatusInteractorTest.java | 2 +- 10 files changed, 13 insertions(+), 13 deletions(-) rename src/main/java/interface_adapter/{MarketStatus => market_status}/MarketStatusController.java (74%) rename src/main/java/interface_adapter/{MarketStatus => market_status}/MarketStatusPresenter.java (91%) rename src/main/java/interface_adapter/{MarketStatus => market_status}/MarketStatusViewModel.java (97%) rename src/main/java/use_case/{MarketStatus => market_status}/MarketStatusDataAccessInterface.java (80%) rename src/main/java/use_case/{MarketStatus => market_status}/MarketStatusInputBoundary.java (68%) rename src/main/java/use_case/{MarketStatus => market_status}/MarketStatusInteractor.java (96%) rename src/main/java/use_case/{MarketStatus => market_status}/MarketStatusOutputBoundary.java (83%) rename src/main/java/use_case/{MarketStatus => market_status}/MarketStatusResponseModel.java (90%) rename src/test/java/use_case/{MarketStatus => market_status}/MarketStatusInteractorTest.java (99%) diff --git a/src/main/java/data_access/MarketStatusDataAccessObject.java b/src/main/java/data_access/MarketStatusDataAccessObject.java index 37375ddb..2c3863af 100644 --- a/src/main/java/data_access/MarketStatusDataAccessObject.java +++ b/src/main/java/data_access/MarketStatusDataAccessObject.java @@ -3,7 +3,7 @@ import entity.MarketStatus; import org.json.JSONObject; import org.json.JSONTokener; -import use_case.MarketStatus.MarketStatusDataAccessInterface; +import use_case.market_status.MarketStatusDataAccessInterface; import java.io.BufferedReader; import java.io.IOException; diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusController.java b/src/main/java/interface_adapter/market_status/MarketStatusController.java similarity index 74% rename from src/main/java/interface_adapter/MarketStatus/MarketStatusController.java rename to src/main/java/interface_adapter/market_status/MarketStatusController.java index f6c18f77..eb116db7 100644 --- a/src/main/java/interface_adapter/MarketStatus/MarketStatusController.java +++ b/src/main/java/interface_adapter/market_status/MarketStatusController.java @@ -1,6 +1,6 @@ -package interface_adapter.MarketStatus; +package interface_adapter.market_status; -import use_case.MarketStatus.MarketStatusInputBoundary; +import use_case.market_status.MarketStatusInputBoundary; public class MarketStatusController { diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java b/src/main/java/interface_adapter/market_status/MarketStatusPresenter.java similarity index 91% rename from src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java rename to src/main/java/interface_adapter/market_status/MarketStatusPresenter.java index d82c508a..1b4db2dc 100644 --- a/src/main/java/interface_adapter/MarketStatus/MarketStatusPresenter.java +++ b/src/main/java/interface_adapter/market_status/MarketStatusPresenter.java @@ -1,7 +1,7 @@ -package interface_adapter.MarketStatus; +package interface_adapter.market_status; -import use_case.MarketStatus.MarketStatusOutputBoundary; -import use_case.MarketStatus.MarketStatusResponseModel; +import use_case.market_status.MarketStatusOutputBoundary; +import use_case.market_status.MarketStatusResponseModel; import entity.MarketStatus; public class MarketStatusPresenter implements MarketStatusOutputBoundary { diff --git a/src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java b/src/main/java/interface_adapter/market_status/MarketStatusViewModel.java similarity index 97% rename from src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java rename to src/main/java/interface_adapter/market_status/MarketStatusViewModel.java index ef8981f1..c42a874b 100644 --- a/src/main/java/interface_adapter/MarketStatus/MarketStatusViewModel.java +++ b/src/main/java/interface_adapter/market_status/MarketStatusViewModel.java @@ -1,4 +1,4 @@ -package interface_adapter.MarketStatus; +package interface_adapter.market_status; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; diff --git a/src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java b/src/main/java/use_case/market_status/MarketStatusDataAccessInterface.java similarity index 80% rename from src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java rename to src/main/java/use_case/market_status/MarketStatusDataAccessInterface.java index 0f66b62d..9e382973 100644 --- a/src/main/java/use_case/MarketStatus/MarketStatusDataAccessInterface.java +++ b/src/main/java/use_case/market_status/MarketStatusDataAccessInterface.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; import entity.MarketStatus; diff --git a/src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java b/src/main/java/use_case/market_status/MarketStatusInputBoundary.java similarity index 68% rename from src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java rename to src/main/java/use_case/market_status/MarketStatusInputBoundary.java index af989f09..8f5fbe90 100644 --- a/src/main/java/use_case/MarketStatus/MarketStatusInputBoundary.java +++ b/src/main/java/use_case/market_status/MarketStatusInputBoundary.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; public interface MarketStatusInputBoundary { void execute(); diff --git a/src/main/java/use_case/MarketStatus/MarketStatusInteractor.java b/src/main/java/use_case/market_status/MarketStatusInteractor.java similarity index 96% rename from src/main/java/use_case/MarketStatus/MarketStatusInteractor.java rename to src/main/java/use_case/market_status/MarketStatusInteractor.java index 53e05935..fb1d71b4 100644 --- a/src/main/java/use_case/MarketStatus/MarketStatusInteractor.java +++ b/src/main/java/use_case/market_status/MarketStatusInteractor.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; import entity.MarketStatus; diff --git a/src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java b/src/main/java/use_case/market_status/MarketStatusOutputBoundary.java similarity index 83% rename from src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java rename to src/main/java/use_case/market_status/MarketStatusOutputBoundary.java index 46ceaa33..0b50a594 100644 --- a/src/main/java/use_case/MarketStatus/MarketStatusOutputBoundary.java +++ b/src/main/java/use_case/market_status/MarketStatusOutputBoundary.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; public interface MarketStatusOutputBoundary { diff --git a/src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java b/src/main/java/use_case/market_status/MarketStatusResponseModel.java similarity index 90% rename from src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java rename to src/main/java/use_case/market_status/MarketStatusResponseModel.java index 0c460f1e..75f938de 100644 --- a/src/main/java/use_case/MarketStatus/MarketStatusResponseModel.java +++ b/src/main/java/use_case/market_status/MarketStatusResponseModel.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; import entity.MarketStatus; diff --git a/src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java b/src/test/java/use_case/market_status/MarketStatusInteractorTest.java similarity index 99% rename from src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java rename to src/test/java/use_case/market_status/MarketStatusInteractorTest.java index 2f52a0d0..233f763c 100644 --- a/src/test/java/use_case/MarketStatus/MarketStatusInteractorTest.java +++ b/src/test/java/use_case/market_status/MarketStatusInteractorTest.java @@ -1,4 +1,4 @@ -package use_case.MarketStatus; +package use_case.market_status; import entity.MarketStatus; import org.junit.jupiter.api.Test; From 05d7755a50f6668a81b8db4978ec1c8877704da5 Mon Sep 17 00:00:00 2001 From: samario Date: Sun, 30 Nov 2025 12:24:48 -0500 Subject: [PATCH 040/103] Integrate Market Status in the toolbar of LoggedInView --- src/main/java/app/AppBuilder.java | 29 +++++++++++++++++-- src/main/java/app/Main.java | 1 + src/main/java/view/LoggedInView.java | 43 ++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 5d7580ae..a30f1b97 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -33,6 +33,14 @@ import use_case.signup.SignupInputBoundary; import use_case.signup.SignupInteractor; import use_case.signup.SignupOutputBoundary; +import interface_adapter.market_status.MarketStatusViewModel; +import interface_adapter.market_status.MarketStatusPresenter; +import interface_adapter.market_status.MarketStatusController; +import use_case.market_status.MarketStatusInputBoundary; +import use_case.market_status.MarketStatusInteractor; +import use_case.market_status.MarketStatusOutputBoundary; +import use_case.market_status.MarketStatusDataAccessInterface; +import data_access.MarketStatusDataAccessObject; import view.*; import javax.swing.*; @@ -48,10 +56,14 @@ public class AppBuilder { // set which data access implementation to use, can be any // of the classes from the data_access package + // This key should be retrieved from the environment + String apiKey = "d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"; + // DAO version using local file storage final FileUserDataAccessObject userDataAccessObject = new FileUserDataAccessObject("users.csv", userFactory); - final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject("d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"); - + final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject(apiKey); + final MarketStatusDataAccessInterface marketStatusDataAccessObject = + new MarketStatusDataAccessObject(apiKey); // DAO version using a shared external database // final DBUserDataAccessObject userDataAccessObject = new DBUserDataAccessObject(userFactory); @@ -64,6 +76,8 @@ public class AppBuilder { private LoginView loginView; private NewsViewModel newsViewModel; private NewsView newsView; + private MarketStatusViewModel marketStatusViewModel; + private MarketStatusController marketStatusController; public AppBuilder() { cardPanel.setLayout(cardLayout); @@ -162,6 +176,17 @@ public AppBuilder addLogoutUseCase() { return this; } + public AppBuilder addMarketStatusUseCase() { + marketStatusViewModel = new MarketStatusViewModel(); + MarketStatusOutputBoundary msPresenter = new MarketStatusPresenter(marketStatusViewModel); + MarketStatusDataAccessInterface msDao = marketStatusDataAccessObject; + MarketStatusInputBoundary msInteractor = new MarketStatusInteractor(msDao, msPresenter); + marketStatusController = new MarketStatusController(msInteractor); + loggedInView.setMarketStatusViewModel(marketStatusViewModel); + marketStatusController.updateStatus(); + return this; + } + public JFrame build() { final JFrame application = new JFrame("Stock Application"); application.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 9fd275f7..b5439932 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -14,6 +14,7 @@ public static void main(String[] args) { .addLoginUseCase() .addChangePasswordUseCase() .addNewsUsecase() + .addMarketStatusUseCase() .build(); application.pack(); diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 7c95cb1c..c5a156f6 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -6,6 +6,7 @@ import interface_adapter.logout.LogoutController; import interface_adapter.news.NewsController; import interface_adapter.ViewManagerModel; +import interface_adapter.market_status.MarketStatusViewModel; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -29,6 +30,8 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private NewsController newsController; private ViewManagerModel viewManagerModel; private String newsViewName; + private JLabel marketStatusLabel; + private MarketStatusViewModel marketStatusViewModel; private final JLabel username; @@ -79,6 +82,13 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { this.add(topToolbar, BorderLayout.NORTH); + marketStatusLabel = new JLabel("Loading market status..."); + marketStatusLabel.setFont(marketStatusLabel.getFont().deriveFont(Font.PLAIN, 11f)); + marketStatusLabel.setForeground(Color.DARK_GRAY); + + topToolbar.add(Box.createHorizontalStrut(20)); + topToolbar.add(marketStatusLabel); + final JPanel centerPanel = new JPanel(); centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS)); @@ -190,4 +200,37 @@ public void setNewsNavigation(ViewManagerModel viewManagerModel, String newsView this.viewManagerModel = viewManagerModel; this.newsViewName = newsViewName; } + + public void setMarketStatusViewModel(MarketStatusViewModel viewModel) { + this.marketStatusViewModel = viewModel; + + // Listen for changes in market status + this.marketStatusViewModel.addPropertyChangeListener(evt -> { + if (!"state".equals(evt.getPropertyName())) return; + updateMarketStatusLabel(); + }); + } + + private void updateMarketStatusLabel() { + if (marketStatusViewModel == null) { + return; + } + + String text = marketStatusViewModel.getStatusText(); + if (text == null || text.isBlank()) { + marketStatusLabel.setText("Market status unavailable"); + marketStatusLabel.setForeground(Color.GRAY); + return; + } + + marketStatusLabel.setText(text); + + if (marketStatusViewModel.getErrorMessage() != null) { + marketStatusLabel.setForeground(Color.RED); + } else if (marketStatusViewModel.isOpen()) { + marketStatusLabel.setForeground(new Color(0, 128, 0)); // green-ish + } else { + marketStatusLabel.setForeground(Color.GRAY); + } + } } \ No newline at end of file From efe93553207ba41cfbb14b3820faabe7b741858b Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Sun, 30 Nov 2025 12:43:06 -0500 Subject: [PATCH 041/103] display --- src/main/java/app/AppBuilder.java | 48 +++++++++++++++++++++ src/main/java/app/Main.java | 2 + src/main/java/view/EarningsHistoryView.java | 18 ++++++++ src/main/java/view/LoggedInView.java | 23 +++++++++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 5d7580ae..1dd28224 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -1,5 +1,15 @@ package app; +import data_access.FinnhubEarningsDataAccessObject; +import interface_adapter.earnings_history.EarningsHistoryController; +import interface_adapter.earnings_history.EarningsHistoryPresenter; +import interface_adapter.earnings_history.EarningsHistoryViewModel; +import use_case.earnings_history.EarningsDataAccessInterface; +import use_case.earnings_history.GetEarningsHistoryInputBoundary; +import use_case.earnings_history.GetEarningsHistoryInteractor; +import use_case.earnings_history.GetEarningsHistoryOutputBoundary; +import view.EarningsHistoryView; + import data_access.FileUserDataAccessObject; import data_access.NewsDataAccessObject; import entity.UserFactory; @@ -51,6 +61,9 @@ public class AppBuilder { // DAO version using local file storage final FileUserDataAccessObject userDataAccessObject = new FileUserDataAccessObject("users.csv", userFactory); final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject("d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"); + // DAO for earnings history + final EarningsDataAccessInterface earningsDataAccessObject = + new FinnhubEarningsDataAccessObject(); // DAO version using a shared external database @@ -64,6 +77,9 @@ public class AppBuilder { private LoginView loginView; private NewsViewModel newsViewModel; private NewsView newsView; + private EarningsHistoryViewModel earningsHistoryViewModel; + private EarningsHistoryView earningsHistoryView; + public AppBuilder() { cardPanel.setLayout(cardLayout); @@ -101,6 +117,27 @@ public AppBuilder addNewsView() { cardPanel.add(newsView, newsView.getViewName()); return this; } + public AppBuilder addEarningsHistoryView() { + // ViewModel + earningsHistoryViewModel = new EarningsHistoryViewModel(); + + // Presenter + interactor + GetEarningsHistoryOutputBoundary outputBoundary = + new EarningsHistoryPresenter(earningsHistoryViewModel); + GetEarningsHistoryInputBoundary interactor = + new GetEarningsHistoryInteractor(earningsDataAccessObject, outputBoundary); + + // Controller + EarningsHistoryController controller = + new EarningsHistoryController(interactor, earningsHistoryViewModel); + + // Swing view + earningsHistoryView = new EarningsHistoryView(controller, earningsHistoryViewModel); + + cardPanel.add(earningsHistoryView, earningsHistoryView.getViewName()); + return this; + } + public AppBuilder addSignupUseCase() { final SignupOutputBoundary signupOutputBoundary = new SignupPresenter(viewManagerModel, @@ -145,6 +182,17 @@ public AppBuilder addNewsUsecase() { return this; } + public AppBuilder addEarningsHistoryUseCase() { + // Logged-in page: History button → Earnings history view + loggedInView.setHistoryNavigation( + viewManagerModel, earningsHistoryView.getViewName()); + + // Earnings history page: Back button → Logged-in view + earningsHistoryView.setBackNavigation( + viewManagerModel, loggedInView.getViewName()); + + return this; + } /** * Adds the Logout Use Case to the application. diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 9fd275f7..93ed856b 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -10,10 +10,12 @@ public static void main(String[] args) { .addSignupView() .addLoggedInView() .addNewsView() + .addEarningsHistoryView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() .addNewsUsecase() + .addEarningsHistoryUsecase() .build(); application.pack(); diff --git a/src/main/java/view/EarningsHistoryView.java b/src/main/java/view/EarningsHistoryView.java index f77f1c4d..147eeade 100644 --- a/src/main/java/view/EarningsHistoryView.java +++ b/src/main/java/view/EarningsHistoryView.java @@ -4,6 +4,7 @@ import interface_adapter.earnings_history.EarningsHistoryController; import interface_adapter.earnings_history.EarningsHistoryState; import interface_adapter.earnings_history.EarningsHistoryViewModel; +import interface_adapter.ViewManagerModel; import javax.swing.*; import javax.swing.table.DefaultTableModel; @@ -14,11 +15,13 @@ public class EarningsHistoryView extends JPanel implements PropertyChangeListener { + private final String viewName = "earnings history"; private final EarningsHistoryController controller; private final EarningsHistoryViewModel viewModel; private final JTextField symbolField = new JTextField("", 25); private final JButton loadButton = new JButton("Load Earnings"); + private final JButton backButton = new JButton("Back"); private final DefaultTableModel tableModel = new DefaultTableModel( new Object[]{"Period", "Actual", "Estimate", "Surprise"}, 0 @@ -39,6 +42,7 @@ public EarningsHistoryView(EarningsHistoryController controller, topPanel.add(new JLabel("Company Symbol:")); topPanel.add(symbolField); topPanel.add(loadButton); + topPanel.add(backButton); add(topPanel, BorderLayout.NORTH); table.setFillsViewportHeight(true); @@ -50,6 +54,20 @@ public EarningsHistoryView(EarningsHistoryController controller, loadButton.setFont(new Font("Segoe UI", Font.BOLD, 12)); } + public String getViewName() { + return viewName; + } + + // NEW: AppBuilder calls this so the Back button knows where to go + public void setBackNavigation(ViewManagerModel viewManagerModel, + String loggedInViewName) { + backButton.addActionListener(e -> { + viewManagerModel.setState(loggedInViewName); + viewManagerModel.firePropertyChange(); + }); + } + + @Override public void propertyChange(PropertyChangeEvent evt) { EarningsHistoryState state = (EarningsHistoryState) evt.getNewValue(); diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 7c95cb1c..228e6c3e 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -29,6 +29,7 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private NewsController newsController; private ViewManagerModel viewManagerModel; private String newsViewName; + private String historyViewName; private final JLabel username; @@ -41,6 +42,7 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JButton searchButton = new JButton("Search"); private final JTextArea stockInfoArea = new JTextArea(); private final JButton addToWatchlistButton = new JButton("Add to Watchlist"); + private final JButton historyButton; public LoggedInView(LoggedInViewModel loggedInViewModel) { this.loggedInViewModel = loggedInViewModel; @@ -67,7 +69,13 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { }); final JButton filterSearchButton = new JButton("Filter Search"); - final JButton historyButton = new JButton("History"); + historyButton = new JButton("History"); + historyButton.addActionListener(e -> { + if (viewManagerModel != null && historyViewName != null) { + viewManagerModel.setState(historyViewName); + viewManagerModel.firePropertyChange(); + } + }); final JButton marketOpenButton = new JButton("Market Open"); final JButton accountButton = new JButton("Account"); @@ -114,6 +122,8 @@ private void documentListenerHelper() { loggedInViewModel.setState(currentState); } + + @Override public void insertUpdate(DocumentEvent e) { documentListenerHelper(); @@ -190,4 +200,13 @@ public void setNewsNavigation(ViewManagerModel viewManagerModel, String newsView this.viewManagerModel = viewManagerModel; this.newsViewName = newsViewName; } -} \ No newline at end of file + + public void setHistoryNavigation(ViewManagerModel viewManagerModel, + String historyViewName) { + this.viewManagerModel = viewManagerModel; + this.historyViewName = historyViewName; + } + +} + + From 12ae9fb46aa3a5ebcead98e62df505b3891695ac Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sun, 30 Nov 2025 12:43:30 -0500 Subject: [PATCH 042/103] Fixed small conflicts in PR merge --- src/main/java/app/AppBuilder.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index cfaba2b7..3be7953b 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -104,7 +104,8 @@ public AppBuilder addFilterSearchView() { filterSearchViewModel = new FilterSearchViewModel(); filterSearchView = new FilterSearchView(filterSearchViewModel); cardPanel.add(filterSearchView, filterSearchView.getViewName()); - return this; + return this; + } public AppBuilder addNewsView() { newsViewModel = new NewsViewModel(); @@ -155,10 +156,12 @@ public AppBuilder addChangePasswordUseCase() { public AppBuilder addFilterSearchUseCase() { final FilterSearchOutputBoundary filterSearchOutputBoundary = new FilterSearchPresenter(filterSearchViewModel); final FilterSearchInputBoundary filterSearchInteractor = - new FilterSearchInteractor( filterSearchDataAccessObject, filterSearchOutputBoundary); + new FilterSearchInteractor(filterSearchDataAccessObject, filterSearchOutputBoundary); FilterSearchController filterSearchController = new FilterSearchController(filterSearchInteractor); filterSearchView.setFilterSearchController(filterSearchController); + return this; + } public AppBuilder addNewsUsecase() { // Logged-in page: News button → News view From 4169a2a81021a739271e56ee0fe88b5121538f5b Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Sun, 30 Nov 2025 12:47:54 -0500 Subject: [PATCH 043/103] Connect History button to earnings history view --- src/main/java/view/LoggedInView.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 228e6c3e..107702af 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -206,7 +206,6 @@ public void setHistoryNavigation(ViewManagerModel viewManagerModel, this.viewManagerModel = viewManagerModel; this.historyViewName = historyViewName; } - } From 7b6b881c3713cadc9b8d06ab287cdc341a14bcb5 Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Sun, 30 Nov 2025 12:52:28 -0500 Subject: [PATCH 044/103] improvement --- src/main/java/view/EarningsHistoryView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/view/EarningsHistoryView.java b/src/main/java/view/EarningsHistoryView.java index 147eeade..8bed6df0 100644 --- a/src/main/java/view/EarningsHistoryView.java +++ b/src/main/java/view/EarningsHistoryView.java @@ -58,7 +58,7 @@ public String getViewName() { return viewName; } - // NEW: AppBuilder calls this so the Back button knows where to go + // AppBuilder calls this so the Back button knows where to go public void setBackNavigation(ViewManagerModel viewManagerModel, String loggedInViewName) { backButton.addActionListener(e -> { From 965185c7a9e4b922714de1225235c8b4fde4dedd Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sun, 30 Nov 2025 12:52:50 -0500 Subject: [PATCH 045/103] small changes --- src/main/java/app/AppBuilder.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index cfaba2b7..3be7953b 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -104,7 +104,8 @@ public AppBuilder addFilterSearchView() { filterSearchViewModel = new FilterSearchViewModel(); filterSearchView = new FilterSearchView(filterSearchViewModel); cardPanel.add(filterSearchView, filterSearchView.getViewName()); - return this; + return this; + } public AppBuilder addNewsView() { newsViewModel = new NewsViewModel(); @@ -155,10 +156,12 @@ public AppBuilder addChangePasswordUseCase() { public AppBuilder addFilterSearchUseCase() { final FilterSearchOutputBoundary filterSearchOutputBoundary = new FilterSearchPresenter(filterSearchViewModel); final FilterSearchInputBoundary filterSearchInteractor = - new FilterSearchInteractor( filterSearchDataAccessObject, filterSearchOutputBoundary); + new FilterSearchInteractor(filterSearchDataAccessObject, filterSearchOutputBoundary); FilterSearchController filterSearchController = new FilterSearchController(filterSearchInteractor); filterSearchView.setFilterSearchController(filterSearchController); + return this; + } public AppBuilder addNewsUsecase() { // Logged-in page: News button → News view From d27f50c1579f36d8f3da7279a5e64ef513afd81b Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Sun, 30 Nov 2025 13:08:54 -0500 Subject: [PATCH 046/103] Added filter searcg button code and back button --- src/main/java/app/AppBuilder.java | 5 +++++ src/main/java/view/FilterSearchView.java | 20 ++++++++++++-------- src/main/java/view/LoggedInView.java | 15 +++++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 05c0031c..5500d0bf 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -72,6 +72,8 @@ public class AppBuilder { final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject(apiKey); final MarketStatusDataAccessInterface marketStatusDataAccessObject = new MarketStatusDataAccessObject(apiKey); + final FilterSearchDataAccessObject filterSearchDataAccessObject = + new FilterSearchDataAccessObject(apiKey); // DAO version using a shared external database // final DBUserDataAccessObject userDataAccessObject = new DBUserDataAccessObject(userFactory); @@ -174,6 +176,9 @@ public AppBuilder addFilterSearchUseCase() { FilterSearchController filterSearchController = new FilterSearchController(filterSearchInteractor); filterSearchView.setFilterSearchController(filterSearchController); + + loggedInView.setFilterSearchNavigation(viewManagerModel, filterSearchView.getViewName()); + filterSearchView.setBackNavigation(viewManagerModel, loggedInView.getViewName()); return this; } diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java index 74f32822..08d53180 100644 --- a/src/main/java/view/FilterSearchView.java +++ b/src/main/java/view/FilterSearchView.java @@ -2,6 +2,7 @@ import data_access.FilterSearchDataAccessObject; import entity.Stock; +import interface_adapter.ViewManagerModel; import interface_adapter.filter_search.FilterSearchController; import interface_adapter.filter_search.FilterSearchState; import interface_adapter.filter_search.FilterSearchViewModel; @@ -25,6 +26,9 @@ public class FilterSearchView extends JPanel implements PropertyChangeListener { private FilterSearchController filterSearchController; private final FilterSearchViewModel filterSearchViewModel; + private ViewManagerModel viewManagerModel; + private String homeViewName; + // Table-related fields private JTable table; private DefaultTableModel tableModel; @@ -108,14 +112,10 @@ public boolean isCellEditable(int row, int column) { // Home Button backToHomeButton.addActionListener(e -> { - // TODO: implement navigation back to the main/home view. - // For now, you can leave this empty or show a message: - JOptionPane.showMessageDialog( - this, - "Back to Home is not implemented yet.", - "Info", - JOptionPane.INFORMATION_MESSAGE - ); + if (viewManagerModel != null && homeViewName != null) { + viewManagerModel.setState(homeViewName); + viewManagerModel.firePropertyChange(); + } }); // Search Button @@ -188,6 +188,10 @@ public void propertyChange(PropertyChangeEvent evt) { } System.out.println("TABLE rowCount = " + tableModel.getRowCount()); } + public void setBackNavigation(ViewManagerModel viewManagerModel, String homeViewName) { + this.viewManagerModel = viewManagerModel; + this.homeViewName = homeViewName; + } } diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index c5a156f6..4b489911 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -30,6 +30,7 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private NewsController newsController; private ViewManagerModel viewManagerModel; private String newsViewName; + private String filterSearchViewName; private JLabel marketStatusLabel; private MarketStatusViewModel marketStatusViewModel; @@ -70,6 +71,15 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { }); final JButton filterSearchButton = new JButton("Filter Search"); + filterSearchButton.addActionListener(e -> { + System.out.println("Filter Search button clicked"); // debug + if (viewManagerModel != null && newsViewName != null) { + viewManagerModel.setState(filterSearchViewName); + viewManagerModel.firePropertyChange(); + } + }); + + final JButton historyButton = new JButton("History"); final JButton marketOpenButton = new JButton("Market Open"); final JButton accountButton = new JButton("Account"); @@ -201,6 +211,11 @@ public void setNewsNavigation(ViewManagerModel viewManagerModel, String newsView this.newsViewName = newsViewName; } + public void setFilterSearchNavigation(ViewManagerModel viewManagerModel, String filterSearchViewName) { + this.viewManagerModel = viewManagerModel; + this.filterSearchViewName = filterSearchViewName; + } + public void setMarketStatusViewModel(MarketStatusViewModel viewModel) { this.marketStatusViewModel = viewModel; From b5a638b1d43a277d53823ff7853aad7d9c292a28 Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Sun, 30 Nov 2025 16:27:13 -0500 Subject: [PATCH 047/103] fixerror --- src/main/java/app/AppBuilder.java | 4 ++-- src/main/java/app/Main.java | 2 +- src/main/java/view/LoggedInView.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 5458adce..6a9b7983 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -79,11 +79,11 @@ public class AppBuilder { // DAO version using local file storage final FileUserDataAccessObject userDataAccessObject = new FileUserDataAccessObject("users.csv", userFactory); - final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject("d4lpdgpr01qr851prp30d4lpdgpr01qr851prp3g"); // DAO for earnings history final EarningsDataAccessInterface earningsDataAccessObject = new FinnhubEarningsDataAccessObject(); - + final FilterSearchDataAccessInterface filterSearchDataAccessObject = + new FilterSearchDataAccessObject(apiKey); final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject(apiKey); final MarketStatusDataAccessInterface marketStatusDataAccessObject = new MarketStatusDataAccessObject(apiKey); diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 1afb856e..54eab0b3 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -20,7 +20,7 @@ public static void main(String[] args) { .addLoginUseCase() .addChangePasswordUseCase() .addNewsUsecase() - .addEarningsHistoryUsecase() + .addEarningsHistoryUseCase() .addMarketStatusUseCase() .build(); diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 4c44d974..a81e8174 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -216,7 +216,7 @@ public void setHistoryNavigation(ViewManagerModel viewManagerModel, this.viewManagerModel = viewManagerModel; this.historyViewName = historyViewName; } -} + public void setMarketStatusViewModel(MarketStatusViewModel viewModel) { From 3ace0963b712f6c4c42d974c5d83513e5ca55faa Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 22:57:53 -0500 Subject: [PATCH 048/103] Add stockquote in entities for quote from finnhub api --- src/main/java/entity/StockQuote.java | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/main/java/entity/StockQuote.java diff --git a/src/main/java/entity/StockQuote.java b/src/main/java/entity/StockQuote.java new file mode 100644 index 00000000..1276032e --- /dev/null +++ b/src/main/java/entity/StockQuote.java @@ -0,0 +1,55 @@ +package entity; + +public class StockQuote { + private final String symbol; + private final double currentPrice; + private final double open; + private final double high; + private final double low; + private final double previousClose; + private final long timestamp; + + public StockQuote(String symbol, + double currentPrice, + double open, + double high, + double low, + double previousClose, + long timestamp) { + this.symbol = symbol; + this.currentPrice = currentPrice; + this.open = open; + this.high = high; + this.low = low; + this.previousClose = previousClose; + this.timestamp = timestamp; + } + + public String getSymbol() { + return symbol; + } + + public double getCurrentPrice() { + return currentPrice; + } + + public double getOpen() { + return open; + } + + public double getHigh() { + return high; + } + + public double getLow() { + return low; + } + + public double getPreviousClose() { + return previousClose; + } + + public long getTimestamp() { + return timestamp; + } +} From a51c8be14a80a9deadb7cfceabdcb1c882c97186 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:13:53 -0500 Subject: [PATCH 049/103] adding files for my stock feature --- .../FinnhubStockSearchDataAccessObject.java | 102 ++++++++++++++++++ .../StockSearchDataAccessInterface.java | 4 + .../StockSearchInputBoundary.java | 4 + .../stock_search/StockSearchInteractor.java | 4 + .../StockSearchOutputBoundary.java | 4 + .../stock_search/StockSearchRequestModel.java | 4 + .../StockSearchResponseModel.java | 4 + 7 files changed, 126 insertions(+) create mode 100644 src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java create mode 100644 src/main/java/use_case/stock_search/StockSearchDataAccessInterface.java create mode 100644 src/main/java/use_case/stock_search/StockSearchInputBoundary.java create mode 100644 src/main/java/use_case/stock_search/StockSearchInteractor.java create mode 100644 src/main/java/use_case/stock_search/StockSearchOutputBoundary.java create mode 100644 src/main/java/use_case/stock_search/StockSearchRequestModel.java create mode 100644 src/main/java/use_case/stock_search/StockSearchResponseModel.java diff --git a/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java b/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java new file mode 100644 index 00000000..70fe308b --- /dev/null +++ b/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java @@ -0,0 +1,102 @@ +package data_access.stock_search; + +import data_access.*; +import entity.StockQuote; +import use_case.stock_search.StockSearchDataAccessInterface; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +public class FinnhubStockSearchDataAccessObject implements StockSearchDataAccessInterface { + private static final String BASE_URL = "https://finnhub.io/api/v1/quote"; + + private final String apiKey; + + public FinnhubStockSearchDataAccessObject(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public StockQuote loadQuote(String symbol) throws Exception { + if (symbol == null || symbol.isBlank()) { + throw new IllegalArgumentException("Symbol must not be empty."); + } + + StringBuilder urlBuilder = new StringBuilder(BASE_URL) + .append("?symbol=") + .append(URLEncoder.encode(symbol, StandardCharsets.UTF_8)) + .append("&token=") + .append(URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); + + JSONObject json = fetchJsonObject(urlBuilder.toString()); + return parseQuote(symbol, json); + } + + private JSONObject fetchJsonObject(String urlString) throws IOException { + HttpURLConnection connection = null; + InputStream inputStream = null; + + try { + URL url = new URL(urlString); + connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(10_000); + connection.setReadTimeout(10_000); + + int status = connection.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + String errorBody = readStream(connection.getErrorStream()); + throw new IOException("HTTP error " + status + " from Finnhub: " + errorBody); + } + + inputStream = connection.getInputStream(); + String body = readStream(inputStream); + return new JSONObject(body); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ignored) {} + } + if (connection != null) { + connection.disconnect(); + } + } + } + + private String readStream(InputStream stream) throws IOException { + if (stream == null) return ""; + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(stream, StandardCharsets.UTF_8))) { + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString(); + } + } + + private StockQuote parseQuote(String symbol, JSONObject obj) { + if (obj == null) { + throw new IllegalStateException("Empty response from Finnhub quote endpoint."); + } + + double current = obj.optDouble("c", Double.NaN); + double open = obj.optDouble("o", Double.NaN); + double high = obj.optDouble("h", Double.NaN); + double low = obj.optDouble("l", Double.NaN); + double prevClose = obj.optDouble("pc", Double.NaN); + long t = obj.optLong("t", 0L); + + return new StockQuote(symbol, current, open, high, low, prevClose, t); + } +} diff --git a/src/main/java/use_case/stock_search/StockSearchDataAccessInterface.java b/src/main/java/use_case/stock_search/StockSearchDataAccessInterface.java new file mode 100644 index 00000000..7b9be43a --- /dev/null +++ b/src/main/java/use_case/stock_search/StockSearchDataAccessInterface.java @@ -0,0 +1,4 @@ +package use_case.stock_search; + +public class StockSearchDataAccessInterface { +} diff --git a/src/main/java/use_case/stock_search/StockSearchInputBoundary.java b/src/main/java/use_case/stock_search/StockSearchInputBoundary.java new file mode 100644 index 00000000..b84c35b3 --- /dev/null +++ b/src/main/java/use_case/stock_search/StockSearchInputBoundary.java @@ -0,0 +1,4 @@ +package use_case.stock_search; + +public class StockSearchInputBoundary { +} diff --git a/src/main/java/use_case/stock_search/StockSearchInteractor.java b/src/main/java/use_case/stock_search/StockSearchInteractor.java new file mode 100644 index 00000000..d36c1c3f --- /dev/null +++ b/src/main/java/use_case/stock_search/StockSearchInteractor.java @@ -0,0 +1,4 @@ +package use_case.stock_search; + +public class StockSearchInteractor { +} diff --git a/src/main/java/use_case/stock_search/StockSearchOutputBoundary.java b/src/main/java/use_case/stock_search/StockSearchOutputBoundary.java new file mode 100644 index 00000000..70eaa008 --- /dev/null +++ b/src/main/java/use_case/stock_search/StockSearchOutputBoundary.java @@ -0,0 +1,4 @@ +package use_case.stock_search; + +public class StockSearchOutputBoundary { +} diff --git a/src/main/java/use_case/stock_search/StockSearchRequestModel.java b/src/main/java/use_case/stock_search/StockSearchRequestModel.java new file mode 100644 index 00000000..45eee9c8 --- /dev/null +++ b/src/main/java/use_case/stock_search/StockSearchRequestModel.java @@ -0,0 +1,4 @@ +package use_case.stock_search; + +public class StockSearchRequestModel { +} diff --git a/src/main/java/use_case/stock_search/StockSearchResponseModel.java b/src/main/java/use_case/stock_search/StockSearchResponseModel.java new file mode 100644 index 00000000..c2b26d74 --- /dev/null +++ b/src/main/java/use_case/stock_search/StockSearchResponseModel.java @@ -0,0 +1,4 @@ +package use_case.stock_search; + +public class StockSearchResponseModel { +} From a02b84532de3e8add0ec65cdcf6c1d139e32f012 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:16:37 -0500 Subject: [PATCH 050/103] contents for stocksearchdatainterface --- .../stock_search/StockSearchDataAccessInterface.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/use_case/stock_search/StockSearchDataAccessInterface.java b/src/main/java/use_case/stock_search/StockSearchDataAccessInterface.java index 7b9be43a..451d0f9c 100644 --- a/src/main/java/use_case/stock_search/StockSearchDataAccessInterface.java +++ b/src/main/java/use_case/stock_search/StockSearchDataAccessInterface.java @@ -1,4 +1,7 @@ package use_case.stock_search; -public class StockSearchDataAccessInterface { -} +import entity.StockQuote; + +public interface StockSearchDataAccessInterface { + StockQuote loadQuote(String symbol) throws Exception; +} \ No newline at end of file From 9705b2b3526dbd34f5a75ff333afbc4f1c8bb5d8 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:17:15 -0500 Subject: [PATCH 051/103] adding stocksearchinputboundary --- .../java/use_case/stock_search/StockSearchInputBoundary.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/use_case/stock_search/StockSearchInputBoundary.java b/src/main/java/use_case/stock_search/StockSearchInputBoundary.java index b84c35b3..40456e43 100644 --- a/src/main/java/use_case/stock_search/StockSearchInputBoundary.java +++ b/src/main/java/use_case/stock_search/StockSearchInputBoundary.java @@ -1,4 +1,5 @@ package use_case.stock_search; -public class StockSearchInputBoundary { +public interface StockSearchInputBoundary { + void execute(StockSearchRequestModel requestModel); } From e74887347916a45343dd7208dfe2d618651f7d2b Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:18:43 -0500 Subject: [PATCH 052/103] adding search interactor file update --- .../stock_search/StockSearchInteractor.java | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/main/java/use_case/stock_search/StockSearchInteractor.java b/src/main/java/use_case/stock_search/StockSearchInteractor.java index d36c1c3f..73b3d4f1 100644 --- a/src/main/java/use_case/stock_search/StockSearchInteractor.java +++ b/src/main/java/use_case/stock_search/StockSearchInteractor.java @@ -1,4 +1,44 @@ package use_case.stock_search; -public class StockSearchInteractor { +import entity.StockQuote; + +public class StockSearchInteractor implements StockSearchInputBoundary { + + private final StockSearchDataAccessInterface dataAccess; + private final StockSearchOutputBoundary presenter; + + public StockSearchInteractor(StockSearchDataAccessInterface dataAccess, + StockSearchOutputBoundary presenter) { + this.dataAccess = dataAccess; + this.presenter = presenter; + } + + @Override + public void execute(StockSearchRequestModel requestModel) { + String symbol = requestModel.getSymbol(); + + if (symbol == null || symbol.trim().isEmpty()) { + presenter.prepareFailView("Please enter a symbol."); + return; + } + + symbol = symbol.trim().toUpperCase(); + + try { + StockQuote quote = dataAccess.loadQuote(symbol); + + StockSearchResponseModel responseModel = new StockSearchResponseModel( + quote.getSymbol(), + quote.getCurrentPrice(), + quote.getOpen(), + quote.getHigh(), + quote.getLow(), + quote.getPreviousClose() + ); + presenter.prepareSuccessView(responseModel); + } + catch (Exception e) { + presenter.prepareFailView("Unable to load quote. " + e.getMessage()); + } + } } From aacb6a2112c425a87f586e8d9fecfee5aea64c6b Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:19:51 -0500 Subject: [PATCH 053/103] search output boundary file content --- .../java/use_case/stock_search/StockSearchOutputBoundary.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/use_case/stock_search/StockSearchOutputBoundary.java b/src/main/java/use_case/stock_search/StockSearchOutputBoundary.java index 70eaa008..22e14512 100644 --- a/src/main/java/use_case/stock_search/StockSearchOutputBoundary.java +++ b/src/main/java/use_case/stock_search/StockSearchOutputBoundary.java @@ -1,4 +1,6 @@ package use_case.stock_search; -public class StockSearchOutputBoundary { +public interface StockSearchOutputBoundary { + void prepareSuccessView(StockSearchResponseModel responseModel); + void prepareFailView(String errorMessage); } From 5c132ea6110a8c13a7a210250a55b560a5fd87b8 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:20:26 -0500 Subject: [PATCH 054/103] request model code --- .../use_case/stock_search/StockSearchRequestModel.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/use_case/stock_search/StockSearchRequestModel.java b/src/main/java/use_case/stock_search/StockSearchRequestModel.java index 45eee9c8..b6fa6d27 100644 --- a/src/main/java/use_case/stock_search/StockSearchRequestModel.java +++ b/src/main/java/use_case/stock_search/StockSearchRequestModel.java @@ -1,4 +1,13 @@ package use_case.stock_search; public class StockSearchRequestModel { + private final String symbol; + + public StockSearchRequestModel(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } } From 31454ae6d4addaa7061288409166cfe670088bc9 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:20:58 -0500 Subject: [PATCH 055/103] search response addition feature --- .../StockSearchResponseModel.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/main/java/use_case/stock_search/StockSearchResponseModel.java b/src/main/java/use_case/stock_search/StockSearchResponseModel.java index c2b26d74..bcc821ec 100644 --- a/src/main/java/use_case/stock_search/StockSearchResponseModel.java +++ b/src/main/java/use_case/stock_search/StockSearchResponseModel.java @@ -1,4 +1,48 @@ package use_case.stock_search; public class StockSearchResponseModel { + private final String symbol; + private final double currentPrice; + private final double open; + private final double high; + private final double low; + private final double previousClose; + + public StockSearchResponseModel(String symbol, + double currentPrice, + double open, + double high, + double low, + double previousClose) { + this.symbol = symbol; + this.currentPrice = currentPrice; + this.open = open; + this.high = high; + this.low = low; + this.previousClose = previousClose; + } + + public String getSymbol() { + return symbol; + } + + public double getCurrentPrice() { + return currentPrice; + } + + public double getOpen() { + return open; + } + + public double getHigh() { + return high; + } + + public double getLow() { + return low; + } + + public double getPreviousClose() { + return previousClose; + } } From c5c75a195db7c1bae254d43099b9d42aaee4c53e Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:29:33 -0500 Subject: [PATCH 056/103] adding searchcontroller --- .../interface_adapter/stock_search/StockSearchController.java | 4 ++++ .../interface_adapter/stock_search/StockSearchPresenter.java | 4 ++++ .../interface_adapter/stock_search/StockSearchViewModel.java | 4 ++++ 3 files changed, 12 insertions(+) create mode 100644 src/main/java/interface_adapter/stock_search/StockSearchController.java create mode 100644 src/main/java/interface_adapter/stock_search/StockSearchPresenter.java create mode 100644 src/main/java/interface_adapter/stock_search/StockSearchViewModel.java diff --git a/src/main/java/interface_adapter/stock_search/StockSearchController.java b/src/main/java/interface_adapter/stock_search/StockSearchController.java new file mode 100644 index 00000000..f27951fb --- /dev/null +++ b/src/main/java/interface_adapter/stock_search/StockSearchController.java @@ -0,0 +1,4 @@ +package interface_adapter.stock_search; + +public class StockSearchController { +} diff --git a/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java b/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java new file mode 100644 index 00000000..86a6ebd9 --- /dev/null +++ b/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java @@ -0,0 +1,4 @@ +package interface_adapter.stock_search; + +public class StockSearchPresenter { +} diff --git a/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java b/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java new file mode 100644 index 00000000..13f974e9 --- /dev/null +++ b/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java @@ -0,0 +1,4 @@ +package interface_adapter.stock_search; + +public class StockSearchViewModel { +} From edbe1b638b9648cc10b67242c14fbfa5172b0d22 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:30:19 -0500 Subject: [PATCH 057/103] adding controller code --- .../stock_search/StockSearchViewModel.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java b/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java index 13f974e9..dec31ae2 100644 --- a/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java +++ b/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java @@ -1,4 +1,27 @@ package interface_adapter.stock_search; -public class StockSearchViewModel { +import use_case.stock_search.StockSearchInputBoundary; +import use_case.stock_search.StockSearchRequestModel; + +public class StockSearchController { + + private final StockSearchInputBoundary interactor; + private final StockSearchViewModel viewModel; + + public StockSearchController(StockSearchInputBoundary interactor, + StockSearchViewModel viewModel) { + this.interactor = interactor; + this.viewModel = viewModel; + } + + public void search(String symbol) { + viewModel.setLoading(true); + viewModel.setErrorMessage(null); + viewModel.firePropertyChanged(); + + new Thread(() -> { + StockSearchRequestModel request = new StockSearchRequestModel(symbol); + interactor.execute(request); + }).start(); + } } From 89774467e1242de144250184d670817ba528f735 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:31:03 -0500 Subject: [PATCH 058/103] adding presenter for stock search --- .../stock_search/StockSearchPresenter.java | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java b/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java index 86a6ebd9..628e162e 100644 --- a/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java +++ b/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java @@ -1,4 +1,37 @@ package interface_adapter.stock_search; -public class StockSearchPresenter { +import use_case.stock_search.StockSearchOutputBoundary; +import use_case.stock_search.StockSearchResponseModel; + +public class StockSearchPresenter implements StockSearchOutputBoundary { + + private final StockSearchViewModel viewModel; + + public StockSearchPresenter(StockSearchViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void prepareSuccessView(StockSearchResponseModel responseModel) { + StringBuilder sb = new StringBuilder(); + sb.append(responseModel.getSymbol()).append("\n"); + sb.append("Current: ").append(responseModel.getCurrentPrice()).append("\n"); + sb.append("Open: ").append(responseModel.getOpen()).append("\n"); + sb.append("High: ").append(responseModel.getHigh()).append("\n"); + sb.append("Low: ").append(responseModel.getLow()).append("\n"); + sb.append("Prev: ").append(responseModel.getPreviousClose()).append("\n"); + + viewModel.setInfoText(sb.toString()); + viewModel.setErrorMessage(null); + viewModel.setLoading(false); + viewModel.firePropertyChanged(); + } + + @Override + public void prepareFailView(String errorMessage) { + viewModel.setInfoText(null); + viewModel.setErrorMessage(errorMessage); + viewModel.setLoading(false); + viewModel.firePropertyChanged(); + } } From c21ac674e34e7b5e8adf49ce4df057e4be4bec5d Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:32:18 -0500 Subject: [PATCH 059/103] changed error in view and added view code --- .../stock_search/StockSearchViewModel.java | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java b/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java index dec31ae2..c64be5f9 100644 --- a/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java +++ b/src/main/java/interface_adapter/stock_search/StockSearchViewModel.java @@ -1,27 +1,45 @@ package interface_adapter.stock_search; -import use_case.stock_search.StockSearchInputBoundary; -import use_case.stock_search.StockSearchRequestModel; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; -public class StockSearchController { +public class StockSearchViewModel { - private final StockSearchInputBoundary interactor; - private final StockSearchViewModel viewModel; + private final PropertyChangeSupport support = new PropertyChangeSupport(this); - public StockSearchController(StockSearchInputBoundary interactor, - StockSearchViewModel viewModel) { - this.interactor = interactor; - this.viewModel = viewModel; + private String infoText; + private String errorMessage; + private boolean loading; + + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + public void firePropertyChanged() { + support.firePropertyChange("state", null, this); + } + + public String getInfoText() { + return infoText; + } + + public void setInfoText(String infoText) { + this.infoText = infoText; + } + + public String getErrorMessage() { + return errorMessage; } - public void search(String symbol) { - viewModel.setLoading(true); - viewModel.setErrorMessage(null); - viewModel.firePropertyChanged(); + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public boolean isLoading() { + return loading; + } - new Thread(() -> { - StockSearchRequestModel request = new StockSearchRequestModel(symbol); - interactor.execute(request); - }).start(); + public void setLoading(boolean loading) { + this.loading = loading; } } From 6ef1e3409c2b5c2455b397527f1fe71fba262ae8 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:33:04 -0500 Subject: [PATCH 060/103] adding relevant components on app builder --- src/main/java/app/AppBuilder.java | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 6a9b7983..025c74cb 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -12,12 +12,16 @@ import data_access.FileUserDataAccessObject; import data_access.FilterSearchDataAccessObject; +import data_access.stock_search.FinnhubStockSearchDataAccessObject; import data_access.NewsDataAccessObject; import entity.UserFactory; import interface_adapter.ViewManagerModel; import interface_adapter.filter_search.FilterSearchController; import interface_adapter.filter_search.FilterSearchPresenter; import interface_adapter.filter_search.FilterSearchViewModel; +import interface_adapter.stock_search.StockSearchController; +import interface_adapter.stock_search.StockSearchPresenter; +import interface_adapter.stock_search.StockSearchViewModel; import interface_adapter.logged_in.ChangePasswordController; import interface_adapter.logged_in.ChangePasswordPresenter; import interface_adapter.logged_in.LoggedInViewModel; @@ -39,6 +43,10 @@ import use_case.filter_search.FilterSearchInputBoundary; import use_case.filter_search.FilterSearchInteractor; import use_case.filter_search.FilterSearchOutputBoundary; +import use_case.stock_search.StockSearchDataAccessInterface; +import use_case.stock_search.StockSearchInputBoundary; +import use_case.stock_search.StockSearchInteractor; +import use_case.stock_search.StockSearchOutputBoundary; import use_case.login.LoginInputBoundary; import use_case.login.LoginInteractor; import use_case.login.LoginOutputBoundary; @@ -84,6 +92,8 @@ public class AppBuilder { new FinnhubEarningsDataAccessObject(); final FilterSearchDataAccessInterface filterSearchDataAccessObject = new FilterSearchDataAccessObject(apiKey); + final StockSearchDataAccessInterface stockSearchDataAccessObject = + new FinnhubStockSearchDataAccessObject(apiKey); final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject(apiKey); final MarketStatusDataAccessInterface marketStatusDataAccessObject = new MarketStatusDataAccessObject(apiKey); @@ -99,6 +109,8 @@ public class AppBuilder { private LoginView loginView; private FilterSearchViewModel filterSearchViewModel; private FilterSearchView filterSearchView; + private StockSearchViewModel stockSearchViewModel; + private StockSearchController stockSearchController; private NewsViewModel newsViewModel; private NewsView newsView; private EarningsHistoryViewModel earningsHistoryViewModel; @@ -138,7 +150,7 @@ public AppBuilder addFilterSearchView() { cardPanel.add(filterSearchView, filterSearchView.getViewName()); return this; } - + public AppBuilder addNewsView() { newsViewModel = new NewsViewModel(); NewsOutputBoundary newsOutputBoundary = new NewsPresenter(newsViewModel); @@ -215,7 +227,18 @@ public AppBuilder addFilterSearchUseCase() { filterSearchView.setFilterSearchController(filterSearchController); return this; } - + public AppBuilder addStockSearchUseCase() { + stockSearchViewModel = new StockSearchViewModel(); + StockSearchOutputBoundary outputBoundary = new StockSearchPresenter(stockSearchViewModel); + StockSearchInputBoundary interactor = + new StockSearchInteractor(stockSearchDataAccessObject, outputBoundary); + + stockSearchController = new StockSearchController(interactor, stockSearchViewModel); + loggedInView.setStockSearchController(stockSearchController); + loggedInView.setStockSearchViewModel(stockSearchViewModel); + return this; + } + public AppBuilder addNewsUsecase() { // Logged-in page: News button → News view loggedInView.setNewsNavigation(viewManagerModel, newsView.getViewName()); From 286c3046419777bc08512efc50803a91cffc1300 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:33:40 -0500 Subject: [PATCH 061/103] adding relevant starting files on main --- src/main/java/app/Main.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 54eab0b3..7bea6e4b 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -16,6 +16,7 @@ public static void main(String[] args) { .addFilterSearchUseCase() .addNewsView() .addEarningsHistoryView() + .addStockSearchUseCase() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() From ee957c4985adbf2464644379803608737e9c95af Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:37:47 -0500 Subject: [PATCH 062/103] changing logged in view to show changes --- src/main/java/view/LoggedInView.java | 32 ++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index a81e8174..862c0af0 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -3,6 +3,8 @@ import interface_adapter.logged_in.ChangePasswordController; import interface_adapter.logged_in.LoggedInState; import interface_adapter.logged_in.LoggedInViewModel; +import interface_adapter.stock_search.StockSearchController; +import interface_adapter.stock_search.StockSearchViewModel; import interface_adapter.logout.LogoutController; import interface_adapter.news.NewsController; import interface_adapter.ViewManagerModel; @@ -34,6 +36,9 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private JLabel marketStatusLabel; private MarketStatusViewModel marketStatusViewModel; + private StockSearchController stockSearchController; + private StockSearchViewModel stockSearchViewModel; + private final JLabel username; private final JButton logOut; @@ -119,6 +124,12 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { this.add(centerPanel, BorderLayout.CENTER); + searchButton.addActionListener(e -> { + if (stockSearchController != null) { + stockSearchController.search(searchStockField.getText()); + } + }); + final JPanel bottomPanel = new JPanel(new BorderLayout()); bottomPanel.add(addToWatchlistButton, BorderLayout.CENTER); @@ -202,6 +213,27 @@ public void setChangePasswordController(ChangePasswordController changePasswordC this.changePasswordController = changePasswordController; } + public void setStockSearchController(StockSearchController stockSearchController) { + this.stockSearchController = stockSearchController; + } + + public void setStockSearchViewModel(StockSearchViewModel stockSearchViewModel) { + this.stockSearchViewModel = stockSearchViewModel; + this.stockSearchViewModel.addPropertyChangeListener(evt -> { + if (!"state".equals(evt.getPropertyName())) { + return; + } + StockSearchViewModel vm = (StockSearchViewModel) evt.getNewValue(); + if (vm.getErrorMessage() != null && !vm.getErrorMessage().isBlank()) { + stockInfoArea.setText(vm.getErrorMessage()); + } else if (vm.getInfoText() != null) { + stockInfoArea.setText(vm.getInfoText()); + } else { + stockInfoArea.setText(""); + } + }); + } + public void setLogoutController(LogoutController logoutController) { // TODO: save the logout controller in the instance variable. } From 45418775ba1b7e235ca6bacabfd43974759e953c Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Sun, 30 Nov 2025 23:40:44 -0500 Subject: [PATCH 063/103] adding search controller --- .../stock_search/StockSearchController.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main/java/interface_adapter/stock_search/StockSearchController.java b/src/main/java/interface_adapter/stock_search/StockSearchController.java index f27951fb..dec31ae2 100644 --- a/src/main/java/interface_adapter/stock_search/StockSearchController.java +++ b/src/main/java/interface_adapter/stock_search/StockSearchController.java @@ -1,4 +1,27 @@ package interface_adapter.stock_search; +import use_case.stock_search.StockSearchInputBoundary; +import use_case.stock_search.StockSearchRequestModel; + public class StockSearchController { + + private final StockSearchInputBoundary interactor; + private final StockSearchViewModel viewModel; + + public StockSearchController(StockSearchInputBoundary interactor, + StockSearchViewModel viewModel) { + this.interactor = interactor; + this.viewModel = viewModel; + } + + public void search(String symbol) { + viewModel.setLoading(true); + viewModel.setErrorMessage(null); + viewModel.firePropertyChanged(); + + new Thread(() -> { + StockSearchRequestModel request = new StockSearchRequestModel(symbol); + interactor.execute(request); + }).start(); + } } From b8726d3d4f420fc7664f32aae445804125195605 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Mon, 1 Dec 2025 00:58:02 -0500 Subject: [PATCH 064/103] improving searchability on filtersearch by adding symbol, name, isin, or cusip search --- .../FinnhubStockSearchDataAccessObject.java | 81 ++++++++++++++++--- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java b/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java index 70fe308b..a3143e73 100644 --- a/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java +++ b/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java @@ -1,9 +1,9 @@ package data_access.stock_search; -import data_access.*; import entity.StockQuote; -import use_case.stock_search.StockSearchDataAccessInterface; +import org.json.JSONArray; import org.json.JSONObject; +import use_case.stock_search.StockSearchDataAccessInterface; import java.io.BufferedReader; import java.io.IOException; @@ -15,7 +15,8 @@ import java.nio.charset.StandardCharsets; public class FinnhubStockSearchDataAccessObject implements StockSearchDataAccessInterface { - private static final String BASE_URL = "https://finnhub.io/api/v1/quote"; + private static final String QUOTE_URL = "https://finnhub.io/api/v1/quote"; + private static final String SEARCH_URL = "https://finnhub.io/api/v1/search"; private final String apiKey; @@ -24,19 +25,81 @@ public FinnhubStockSearchDataAccessObject(String apiKey) { } @Override - public StockQuote loadQuote(String symbol) throws Exception { - if (symbol == null || symbol.isBlank()) { - throw new IllegalArgumentException("Symbol must not be empty."); + public StockQuote loadQuote(String userQuery) throws Exception { + if (userQuery == null || userQuery.isBlank()) { + throw new IllegalArgumentException("Symbol or name must not be empty."); + } + + String query = userQuery.trim(); + + String resolvedSymbol = resolveSymbol(query); + if (resolvedSymbol == null || resolvedSymbol.isBlank()) { + throw new IllegalStateException("No matching symbol found for: " + query); } - StringBuilder urlBuilder = new StringBuilder(BASE_URL) + StringBuilder urlBuilder = new StringBuilder(QUOTE_URL) .append("?symbol=") - .append(URLEncoder.encode(symbol, StandardCharsets.UTF_8)) + .append(URLEncoder.encode(resolvedSymbol, StandardCharsets.UTF_8)) + .append("&token=") + .append(URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); + + JSONObject json = fetchJsonObject(urlBuilder.toString()); + return parseQuote(resolvedSymbol, json); + } + + /** + * Resolve an arbitrary query (symbol, name, ISIN, CUSIP) to a concrete ticker symbol + * using Finnhub's /search endpoint. + */ + private String resolveSymbol(String query) throws IOException { + StringBuilder urlBuilder = new StringBuilder(SEARCH_URL) + .append("?q=") + .append(URLEncoder.encode(query, StandardCharsets.UTF_8)) .append("&token=") .append(URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); JSONObject json = fetchJsonObject(urlBuilder.toString()); - return parseQuote(symbol, json); + if (json == null) { + return null; + } + + JSONArray results = json.optJSONArray("result"); + if (results == null || results.length() == 0) { + return null; + } + + // 1) Exact symbol match (case-insensitive) + for (int i = 0; i < results.length(); i++) { + JSONObject r = results.optJSONObject(i); + if (r == null) continue; + String sym = r.optString("symbol", ""); + if (!sym.isEmpty() && sym.equalsIgnoreCase(query)) { + return sym; + } + } + + String lowerQuery = query.toLowerCase(); + + // 2) Description contains query (case-insensitive) + for (int i = 0; i < results.length(); i++) { + JSONObject r = results.optJSONObject(i); + if (r == null) continue; + String desc = r.optString("description", ""); + if (!desc.isEmpty() && desc.toLowerCase().contains(lowerQuery)) { + String sym = r.optString("symbol", ""); + if (!sym.isEmpty()) { + return sym; + } + } + } + + // 3) Fallback to the first result's symbol + JSONObject first = results.optJSONObject(0); + if (first == null) { + return null; + } + String sym = first.optString("symbol", ""); + return sym.isEmpty() ? null : sym; } private JSONObject fetchJsonObject(String urlString) throws IOException { From bf79a5b1d76a2663d6c86102e5fa82a2f719a502 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Mon, 1 Dec 2025 01:05:13 -0500 Subject: [PATCH 065/103] adding extended version --- src/main/java/entity/StockQuote.java | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/main/java/entity/StockQuote.java b/src/main/java/entity/StockQuote.java index 1276032e..6a4f027a 100644 --- a/src/main/java/entity/StockQuote.java +++ b/src/main/java/entity/StockQuote.java @@ -2,6 +2,10 @@ public class StockQuote { private final String symbol; + private final String companyName; + private final String exchange; + private final String industry; + private final double marketCap; private final double currentPrice; private final double open; private final double high; @@ -10,6 +14,10 @@ public class StockQuote { private final long timestamp; public StockQuote(String symbol, + String companyName, + String exchange, + String industry, + double marketCap, double currentPrice, double open, double high, @@ -17,6 +25,10 @@ public StockQuote(String symbol, double previousClose, long timestamp) { this.symbol = symbol; + this.companyName = companyName; + this.exchange = exchange; + this.industry = industry; + this.marketCap = marketCap; this.currentPrice = currentPrice; this.open = open; this.high = high; @@ -29,6 +41,22 @@ public String getSymbol() { return symbol; } + public String getCompanyName() { + return companyName; + } + + public String getExchange() { + return exchange; + } + + public String getIndustry() { + return industry; + } + + public double getMarketCap() { + return marketCap; + } + public double getCurrentPrice() { return currentPrice; } From e2c6bde74732f2e8460450674dd54a4ff4317b24 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Mon, 1 Dec 2025 01:05:48 -0500 Subject: [PATCH 066/103] adding extended version in dataaccess class --- .../FinnhubStockSearchDataAccessObject.java | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java b/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java index a3143e73..940d02a1 100644 --- a/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java +++ b/src/main/java/data_access/stock_search/FinnhubStockSearchDataAccessObject.java @@ -17,6 +17,7 @@ public class FinnhubStockSearchDataAccessObject implements StockSearchDataAccessInterface { private static final String QUOTE_URL = "https://finnhub.io/api/v1/quote"; private static final String SEARCH_URL = "https://finnhub.io/api/v1/search"; + private static final String PROFILE_URL = "https://finnhub.io/api/v1/stock/profile2"; private final String apiKey; @@ -43,8 +44,18 @@ public StockQuote loadQuote(String userQuery) throws Exception { .append("&token=") .append(URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); - JSONObject json = fetchJsonObject(urlBuilder.toString()); - return parseQuote(resolvedSymbol, json); + JSONObject quoteJson = fetchJsonObject(urlBuilder.toString()); + + // Load basic profile information for extra context + StringBuilder profileUrlBuilder = new StringBuilder(PROFILE_URL) + .append("?symbol=") + .append(URLEncoder.encode(resolvedSymbol, StandardCharsets.UTF_8)) + .append("&token=") + .append(URLEncoder.encode(apiKey, StandardCharsets.UTF_8)); + + JSONObject profileJson = fetchJsonObject(profileUrlBuilder.toString()); + + return parseQuote(resolvedSymbol, quoteJson, profileJson); } /** @@ -148,18 +159,33 @@ private String readStream(InputStream stream) throws IOException { } } - private StockQuote parseQuote(String symbol, JSONObject obj) { - if (obj == null) { + private StockQuote parseQuote(String symbol, JSONObject quoteJson, JSONObject profileJson) { + if (quoteJson == null) { throw new IllegalStateException("Empty response from Finnhub quote endpoint."); } - double current = obj.optDouble("c", Double.NaN); - double open = obj.optDouble("o", Double.NaN); - double high = obj.optDouble("h", Double.NaN); - double low = obj.optDouble("l", Double.NaN); - double prevClose = obj.optDouble("pc", Double.NaN); - long t = obj.optLong("t", 0L); + double current = quoteJson.optDouble("c", Double.NaN); + double open = quoteJson.optDouble("o", Double.NaN); + double high = quoteJson.optDouble("h", Double.NaN); + double low = quoteJson.optDouble("l", Double.NaN); + double prevClose = quoteJson.optDouble("pc", Double.NaN); + long t = quoteJson.optLong("t", 0L); + + String companyName = null; + String exchange = null; + String industry = null; + double marketCap = Double.NaN; + + if (profileJson != null) { + // Fields from /stock/profile2 + companyName = profileJson.optString("name", null); + exchange = profileJson.optString("exchange", null); + // Finnhub industry field can be "finnhubIndustry" or sector-like labels + industry = profileJson.optString("finnhubIndustry", null); + marketCap = profileJson.optDouble("marketCapitalization", Double.NaN); + } - return new StockQuote(symbol, current, open, high, low, prevClose, t); + return new StockQuote(symbol, companyName, exchange, industry, marketCap, + current, open, high, low, prevClose, t); } } From 81c1a6ccb6d50e8fb5caec51e4174e435dabc9bf Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Mon, 1 Dec 2025 01:06:26 -0500 Subject: [PATCH 067/103] adding new feature in response --- .../StockSearchResponseModel.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/main/java/use_case/stock_search/StockSearchResponseModel.java b/src/main/java/use_case/stock_search/StockSearchResponseModel.java index bcc821ec..e5b5792a 100644 --- a/src/main/java/use_case/stock_search/StockSearchResponseModel.java +++ b/src/main/java/use_case/stock_search/StockSearchResponseModel.java @@ -2,6 +2,10 @@ public class StockSearchResponseModel { private final String symbol; + private final String companyName; + private final String exchange; + private final String industry; + private final double marketCap; private final double currentPrice; private final double open; private final double high; @@ -9,12 +13,20 @@ public class StockSearchResponseModel { private final double previousClose; public StockSearchResponseModel(String symbol, + String companyName, + String exchange, + String industry, + double marketCap, double currentPrice, double open, double high, double low, double previousClose) { this.symbol = symbol; + this.companyName = companyName; + this.exchange = exchange; + this.industry = industry; + this.marketCap = marketCap; this.currentPrice = currentPrice; this.open = open; this.high = high; @@ -26,6 +38,22 @@ public String getSymbol() { return symbol; } + public String getCompanyName() { + return companyName; + } + + public String getExchange() { + return exchange; + } + + public String getIndustry() { + return industry; + } + + public double getMarketCap() { + return marketCap; + } + public double getCurrentPrice() { return currentPrice; } From 21e93a33e5eb62955246121d3e965be987a1bc9f Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Mon, 1 Dec 2025 01:07:02 -0500 Subject: [PATCH 068/103] adding extended version in interactor --- .../StockSearchResponseModel.java | 116 +++++++----------- 1 file changed, 44 insertions(+), 72 deletions(-) diff --git a/src/main/java/use_case/stock_search/StockSearchResponseModel.java b/src/main/java/use_case/stock_search/StockSearchResponseModel.java index e5b5792a..e7f253be 100644 --- a/src/main/java/use_case/stock_search/StockSearchResponseModel.java +++ b/src/main/java/use_case/stock_search/StockSearchResponseModel.java @@ -1,76 +1,48 @@ package use_case.stock_search; -public class StockSearchResponseModel { - private final String symbol; - private final String companyName; - private final String exchange; - private final String industry; - private final double marketCap; - private final double currentPrice; - private final double open; - private final double high; - private final double low; - private final double previousClose; - - public StockSearchResponseModel(String symbol, - String companyName, - String exchange, - String industry, - double marketCap, - double currentPrice, - double open, - double high, - double low, - double previousClose) { - this.symbol = symbol; - this.companyName = companyName; - this.exchange = exchange; - this.industry = industry; - this.marketCap = marketCap; - this.currentPrice = currentPrice; - this.open = open; - this.high = high; - this.low = low; - this.previousClose = previousClose; - } - - public String getSymbol() { - return symbol; - } - - public String getCompanyName() { - return companyName; - } - - public String getExchange() { - return exchange; - } - - public String getIndustry() { - return industry; - } - - public double getMarketCap() { - return marketCap; - } - - public double getCurrentPrice() { - return currentPrice; - } - - public double getOpen() { - return open; - } - - public double getHigh() { - return high; - } - - public double getLow() { - return low; - } - - public double getPreviousClose() { - return previousClose; +import entity.StockQuote; + +public class StockSearchInteractor implements StockSearchInputBoundary { + + private final StockSearchDataAccessInterface dataAccess; + private final StockSearchOutputBoundary presenter; + + public StockSearchInteractor(StockSearchDataAccessInterface dataAccess, + StockSearchOutputBoundary presenter) { + this.dataAccess = dataAccess; + this.presenter = presenter; + } + + @Override + public void execute(StockSearchRequestModel requestModel) { + String symbol = requestModel.getSymbol(); + + if (symbol == null || symbol.trim().isEmpty()) { + presenter.prepareFailView("Please enter a symbol."); + return; + } + + symbol = symbol.trim().toUpperCase(); + + try { + StockQuote quote = dataAccess.loadQuote(symbol); + + StockSearchResponseModel responseModel = new StockSearchResponseModel( + quote.getSymbol(), + quote.getCompanyName(), + quote.getExchange(), + quote.getIndustry(), + quote.getMarketCap(), + quote.getCurrentPrice(), + quote.getOpen(), + quote.getHigh(), + quote.getLow(), + quote.getPreviousClose() + ); + presenter.prepareSuccessView(responseModel); + } + catch (Exception e) { + presenter.prepareFailView("Unable to load quote. " + e.getMessage()); + } } } From 6d289000f7a1497e2d5705d3dd7dcf54ac310e33 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Mon, 1 Dec 2025 01:07:37 -0500 Subject: [PATCH 069/103] adding new feature in presentor --- .../stock_search/StockSearchPresenter.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java b/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java index 628e162e..275c2b73 100644 --- a/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java +++ b/src/main/java/interface_adapter/stock_search/StockSearchPresenter.java @@ -14,7 +14,34 @@ public StockSearchPresenter(StockSearchViewModel viewModel) { @Override public void prepareSuccessView(StockSearchResponseModel responseModel) { StringBuilder sb = new StringBuilder(); - sb.append(responseModel.getSymbol()).append("\n"); + String symbol = responseModel.getSymbol(); + String name = responseModel.getCompanyName(); + String exchange = responseModel.getExchange(); + String industry = responseModel.getIndustry(); + double marketCap = responseModel.getMarketCap(); + + if (name != null && !name.isBlank()) { + sb.append(symbol != null ? symbol : ""); + sb.append(" - "); + sb.append(name); + } else { + sb.append(symbol != null ? symbol : ""); + } + + if (exchange != null && !exchange.isBlank()) { + sb.append(" (").append(exchange).append(")"); + } + sb.append("\n"); + + if (industry != null && !industry.isBlank()) { + sb.append("Industry: ").append(industry).append("\n"); + } + + if (!Double.isNaN(marketCap) && marketCap > 0) { + sb.append("Market Cap: ").append(marketCap).append(" B").append("\n"); + } + + sb.append("\n"); sb.append("Current: ").append(responseModel.getCurrentPrice()).append("\n"); sb.append("Open: ").append(responseModel.getOpen()).append("\n"); sb.append("High: ").append(responseModel.getHigh()).append("\n"); From 4b1a345f626ee03308aa28145d06569d376997ae Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Mon, 1 Dec 2025 01:12:56 -0500 Subject: [PATCH 070/103] fixing bugs --- .../stock_search/StockSearchInteractor.java | 4 + .../StockSearchResponseModel.java | 116 +++++++++++------- 2 files changed, 76 insertions(+), 44 deletions(-) diff --git a/src/main/java/use_case/stock_search/StockSearchInteractor.java b/src/main/java/use_case/stock_search/StockSearchInteractor.java index 73b3d4f1..e7f253be 100644 --- a/src/main/java/use_case/stock_search/StockSearchInteractor.java +++ b/src/main/java/use_case/stock_search/StockSearchInteractor.java @@ -29,6 +29,10 @@ public void execute(StockSearchRequestModel requestModel) { StockSearchResponseModel responseModel = new StockSearchResponseModel( quote.getSymbol(), + quote.getCompanyName(), + quote.getExchange(), + quote.getIndustry(), + quote.getMarketCap(), quote.getCurrentPrice(), quote.getOpen(), quote.getHigh(), diff --git a/src/main/java/use_case/stock_search/StockSearchResponseModel.java b/src/main/java/use_case/stock_search/StockSearchResponseModel.java index e7f253be..e5b5792a 100644 --- a/src/main/java/use_case/stock_search/StockSearchResponseModel.java +++ b/src/main/java/use_case/stock_search/StockSearchResponseModel.java @@ -1,48 +1,76 @@ package use_case.stock_search; -import entity.StockQuote; - -public class StockSearchInteractor implements StockSearchInputBoundary { - - private final StockSearchDataAccessInterface dataAccess; - private final StockSearchOutputBoundary presenter; - - public StockSearchInteractor(StockSearchDataAccessInterface dataAccess, - StockSearchOutputBoundary presenter) { - this.dataAccess = dataAccess; - this.presenter = presenter; - } - - @Override - public void execute(StockSearchRequestModel requestModel) { - String symbol = requestModel.getSymbol(); - - if (symbol == null || symbol.trim().isEmpty()) { - presenter.prepareFailView("Please enter a symbol."); - return; - } - - symbol = symbol.trim().toUpperCase(); - - try { - StockQuote quote = dataAccess.loadQuote(symbol); - - StockSearchResponseModel responseModel = new StockSearchResponseModel( - quote.getSymbol(), - quote.getCompanyName(), - quote.getExchange(), - quote.getIndustry(), - quote.getMarketCap(), - quote.getCurrentPrice(), - quote.getOpen(), - quote.getHigh(), - quote.getLow(), - quote.getPreviousClose() - ); - presenter.prepareSuccessView(responseModel); - } - catch (Exception e) { - presenter.prepareFailView("Unable to load quote. " + e.getMessage()); - } +public class StockSearchResponseModel { + private final String symbol; + private final String companyName; + private final String exchange; + private final String industry; + private final double marketCap; + private final double currentPrice; + private final double open; + private final double high; + private final double low; + private final double previousClose; + + public StockSearchResponseModel(String symbol, + String companyName, + String exchange, + String industry, + double marketCap, + double currentPrice, + double open, + double high, + double low, + double previousClose) { + this.symbol = symbol; + this.companyName = companyName; + this.exchange = exchange; + this.industry = industry; + this.marketCap = marketCap; + this.currentPrice = currentPrice; + this.open = open; + this.high = high; + this.low = low; + this.previousClose = previousClose; + } + + public String getSymbol() { + return symbol; + } + + public String getCompanyName() { + return companyName; + } + + public String getExchange() { + return exchange; + } + + public String getIndustry() { + return industry; + } + + public double getMarketCap() { + return marketCap; + } + + public double getCurrentPrice() { + return currentPrice; + } + + public double getOpen() { + return open; + } + + public double getHigh() { + return high; + } + + public double getLow() { + return low; + } + + public double getPreviousClose() { + return previousClose; } } From 0da65da0e624acc4ae859c20575fa06760c50b03 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Mon, 1 Dec 2025 01:34:22 -0500 Subject: [PATCH 071/103] adding test case --- .../StockSearchInteractorTest.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/test/java/use_case/stock_search/StockSearchInteractorTest.java diff --git a/src/test/java/use_case/stock_search/StockSearchInteractorTest.java b/src/test/java/use_case/stock_search/StockSearchInteractorTest.java new file mode 100644 index 00000000..127a3152 --- /dev/null +++ b/src/test/java/use_case/stock_search/StockSearchInteractorTest.java @@ -0,0 +1,134 @@ +package use_case.stock_search; + +import entity.StockQuote; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class StockSearchInteractorTest { + + /** + * Simple in-memory stub for StockSearchDataAccessInterface. + */ + private static class InMemoryStockSearchDao implements StockSearchDataAccessInterface { + + StockQuote quoteToReturn; + boolean throwOnLoad = false; + String lastSymbol; + + @Override + public StockQuote loadQuote(String symbol) throws Exception { + lastSymbol = symbol; + if (throwOnLoad) { + throw new Exception("Load error"); + } + return quoteToReturn; + } + } + + /** + * Recording presenter that captures the last success or error. + */ + private static class RecordingPresenter implements StockSearchOutputBoundary { + + StockSearchResponseModel lastSuccess; + String lastError; + + @Override + public void prepareSuccessView(StockSearchResponseModel responseModel) { + lastSuccess = responseModel; + } + + @Override + public void prepareFailView(String errorMessage) { + lastError = errorMessage; + } + } + + private StockQuote sampleQuote(String symbol) { + return new StockQuote( + symbol, + "Sample Company", + "NASDAQ", + "Technology", + 1000.0, + 150.0, + 148.0, + 151.0, + 147.5, + 149.0, + 1_700_000_000L + ); + } + + @Test + void execute_success_loadsQuoteAndCallsPresenter() throws Exception { + // Arrange + InMemoryStockSearchDao dao = new InMemoryStockSearchDao(); + RecordingPresenter presenter = new RecordingPresenter(); + dao.quoteToReturn = sampleQuote("AAPL"); + + StockSearchInteractor interactor = new StockSearchInteractor(dao, presenter); + + StockSearchRequestModel request = new StockSearchRequestModel("aapl"); + + // Act + interactor.execute(request); + + // Assert DAO was called with trimmed/uppercased symbol + assertEquals("AAPL", dao.lastSymbol); + + // Assert presenter received success + assertNotNull(presenter.lastSuccess); + assertNull(presenter.lastError); + + StockSearchResponseModel response = presenter.lastSuccess; + assertEquals("AAPL", response.getSymbol()); + assertEquals("Sample Company", response.getCompanyName()); + assertEquals("NASDAQ", response.getExchange()); + assertEquals("Technology", response.getIndustry()); + assertEquals(1000.0, response.getMarketCap()); + assertEquals(150.0, response.getCurrentPrice()); + } + + @Test + void execute_blankSymbol_reportsErrorAndDoesNotCallDao() throws Exception { + // Arrange + InMemoryStockSearchDao dao = new InMemoryStockSearchDao(); + RecordingPresenter presenter = new RecordingPresenter(); + + StockSearchInteractor interactor = new StockSearchInteractor(dao, presenter); + + StockSearchRequestModel request = new StockSearchRequestModel(" "); + + // Act + interactor.execute(request); + + // Assert: DAO should not be called + assertNull(dao.lastSymbol); + + // Assert: error message from presenter + assertNull(presenter.lastSuccess); + assertEquals("Please enter a symbol.", presenter.lastError); + } + + @Test + void execute_daoThrowsException_reportsError() throws Exception { + // Arrange + InMemoryStockSearchDao dao = new InMemoryStockSearchDao(); + RecordingPresenter presenter = new RecordingPresenter(); + dao.throwOnLoad = true; + + StockSearchInteractor interactor = new StockSearchInteractor(dao, presenter); + + StockSearchRequestModel request = new StockSearchRequestModel("AAPL"); + + // Act + interactor.execute(request); + + // Assert: presenter should receive an error message + assertNull(presenter.lastSuccess); + assertNotNull(presenter.lastError); + assertTrue(presenter.lastError.startsWith("Unable to load quote.")); + } +} From 90f60149e4f54255b2424db30b3b87d5215bde43 Mon Sep 17 00:00:00 2001 From: zoe Date: Mon, 1 Dec 2025 08:40:24 -0500 Subject: [PATCH 072/103] Created watchlist use case --- src/main/java/app/AppBuilder.java | 2 -- .../data_access/FileUserDataAccessObject.java | 3 +- src/main/java/entity/User.java | 7 ++++ .../watchlist/WatchlistInputBoundary.java | 8 +++++ .../watchlist/WatchlistInputData.java | 19 +++++++++++ .../watchlist/WatchlistInteractor.java | 34 +++++++++++++++++++ .../watchlist/WatchlistOutputBoundary.java | 8 +++++ .../watchlist/WatchlistOutputData.java | 15 ++++++++ .../WatchlistUserDataAccessInterface.java | 11 ++++++ src/main/java/view/LoggedInView.java | 2 -- 10 files changed, 104 insertions(+), 5 deletions(-) create mode 100644 src/main/java/use_case/watchlist/WatchlistInputBoundary.java create mode 100644 src/main/java/use_case/watchlist/WatchlistInputData.java create mode 100644 src/main/java/use_case/watchlist/WatchlistInteractor.java create mode 100644 src/main/java/use_case/watchlist/WatchlistOutputBoundary.java create mode 100644 src/main/java/use_case/watchlist/WatchlistOutputData.java create mode 100644 src/main/java/use_case/watchlist/WatchlistUserDataAccessInterface.java diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 7e7a855d..6bfed77d 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -87,8 +87,6 @@ public class AppBuilder { final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject(apiKey); final MarketStatusDataAccessInterface marketStatusDataAccessObject = new MarketStatusDataAccessObject(apiKey); - final FilterSearchDataAccessObject filterSearchDataAccessObject = - new FilterSearchDataAccessObject(apiKey); // DAO version using a shared external database // final DBUserDataAccessObject userDataAccessObject = new DBUserDataAccessObject(userFactory); diff --git a/src/main/java/data_access/FileUserDataAccessObject.java b/src/main/java/data_access/FileUserDataAccessObject.java index 4cae88c5..c90e0b80 100644 --- a/src/main/java/data_access/FileUserDataAccessObject.java +++ b/src/main/java/data_access/FileUserDataAccessObject.java @@ -6,6 +6,7 @@ import use_case.login.LoginUserDataAccessInterface; import use_case.logout.LogoutUserDataAccessInterface; import use_case.signup.SignupUserDataAccessInterface; +import use_case.watchlist.WatchlistUserDataAccessInterface; import java.io.*; import java.util.HashMap; @@ -18,7 +19,7 @@ public class FileUserDataAccessObject implements SignupUserDataAccessInterface, LoginUserDataAccessInterface, ChangePasswordUserDataAccessInterface, - LogoutUserDataAccessInterface { + LogoutUserDataAccessInterface, WatchlistUserDataAccessInterface { private static final String HEADER = "username,password"; diff --git a/src/main/java/entity/User.java b/src/main/java/entity/User.java index bf9783d0..49257ddb 100644 --- a/src/main/java/entity/User.java +++ b/src/main/java/entity/User.java @@ -1,5 +1,9 @@ package entity; +import org.json.JSONObject; + +import java.util.ArrayList; + /** * A simple entity representing a user. Users have a username and password.. */ @@ -7,6 +11,7 @@ public class User { private final String name; private final String password; + private final ArrayList watchlist = new ArrayList<>(); /** * Creates a new user with the given non-empty name and non-empty password. @@ -33,4 +38,6 @@ public String getPassword() { return password; } + public ArrayList getWatchlist() { return watchlist; } + } diff --git a/src/main/java/use_case/watchlist/WatchlistInputBoundary.java b/src/main/java/use_case/watchlist/WatchlistInputBoundary.java new file mode 100644 index 00000000..1e452f5c --- /dev/null +++ b/src/main/java/use_case/watchlist/WatchlistInputBoundary.java @@ -0,0 +1,8 @@ +package use_case.watchlist; + +public interface WatchlistInputBoundary { + /* + * Executes the Watchlist Use Case. + */ + void execute(WatchlistInputData watchlistInputData); +} diff --git a/src/main/java/use_case/watchlist/WatchlistInputData.java b/src/main/java/use_case/watchlist/WatchlistInputData.java new file mode 100644 index 00000000..36c5070a --- /dev/null +++ b/src/main/java/use_case/watchlist/WatchlistInputData.java @@ -0,0 +1,19 @@ +package use_case.watchlist; + +import org.json.JSONObject; + +public class WatchlistInputData { + + private final String username; + private final JSONObject watchlistInputData; + + public WatchlistInputData(String username, JSONObject watchlistInputData) { + this.username = username; + this.watchlistInputData = watchlistInputData; + } + public JSONObject getWatchlistInputData() { + return watchlistInputData; + } + public String getUsername() { + return username; } +} diff --git a/src/main/java/use_case/watchlist/WatchlistInteractor.java b/src/main/java/use_case/watchlist/WatchlistInteractor.java new file mode 100644 index 00000000..197352b9 --- /dev/null +++ b/src/main/java/use_case/watchlist/WatchlistInteractor.java @@ -0,0 +1,34 @@ +package use_case.watchlist; + + +import entity.User; + +public class WatchlistInteractor implements WatchlistInputBoundary { + private final WatchlistUserDataAccessInterface wlUserDataAccessObject; + private final WatchlistOutputBoundary watchlistPresenter; + + public WatchlistInteractor(WatchlistUserDataAccessInterface wlUserDataAccessInterface, + WatchlistOutputBoundary watchlistOutputBoundary) { + this.wlUserDataAccessObject = wlUserDataAccessInterface; + this.watchlistPresenter = watchlistOutputBoundary; + } + + public void execute(WatchlistInputData watchlistInputData) { + String username = watchlistInputData.getUsername(); + + if (!wlUserDataAccessObject.existsByName(username)) { + watchlistPresenter.prepareFailView("User does not exist."); + return; + } + User user = wlUserDataAccessObject.get(username); + + user.getWatchlist().add(watchlistInputData.getWatchlistInputData()); + + wlUserDataAccessObject.save(user); + + watchlistPresenter.prepareSuccessView( + new WatchlistOutputData(user.getWatchlist()) + ); + } + +} diff --git a/src/main/java/use_case/watchlist/WatchlistOutputBoundary.java b/src/main/java/use_case/watchlist/WatchlistOutputBoundary.java new file mode 100644 index 00000000..3708914a --- /dev/null +++ b/src/main/java/use_case/watchlist/WatchlistOutputBoundary.java @@ -0,0 +1,8 @@ +package use_case.watchlist; + +public interface WatchlistOutputBoundary { + + void prepareSuccessView(WatchlistOutputData outputData); + + void prepareFailView(String errorMessage); +} diff --git a/src/main/java/use_case/watchlist/WatchlistOutputData.java b/src/main/java/use_case/watchlist/WatchlistOutputData.java new file mode 100644 index 00000000..ac38c497 --- /dev/null +++ b/src/main/java/use_case/watchlist/WatchlistOutputData.java @@ -0,0 +1,15 @@ +package use_case.watchlist; + +import org.json.JSONObject; + +import java.util.ArrayList; + +public class WatchlistOutputData { + private final ArrayList watchlist; + public WatchlistOutputData(ArrayList watchlist) { + this.watchlist = watchlist; + } + public ArrayList getWatchlist() { + return watchlist; + } +} diff --git a/src/main/java/use_case/watchlist/WatchlistUserDataAccessInterface.java b/src/main/java/use_case/watchlist/WatchlistUserDataAccessInterface.java new file mode 100644 index 00000000..ad5a8e80 --- /dev/null +++ b/src/main/java/use_case/watchlist/WatchlistUserDataAccessInterface.java @@ -0,0 +1,11 @@ +package use_case.watchlist; + +import entity.User; + +public interface WatchlistUserDataAccessInterface { + User get(String username); + + void save(User user); + + boolean existsByName(String username); +} diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 4971042b..b6b635d6 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -46,7 +46,6 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JButton searchButton = new JButton("Search"); private final JTextArea stockInfoArea = new JTextArea(); private final JButton addToWatchlistButton = new JButton("Add to Watchlist"); - private final JButton historyButton; public LoggedInView(LoggedInViewModel loggedInViewModel) { this.loggedInViewModel = loggedInViewModel; @@ -83,7 +82,6 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { final JButton historyButton = new JButton("History"); - historyButton = new JButton("History"); historyButton.addActionListener(e -> { if (viewManagerModel != null && historyViewName != null) { viewManagerModel.setState(historyViewName); From 8d1220f23e0e54d60e3b9660a7cd76fb9324f6ca Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Mon, 1 Dec 2025 11:53:21 -0500 Subject: [PATCH 073/103] changecomment --- .../use_case/earnings_history/GetEarningsHistoryInteractor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/use_case/earnings_history/GetEarningsHistoryInteractor.java b/src/main/java/use_case/earnings_history/GetEarningsHistoryInteractor.java index 1242a8a3..37db09e3 100644 --- a/src/main/java/use_case/earnings_history/GetEarningsHistoryInteractor.java +++ b/src/main/java/use_case/earnings_history/GetEarningsHistoryInteractor.java @@ -28,7 +28,7 @@ public void execute(GetEarningsHistoryInputData inputData) { symbol = symbol.trim().toUpperCase(); try { - // IMPORTANT: method name matches the interface + // method name matches the interface List records = dataAccess.getEarningsFor(symbol); if (records == null) { From df6baa1b0bcaaa1497aff3151f9c6440a9dbb76a Mon Sep 17 00:00:00 2001 From: samario Date: Mon, 1 Dec 2025 12:58:48 -0500 Subject: [PATCH 074/103] Delete repeat initialization of History button --- src/main/java/view/LoggedInView.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 4971042b..b6b635d6 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -46,7 +46,6 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JButton searchButton = new JButton("Search"); private final JTextArea stockInfoArea = new JTextArea(); private final JButton addToWatchlistButton = new JButton("Add to Watchlist"); - private final JButton historyButton; public LoggedInView(LoggedInViewModel loggedInViewModel) { this.loggedInViewModel = loggedInViewModel; @@ -83,7 +82,6 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { final JButton historyButton = new JButton("History"); - historyButton = new JButton("History"); historyButton.addActionListener(e -> { if (viewManagerModel != null && historyViewName != null) { viewManagerModel.setState(historyViewName); From 5f62ec44229d84c07faf0c937ae8bf07eeb3205c Mon Sep 17 00:00:00 2001 From: samario Date: Mon, 1 Dec 2025 13:13:58 -0500 Subject: [PATCH 075/103] Delete the strange introduction of filterSearchDAO which has already been introduced once above --- src/main/java/app/AppBuilder.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 7e7a855d..6bfed77d 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -87,8 +87,6 @@ public class AppBuilder { final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject(apiKey); final MarketStatusDataAccessInterface marketStatusDataAccessObject = new MarketStatusDataAccessObject(apiKey); - final FilterSearchDataAccessObject filterSearchDataAccessObject = - new FilterSearchDataAccessObject(apiKey); // DAO version using a shared external database // final DBUserDataAccessObject userDataAccessObject = new DBUserDataAccessObject(userFactory); From 4f7554687443508385e18f844f44af33a3bca7d7 Mon Sep 17 00:00:00 2001 From: zoe Date: Mon, 1 Dec 2025 13:15:44 -0500 Subject: [PATCH 076/103] made account --- src/main/java/app/AppBuilder.java | 39 ++++++++ src/main/java/app/Main.java | 1 + .../account/AccountController.java | 24 +++++ .../account/AccountPresenter.java | 29 ++++++ .../account/AccountState.java | 30 ++++++ .../account/AccountViewModel.java | 32 ++++++ src/main/java/view/AccountView.java | 99 +++++++++++++++++++ src/main/java/view/LoggedInView.java | 25 +++++ 8 files changed, 279 insertions(+) create mode 100644 src/main/java/interface_adapter/account/AccountController.java create mode 100644 src/main/java/interface_adapter/account/AccountPresenter.java create mode 100644 src/main/java/interface_adapter/account/AccountState.java create mode 100644 src/main/java/interface_adapter/account/AccountViewModel.java create mode 100644 src/main/java/view/AccountView.java diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 6bfed77d..42f526aa 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -1,6 +1,9 @@ package app; import data_access.FinnhubEarningsDataAccessObject; +import interface_adapter.account.AccountController; +import interface_adapter.account.AccountPresenter; +import interface_adapter.account.AccountViewModel; import interface_adapter.earnings_history.EarningsHistoryController; import interface_adapter.earnings_history.EarningsHistoryPresenter; import interface_adapter.earnings_history.EarningsHistoryViewModel; @@ -8,6 +11,10 @@ import use_case.earnings_history.GetEarningsHistoryInputBoundary; import use_case.earnings_history.GetEarningsHistoryInteractor; import use_case.earnings_history.GetEarningsHistoryOutputBoundary; +import use_case.watchlist.WatchlistInputBoundary; +import use_case.watchlist.WatchlistInteractor; +import use_case.watchlist.WatchlistOutputBoundary; +import use_case.watchlist.WatchlistUserDataAccessInterface; import view.EarningsHistoryView; import data_access.FileUserDataAccessObject; @@ -82,6 +89,7 @@ public class AppBuilder { // DAO for earnings history final EarningsDataAccessInterface earningsDataAccessObject = new FinnhubEarningsDataAccessObject(); + final WatchlistUserDataAccessInterface watchlistDataAccessObject = userDataAccessObject; final FilterSearchDataAccessInterface filterSearchDataAccessObject = new FilterSearchDataAccessObject(apiKey); final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject(apiKey); @@ -103,6 +111,8 @@ public class AppBuilder { private NewsView newsView; private EarningsHistoryViewModel earningsHistoryViewModel; private EarningsHistoryView earningsHistoryView; + private AccountView accountView; + private AccountViewModel accountViewModel; private MarketStatusViewModel marketStatusViewModel; private MarketStatusController marketStatusController; @@ -240,6 +250,35 @@ public AppBuilder addEarningsHistoryUseCase() { return this; } + public AppBuilder addAccount() { + accountViewModel = new AccountViewModel(); + WatchlistOutputBoundary watchlistPresenter = + new AccountPresenter(accountViewModel); + WatchlistInputBoundary watchlistInteractor = + new WatchlistInteractor(watchlistDataAccessObject, watchlistPresenter); + AccountController accountController = new AccountController(watchlistInteractor); + + accountView = new AccountView(accountViewModel); + accountView.setController(accountController); + cardPanel.add(accountView, accountView.getViewName()); + + // Logged in page: Account button → Account view + loggedInView.setAccountNavigation( + viewManagerModel, accountView.getViewName()); + viewManagerModel.addPropertyChangeListener(evt -> { + if (viewManagerModel.getState().equals(accountView.getViewName())) { + String username = userDataAccessObject.getCurrentUsername(); + accountView.loadAccount(username); + } + }); + + // Account page: Back button → Logged-in view + accountView.setBackNavigation( + viewManagerModel, loggedInView.getViewName()); + + return this; + } + /** * Adds the Logout Use Case to the application. * @return this builder diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 54eab0b3..819d363c 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -21,6 +21,7 @@ public static void main(String[] args) { .addChangePasswordUseCase() .addNewsUsecase() .addEarningsHistoryUseCase() + .addAccount() .addMarketStatusUseCase() .build(); diff --git a/src/main/java/interface_adapter/account/AccountController.java b/src/main/java/interface_adapter/account/AccountController.java new file mode 100644 index 00000000..5e5389ba --- /dev/null +++ b/src/main/java/interface_adapter/account/AccountController.java @@ -0,0 +1,24 @@ +package interface_adapter.account; + +import org.json.JSONObject; +import use_case.watchlist.WatchlistInputBoundary; +import use_case.watchlist.WatchlistInputData; + +public class AccountController { + + private final WatchlistInputBoundary interactor; + + public AccountController(WatchlistInputBoundary watchlistInputBoundary) { + this.interactor = watchlistInputBoundary; + } + + public void addToWatchlist(String username, JSONObject watchlistInputData) { + WatchlistInputData inputData = new WatchlistInputData(username, watchlistInputData); + interactor.execute(inputData); + } + + public void loadWatchlist(String username) { + WatchlistInputData inputData = new WatchlistInputData(username, null); + interactor.execute(inputData); + } +} diff --git a/src/main/java/interface_adapter/account/AccountPresenter.java b/src/main/java/interface_adapter/account/AccountPresenter.java new file mode 100644 index 00000000..708da4e1 --- /dev/null +++ b/src/main/java/interface_adapter/account/AccountPresenter.java @@ -0,0 +1,29 @@ +package interface_adapter.account; + +import use_case.watchlist.WatchlistOutputBoundary; +import use_case.watchlist.WatchlistOutputData; + +import java.util.ArrayList; + +public class AccountPresenter implements WatchlistOutputBoundary { + + private final AccountViewModel viewModel; + + public AccountPresenter(AccountViewModel viewModel) { + this.viewModel = viewModel; + } + + public void prepareSuccessView(WatchlistOutputData watchlistOutputData) { + AccountState state = viewModel.getState(); + state.setWatchlist(watchlistOutputData.getWatchlist()); + viewModel.setState(state); + viewModel.firePropertyChange(); + } + + public void prepareFailView(String errorMessage) { + viewModel.getState().setWatchlist(new ArrayList<>()); + viewModel.firePropertyChange(); + } + +} + diff --git a/src/main/java/interface_adapter/account/AccountState.java b/src/main/java/interface_adapter/account/AccountState.java new file mode 100644 index 00000000..61b7cb44 --- /dev/null +++ b/src/main/java/interface_adapter/account/AccountState.java @@ -0,0 +1,30 @@ +package interface_adapter.account; + +import org.json.JSONObject; + +import java.util.ArrayList; + +public class AccountState { + private String username; + private ArrayList watchlist = new ArrayList<>(); + + public ArrayList getWatchlist() { + if (watchlist == null) { + watchlist = new ArrayList<>(); + } + return watchlist; + } + + public void setWatchlist(ArrayList watchlist) { + this.watchlist = watchlist; + } + + public void addToWatchlist(JSONObject watchlistInputData) { + this.watchlist.add(watchlistInputData); + } + + public String getUsername() { return username; } + + public void setUsername(String username) { this.username = username; } + +} diff --git a/src/main/java/interface_adapter/account/AccountViewModel.java b/src/main/java/interface_adapter/account/AccountViewModel.java new file mode 100644 index 00000000..0ba98978 --- /dev/null +++ b/src/main/java/interface_adapter/account/AccountViewModel.java @@ -0,0 +1,32 @@ +package interface_adapter.account; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + +public class AccountViewModel { + private AccountState state = new AccountState(); + private final PropertyChangeSupport support = new PropertyChangeSupport(this); + + public AccountState getState() { + return state; + } + + public void setState(AccountState newState) { + this.state = newState; + support.firePropertyChange("state", null, newState); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + support.removePropertyChangeListener(listener); + } + + public void firePropertyChange() { + support.firePropertyChange("watchlist", null, state.getWatchlist()); + } + + +} diff --git a/src/main/java/view/AccountView.java b/src/main/java/view/AccountView.java new file mode 100644 index 00000000..19234497 --- /dev/null +++ b/src/main/java/view/AccountView.java @@ -0,0 +1,99 @@ +package view; + +import interface_adapter.ViewManagerModel; +import interface_adapter.account.AccountController; +import interface_adapter.account.AccountViewModel; +import org.json.JSONObject; + +import javax.swing.*; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.ArrayList; + +public class AccountView extends JPanel implements PropertyChangeListener { + private AccountController accountController; + private ViewManagerModel viewManagerModel; + private String homeViewName; + private final String viewName = "account"; + private final AccountViewModel viewModel; + private final JPanel watchlistPanel; + private final JScrollPane scrollPane; + private final JLabel usernameLabel; + private final JButton backButton; + + public AccountView(AccountViewModel accountViewModel) { + this.viewModel = accountViewModel; + this.viewModel.addPropertyChangeListener(this); + + setLayout(new BorderLayout()); + + JPanel topPanel = new JPanel(new BorderLayout()); + backButton = new JButton("Back"); + topPanel.add(backButton, BorderLayout.EAST); + + backButton.addActionListener(e -> { + if (viewManagerModel != null && homeViewName != null) { + viewManagerModel.setState(homeViewName); + viewManagerModel.firePropertyChange(); + } + }); + + usernameLabel = new JLabel("Username: "); + usernameLabel.setHorizontalAlignment(SwingConstants.CENTER); + topPanel.add(usernameLabel, BorderLayout.CENTER); + + add(topPanel, BorderLayout.NORTH); + + watchlistPanel = new JPanel(); + watchlistPanel.setLayout(new BoxLayout(watchlistPanel, BoxLayout.Y_AXIS)); + scrollPane = new JScrollPane(watchlistPanel); + scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + + add(scrollPane, BorderLayout.CENTER); + } + + public void propertyChange(PropertyChangeEvent evt) { + String username = viewModel.getState().getUsername(); + usernameLabel.setText("Username: " + username); + + watchlistPanel.removeAll(); + ArrayList watchlist = viewModel.getState().getWatchlist(); + if (watchlist == null) { + watchlist = new ArrayList<>(); + for (JSONObject item : watchlist) { + JPanel itemPanel = new JPanel(); + itemPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); + itemPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY)); + + JLabel stockLabel = new JLabel(item.getString("stock")); + itemPanel.add(stockLabel); + + watchlistPanel.add(itemPanel); + } + } + + watchlistPanel.revalidate(); + watchlistPanel.repaint(); + } + + public String getViewName() { + return viewName; + } + + public void loadAccount(String username) { + if (accountController != null) { + accountController.loadWatchlist(username); + } + } + + public void setBackNavigation(ViewManagerModel viewManagerModel, String homeViewName) { + this.viewManagerModel = viewManagerModel; + this.homeViewName = homeViewName; + } + + public void setController(AccountController accountController) { + this.accountController = accountController; + } + +} diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index b6b635d6..35bf5939 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -1,5 +1,6 @@ package view; +import interface_adapter.account.AccountController; import interface_adapter.logged_in.ChangePasswordController; import interface_adapter.logged_in.LoggedInState; import interface_adapter.logged_in.LoggedInViewModel; @@ -28,10 +29,12 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private ChangePasswordController changePasswordController = null; private LogoutController logoutController; private NewsController newsController; + private AccountController accountController; private ViewManagerModel viewManagerModel; private String newsViewName; private String filterSearchViewName; private String historyViewName; + private String accountViewName; private JLabel marketStatusLabel; private MarketStatusViewModel marketStatusViewModel; @@ -89,7 +92,15 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { } }); final JButton marketOpenButton = new JButton("Market Open"); + final JButton accountButton = new JButton("Account"); + accountButton.addActionListener(e -> { + System.out.println("Account button clicked"); // debug + if (viewManagerModel != null && newsViewName != null) { + viewManagerModel.setState(accountViewName); + viewManagerModel.firePropertyChange(); + } + }); topToolbar.add(newsButton); topToolbar.add(filterSearchButton); @@ -131,6 +142,14 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { final JPanel bottomPanel = new JPanel(new BorderLayout()); bottomPanel.add(addToWatchlistButton, BorderLayout.CENTER); +// addToWatchlistButton.addActionListener(e -> { +// if (accountController != null) { +// +// JSONObject newItem = ...; // get stock info from somewhere +// accountController.addToWatchlist(username, newItem); +// } +// }); + this.add(bottomPanel, BorderLayout.SOUTH); passwordInputField.getDocument().addDocumentListener(new DocumentListener() { @@ -231,6 +250,12 @@ public void setHistoryNavigation(ViewManagerModel viewManagerModel, this.historyViewName = historyViewName; } + public void setAccountNavigation(ViewManagerModel viewManagerModel, + String accountViewName) { + this.viewManagerModel = viewManagerModel; + this.accountViewName = accountViewName; + } + public void setMarketStatusViewModel(MarketStatusViewModel viewModel) { From 443b84f96e3e0bcaa69051539287a957d56b5ffc Mon Sep 17 00:00:00 2001 From: zoe Date: Mon, 1 Dec 2025 13:23:21 -0500 Subject: [PATCH 077/103] made account --- src/main/java/view/AccountView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/view/AccountView.java b/src/main/java/view/AccountView.java index 19234497..981fc51a 100644 --- a/src/main/java/view/AccountView.java +++ b/src/main/java/view/AccountView.java @@ -30,7 +30,7 @@ public AccountView(AccountViewModel accountViewModel) { JPanel topPanel = new JPanel(new BorderLayout()); backButton = new JButton("Back"); - topPanel.add(backButton, BorderLayout.EAST); + topPanel.add(backButton, BorderLayout.WEST); backButton.addActionListener(e -> { if (viewManagerModel != null && homeViewName != null) { From 77efb4bc26b0d126cc6261cce0713c0025803198 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Mon, 1 Dec 2025 13:32:05 -0500 Subject: [PATCH 078/103] Real time search function implemented --- src/main/java/app/AppBuilder.java | 27 +++++++++++++++++++++++++-- src/main/java/app/Main.java | 2 ++ src/main/java/view/LoggedInView.java | 26 ++++++++++++++++++++++---- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 7e7a855d..9ac7f3c2 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -59,6 +59,8 @@ import use_case.market_status.MarketStatusOutputBoundary; import use_case.market_status.MarketStatusDataAccessInterface; import data_access.MarketStatusDataAccessObject; +import data_access.FinnhubTradeDataAccessObject; +import use_case.TradeFeed; import view.*; import javax.swing.*; @@ -87,8 +89,7 @@ public class AppBuilder { final NewsDataAccessObject newsDataAccessObject = new NewsDataAccessObject(apiKey); final MarketStatusDataAccessInterface marketStatusDataAccessObject = new MarketStatusDataAccessObject(apiKey); - final FilterSearchDataAccessObject filterSearchDataAccessObject = - new FilterSearchDataAccessObject(apiKey); + // DAO version using a shared external database // final DBUserDataAccessObject userDataAccessObject = new DBUserDataAccessObject(userFactory); @@ -105,6 +106,7 @@ public class AppBuilder { private NewsView newsView; private EarningsHistoryViewModel earningsHistoryViewModel; private EarningsHistoryView earningsHistoryView; + private TradeView tradeView; private MarketStatusViewModel marketStatusViewModel; private MarketStatusController marketStatusController; @@ -173,6 +175,15 @@ public AppBuilder addEarningsHistoryView() { return this; } + public AppBuilder addTradeView() { + TradeFeed tradeFeed = new FinnhubTradeDataAccessObject(); + tradeView = new TradeView(tradeFeed); + String viewName = tradeView.getViewName(); + System.out.println("Adding TradeView with name: " + viewName); // debug + cardPanel.add(tradeView, viewName); + return this; + } + public AppBuilder addSignupUseCase() { final SignupOutputBoundary signupOutputBoundary = new SignupPresenter(viewManagerModel, @@ -269,6 +280,18 @@ public AppBuilder addMarketStatusUseCase() { return this; } + public AppBuilder addRealtimeTradeUseCase() { + // Logged-in page: Realtime Trade button → Trade view + String tradeViewName = tradeView.getViewName(); + System.out.println("Setting up Realtime Trade navigation to view: " + tradeViewName); // debug + loggedInView.setRealtimeTradeNavigation(viewManagerModel, tradeViewName); + + // Trade page: Back button → Logged-in view + tradeView.setBackNavigation(viewManagerModel, loggedInView.getViewName()); + + return this; + } + public JFrame build() { final JFrame application = new JFrame("Stock Application"); application.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 54eab0b3..2b930cc1 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -16,12 +16,14 @@ public static void main(String[] args) { .addFilterSearchUseCase() .addNewsView() .addEarningsHistoryView() + .addTradeView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() .addNewsUsecase() .addEarningsHistoryUseCase() .addMarketStatusUseCase() + .addRealtimeTradeUseCase() .build(); application.pack(); diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 4971042b..bea445f4 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -32,6 +32,7 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private String newsViewName; private String filterSearchViewName; private String historyViewName; + private String realtimeTradeViewName; private JLabel marketStatusLabel; private MarketStatusViewModel marketStatusViewModel; @@ -41,12 +42,11 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JTextField passwordInputField = new JTextField(15); private final JButton changePassword; - private final JTextField searchStockField = new JTextField(25); private final JButton searchButton = new JButton("Search"); private final JTextArea stockInfoArea = new JTextArea(); private final JButton addToWatchlistButton = new JButton("Add to Watchlist"); - private final JButton historyButton; + final JButton historyButton = new JButton("History"); public LoggedInView(LoggedInViewModel loggedInViewModel) { this.loggedInViewModel = loggedInViewModel; @@ -83,19 +83,33 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { final JButton historyButton = new JButton("History"); - historyButton = new JButton("History"); historyButton.addActionListener(e -> { if (viewManagerModel != null && historyViewName != null) { viewManagerModel.setState(historyViewName); viewManagerModel.firePropertyChange(); } }); + + final JButton realtimeTradeButton = new JButton("Realtime Trade"); + realtimeTradeButton.addActionListener(e -> { + System.out.println("Realtime Trade button clicked"); // debug + System.out.println("viewManagerModel: " + viewManagerModel); // debug + System.out.println("realtimeTradeViewName: " + realtimeTradeViewName); // debug + if (viewManagerModel != null && realtimeTradeViewName != null) { + viewManagerModel.setState(realtimeTradeViewName); + viewManagerModel.firePropertyChange(); + } else { + System.out.println("ERROR: Cannot navigate - viewManagerModel or realtimeTradeViewName is null"); // debug + } + }); + final JButton marketOpenButton = new JButton("Market Open"); final JButton accountButton = new JButton("Account"); topToolbar.add(newsButton); topToolbar.add(filterSearchButton); topToolbar.add(historyButton); + topToolbar.add(realtimeTradeButton); topToolbar.add(marketOpenButton); topToolbar.add(accountButton); @@ -233,7 +247,11 @@ public void setHistoryNavigation(ViewManagerModel viewManagerModel, this.historyViewName = historyViewName; } - + public void setRealtimeTradeNavigation(ViewManagerModel viewManagerModel, + String tradeViewName) { + this.viewManagerModel = viewManagerModel; + this.realtimeTradeViewName = tradeViewName; + } public void setMarketStatusViewModel(MarketStatusViewModel viewModel) { this.marketStatusViewModel = viewModel; From 254e4576caa1520d04ce849e96c0e4dfee526fbe Mon Sep 17 00:00:00 2001 From: samario Date: Mon, 1 Dec 2025 13:37:12 -0500 Subject: [PATCH 079/103] Delete useless Market Open button on toolbar. Only leave the string --- src/main/java/view/LoggedInView.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 03b66651..35e2e972 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -93,23 +93,22 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { viewManagerModel.firePropertyChange(); } }); - final JButton marketOpenButton = new JButton("Market Open"); + + final JButton accountButton = new JButton("Account"); topToolbar.add(newsButton); topToolbar.add(filterSearchButton); topToolbar.add(historyButton); - topToolbar.add(marketOpenButton); topToolbar.add(accountButton); - this.add(topToolbar, BorderLayout.NORTH); - marketStatusLabel = new JLabel("Loading market status..."); marketStatusLabel.setFont(marketStatusLabel.getFont().deriveFont(Font.PLAIN, 11f)); marketStatusLabel.setForeground(Color.DARK_GRAY); topToolbar.add(Box.createHorizontalStrut(20)); topToolbar.add(marketStatusLabel); + this.add(topToolbar, BorderLayout.NORTH); final JPanel centerPanel = new JPanel(); centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS)); From f25ddcddc74f8af83b32658a8c82981a1a272417 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Mon, 1 Dec 2025 13:38:13 -0500 Subject: [PATCH 080/103] Real time search function implemented --- .../FinnhubTradeDataAccessObject.java | 185 ++++++++ .../java/data_access/FinnhubTradeFeed.java | 192 ++++++++ src/main/java/entity/Trade.java | 38 ++ src/main/java/use_case/TradeFeed.java | 21 + src/main/java/use_case/TradeListener.java | 21 + src/main/java/view/TradeView.java | 430 ++++++++++++++++++ 6 files changed, 887 insertions(+) create mode 100644 src/main/java/data_access/FinnhubTradeDataAccessObject.java create mode 100644 src/main/java/data_access/FinnhubTradeFeed.java create mode 100644 src/main/java/entity/Trade.java create mode 100644 src/main/java/use_case/TradeFeed.java create mode 100644 src/main/java/use_case/TradeListener.java create mode 100644 src/main/java/view/TradeView.java diff --git a/src/main/java/data_access/FinnhubTradeDataAccessObject.java b/src/main/java/data_access/FinnhubTradeDataAccessObject.java new file mode 100644 index 00000000..b7967289 --- /dev/null +++ b/src/main/java/data_access/FinnhubTradeDataAccessObject.java @@ -0,0 +1,185 @@ +package data_access; + +import entity.Trade; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import use_case.TradeFeed; +import use_case.TradeListener; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Finnhub-specific implementation of the TradeFeed abstraction. + * This class belongs to the data_access / infrastructure layer and knows about WebSockets and JSON format. + */ +public class FinnhubTradeDataAccessObject implements TradeFeed { + + private static final Logger LOGGER = Logger.getLogger(FinnhubTradeDataAccessObject.class.getName()); + + private static final String API_KEY = "d4977ehr01qshn3kvpt0d4977ehr01qshn3kvptg"; + private static final String WEB_SOCKET_URL = "wss://ws.finnhub.io?token=" + API_KEY; + + private WebSocket webSocket; + private OkHttpClient client; + private TradeListener listener; + private String currentSymbol; + + @Override + public void connect(String symbol, TradeListener listener) { + this.listener = listener; + this.currentSymbol = symbol; + + if (webSocket != null) { + LOGGER.info("Closing existing Finnhub connection..."); + webSocket.close(1000, "Reconnecting"); + } + + client = new OkHttpClient.Builder() + .readTimeout(0, TimeUnit.MILLISECONDS) + .build(); + + Request request = new Request.Builder().url(WEB_SOCKET_URL).build(); + + if (listener != null) { + listener.onStatusChanged("Status: Connecting...", false); + } + + webSocket = client.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket ws, Response response) { + LOGGER.info("WebSocket Opened. Subscribing to " + currentSymbol); + String subscribeMsg = String.format("{\"type\":\"subscribe\",\"symbol\":\"%s\"}", currentSymbol); + ws.send(subscribeMsg); + } + + @Override + public void onMessage(WebSocket ws, String text) { + processMessage(text); + } + + @Override + public void onClosing(WebSocket ws, int code, String reason) { + LOGGER.info("WebSocket Closing: Code " + code + ", Reason: " + reason); + ws.close(1000, null); + } + + @Override + public void onFailure(WebSocket ws, Throwable t, Response response) { + LOGGER.log(Level.SEVERE, "WebSocket Failure: " + t.getMessage(), t); + if (listener != null) { + listener.onStatusChanged("Status: Failure! See console.", true); + } + } + + @Override + public void onClosed(WebSocket ws, int code, String reason) { + LOGGER.info("WebSocket Closed."); + webSocket = null; + if (listener != null) { + listener.onStatusChanged("Status: Disconnected", false); + } + } + }); + } + + @Override + public void disconnect() { + if (webSocket != null) { + webSocket.close(1000, "User disconnect"); + webSocket = null; + } + if (client != null) { + client.dispatcher().executorService().shutdown(); + client = null; + } + } + + /** + * Small internal JSON parsing helper: extracts a value from a JSON string using basic String methods. + * Kept here in the infrastructure layer so UI and domain remain free of parsing details. + */ + private String extractValue(String json, String key) { + String keySearch = "\"" + key + "\":"; + int keyIndex = json.indexOf(keySearch); + + if (keyIndex == -1) { + return null; + } + + int valueStart = keyIndex + keySearch.length(); + char firstChar = json.charAt(valueStart); + + if (firstChar == '"') { + valueStart++; + int valueEnd = json.indexOf('"', valueStart); + if (valueEnd != -1) { + return json.substring(valueStart, valueEnd); + } + } else { + int valueEnd = valueStart; + while (valueEnd < json.length() && (Character.isDigit(json.charAt(valueEnd)) || json.charAt(valueEnd) == '.' || json.charAt(valueEnd) == '-')) { + valueEnd++; + } + int commaIndex = json.indexOf(',', valueStart); + int braceIndex = json.indexOf('}', valueStart); + + int terminationIndex = json.length(); + if (commaIndex != -1) terminationIndex = Math.min(terminationIndex, commaIndex); + if (braceIndex != -1) terminationIndex = Math.min(terminationIndex, braceIndex); + + if (terminationIndex > valueStart) { + return json.substring(valueStart, terminationIndex).trim(); + } + } + return null; + } + + /** + * Parses the incoming JSON message using manual string manipulation and dispatches Trade domain objects. + */ + private void processMessage(String jsonMessage) { + try { + if (jsonMessage.contains("\"type\":\"trade\"")) { + String symbol = extractValue(jsonMessage, "s"); + + String priceStr = extractValue(jsonMessage, "p"); + double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; + + String volumeStr = extractValue(jsonMessage, "v"); + double volume = (volumeStr != null) ? Double.parseDouble(volumeStr) : 0.0; + + String timestampStr = extractValue(jsonMessage, "t"); + long timestamp = (timestampStr != null) ? Long.parseLong(timestampStr) : 0L; + + Instant ts = timestamp > 0 ? Instant.ofEpochMilli(timestamp) : null; + + if (listener != null) { + // First successful trade means we are effectively connected for this symbol. + listener.onStatusChanged("Status: Connected to " + currentSymbol, false); + + Trade trade = new Trade(symbol, price, volume, ts); + listener.onTrade(trade); + } + } else if (jsonMessage.contains("\"type\":\"ping\"")) { + // No-op: connection keep-alive + } else if (jsonMessage.contains("\"type\":\"error\"")) { + // Handle error messages from the API + String errorMsg = extractValue(jsonMessage, "msg"); + if (listener != null) { + String errorText = errorMsg != null ? errorMsg : "Unknown error"; + listener.onStatusChanged("Status: Error - " + errorText, true); + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error processing JSON message with manual parsing: " + e.getMessage() + + "\nMessage: " + jsonMessage); + } + } +} + diff --git a/src/main/java/data_access/FinnhubTradeFeed.java b/src/main/java/data_access/FinnhubTradeFeed.java new file mode 100644 index 00000000..b4a9e1e7 --- /dev/null +++ b/src/main/java/data_access/FinnhubTradeFeed.java @@ -0,0 +1,192 @@ +package data_access; + +import entity.Trade; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import use_case.TradeFeed; +import use_case.TradeListener; + +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Finnhub-specific implementation of the TradeFeed abstraction. + * This class belongs to the data_access / infrastructure layer and knows about WebSockets and JSON format. + */ +public class FinnhubTradeFeed implements TradeFeed { + + private static final Logger LOGGER = Logger.getLogger(FinnhubTradeFeed.class.getName()); + + private static final String API_KEY = "d4977ehr01qshn3kvpt0d4977ehr01qshn3kvptg"; + private static final String WEB_SOCKET_URL = "wss://ws.finnhub.io?token=" + API_KEY; + + private WebSocket webSocket; + private OkHttpClient client; + private TradeListener listener; + private String currentSymbol; + + @Override + public void connect(String symbol, TradeListener listener) { + this.listener = listener; + this.currentSymbol = symbol; + + if (webSocket != null) { + LOGGER.info("Closing existing Finnhub connection..."); + webSocket.close(1000, "Reconnecting"); + } + + client = new OkHttpClient.Builder() + .readTimeout(0, TimeUnit.MILLISECONDS) + .build(); + + Request request = new Request.Builder().url(WEB_SOCKET_URL).build(); + + if (listener != null) { + listener.onStatusChanged("Status: Connecting...", false); + } + + webSocket = client.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket ws, Response response) { + LOGGER.info("WebSocket Opened. Subscribing to " + currentSymbol); + String subscribeMsg = String.format("{\"type\":\"subscribe\",\"symbol\":\"%s\"}", currentSymbol); + ws.send(subscribeMsg); + } + + @Override + public void onMessage(WebSocket ws, String text) { + processMessage(text); + } + + @Override + public void onClosing(WebSocket ws, int code, String reason) { + LOGGER.info("WebSocket Closing: Code " + code + ", Reason: " + reason); + ws.close(1000, null); + } + + @Override + public void onFailure(WebSocket ws, Throwable t, Response response) { + LOGGER.log(Level.SEVERE, "WebSocket Failure: " + t.getMessage(), t); + if (listener != null) { + String errorMessage = "Status: Failure! See console."; + // Check for 429 rate limiting error + if (t.getMessage() != null && t.getMessage().contains("429")) { + errorMessage = "Status: 429 Too Many Requests - Rate limit exceeded"; + } else if (response != null && response.code() == 429) { + errorMessage = "Status: 429 Too Many Requests - Rate limit exceeded"; + } + listener.onStatusChanged(errorMessage, true); + } + } + + @Override + public void onClosed(WebSocket ws, int code, String reason) { + LOGGER.info("WebSocket Closed."); + webSocket = null; + if (listener != null) { + listener.onStatusChanged("Status: Disconnected", false); + } + } + }); + } + + @Override + public void disconnect() { + if (webSocket != null) { + webSocket.close(1000, "User disconnect"); + webSocket = null; + } + if (client != null) { + client.dispatcher().executorService().shutdown(); + client = null; + } + } + + /** + * Small internal JSON parsing helper: extracts a value from a JSON string using basic String methods. + * Kept here in the infrastructure layer so UI and domain remain free of parsing details. + */ + private String extractValue(String json, String key) { + String keySearch = "\"" + key + "\":"; + int keyIndex = json.indexOf(keySearch); + + if (keyIndex == -1) { + return null; + } + + int valueStart = keyIndex + keySearch.length(); + char firstChar = json.charAt(valueStart); + + if (firstChar == '"') { + valueStart++; + int valueEnd = json.indexOf('"', valueStart); + if (valueEnd != -1) { + return json.substring(valueStart, valueEnd); + } + } else { + int valueEnd = valueStart; + while (valueEnd < json.length() && (Character.isDigit(json.charAt(valueEnd)) || json.charAt(valueEnd) == '.' || json.charAt(valueEnd) == '-')) { + valueEnd++; + } + int commaIndex = json.indexOf(',', valueStart); + int braceIndex = json.indexOf('}', valueStart); + + int terminationIndex = json.length(); + if (commaIndex != -1) terminationIndex = Math.min(terminationIndex, commaIndex); + if (braceIndex != -1) terminationIndex = Math.min(terminationIndex, braceIndex); + + if (terminationIndex > valueStart) { + return json.substring(valueStart, terminationIndex).trim(); + } + } + return null; + } + + /** + * Parses the incoming JSON message using manual string manipulation and dispatches Trade domain objects. + */ + private void processMessage(String jsonMessage) { + try { + if (jsonMessage.contains("\"type\":\"trade\"")) { + String symbol = extractValue(jsonMessage, "s"); + + String priceStr = extractValue(jsonMessage, "p"); + double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; + + String volumeStr = extractValue(jsonMessage, "v"); + double volume = (volumeStr != null) ? Double.parseDouble(volumeStr) : 0.0; + + String timestampStr = extractValue(jsonMessage, "t"); + long timestamp = (timestampStr != null) ? Long.parseLong(timestampStr) : 0L; + + Instant ts = timestamp > 0 ? Instant.ofEpochMilli(timestamp) : null; + + if (listener != null) { + // First successful trade means we are effectively connected for this symbol. + listener.onStatusChanged("Status: Connected to " + currentSymbol, false); + + Trade trade = new Trade(symbol, price, volume, ts); + listener.onTrade(trade); + } + } else if (jsonMessage.contains("\"type\":\"ping\"")) { + // No-op: connection keep-alive + } else if (jsonMessage.contains("\"type\":\"error\"")) { + // Handle error messages from the API + String errorMsg = extractValue(jsonMessage, "msg"); + if (listener != null) { + String errorText = errorMsg != null ? errorMsg : "Unknown error"; + listener.onStatusChanged("Status: Error - " + errorText, true); + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error processing JSON message with manual parsing: " + e.getMessage() + + "\nMessage: " + jsonMessage); + } + } +} + diff --git a/src/main/java/entity/Trade.java b/src/main/java/entity/Trade.java new file mode 100644 index 00000000..e9b8e8cb --- /dev/null +++ b/src/main/java/entity/Trade.java @@ -0,0 +1,38 @@ +package entity; + +import java.time.Instant; + +/** + * Domain model representing a single trade tick. + */ +public class Trade { + + private final String symbol; + private final double price; + private final double volume; + private final Instant timestamp; + + public Trade(String symbol, double price, double volume, Instant timestamp) { + this.symbol = symbol; + this.price = price; + this.volume = volume; + this.timestamp = timestamp; + } + + public String getSymbol() { + return symbol; + } + + public double getPrice() { + return price; + } + + public double getVolume() { + return volume; + } + + public Instant getTimestamp() { + return timestamp; + } +} + diff --git a/src/main/java/use_case/TradeFeed.java b/src/main/java/use_case/TradeFeed.java new file mode 100644 index 00000000..5eff9f2e --- /dev/null +++ b/src/main/java/use_case/TradeFeed.java @@ -0,0 +1,21 @@ +package use_case; + +/** + * Abstraction for any real-time trade data source. + * Infrastructure implementations (e.g., Finnhub WebSocket) should implement this. + */ +public interface TradeFeed { + + /** + * Starts or (re)connects the feed and begins streaming trades to the listener. + * @param symbol The trading symbol to subscribe to (e.g., "BINANCE:BTCUSDT", "AAPL") + * @param listener The listener to receive trade updates + */ + void connect(String symbol, TradeListener listener); + + /** + * Gracefully disconnects the feed. + */ + void disconnect(); +} + diff --git a/src/main/java/use_case/TradeListener.java b/src/main/java/use_case/TradeListener.java new file mode 100644 index 00000000..4d8b2f18 --- /dev/null +++ b/src/main/java/use_case/TradeListener.java @@ -0,0 +1,21 @@ +package use_case; + +import entity.Trade; + +/** + * Callback interface that presentation layer (e.g., Swing UI) implements + * to receive trade updates and connection status from a TradeFeed. + */ +public interface TradeListener { + + /** + * Called when a new trade is received from the data source. + */ + void onTrade(Trade trade); + + /** + * Called when the underlying connection status changes (connecting, connected, failed, etc.). + */ + void onStatusChanged(String statusText, boolean isError); +} + diff --git a/src/main/java/view/TradeView.java b/src/main/java/view/TradeView.java new file mode 100644 index 00000000..be63732e --- /dev/null +++ b/src/main/java/view/TradeView.java @@ -0,0 +1,430 @@ +package view; + +import entity.Trade; +import interface_adapter.ViewManagerModel; +import use_case.TradeFeed; +import use_case.TradeListener; + +import javax.swing.*; +import java.awt.*; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.logging.Logger; + +/** + * Swing view for displaying real-time trade data. + * Depends only on the TradeFeed abstraction and domain Trade entity. + */ +public class TradeView extends JPanel { + + private static final Logger LOGGER = Logger.getLogger(TradeView.class.getName()); + + private final String viewName = "trade"; + private ViewManagerModel viewManagerModel; + private String homeViewName; + + private final TradeFeed tradeFeed; + + // Trade data labels + private final JLabel symbolLabel = new JLabel("---"); + private final JLabel priceLabel = new JLabel("---"); + private final JLabel volumeLabel = new JLabel("---"); + private final JLabel timestampLabel = new JLabel("---"); + private final JLabel statusLabel = new JLabel("Status: Disconnected", SwingConstants.CENTER); + private final JTextField symbolInputField = new JTextField(20); + private JButton connectButton; + private JButton disconnectButton; + private String currentSymbol = ""; + private long connectionStartTime = 0; + private long lastConnectionAttemptTime = 0; + private boolean isConnected = false; + private static final long SYMBOL_NOT_FOUND_TIMEOUT = 10000; // 10 seconds + private static final long CONNECTION_COOLDOWN_MS = 5000; // 5 seconds between connection attempts + + /** + * Initializes the GUI components. + */ + public TradeView(TradeFeed tradeFeed) { + this.tradeFeed = tradeFeed; + setLayout(new BorderLayout()); + + // --- Top: Back button + status --- + JPanel topPanel = new JPanel(new BorderLayout()); + + JPanel backPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton backButton = new JButton("Back to Home"); + backButton.addActionListener(e -> { + if (viewManagerModel != null && homeViewName != null) { + viewManagerModel.setState(homeViewName); + viewManagerModel.firePropertyChange(); + } + }); + backPanel.add(backButton); + topPanel.add(backPanel, BorderLayout.WEST); + + statusLabel.setFont(new Font("Arial", Font.BOLD, 14)); + statusLabel.setBorder(BorderFactory.createEmptyBorder(8, 8, 4, 8)); + topPanel.add(statusLabel, BorderLayout.CENTER); + + add(topPanel, BorderLayout.NORTH); + + // --- Center: Trade data display --- + JPanel mainPanel = createMainDashboardCard(); + mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + add(mainPanel, BorderLayout.CENTER); + } + + /** + * Creates the main dashboard card that shows the trade data and connect button. + */ + private JPanel createMainDashboardCard() { + JPanel root = new JPanel(new BorderLayout(10, 10)); + + // Search panel at top with Connect and Disconnect buttons + JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); + searchPanel.add(new JLabel("Symbol:")); + symbolInputField.setText("BINANCE:BTCUSDT"); // Default value + searchPanel.add(symbolInputField); + connectButton = new JButton("Connect"); + connectButton.addActionListener(e -> onConnectClicked()); + searchPanel.add(connectButton); + disconnectButton = new JButton("Disconnect"); + disconnectButton.addActionListener(e -> onDisconnectClicked()); + disconnectButton.setEnabled(false); // Initially disabled + searchPanel.add(disconnectButton); + root.add(searchPanel, BorderLayout.NORTH); + + // Data display panel + JPanel dataPanel = new JPanel(new GridLayout(4, 2, 10, 10)); + dataPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + styleLabel(symbolLabel); + styleLabel(priceLabel); + styleLabel(volumeLabel); + styleLabel(timestampLabel); + + dataPanel.add(new JLabel("Symbol:")); + dataPanel.add(symbolLabel); + + dataPanel.add(new JLabel("Last Price (P):")); + dataPanel.add(priceLabel); + + dataPanel.add(new JLabel("Volume (V):")); + dataPanel.add(volumeLabel); + + dataPanel.add(new JLabel("Timestamp (T):")); + dataPanel.add(timestampLabel); + + root.add(dataPanel, BorderLayout.CENTER); + + return root; + } + + /** + * Helper to apply basic styling to the data display labels. + */ + private void styleLabel(JLabel label) { + label.setFont(new Font("Monospaced", Font.BOLD, 18)); + label.setForeground(new Color(0, 128, 0)); // Green color for data + } + + /** + * Handles the Connect button click by delegating to the TradeFeed. + */ + private void onConnectClicked() { + if (tradeFeed != null) { + // Check if already connected - user must disconnect first + if (isConnected) { + JOptionPane.showMessageDialog( + this, + "Please disconnect from the current trade before connecting to a new one.", + "Already Connected", + JOptionPane.WARNING_MESSAGE + ); + return; + } + + String symbol = symbolInputField.getText().trim(); + if (symbol.isEmpty()) { + statusLabel.setText("Status: Please enter a symbol"); + statusLabel.setForeground(Color.RED); + return; + } + + // Check rate limiting - prevent too frequent connection attempts + long currentTime = System.currentTimeMillis(); + long timeSinceLastAttempt = currentTime - lastConnectionAttemptTime; + + if (timeSinceLastAttempt < CONNECTION_COOLDOWN_MS) { + long remainingTime = (CONNECTION_COOLDOWN_MS - timeSinceLastAttempt) / 1000; + JOptionPane.showMessageDialog( + this, + "Please wait " + remainingTime + " second(s) before connecting again.\nToo many requests may result in rate limiting.", + "Rate Limit", + JOptionPane.WARNING_MESSAGE + ); + return; + } + + // Update last connection attempt time + lastConnectionAttemptTime = currentTime; + + // Disconnect previous connection if any + tradeFeed.disconnect(); + + // Reset state + currentSymbol = symbol; + connectionStartTime = System.currentTimeMillis(); + symbolLabel.setText("---"); + priceLabel.setText("---"); + volumeLabel.setText("---"); + timestampLabel.setText("---"); + + // Disable connect button during connection attempt, enable disconnect button + connectButton.setEnabled(false); + disconnectButton.setEnabled(true); + + // Immediately show "Connecting..." while we wait for the first trade. + statusLabel.setText("Status: Connecting..."); + statusLabel.setForeground(Color.ORANGE); + + tradeFeed.connect(symbol, new TradeListener() { + @Override + public void onTrade(Trade trade) { + connectionStartTime = 0; // Reset timeout on first trade + isConnected = true; // Mark as connected on first trade + SwingUtilities.invokeLater(() -> { + connectButton.setEnabled(false); // Disable connect when connected + disconnectButton.setEnabled(true); // Enable disconnect when connected + }); + updateTradeOnUi(trade); + } + + @Override + public void onStatusChanged(String statusText, boolean isError) { + SwingUtilities.invokeLater(() -> { + if (isError) { + // On error, re-enable connect button and disable disconnect + connectButton.setEnabled(true); + disconnectButton.setEnabled(false); + isConnected = false; + } else if (statusText.contains("Disconnected")) { + // On disconnect, reset button states + connectButton.setEnabled(true); + disconnectButton.setEnabled(false); + isConnected = false; + } + }); + updateStatusOnUi(statusText, isError); + + // Combined error handling for rate limiting and symbol not found + if (isError) { + boolean isRateLimit = statusText.contains("429") || statusText.contains("Too Many Requests") || statusText.contains("Rate limit"); + boolean isOtherError = statusText.contains("Error") || statusText.contains("Failure"); + + if (isRateLimit || isOtherError) { + SwingUtilities.invokeLater(() -> { + String message; + if (isRateLimit) { + message = "Connection failed: Too Many Requests (429).\n" + + "This may also occur if the symbol '" + currentSymbol + "' is invalid.\n\n" + + "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + + " seconds before trying again.\n" + + "If the problem persists, please verify the symbol is correct."; + // Enforce cooldown after 429 error + lastConnectionAttemptTime = System.currentTimeMillis(); + startCooldownTimer(); + } else { + message = "Connection failed: Symbol '" + currentSymbol + "' not found or invalid.\n" + + "This may also occur if requests are made too frequently.\n\n" + + "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + + " seconds before trying again.\n" + + "If the problem persists, please verify the symbol is correct."; + // Enforce cooldown for other errors too + lastConnectionAttemptTime = System.currentTimeMillis(); + startCooldownTimer(); + } + + JOptionPane.showMessageDialog( + TradeView.this, + message, + "Connection Error", + JOptionPane.ERROR_MESSAGE + ); + }); + } + } + } + }); + + // Start a timeout check for symbol not found + startSymbolNotFoundCheck(); + } + } + + /** + * Handles the Disconnect button click. + */ + private void onDisconnectClicked() { + if (tradeFeed != null) { + // Disconnect from the trade feed + tradeFeed.disconnect(); + + // Reset all info to default + resetToDefault(); + } + } + + /** + * Resets all information to default values. + */ + private void resetToDefault() { + // Reset connection state + isConnected = false; + connectionStartTime = 0; + currentSymbol = ""; + + // Reset UI labels to default + symbolLabel.setText("---"); + priceLabel.setText("---"); + volumeLabel.setText("---"); + timestampLabel.setText("---"); + statusLabel.setText("Status: Disconnected"); + statusLabel.setForeground(Color.BLACK); + + // Reset button states + connectButton.setEnabled(true); + disconnectButton.setEnabled(false); + connectButton.setText("Connect"); + } + + /** + * Starts a background thread to check if symbol is not found after timeout. + */ + private void startSymbolNotFoundCheck() { + new Thread(() -> { + try { + Thread.sleep(SYMBOL_NOT_FOUND_TIMEOUT); + // If we're still connecting after timeout and no trades received + if (connectionStartTime > 0 && System.currentTimeMillis() - connectionStartTime >= SYMBOL_NOT_FOUND_TIMEOUT) { + SwingUtilities.invokeLater(() -> { + if (symbolLabel.getText().equals("---")) { + // No trades received - could be symbol not found or rate limiting + String message = "Connection timeout: Symbol '" + currentSymbol + "' not found or no trades available.\n" + + "This may also occur if requests are made too frequently.\n\n" + + "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + + " seconds before trying again.\n" + + "If the problem persists, please verify the symbol is correct."; + + JOptionPane.showMessageDialog( + TradeView.this, + message, + "Connection Error", + JOptionPane.ERROR_MESSAGE + ); + statusLabel.setText("Status: Connection failed"); + statusLabel.setForeground(Color.RED); + tradeFeed.disconnect(); + resetToDefault(); + // Enforce cooldown after timeout + lastConnectionAttemptTime = System.currentTimeMillis(); + startCooldownTimer(); + } + }); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }).start(); + } + + /** + * Starts a cooldown timer that disables the connect button and shows countdown. + */ + private void startCooldownTimer() { + connectButton.setEnabled(false); + new Thread(() -> { + try { + for (int remaining = (int)(CONNECTION_COOLDOWN_MS / 1000); remaining > 0; remaining--) { + final int seconds = remaining; + SwingUtilities.invokeLater(() -> { + connectButton.setText("Wait " + seconds + "s"); + statusLabel.setText("Status: Rate limited - wait " + seconds + " second(s)"); + statusLabel.setForeground(Color.ORANGE); + }); + Thread.sleep(1000); + } + SwingUtilities.invokeLater(() -> { + connectButton.setText("Connect"); + connectButton.setEnabled(true); + disconnectButton.setEnabled(false); + statusLabel.setText("Status: Ready to connect"); + statusLabel.setForeground(Color.BLACK); + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + SwingUtilities.invokeLater(() -> { + connectButton.setText("Connect"); + connectButton.setEnabled(true); + disconnectButton.setEnabled(false); + }); + } + }).start(); + } + + /** + * Updates UI labels when a new trade is received. + */ + private void updateTradeOnUi(Trade trade) { + SwingUtilities.invokeLater(() -> { + String symbol = trade.getSymbol(); + double price = trade.getPrice(); + double volume = trade.getVolume(); + Instant timestamp = trade.getTimestamp(); + + symbolLabel.setText(symbol != null ? symbol : "N/A"); + priceLabel.setText(String.format("$%,.2f", price)); + volumeLabel.setText(String.format("%,.4f", volume)); + + if (timestamp != null) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); + String time = timestamp + .atZone(ZoneId.systemDefault()) + .toLocalTime() + .format(formatter); + timestampLabel.setText(time); + } else { + timestampLabel.setText("N/A"); + } + }); + } + + /** + * Updates the status label on the Event Dispatch Thread. + */ + private void updateStatusOnUi(String statusText, boolean isError) { + SwingUtilities.invokeLater(() -> { + statusLabel.setText(statusText); + if (isError) { + statusLabel.setForeground(Color.RED); + } else if (statusText != null && statusText.contains("Connected")) { + statusLabel.setForeground(new Color(0, 128, 0)); + } else if (statusText != null && statusText.contains("Connecting")) { + statusLabel.setForeground(Color.ORANGE); + } else { + statusLabel.setForeground(Color.BLACK); + } + }); + } + + public String getViewName() { + return this.viewName; + } + + public void setBackNavigation(ViewManagerModel viewManagerModel, String homeViewName) { + this.viewManagerModel = viewManagerModel; + this.homeViewName = homeViewName; + } +} + From b05bbc7066fadd19685b6557f7db798ce0703c6e Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Mon, 1 Dec 2025 13:51:29 -0500 Subject: [PATCH 081/103] Revert "21 current market trades new files added" --- .../FinnhubTradeDataAccessObject.java | 185 -------- .../java/data_access/FinnhubTradeFeed.java | 192 -------- src/main/java/entity/Trade.java | 38 -- src/main/java/use_case/TradeFeed.java | 21 - src/main/java/use_case/TradeListener.java | 21 - src/main/java/view/TradeView.java | 430 ------------------ 6 files changed, 887 deletions(-) delete mode 100644 src/main/java/data_access/FinnhubTradeDataAccessObject.java delete mode 100644 src/main/java/data_access/FinnhubTradeFeed.java delete mode 100644 src/main/java/entity/Trade.java delete mode 100644 src/main/java/use_case/TradeFeed.java delete mode 100644 src/main/java/use_case/TradeListener.java delete mode 100644 src/main/java/view/TradeView.java diff --git a/src/main/java/data_access/FinnhubTradeDataAccessObject.java b/src/main/java/data_access/FinnhubTradeDataAccessObject.java deleted file mode 100644 index b7967289..00000000 --- a/src/main/java/data_access/FinnhubTradeDataAccessObject.java +++ /dev/null @@ -1,185 +0,0 @@ -package data_access; - -import entity.Trade; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import use_case.TradeFeed; -import use_case.TradeListener; - -import java.time.Instant; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Finnhub-specific implementation of the TradeFeed abstraction. - * This class belongs to the data_access / infrastructure layer and knows about WebSockets and JSON format. - */ -public class FinnhubTradeDataAccessObject implements TradeFeed { - - private static final Logger LOGGER = Logger.getLogger(FinnhubTradeDataAccessObject.class.getName()); - - private static final String API_KEY = "d4977ehr01qshn3kvpt0d4977ehr01qshn3kvptg"; - private static final String WEB_SOCKET_URL = "wss://ws.finnhub.io?token=" + API_KEY; - - private WebSocket webSocket; - private OkHttpClient client; - private TradeListener listener; - private String currentSymbol; - - @Override - public void connect(String symbol, TradeListener listener) { - this.listener = listener; - this.currentSymbol = symbol; - - if (webSocket != null) { - LOGGER.info("Closing existing Finnhub connection..."); - webSocket.close(1000, "Reconnecting"); - } - - client = new OkHttpClient.Builder() - .readTimeout(0, TimeUnit.MILLISECONDS) - .build(); - - Request request = new Request.Builder().url(WEB_SOCKET_URL).build(); - - if (listener != null) { - listener.onStatusChanged("Status: Connecting...", false); - } - - webSocket = client.newWebSocket(request, new WebSocketListener() { - @Override - public void onOpen(WebSocket ws, Response response) { - LOGGER.info("WebSocket Opened. Subscribing to " + currentSymbol); - String subscribeMsg = String.format("{\"type\":\"subscribe\",\"symbol\":\"%s\"}", currentSymbol); - ws.send(subscribeMsg); - } - - @Override - public void onMessage(WebSocket ws, String text) { - processMessage(text); - } - - @Override - public void onClosing(WebSocket ws, int code, String reason) { - LOGGER.info("WebSocket Closing: Code " + code + ", Reason: " + reason); - ws.close(1000, null); - } - - @Override - public void onFailure(WebSocket ws, Throwable t, Response response) { - LOGGER.log(Level.SEVERE, "WebSocket Failure: " + t.getMessage(), t); - if (listener != null) { - listener.onStatusChanged("Status: Failure! See console.", true); - } - } - - @Override - public void onClosed(WebSocket ws, int code, String reason) { - LOGGER.info("WebSocket Closed."); - webSocket = null; - if (listener != null) { - listener.onStatusChanged("Status: Disconnected", false); - } - } - }); - } - - @Override - public void disconnect() { - if (webSocket != null) { - webSocket.close(1000, "User disconnect"); - webSocket = null; - } - if (client != null) { - client.dispatcher().executorService().shutdown(); - client = null; - } - } - - /** - * Small internal JSON parsing helper: extracts a value from a JSON string using basic String methods. - * Kept here in the infrastructure layer so UI and domain remain free of parsing details. - */ - private String extractValue(String json, String key) { - String keySearch = "\"" + key + "\":"; - int keyIndex = json.indexOf(keySearch); - - if (keyIndex == -1) { - return null; - } - - int valueStart = keyIndex + keySearch.length(); - char firstChar = json.charAt(valueStart); - - if (firstChar == '"') { - valueStart++; - int valueEnd = json.indexOf('"', valueStart); - if (valueEnd != -1) { - return json.substring(valueStart, valueEnd); - } - } else { - int valueEnd = valueStart; - while (valueEnd < json.length() && (Character.isDigit(json.charAt(valueEnd)) || json.charAt(valueEnd) == '.' || json.charAt(valueEnd) == '-')) { - valueEnd++; - } - int commaIndex = json.indexOf(',', valueStart); - int braceIndex = json.indexOf('}', valueStart); - - int terminationIndex = json.length(); - if (commaIndex != -1) terminationIndex = Math.min(terminationIndex, commaIndex); - if (braceIndex != -1) terminationIndex = Math.min(terminationIndex, braceIndex); - - if (terminationIndex > valueStart) { - return json.substring(valueStart, terminationIndex).trim(); - } - } - return null; - } - - /** - * Parses the incoming JSON message using manual string manipulation and dispatches Trade domain objects. - */ - private void processMessage(String jsonMessage) { - try { - if (jsonMessage.contains("\"type\":\"trade\"")) { - String symbol = extractValue(jsonMessage, "s"); - - String priceStr = extractValue(jsonMessage, "p"); - double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; - - String volumeStr = extractValue(jsonMessage, "v"); - double volume = (volumeStr != null) ? Double.parseDouble(volumeStr) : 0.0; - - String timestampStr = extractValue(jsonMessage, "t"); - long timestamp = (timestampStr != null) ? Long.parseLong(timestampStr) : 0L; - - Instant ts = timestamp > 0 ? Instant.ofEpochMilli(timestamp) : null; - - if (listener != null) { - // First successful trade means we are effectively connected for this symbol. - listener.onStatusChanged("Status: Connected to " + currentSymbol, false); - - Trade trade = new Trade(symbol, price, volume, ts); - listener.onTrade(trade); - } - } else if (jsonMessage.contains("\"type\":\"ping\"")) { - // No-op: connection keep-alive - } else if (jsonMessage.contains("\"type\":\"error\"")) { - // Handle error messages from the API - String errorMsg = extractValue(jsonMessage, "msg"); - if (listener != null) { - String errorText = errorMsg != null ? errorMsg : "Unknown error"; - listener.onStatusChanged("Status: Error - " + errorText, true); - } - } - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Error processing JSON message with manual parsing: " + e.getMessage() - + "\nMessage: " + jsonMessage); - } - } -} - diff --git a/src/main/java/data_access/FinnhubTradeFeed.java b/src/main/java/data_access/FinnhubTradeFeed.java deleted file mode 100644 index b4a9e1e7..00000000 --- a/src/main/java/data_access/FinnhubTradeFeed.java +++ /dev/null @@ -1,192 +0,0 @@ -package data_access; - -import entity.Trade; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import use_case.TradeFeed; -import use_case.TradeListener; - -import java.time.Instant; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Finnhub-specific implementation of the TradeFeed abstraction. - * This class belongs to the data_access / infrastructure layer and knows about WebSockets and JSON format. - */ -public class FinnhubTradeFeed implements TradeFeed { - - private static final Logger LOGGER = Logger.getLogger(FinnhubTradeFeed.class.getName()); - - private static final String API_KEY = "d4977ehr01qshn3kvpt0d4977ehr01qshn3kvptg"; - private static final String WEB_SOCKET_URL = "wss://ws.finnhub.io?token=" + API_KEY; - - private WebSocket webSocket; - private OkHttpClient client; - private TradeListener listener; - private String currentSymbol; - - @Override - public void connect(String symbol, TradeListener listener) { - this.listener = listener; - this.currentSymbol = symbol; - - if (webSocket != null) { - LOGGER.info("Closing existing Finnhub connection..."); - webSocket.close(1000, "Reconnecting"); - } - - client = new OkHttpClient.Builder() - .readTimeout(0, TimeUnit.MILLISECONDS) - .build(); - - Request request = new Request.Builder().url(WEB_SOCKET_URL).build(); - - if (listener != null) { - listener.onStatusChanged("Status: Connecting...", false); - } - - webSocket = client.newWebSocket(request, new WebSocketListener() { - @Override - public void onOpen(WebSocket ws, Response response) { - LOGGER.info("WebSocket Opened. Subscribing to " + currentSymbol); - String subscribeMsg = String.format("{\"type\":\"subscribe\",\"symbol\":\"%s\"}", currentSymbol); - ws.send(subscribeMsg); - } - - @Override - public void onMessage(WebSocket ws, String text) { - processMessage(text); - } - - @Override - public void onClosing(WebSocket ws, int code, String reason) { - LOGGER.info("WebSocket Closing: Code " + code + ", Reason: " + reason); - ws.close(1000, null); - } - - @Override - public void onFailure(WebSocket ws, Throwable t, Response response) { - LOGGER.log(Level.SEVERE, "WebSocket Failure: " + t.getMessage(), t); - if (listener != null) { - String errorMessage = "Status: Failure! See console."; - // Check for 429 rate limiting error - if (t.getMessage() != null && t.getMessage().contains("429")) { - errorMessage = "Status: 429 Too Many Requests - Rate limit exceeded"; - } else if (response != null && response.code() == 429) { - errorMessage = "Status: 429 Too Many Requests - Rate limit exceeded"; - } - listener.onStatusChanged(errorMessage, true); - } - } - - @Override - public void onClosed(WebSocket ws, int code, String reason) { - LOGGER.info("WebSocket Closed."); - webSocket = null; - if (listener != null) { - listener.onStatusChanged("Status: Disconnected", false); - } - } - }); - } - - @Override - public void disconnect() { - if (webSocket != null) { - webSocket.close(1000, "User disconnect"); - webSocket = null; - } - if (client != null) { - client.dispatcher().executorService().shutdown(); - client = null; - } - } - - /** - * Small internal JSON parsing helper: extracts a value from a JSON string using basic String methods. - * Kept here in the infrastructure layer so UI and domain remain free of parsing details. - */ - private String extractValue(String json, String key) { - String keySearch = "\"" + key + "\":"; - int keyIndex = json.indexOf(keySearch); - - if (keyIndex == -1) { - return null; - } - - int valueStart = keyIndex + keySearch.length(); - char firstChar = json.charAt(valueStart); - - if (firstChar == '"') { - valueStart++; - int valueEnd = json.indexOf('"', valueStart); - if (valueEnd != -1) { - return json.substring(valueStart, valueEnd); - } - } else { - int valueEnd = valueStart; - while (valueEnd < json.length() && (Character.isDigit(json.charAt(valueEnd)) || json.charAt(valueEnd) == '.' || json.charAt(valueEnd) == '-')) { - valueEnd++; - } - int commaIndex = json.indexOf(',', valueStart); - int braceIndex = json.indexOf('}', valueStart); - - int terminationIndex = json.length(); - if (commaIndex != -1) terminationIndex = Math.min(terminationIndex, commaIndex); - if (braceIndex != -1) terminationIndex = Math.min(terminationIndex, braceIndex); - - if (terminationIndex > valueStart) { - return json.substring(valueStart, terminationIndex).trim(); - } - } - return null; - } - - /** - * Parses the incoming JSON message using manual string manipulation and dispatches Trade domain objects. - */ - private void processMessage(String jsonMessage) { - try { - if (jsonMessage.contains("\"type\":\"trade\"")) { - String symbol = extractValue(jsonMessage, "s"); - - String priceStr = extractValue(jsonMessage, "p"); - double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; - - String volumeStr = extractValue(jsonMessage, "v"); - double volume = (volumeStr != null) ? Double.parseDouble(volumeStr) : 0.0; - - String timestampStr = extractValue(jsonMessage, "t"); - long timestamp = (timestampStr != null) ? Long.parseLong(timestampStr) : 0L; - - Instant ts = timestamp > 0 ? Instant.ofEpochMilli(timestamp) : null; - - if (listener != null) { - // First successful trade means we are effectively connected for this symbol. - listener.onStatusChanged("Status: Connected to " + currentSymbol, false); - - Trade trade = new Trade(symbol, price, volume, ts); - listener.onTrade(trade); - } - } else if (jsonMessage.contains("\"type\":\"ping\"")) { - // No-op: connection keep-alive - } else if (jsonMessage.contains("\"type\":\"error\"")) { - // Handle error messages from the API - String errorMsg = extractValue(jsonMessage, "msg"); - if (listener != null) { - String errorText = errorMsg != null ? errorMsg : "Unknown error"; - listener.onStatusChanged("Status: Error - " + errorText, true); - } - } - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Error processing JSON message with manual parsing: " + e.getMessage() - + "\nMessage: " + jsonMessage); - } - } -} - diff --git a/src/main/java/entity/Trade.java b/src/main/java/entity/Trade.java deleted file mode 100644 index e9b8e8cb..00000000 --- a/src/main/java/entity/Trade.java +++ /dev/null @@ -1,38 +0,0 @@ -package entity; - -import java.time.Instant; - -/** - * Domain model representing a single trade tick. - */ -public class Trade { - - private final String symbol; - private final double price; - private final double volume; - private final Instant timestamp; - - public Trade(String symbol, double price, double volume, Instant timestamp) { - this.symbol = symbol; - this.price = price; - this.volume = volume; - this.timestamp = timestamp; - } - - public String getSymbol() { - return symbol; - } - - public double getPrice() { - return price; - } - - public double getVolume() { - return volume; - } - - public Instant getTimestamp() { - return timestamp; - } -} - diff --git a/src/main/java/use_case/TradeFeed.java b/src/main/java/use_case/TradeFeed.java deleted file mode 100644 index 5eff9f2e..00000000 --- a/src/main/java/use_case/TradeFeed.java +++ /dev/null @@ -1,21 +0,0 @@ -package use_case; - -/** - * Abstraction for any real-time trade data source. - * Infrastructure implementations (e.g., Finnhub WebSocket) should implement this. - */ -public interface TradeFeed { - - /** - * Starts or (re)connects the feed and begins streaming trades to the listener. - * @param symbol The trading symbol to subscribe to (e.g., "BINANCE:BTCUSDT", "AAPL") - * @param listener The listener to receive trade updates - */ - void connect(String symbol, TradeListener listener); - - /** - * Gracefully disconnects the feed. - */ - void disconnect(); -} - diff --git a/src/main/java/use_case/TradeListener.java b/src/main/java/use_case/TradeListener.java deleted file mode 100644 index 4d8b2f18..00000000 --- a/src/main/java/use_case/TradeListener.java +++ /dev/null @@ -1,21 +0,0 @@ -package use_case; - -import entity.Trade; - -/** - * Callback interface that presentation layer (e.g., Swing UI) implements - * to receive trade updates and connection status from a TradeFeed. - */ -public interface TradeListener { - - /** - * Called when a new trade is received from the data source. - */ - void onTrade(Trade trade); - - /** - * Called when the underlying connection status changes (connecting, connected, failed, etc.). - */ - void onStatusChanged(String statusText, boolean isError); -} - diff --git a/src/main/java/view/TradeView.java b/src/main/java/view/TradeView.java deleted file mode 100644 index be63732e..00000000 --- a/src/main/java/view/TradeView.java +++ /dev/null @@ -1,430 +0,0 @@ -package view; - -import entity.Trade; -import interface_adapter.ViewManagerModel; -import use_case.TradeFeed; -import use_case.TradeListener; - -import javax.swing.*; -import java.awt.*; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.logging.Logger; - -/** - * Swing view for displaying real-time trade data. - * Depends only on the TradeFeed abstraction and domain Trade entity. - */ -public class TradeView extends JPanel { - - private static final Logger LOGGER = Logger.getLogger(TradeView.class.getName()); - - private final String viewName = "trade"; - private ViewManagerModel viewManagerModel; - private String homeViewName; - - private final TradeFeed tradeFeed; - - // Trade data labels - private final JLabel symbolLabel = new JLabel("---"); - private final JLabel priceLabel = new JLabel("---"); - private final JLabel volumeLabel = new JLabel("---"); - private final JLabel timestampLabel = new JLabel("---"); - private final JLabel statusLabel = new JLabel("Status: Disconnected", SwingConstants.CENTER); - private final JTextField symbolInputField = new JTextField(20); - private JButton connectButton; - private JButton disconnectButton; - private String currentSymbol = ""; - private long connectionStartTime = 0; - private long lastConnectionAttemptTime = 0; - private boolean isConnected = false; - private static final long SYMBOL_NOT_FOUND_TIMEOUT = 10000; // 10 seconds - private static final long CONNECTION_COOLDOWN_MS = 5000; // 5 seconds between connection attempts - - /** - * Initializes the GUI components. - */ - public TradeView(TradeFeed tradeFeed) { - this.tradeFeed = tradeFeed; - setLayout(new BorderLayout()); - - // --- Top: Back button + status --- - JPanel topPanel = new JPanel(new BorderLayout()); - - JPanel backPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - JButton backButton = new JButton("Back to Home"); - backButton.addActionListener(e -> { - if (viewManagerModel != null && homeViewName != null) { - viewManagerModel.setState(homeViewName); - viewManagerModel.firePropertyChange(); - } - }); - backPanel.add(backButton); - topPanel.add(backPanel, BorderLayout.WEST); - - statusLabel.setFont(new Font("Arial", Font.BOLD, 14)); - statusLabel.setBorder(BorderFactory.createEmptyBorder(8, 8, 4, 8)); - topPanel.add(statusLabel, BorderLayout.CENTER); - - add(topPanel, BorderLayout.NORTH); - - // --- Center: Trade data display --- - JPanel mainPanel = createMainDashboardCard(); - mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - add(mainPanel, BorderLayout.CENTER); - } - - /** - * Creates the main dashboard card that shows the trade data and connect button. - */ - private JPanel createMainDashboardCard() { - JPanel root = new JPanel(new BorderLayout(10, 10)); - - // Search panel at top with Connect and Disconnect buttons - JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); - searchPanel.add(new JLabel("Symbol:")); - symbolInputField.setText("BINANCE:BTCUSDT"); // Default value - searchPanel.add(symbolInputField); - connectButton = new JButton("Connect"); - connectButton.addActionListener(e -> onConnectClicked()); - searchPanel.add(connectButton); - disconnectButton = new JButton("Disconnect"); - disconnectButton.addActionListener(e -> onDisconnectClicked()); - disconnectButton.setEnabled(false); // Initially disabled - searchPanel.add(disconnectButton); - root.add(searchPanel, BorderLayout.NORTH); - - // Data display panel - JPanel dataPanel = new JPanel(new GridLayout(4, 2, 10, 10)); - dataPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - styleLabel(symbolLabel); - styleLabel(priceLabel); - styleLabel(volumeLabel); - styleLabel(timestampLabel); - - dataPanel.add(new JLabel("Symbol:")); - dataPanel.add(symbolLabel); - - dataPanel.add(new JLabel("Last Price (P):")); - dataPanel.add(priceLabel); - - dataPanel.add(new JLabel("Volume (V):")); - dataPanel.add(volumeLabel); - - dataPanel.add(new JLabel("Timestamp (T):")); - dataPanel.add(timestampLabel); - - root.add(dataPanel, BorderLayout.CENTER); - - return root; - } - - /** - * Helper to apply basic styling to the data display labels. - */ - private void styleLabel(JLabel label) { - label.setFont(new Font("Monospaced", Font.BOLD, 18)); - label.setForeground(new Color(0, 128, 0)); // Green color for data - } - - /** - * Handles the Connect button click by delegating to the TradeFeed. - */ - private void onConnectClicked() { - if (tradeFeed != null) { - // Check if already connected - user must disconnect first - if (isConnected) { - JOptionPane.showMessageDialog( - this, - "Please disconnect from the current trade before connecting to a new one.", - "Already Connected", - JOptionPane.WARNING_MESSAGE - ); - return; - } - - String symbol = symbolInputField.getText().trim(); - if (symbol.isEmpty()) { - statusLabel.setText("Status: Please enter a symbol"); - statusLabel.setForeground(Color.RED); - return; - } - - // Check rate limiting - prevent too frequent connection attempts - long currentTime = System.currentTimeMillis(); - long timeSinceLastAttempt = currentTime - lastConnectionAttemptTime; - - if (timeSinceLastAttempt < CONNECTION_COOLDOWN_MS) { - long remainingTime = (CONNECTION_COOLDOWN_MS - timeSinceLastAttempt) / 1000; - JOptionPane.showMessageDialog( - this, - "Please wait " + remainingTime + " second(s) before connecting again.\nToo many requests may result in rate limiting.", - "Rate Limit", - JOptionPane.WARNING_MESSAGE - ); - return; - } - - // Update last connection attempt time - lastConnectionAttemptTime = currentTime; - - // Disconnect previous connection if any - tradeFeed.disconnect(); - - // Reset state - currentSymbol = symbol; - connectionStartTime = System.currentTimeMillis(); - symbolLabel.setText("---"); - priceLabel.setText("---"); - volumeLabel.setText("---"); - timestampLabel.setText("---"); - - // Disable connect button during connection attempt, enable disconnect button - connectButton.setEnabled(false); - disconnectButton.setEnabled(true); - - // Immediately show "Connecting..." while we wait for the first trade. - statusLabel.setText("Status: Connecting..."); - statusLabel.setForeground(Color.ORANGE); - - tradeFeed.connect(symbol, new TradeListener() { - @Override - public void onTrade(Trade trade) { - connectionStartTime = 0; // Reset timeout on first trade - isConnected = true; // Mark as connected on first trade - SwingUtilities.invokeLater(() -> { - connectButton.setEnabled(false); // Disable connect when connected - disconnectButton.setEnabled(true); // Enable disconnect when connected - }); - updateTradeOnUi(trade); - } - - @Override - public void onStatusChanged(String statusText, boolean isError) { - SwingUtilities.invokeLater(() -> { - if (isError) { - // On error, re-enable connect button and disable disconnect - connectButton.setEnabled(true); - disconnectButton.setEnabled(false); - isConnected = false; - } else if (statusText.contains("Disconnected")) { - // On disconnect, reset button states - connectButton.setEnabled(true); - disconnectButton.setEnabled(false); - isConnected = false; - } - }); - updateStatusOnUi(statusText, isError); - - // Combined error handling for rate limiting and symbol not found - if (isError) { - boolean isRateLimit = statusText.contains("429") || statusText.contains("Too Many Requests") || statusText.contains("Rate limit"); - boolean isOtherError = statusText.contains("Error") || statusText.contains("Failure"); - - if (isRateLimit || isOtherError) { - SwingUtilities.invokeLater(() -> { - String message; - if (isRateLimit) { - message = "Connection failed: Too Many Requests (429).\n" + - "This may also occur if the symbol '" + currentSymbol + "' is invalid.\n\n" + - "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + - " seconds before trying again.\n" + - "If the problem persists, please verify the symbol is correct."; - // Enforce cooldown after 429 error - lastConnectionAttemptTime = System.currentTimeMillis(); - startCooldownTimer(); - } else { - message = "Connection failed: Symbol '" + currentSymbol + "' not found or invalid.\n" + - "This may also occur if requests are made too frequently.\n\n" + - "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + - " seconds before trying again.\n" + - "If the problem persists, please verify the symbol is correct."; - // Enforce cooldown for other errors too - lastConnectionAttemptTime = System.currentTimeMillis(); - startCooldownTimer(); - } - - JOptionPane.showMessageDialog( - TradeView.this, - message, - "Connection Error", - JOptionPane.ERROR_MESSAGE - ); - }); - } - } - } - }); - - // Start a timeout check for symbol not found - startSymbolNotFoundCheck(); - } - } - - /** - * Handles the Disconnect button click. - */ - private void onDisconnectClicked() { - if (tradeFeed != null) { - // Disconnect from the trade feed - tradeFeed.disconnect(); - - // Reset all info to default - resetToDefault(); - } - } - - /** - * Resets all information to default values. - */ - private void resetToDefault() { - // Reset connection state - isConnected = false; - connectionStartTime = 0; - currentSymbol = ""; - - // Reset UI labels to default - symbolLabel.setText("---"); - priceLabel.setText("---"); - volumeLabel.setText("---"); - timestampLabel.setText("---"); - statusLabel.setText("Status: Disconnected"); - statusLabel.setForeground(Color.BLACK); - - // Reset button states - connectButton.setEnabled(true); - disconnectButton.setEnabled(false); - connectButton.setText("Connect"); - } - - /** - * Starts a background thread to check if symbol is not found after timeout. - */ - private void startSymbolNotFoundCheck() { - new Thread(() -> { - try { - Thread.sleep(SYMBOL_NOT_FOUND_TIMEOUT); - // If we're still connecting after timeout and no trades received - if (connectionStartTime > 0 && System.currentTimeMillis() - connectionStartTime >= SYMBOL_NOT_FOUND_TIMEOUT) { - SwingUtilities.invokeLater(() -> { - if (symbolLabel.getText().equals("---")) { - // No trades received - could be symbol not found or rate limiting - String message = "Connection timeout: Symbol '" + currentSymbol + "' not found or no trades available.\n" + - "This may also occur if requests are made too frequently.\n\n" + - "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + - " seconds before trying again.\n" + - "If the problem persists, please verify the symbol is correct."; - - JOptionPane.showMessageDialog( - TradeView.this, - message, - "Connection Error", - JOptionPane.ERROR_MESSAGE - ); - statusLabel.setText("Status: Connection failed"); - statusLabel.setForeground(Color.RED); - tradeFeed.disconnect(); - resetToDefault(); - // Enforce cooldown after timeout - lastConnectionAttemptTime = System.currentTimeMillis(); - startCooldownTimer(); - } - }); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }).start(); - } - - /** - * Starts a cooldown timer that disables the connect button and shows countdown. - */ - private void startCooldownTimer() { - connectButton.setEnabled(false); - new Thread(() -> { - try { - for (int remaining = (int)(CONNECTION_COOLDOWN_MS / 1000); remaining > 0; remaining--) { - final int seconds = remaining; - SwingUtilities.invokeLater(() -> { - connectButton.setText("Wait " + seconds + "s"); - statusLabel.setText("Status: Rate limited - wait " + seconds + " second(s)"); - statusLabel.setForeground(Color.ORANGE); - }); - Thread.sleep(1000); - } - SwingUtilities.invokeLater(() -> { - connectButton.setText("Connect"); - connectButton.setEnabled(true); - disconnectButton.setEnabled(false); - statusLabel.setText("Status: Ready to connect"); - statusLabel.setForeground(Color.BLACK); - }); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - SwingUtilities.invokeLater(() -> { - connectButton.setText("Connect"); - connectButton.setEnabled(true); - disconnectButton.setEnabled(false); - }); - } - }).start(); - } - - /** - * Updates UI labels when a new trade is received. - */ - private void updateTradeOnUi(Trade trade) { - SwingUtilities.invokeLater(() -> { - String symbol = trade.getSymbol(); - double price = trade.getPrice(); - double volume = trade.getVolume(); - Instant timestamp = trade.getTimestamp(); - - symbolLabel.setText(symbol != null ? symbol : "N/A"); - priceLabel.setText(String.format("$%,.2f", price)); - volumeLabel.setText(String.format("%,.4f", volume)); - - if (timestamp != null) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss.SSS"); - String time = timestamp - .atZone(ZoneId.systemDefault()) - .toLocalTime() - .format(formatter); - timestampLabel.setText(time); - } else { - timestampLabel.setText("N/A"); - } - }); - } - - /** - * Updates the status label on the Event Dispatch Thread. - */ - private void updateStatusOnUi(String statusText, boolean isError) { - SwingUtilities.invokeLater(() -> { - statusLabel.setText(statusText); - if (isError) { - statusLabel.setForeground(Color.RED); - } else if (statusText != null && statusText.contains("Connected")) { - statusLabel.setForeground(new Color(0, 128, 0)); - } else if (statusText != null && statusText.contains("Connecting")) { - statusLabel.setForeground(Color.ORANGE); - } else { - statusLabel.setForeground(Color.BLACK); - } - }); - } - - public String getViewName() { - return this.viewName; - } - - public void setBackNavigation(ViewManagerModel viewManagerModel, String homeViewName) { - this.viewManagerModel = viewManagerModel; - this.homeViewName = homeViewName; - } -} - From cbde571222df7525e1ba2c32b26a63dce5c99629 Mon Sep 17 00:00:00 2001 From: Shivam Bavaria Date: Mon, 1 Dec 2025 13:52:20 -0500 Subject: [PATCH 082/103] Revert "Real time search function implemented" --- src/main/java/app/AppBuilder.java | 25 ------------------------- src/main/java/app/Main.java | 3 +-- src/main/java/view/LoggedInView.java | 24 ++---------------------- 3 files changed, 3 insertions(+), 49 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 95624cdc..f8243e00 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -67,8 +67,6 @@ import use_case.market_status.MarketStatusOutputBoundary; import use_case.market_status.MarketStatusDataAccessInterface; import data_access.MarketStatusDataAccessObject; -import data_access.FinnhubTradeDataAccessObject; -import use_case.TradeFeed; import view.*; import javax.swing.*; @@ -100,7 +98,6 @@ public class AppBuilder { final MarketStatusDataAccessInterface marketStatusDataAccessObject = new MarketStatusDataAccessObject(apiKey); - // DAO version using a shared external database // final DBUserDataAccessObject userDataAccessObject = new DBUserDataAccessObject(userFactory); @@ -118,7 +115,6 @@ public class AppBuilder { private NewsView newsView; private EarningsHistoryViewModel earningsHistoryViewModel; private EarningsHistoryView earningsHistoryView; - private TradeView tradeView; private MarketStatusViewModel marketStatusViewModel; private MarketStatusController marketStatusController; @@ -187,15 +183,6 @@ public AppBuilder addEarningsHistoryView() { return this; } - public AppBuilder addTradeView() { - TradeFeed tradeFeed = new FinnhubTradeDataAccessObject(); - tradeView = new TradeView(tradeFeed); - String viewName = tradeView.getViewName(); - System.out.println("Adding TradeView with name: " + viewName); // debug - cardPanel.add(tradeView, viewName); - return this; - } - public AppBuilder addSignupUseCase() { final SignupOutputBoundary signupOutputBoundary = new SignupPresenter(viewManagerModel, @@ -303,18 +290,6 @@ public AppBuilder addMarketStatusUseCase() { return this; } - public AppBuilder addRealtimeTradeUseCase() { - // Logged-in page: Realtime Trade button → Trade view - String tradeViewName = tradeView.getViewName(); - System.out.println("Setting up Realtime Trade navigation to view: " + tradeViewName); // debug - loggedInView.setRealtimeTradeNavigation(viewManagerModel, tradeViewName); - - // Trade page: Back button → Logged-in view - tradeView.setBackNavigation(viewManagerModel, loggedInView.getViewName()); - - return this; - } - public JFrame build() { final JFrame application = new JFrame("Stock Application"); application.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 2b930cc1..7bea6e4b 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -16,14 +16,13 @@ public static void main(String[] args) { .addFilterSearchUseCase() .addNewsView() .addEarningsHistoryView() - .addTradeView() + .addStockSearchUseCase() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() .addNewsUsecase() .addEarningsHistoryUseCase() .addMarketStatusUseCase() - .addRealtimeTradeUseCase() .build(); application.pack(); diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 3a1c1591..03b66651 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -34,7 +34,6 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private String newsViewName; private String filterSearchViewName; private String historyViewName; - private String realtimeTradeViewName; private JLabel marketStatusLabel; private MarketStatusViewModel marketStatusViewModel; @@ -47,11 +46,11 @@ public class LoggedInView extends JPanel implements ActionListener, PropertyChan private final JTextField passwordInputField = new JTextField(15); private final JButton changePassword; + private final JTextField searchStockField = new JTextField(25); private final JButton searchButton = new JButton("Search"); private final JTextArea stockInfoArea = new JTextArea(); private final JButton addToWatchlistButton = new JButton("Add to Watchlist"); - final JButton historyButton = new JButton("History"); public LoggedInView(LoggedInViewModel loggedInViewModel) { this.loggedInViewModel = loggedInViewModel; @@ -94,27 +93,12 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { viewManagerModel.firePropertyChange(); } }); - - final JButton realtimeTradeButton = new JButton("Realtime Trade"); - realtimeTradeButton.addActionListener(e -> { - System.out.println("Realtime Trade button clicked"); // debug - System.out.println("viewManagerModel: " + viewManagerModel); // debug - System.out.println("realtimeTradeViewName: " + realtimeTradeViewName); // debug - if (viewManagerModel != null && realtimeTradeViewName != null) { - viewManagerModel.setState(realtimeTradeViewName); - viewManagerModel.firePropertyChange(); - } else { - System.out.println("ERROR: Cannot navigate - viewManagerModel or realtimeTradeViewName is null"); // debug - } - }); - final JButton marketOpenButton = new JButton("Market Open"); final JButton accountButton = new JButton("Account"); topToolbar.add(newsButton); topToolbar.add(filterSearchButton); topToolbar.add(historyButton); - topToolbar.add(realtimeTradeButton); topToolbar.add(marketOpenButton); topToolbar.add(accountButton); @@ -279,11 +263,7 @@ public void setHistoryNavigation(ViewManagerModel viewManagerModel, this.historyViewName = historyViewName; } - public void setRealtimeTradeNavigation(ViewManagerModel viewManagerModel, - String tradeViewName) { - this.viewManagerModel = viewManagerModel; - this.realtimeTradeViewName = tradeViewName; - } + public void setMarketStatusViewModel(MarketStatusViewModel viewModel) { this.marketStatusViewModel = viewModel; From e5ea2b0c75211af6498ee91a89315d2bf04fbb32 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Mon, 1 Dec 2025 13:56:00 -0500 Subject: [PATCH 083/103] readme updated --- README.md | 153 +++++++++++++++++- src/main/java/app/AppBuilder.java | 3 +- .../FinnhubTradeDataAccessObject.java | 4 +- .../java/data_access/FinnhubTradeFeed.java | 4 +- .../java/use_case/{ => trade}/TradeFeed.java | 2 +- .../use_case/{ => trade}/TradeListener.java | 2 +- src/main/java/view/TradeView.java | 7 +- 7 files changed, 161 insertions(+), 14 deletions(-) rename src/main/java/use_case/{ => trade}/TradeFeed.java (95%) rename src/main/java/use_case/{ => trade}/TradeListener.java (95%) diff --git a/README.md b/README.md index 574b7def..50135fb0 100644 --- a/README.md +++ b/README.md @@ -1 +1,152 @@ -# Stock Project \ No newline at end of file +# Stock Project + +A comprehensive stock market application built with Java Swing, following Clean Architecture principles. This application provides real-time stock data, news, earnings history, and market status information. + +## Features + +### User Management +- **User Registration**: Create a new account with username and password +- **User Login**: Secure authentication to access the application +- **Password Management**: Change password functionality +- **User Logout**: Secure session termination + +### Stock Information +- **Stock Search**: Search for stocks by symbol with detailed information +- **Filter Search**: Advanced filtering capabilities for stock discovery +- **Real-Time Trade Feed**: Live WebSocket connection to receive real-time trade data + - Connect to any trading symbol (e.g., `BINANCE:BTCUSDT`, `AAPL`, `TSLA`) + - View real-time price, volume, and timestamp updates + - Rate limiting protection to prevent API throttling + - Automatic error handling for invalid symbols or connection issues + +### Market Data +- **Market News**: View latest market and company-specific news articles +- **Earnings History**: Access historical earnings data for companies +- **Market Status**: Real-time market open/closed status indicator + +## Architecture + +This project follows **Clean Architecture** principles with clear separation of concerns: + +``` +src/main/java/ +├── app/ # Application entry point and dependency injection +├── data_access/ # Infrastructure layer (API clients, file I/O) +├── entity/ # Domain models +├── interface_adapter/ # Adapters (ViewModels, Controllers, Presenters) +├── use_case/ # Business logic (Interactors) +└── view/ # Presentation layer (Swing UI) +``` + +### Key Components + +- **Entity Layer**: Domain models (`User`, `Stock`, `Trade`, `NewsArticle`, etc.) +- **Use Case Layer**: Business logic and application rules +- **Interface Adapter Layer**: Controllers, Presenters, and ViewModels +- **Framework Layer**: Data access objects and UI components + +## Getting Started + +### Prerequisites +- Java 15 or higher +- Maven 3.6+ +- Internet connection (for API access) + +### Running the Application + +1. Clone the repository +2. Navigate to the project directory +3. Build the project: + ```bash + mvn clean compile + ``` +4. Run the application: + ```bash + mvn exec:java -Dexec.mainClass="app.Main" + ``` + +Or run directly from your IDE by executing `app.Main`. + +### First Time Setup + +1. **Create an Account**: Click "Sign Up" to create a new user account +2. **Login**: Use your credentials to log in +3. **Explore Features**: Navigate using the top toolbar buttons + +## Usage Guide + +### Real-Time Trade Feed + +1. Click the **"Realtime Trade"** button in the top toolbar +2. Enter a trading symbol in the input field (default: `BINANCE:BTCUSDT`) +3. Click **"Connect"** to start receiving real-time trade data +4. View live updates for: + - Symbol + - Last Price + - Volume + - Timestamp +5. Click **"Disconnect"** to stop the feed before connecting to a new symbol + +**Important Notes:** +- You must disconnect before connecting to a new symbol +- Rate limiting is enforced (5-second cooldown between connections) +- Invalid symbols or connection errors will display appropriate error messages +- The application automatically handles API rate limits (429 errors) + +### Other Features + +- **News**: View market and company news with filtering options +- **History**: Access earnings history for companies +- **Filter Search**: Search and filter stocks by various criteria +- **Market Status**: View current market status (open/closed) + +## Technical Details + +### Dependencies +- **OkHttp 4.12.0**: For HTTP and WebSocket connections +- **JSON 20240303**: For JSON parsing +- **JUnit 5.8.1**: For testing (test scope) + +### API Integration +- **Finnhub API**: Used for stock data, news, earnings, and real-time trade feeds +- WebSocket connections for real-time data streaming +- REST API for historical and static data + +### Data Storage +- User data stored in `users.csv` (file-based storage) +- Alternative database storage option available (`DBUserDataAccessObject`) + +## Project Structure + +``` +CSC207_Stock_Project/ +├── src/ +│ ├── main/java/ +│ │ ├── app/ # Application setup +│ │ ├── data_access/ # API clients and data sources +│ │ ├── entity/ # Domain models +│ │ ├── interface_adapter/# Adapters +│ │ ├── use_case/ # Business logic +│ │ └── view/ # UI components +│ └── test/java/ # Unit tests +├── pom.xml # Maven configuration +└── README.md # This file +``` + +## Error Handling + +The application includes comprehensive error handling: +- **Rate Limiting**: Automatic cooldown periods to prevent API throttling +- **Connection Errors**: User-friendly error messages for connection issues +- **Invalid Symbols**: Clear feedback when symbols are not found +- **Network Issues**: Graceful handling of network failures + +## Contributing + +This is a course project following Clean Architecture principles. When contributing: +- Maintain separation of concerns +- Follow the existing architecture patterns +- Add appropriate error handling +- Include unit tests for new features + + diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 95624cdc..f176cf24 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -68,7 +68,7 @@ import use_case.market_status.MarketStatusDataAccessInterface; import data_access.MarketStatusDataAccessObject; import data_access.FinnhubTradeDataAccessObject; -import use_case.TradeFeed; +import use_case.trade.TradeFeed; import view.*; import javax.swing.*; @@ -191,7 +191,6 @@ public AppBuilder addTradeView() { TradeFeed tradeFeed = new FinnhubTradeDataAccessObject(); tradeView = new TradeView(tradeFeed); String viewName = tradeView.getViewName(); - System.out.println("Adding TradeView with name: " + viewName); // debug cardPanel.add(tradeView, viewName); return this; } diff --git a/src/main/java/data_access/FinnhubTradeDataAccessObject.java b/src/main/java/data_access/FinnhubTradeDataAccessObject.java index b7967289..d5c5302f 100644 --- a/src/main/java/data_access/FinnhubTradeDataAccessObject.java +++ b/src/main/java/data_access/FinnhubTradeDataAccessObject.java @@ -6,8 +6,8 @@ import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; -import use_case.TradeFeed; -import use_case.TradeListener; +import use_case.trade.TradeFeed; +import use_case.trade.TradeListener; import java.time.Instant; import java.util.concurrent.TimeUnit; diff --git a/src/main/java/data_access/FinnhubTradeFeed.java b/src/main/java/data_access/FinnhubTradeFeed.java index b4a9e1e7..296aa9a5 100644 --- a/src/main/java/data_access/FinnhubTradeFeed.java +++ b/src/main/java/data_access/FinnhubTradeFeed.java @@ -6,8 +6,8 @@ import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; -import use_case.TradeFeed; -import use_case.TradeListener; +import use_case.trade.TradeFeed; +import use_case.trade.TradeListener; import java.time.Instant; import java.util.concurrent.TimeUnit; diff --git a/src/main/java/use_case/TradeFeed.java b/src/main/java/use_case/trade/TradeFeed.java similarity index 95% rename from src/main/java/use_case/TradeFeed.java rename to src/main/java/use_case/trade/TradeFeed.java index 5eff9f2e..afc588b0 100644 --- a/src/main/java/use_case/TradeFeed.java +++ b/src/main/java/use_case/trade/TradeFeed.java @@ -1,4 +1,4 @@ -package use_case; +package use_case.trade; /** * Abstraction for any real-time trade data source. diff --git a/src/main/java/use_case/TradeListener.java b/src/main/java/use_case/trade/TradeListener.java similarity index 95% rename from src/main/java/use_case/TradeListener.java rename to src/main/java/use_case/trade/TradeListener.java index 4d8b2f18..4174c8cd 100644 --- a/src/main/java/use_case/TradeListener.java +++ b/src/main/java/use_case/trade/TradeListener.java @@ -1,4 +1,4 @@ -package use_case; +package use_case.trade; import entity.Trade; diff --git a/src/main/java/view/TradeView.java b/src/main/java/view/TradeView.java index be63732e..9b75d1bd 100644 --- a/src/main/java/view/TradeView.java +++ b/src/main/java/view/TradeView.java @@ -2,15 +2,14 @@ import entity.Trade; import interface_adapter.ViewManagerModel; -import use_case.TradeFeed; -import use_case.TradeListener; +import use_case.trade.TradeFeed; +import use_case.trade.TradeListener; import javax.swing.*; import java.awt.*; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import java.util.logging.Logger; /** * Swing view for displaying real-time trade data. @@ -18,8 +17,6 @@ */ public class TradeView extends JPanel { - private static final Logger LOGGER = Logger.getLogger(TradeView.class.getName()); - private final String viewName = "trade"; private ViewManagerModel viewManagerModel; private String homeViewName; From e2e1da079178494cb0bd02802b9133495e54a01d Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Mon, 1 Dec 2025 14:50:47 -0500 Subject: [PATCH 084/103] history test --- .../GetEarningsHistoryInteractorTest.java | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 src/test/java/use_case/earnings_history/GetEarningsHistoryInteractorTest.java diff --git a/src/test/java/use_case/earnings_history/GetEarningsHistoryInteractorTest.java b/src/test/java/use_case/earnings_history/GetEarningsHistoryInteractorTest.java new file mode 100644 index 00000000..c45deb8a --- /dev/null +++ b/src/test/java/use_case/earnings_history/GetEarningsHistoryInteractorTest.java @@ -0,0 +1,194 @@ +package use_case.earnings_history; + +import entity.EarningsRecord; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for GetEarningsHistoryInteractor. + */ +public class GetEarningsHistoryInteractorTest { + + /** + * Simple in-memory stub for EarningsDataAccessInterface. + * Configure its fields before each test. + */ + private static class InMemoryEarningsDao implements EarningsDataAccessInterface { + + List recordsToReturn = new ArrayList<>(); + boolean throwException = false; + + String lastSymbol; + + @Override + public List getEarningsFor(String symbol) throws IOException { + lastSymbol = symbol; + if (throwException) { + throw new IOException("boom"); + } + return recordsToReturn; + } + } + + /** + * Recording presenter that stores everything the interactor sends. + * It implements **all** methods in GetEarningsHistoryOutputBoundary. + */ + private static class RecordingPresenter implements GetEarningsHistoryOutputBoundary { + + GetEarningsHistoryOutputData lastSuccess; + String lastSymbolErrorMessage; + String lastConnectionErrorMessage; + String lastNoDataMessage; + + @Override + public void prepareSuccessView(GetEarningsHistoryOutputData outputData) { + lastSuccess = outputData; + } + + @Override + public void prepareSymbolErrorView(String message) { + lastSymbolErrorMessage = message; + } + + @Override + public void prepareConnectionErrorView(String message) { + lastConnectionErrorMessage = message; + } + + @Override + public void prepareNoDataView(String message) { + lastNoDataMessage = message; + } + } + + private EarningsRecord sampleRecord(String period) { + return new EarningsRecord(period, 1.23, 1.00, 0.23); + } + + @Test + void blankSymbol_reportsSymbolError_andDoesNotCallDao() { + InMemoryEarningsDao dao = new InMemoryEarningsDao(); + RecordingPresenter presenter = new RecordingPresenter(); + GetEarningsHistoryInteractor interactor = + new GetEarningsHistoryInteractor(dao, presenter); + + // symbol is blank → invalid + GetEarningsHistoryInputData input = + new GetEarningsHistoryInputData(" "); + + interactor.execute(input); + + assertNull(dao.lastSymbol, "DAO must not be called for blank symbol"); + assertNull(presenter.lastSuccess, "No success result expected"); + assertNull(presenter.lastConnectionErrorMessage); + assertNull(presenter.lastNoDataMessage); + assertNotNull(presenter.lastSymbolErrorMessage, "Symbol error should be reported"); + assertTrue(presenter.lastSymbolErrorMessage.toLowerCase().contains("symbol"), + "Error message should mention symbol"); + } + + @Test + void daoThrowsIOException_reportsConnectionError() { + InMemoryEarningsDao dao = new InMemoryEarningsDao(); + dao.throwException = true; + + RecordingPresenter presenter = new RecordingPresenter(); + GetEarningsHistoryInteractor interactor = + new GetEarningsHistoryInteractor(dao, presenter); + + GetEarningsHistoryInputData input = + new GetEarningsHistoryInputData("AAPL"); + + interactor.execute(input); + + assertEquals("AAPL", dao.lastSymbol); + assertNull(presenter.lastSuccess); + assertNull(presenter.lastSymbolErrorMessage); + assertNull(presenter.lastNoDataMessage); + assertNotNull(presenter.lastConnectionErrorMessage, + "Connection/API error should be reported"); + } + + @Test + void daoReturnsNull_reportsSymbolNotSupported() { + InMemoryEarningsDao dao = new InMemoryEarningsDao(); + dao.recordsToReturn = null; // contract: null = company not found / unsupported + + RecordingPresenter presenter = new RecordingPresenter(); + GetEarningsHistoryInteractor interactor = + new GetEarningsHistoryInteractor(dao, presenter); + + GetEarningsHistoryInputData input = + new GetEarningsHistoryInputData("AAPL"); + + interactor.execute(input); + + assertEquals("AAPL", dao.lastSymbol); + assertNull(presenter.lastSuccess); + assertNull(presenter.lastConnectionErrorMessage); + assertNull(presenter.lastNoDataMessage); + assertNotNull(presenter.lastSymbolErrorMessage); + String msg = presenter.lastSymbolErrorMessage.toLowerCase(); + assertTrue(msg.contains("not found") || msg.contains("not supported"), + "Error should mention company not found/unsupported"); + } + + @Test + void daoReturnsEmptyList_reportsNoData() { + InMemoryEarningsDao dao = new InMemoryEarningsDao(); + dao.recordsToReturn = List.of(); // valid symbol, but no earnings + + RecordingPresenter presenter = new RecordingPresenter(); + GetEarningsHistoryInteractor interactor = + new GetEarningsHistoryInteractor(dao, presenter); + + GetEarningsHistoryInputData input = + new GetEarningsHistoryInputData("AAPL"); + + interactor.execute(input); + + assertEquals("AAPL", dao.lastSymbol); + assertNull(presenter.lastSuccess); + assertNull(presenter.lastSymbolErrorMessage); + assertNull(presenter.lastConnectionErrorMessage); + assertNotNull(presenter.lastNoDataMessage); + String msg = presenter.lastNoDataMessage.toLowerCase(); + assertTrue(msg.contains("no earnings") || msg.contains("no data"), + "No-data message should mention missing earnings"); + } + + @Test + void daoReturnsRecords_callsSuccessWithThoseRecords() { + InMemoryEarningsDao dao = new InMemoryEarningsDao(); + dao.recordsToReturn = List.of( + sampleRecord("2024-Q1"), + sampleRecord("2023-Q4") + ); + + RecordingPresenter presenter = new RecordingPresenter(); + GetEarningsHistoryInteractor interactor = + new GetEarningsHistoryInteractor(dao, presenter); + + GetEarningsHistoryInputData input = + new GetEarningsHistoryInputData("AAPL"); + + interactor.execute(input); + + assertEquals("AAPL", dao.lastSymbol); + assertNull(presenter.lastSymbolErrorMessage); + assertNull(presenter.lastConnectionErrorMessage); + assertNull(presenter.lastNoDataMessage); + assertNotNull(presenter.lastSuccess, "Success output expected"); + + List records = presenter.lastSuccess.getRecords(); + assertEquals(2, records.size()); + assertEquals("2024-Q1", records.get(0).getPeriod()); + assertEquals("2023-Q4", records.get(1).getPeriod()); + } +} From 5f3af1b66235f8f513546df1c46beb93994b56c3 Mon Sep 17 00:00:00 2001 From: zoe Date: Mon, 1 Dec 2025 14:54:27 -0500 Subject: [PATCH 085/103] made account --- src/main/java/app/AppBuilder.java | 1 + src/main/java/view/AccountView.java | 26 +++++++++++++++++++------- src/main/java/view/LoggedInView.java | 26 +++++++++++++++++--------- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 1e4847c3..a155e405 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -284,6 +284,7 @@ public AppBuilder addAccount() { accountView = new AccountView(accountViewModel); accountView.setController(accountController); cardPanel.add(accountView, accountView.getViewName()); + loggedInView.setAccountController(accountController); // Logged in page: Account button → Account view loggedInView.setAccountNavigation( diff --git a/src/main/java/view/AccountView.java b/src/main/java/view/AccountView.java index 981fc51a..3f29947f 100644 --- a/src/main/java/view/AccountView.java +++ b/src/main/java/view/AccountView.java @@ -54,25 +54,37 @@ public AccountView(AccountViewModel accountViewModel) { } public void propertyChange(PropertyChangeEvent evt) { + // Update username label String username = viewModel.getState().getUsername(); usernameLabel.setText("Username: " + username); + // Clear previous watchlist items watchlistPanel.removeAll(); + + // Get the watchlist safely ArrayList watchlist = viewModel.getState().getWatchlist(); if (watchlist == null) { watchlist = new ArrayList<>(); - for (JSONObject item : watchlist) { - JPanel itemPanel = new JPanel(); - itemPanel.setLayout(new FlowLayout(FlowLayout.LEFT)); - itemPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY)); + } - JLabel stockLabel = new JLabel(item.getString("stock")); - itemPanel.add(stockLabel); + // Populate watchlist safely + for (JSONObject item : watchlist) { + JPanel itemPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + itemPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY)); - watchlistPanel.add(itemPanel); + // Safely get the info text + String text = "Unknown"; + if (item != null && item.has("info")) { + text = item.getString("info"); } + + JLabel stockLabel = new JLabel(text); + itemPanel.add(stockLabel); + + watchlistPanel.add(itemPanel); } + // Refresh UI watchlistPanel.revalidate(); watchlistPanel.repaint(); } diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 3f895dc1..0923934e 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -10,6 +10,7 @@ import interface_adapter.news.NewsController; import interface_adapter.ViewManagerModel; import interface_adapter.market_status.MarketStatusViewModel; +import org.json.JSONObject; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -101,8 +102,8 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { final JButton accountButton = new JButton("Account"); accountButton.addActionListener(e -> { - System.out.println("Account button clicked"); // debug - if (viewManagerModel != null && newsViewName != null) { + System.out.println("Account button clicked"); + if (viewManagerModel != null && accountViewName != null) { viewManagerModel.setState(accountViewName); viewManagerModel.firePropertyChange(); } @@ -152,13 +153,16 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { final JPanel bottomPanel = new JPanel(new BorderLayout()); bottomPanel.add(addToWatchlistButton, BorderLayout.CENTER); -// addToWatchlistButton.addActionListener(e -> { -// if (accountController != null) { -// -// JSONObject newItem = ...; // get stock info from somewhere -// accountController.addToWatchlist(username, newItem); -// } -// }); + addToWatchlistButton.addActionListener(e -> { + String username = loggedInViewModel.getState().getUsername(); + System.out.println("watchlist button clicked"); + if (accountController != null) { + JSONObject newItem = new JSONObject(); +// newItem.put("stock", stockSymbol); + newItem.put("info", stockInfoArea.getText()); + accountController.addToWatchlist(username, newItem); + } + }); this.add(bottomPanel, BorderLayout.SOUTH); @@ -261,6 +265,10 @@ public void setStockSearchViewModel(StockSearchViewModel stockSearchViewModel) { }); } + public void setAccountController(AccountController accountController) { + this.accountController = accountController; + } + public void setLogoutController(LogoutController logoutController) { // TODO: save the logout controller in the instance variable. } From f892fffe03fcb7c26277f88b57e33d467bfbbc97 Mon Sep 17 00:00:00 2001 From: zoe Date: Mon, 1 Dec 2025 14:58:46 -0500 Subject: [PATCH 086/103] made account --- src/main/java/view/AccountView.java | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main/java/view/AccountView.java b/src/main/java/view/AccountView.java index 3f29947f..da9a4c08 100644 --- a/src/main/java/view/AccountView.java +++ b/src/main/java/view/AccountView.java @@ -67,24 +67,26 @@ public void propertyChange(PropertyChangeEvent evt) { watchlist = new ArrayList<>(); } - // Populate watchlist safely for (JSONObject item : watchlist) { - JPanel itemPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - itemPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY)); + if (item == null) continue; - // Safely get the info text - String text = "Unknown"; - if (item != null && item.has("info")) { + String text = null; + if (item.has("stock")) { + text = item.getString("stock"); + } else if (item.has("info")) { text = item.getString("info"); } - JLabel stockLabel = new JLabel(text); - itemPanel.add(stockLabel); + if (text != null && !text.isEmpty()) { + JPanel itemPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + itemPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY)); - watchlistPanel.add(itemPanel); - } + JLabel stockLabel = new JLabel(text); + itemPanel.add(stockLabel); - // Refresh UI + watchlistPanel.add(itemPanel); + } + } watchlistPanel.revalidate(); watchlistPanel.repaint(); } From fc620b16284478d096cd67b3accb88087b93f0fb Mon Sep 17 00:00:00 2001 From: zoe Date: Mon, 1 Dec 2025 15:00:08 -0500 Subject: [PATCH 087/103] made account --- src/main/java/view/AccountView.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/view/AccountView.java b/src/main/java/view/AccountView.java index da9a4c08..b847c4aa 100644 --- a/src/main/java/view/AccountView.java +++ b/src/main/java/view/AccountView.java @@ -54,14 +54,12 @@ public AccountView(AccountViewModel accountViewModel) { } public void propertyChange(PropertyChangeEvent evt) { - // Update username label + String username = viewModel.getState().getUsername(); usernameLabel.setText("Username: " + username); - // Clear previous watchlist items watchlistPanel.removeAll(); - // Get the watchlist safely ArrayList watchlist = viewModel.getState().getWatchlist(); if (watchlist == null) { watchlist = new ArrayList<>(); From 845b99c984c118db40fba2fcd802d8b863d0d241 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Mon, 1 Dec 2025 15:01:53 -0500 Subject: [PATCH 088/103] architecture updated with fixed bugs --- README.md | 15 - src/main/java/app/AppBuilder.java | 26 +- .../FinnhubTradeDataAccessObject.java | 6 +- .../java/data_access/FinnhubTradeFeed.java | 6 +- .../trade/TradeController.java | 26 ++ .../trade/TradePresenter.java | 36 +++ .../interface_adapter/trade/TradeState.java | 49 +++ .../trade/TradeViewModel.java | 34 +++ .../trade/TradeDataAccessInterface.java | 21 ++ .../use_case/trade/TradeInputBoundary.java | 18 ++ .../java/use_case/trade/TradeInteractor.java | 62 ++++ .../use_case/trade/TradeOutputBoundary.java | 26 ++ .../use_case/trade/TradeRequestModel.java | 17 ++ .../use_case/trade/TradeResponseModel.java | 31 ++ src/main/java/view/TradeView.java | 287 +++++++++--------- 15 files changed, 489 insertions(+), 171 deletions(-) create mode 100644 src/main/java/interface_adapter/trade/TradeController.java create mode 100644 src/main/java/interface_adapter/trade/TradePresenter.java create mode 100644 src/main/java/interface_adapter/trade/TradeState.java create mode 100644 src/main/java/interface_adapter/trade/TradeViewModel.java create mode 100644 src/main/java/use_case/trade/TradeDataAccessInterface.java create mode 100644 src/main/java/use_case/trade/TradeInputBoundary.java create mode 100644 src/main/java/use_case/trade/TradeInteractor.java create mode 100644 src/main/java/use_case/trade/TradeOutputBoundary.java create mode 100644 src/main/java/use_case/trade/TradeRequestModel.java create mode 100644 src/main/java/use_case/trade/TradeResponseModel.java diff --git a/README.md b/README.md index 50135fb0..ef0260a8 100644 --- a/README.md +++ b/README.md @@ -52,21 +52,6 @@ src/main/java/ - Maven 3.6+ - Internet connection (for API access) -### Running the Application - -1. Clone the repository -2. Navigate to the project directory -3. Build the project: - ```bash - mvn clean compile - ``` -4. Run the application: - ```bash - mvn exec:java -Dexec.mainClass="app.Main" - ``` - -Or run directly from your IDE by executing `app.Main`. - ### First Time Setup 1. **Create an Account**: Click "Sign Up" to create a new user account diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index f176cf24..c82d68b4 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -68,7 +68,13 @@ import use_case.market_status.MarketStatusDataAccessInterface; import data_access.MarketStatusDataAccessObject; import data_access.FinnhubTradeDataAccessObject; -import use_case.trade.TradeFeed; +import use_case.trade.TradeDataAccessInterface; +import use_case.trade.TradeInputBoundary; +import use_case.trade.TradeInteractor; +import use_case.trade.TradeOutputBoundary; +import interface_adapter.trade.TradeController; +import interface_adapter.trade.TradePresenter; +import interface_adapter.trade.TradeViewModel; import view.*; import javax.swing.*; @@ -188,10 +194,20 @@ public AppBuilder addEarningsHistoryView() { } public AppBuilder addTradeView() { - TradeFeed tradeFeed = new FinnhubTradeDataAccessObject(); - tradeView = new TradeView(tradeFeed); - String viewName = tradeView.getViewName(); - cardPanel.add(tradeView, viewName); + // ViewModel + TradeViewModel tradeViewModel = new TradeViewModel(); + + // Presenter + Interactor + TradeOutputBoundary tradeOutputBoundary = new TradePresenter(tradeViewModel); + TradeDataAccessInterface tradeDataAccess = new FinnhubTradeDataAccessObject(); + TradeInputBoundary tradeInteractor = new TradeInteractor(tradeDataAccess, tradeOutputBoundary); + + // Controller + TradeController tradeController = new TradeController(tradeInteractor); + + // Swing view + tradeView = new TradeView(tradeController, tradeViewModel); + cardPanel.add(tradeView, tradeView.getViewName()); return this; } diff --git a/src/main/java/data_access/FinnhubTradeDataAccessObject.java b/src/main/java/data_access/FinnhubTradeDataAccessObject.java index d5c5302f..ab995e32 100644 --- a/src/main/java/data_access/FinnhubTradeDataAccessObject.java +++ b/src/main/java/data_access/FinnhubTradeDataAccessObject.java @@ -6,7 +6,7 @@ import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; -import use_case.trade.TradeFeed; +import use_case.trade.TradeDataAccessInterface; import use_case.trade.TradeListener; import java.time.Instant; @@ -15,10 +15,10 @@ import java.util.logging.Logger; /** - * Finnhub-specific implementation of the TradeFeed abstraction. + * Finnhub-specific implementation of the TradeDataAccessInterface. * This class belongs to the data_access / infrastructure layer and knows about WebSockets and JSON format. */ -public class FinnhubTradeDataAccessObject implements TradeFeed { +public class FinnhubTradeDataAccessObject implements TradeDataAccessInterface { private static final Logger LOGGER = Logger.getLogger(FinnhubTradeDataAccessObject.class.getName()); diff --git a/src/main/java/data_access/FinnhubTradeFeed.java b/src/main/java/data_access/FinnhubTradeFeed.java index 296aa9a5..a9480177 100644 --- a/src/main/java/data_access/FinnhubTradeFeed.java +++ b/src/main/java/data_access/FinnhubTradeFeed.java @@ -6,7 +6,7 @@ import okhttp3.Response; import okhttp3.WebSocket; import okhttp3.WebSocketListener; -import use_case.trade.TradeFeed; +import use_case.trade.TradeDataAccessInterface; import use_case.trade.TradeListener; import java.time.Instant; @@ -15,10 +15,10 @@ import java.util.logging.Logger; /** - * Finnhub-specific implementation of the TradeFeed abstraction. + * Finnhub-specific implementation of the TradeDataAccessInterface. * This class belongs to the data_access / infrastructure layer and knows about WebSockets and JSON format. */ -public class FinnhubTradeFeed implements TradeFeed { +public class FinnhubTradeFeed implements TradeDataAccessInterface { private static final Logger LOGGER = Logger.getLogger(FinnhubTradeFeed.class.getName()); diff --git a/src/main/java/interface_adapter/trade/TradeController.java b/src/main/java/interface_adapter/trade/TradeController.java new file mode 100644 index 00000000..8284a3cc --- /dev/null +++ b/src/main/java/interface_adapter/trade/TradeController.java @@ -0,0 +1,26 @@ +package interface_adapter.trade; + +import use_case.trade.TradeInputBoundary; +import use_case.trade.TradeRequestModel; + +/** + * Controller for the Trade use case. + */ +public class TradeController { + + private final TradeInputBoundary tradeInteractor; + + public TradeController(TradeInputBoundary tradeInteractor) { + this.tradeInteractor = tradeInteractor; + } + + public void execute(String symbol) { + TradeRequestModel requestModel = new TradeRequestModel(symbol); + tradeInteractor.execute(requestModel); + } + + public void disconnect() { + tradeInteractor.disconnect(); + } +} + diff --git a/src/main/java/interface_adapter/trade/TradePresenter.java b/src/main/java/interface_adapter/trade/TradePresenter.java new file mode 100644 index 00000000..4f091de3 --- /dev/null +++ b/src/main/java/interface_adapter/trade/TradePresenter.java @@ -0,0 +1,36 @@ +package interface_adapter.trade; + +import entity.Trade; +import use_case.trade.TradeOutputBoundary; +import use_case.trade.TradeResponseModel; + +/** + * Presenter for the Trade use case. + */ +public class TradePresenter implements TradeOutputBoundary { + + private final TradeViewModel viewModel; + + public TradePresenter(TradeViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void prepareSuccessView(TradeResponseModel responseModel) { + Trade trade = responseModel.getTrade(); + if (trade != null) { + viewModel.setCurrentTrade(trade); + } + } + + @Override + public void prepareFailView(String errorMessage) { + viewModel.setStatus(errorMessage, true); + } + + @Override + public void prepareStatusView(String statusText, boolean isError) { + viewModel.setStatus(statusText, isError); + } +} + diff --git a/src/main/java/interface_adapter/trade/TradeState.java b/src/main/java/interface_adapter/trade/TradeState.java new file mode 100644 index 00000000..2964b61e --- /dev/null +++ b/src/main/java/interface_adapter/trade/TradeState.java @@ -0,0 +1,49 @@ +package interface_adapter.trade; + +import entity.Trade; + +/** + * State for the Trade view. + */ +public class TradeState { + private Trade currentTrade; + private String statusText = "Status: Disconnected"; + private boolean isError = false; + private String symbol = ""; + + public TradeState() { + } + + public Trade getCurrentTrade() { + return currentTrade; + } + + public void setCurrentTrade(Trade currentTrade) { + this.currentTrade = currentTrade; + } + + public String getStatusText() { + return statusText; + } + + public void setStatusText(String statusText) { + this.statusText = statusText; + } + + public boolean isError() { + return isError; + } + + public void setError(boolean error) { + isError = error; + } + + public String getSymbol() { + return symbol; + } + + public void setSymbol(String symbol) { + this.symbol = symbol; + } +} + diff --git a/src/main/java/interface_adapter/trade/TradeViewModel.java b/src/main/java/interface_adapter/trade/TradeViewModel.java new file mode 100644 index 00000000..108ad399 --- /dev/null +++ b/src/main/java/interface_adapter/trade/TradeViewModel.java @@ -0,0 +1,34 @@ +package interface_adapter.trade; + +import interface_adapter.ViewModel; + +/** + * ViewModel for the Trade view. + */ +public class TradeViewModel extends ViewModel { + + public TradeViewModel() { + super("trade"); + setState(new TradeState()); + } + + public void setCurrentTrade(entity.Trade trade) { + TradeState state = getState(); + state.setCurrentTrade(trade); + firePropertyChange(); + } + + public void setStatus(String statusText, boolean isError) { + TradeState state = getState(); + state.setStatusText(statusText); + state.setError(isError); + firePropertyChange(); + } + + public void setSymbol(String symbol) { + TradeState state = getState(); + state.setSymbol(symbol); + firePropertyChange(); + } +} + diff --git a/src/main/java/use_case/trade/TradeDataAccessInterface.java b/src/main/java/use_case/trade/TradeDataAccessInterface.java new file mode 100644 index 00000000..93af0d5a --- /dev/null +++ b/src/main/java/use_case/trade/TradeDataAccessInterface.java @@ -0,0 +1,21 @@ +package use_case.trade; + +/** + * Data access interface for trade feed operations. + * Infrastructure implementations (e.g., Finnhub WebSocket) should implement this. + */ +public interface TradeDataAccessInterface { + + /** + * Connects to the trade feed and begins streaming trades for the given symbol. + * @param symbol The trading symbol to subscribe to (e.g., "BINANCE:BTCUSDT", "AAPL") + * @param listener The listener to receive trade updates + */ + void connect(String symbol, TradeListener listener); + + /** + * Gracefully disconnects from the trade feed. + */ + void disconnect(); +} + diff --git a/src/main/java/use_case/trade/TradeInputBoundary.java b/src/main/java/use_case/trade/TradeInputBoundary.java new file mode 100644 index 00000000..a850e47e --- /dev/null +++ b/src/main/java/use_case/trade/TradeInputBoundary.java @@ -0,0 +1,18 @@ +package use_case.trade; + +/** + * Input boundary for the trade use case. + */ +public interface TradeInputBoundary { + /** + * Executes the trade connection use case. + * @param requestModel The request containing the symbol to connect to + */ + void execute(TradeRequestModel requestModel); + + /** + * Executes the trade disconnection use case. + */ + void disconnect(); +} + diff --git a/src/main/java/use_case/trade/TradeInteractor.java b/src/main/java/use_case/trade/TradeInteractor.java new file mode 100644 index 00000000..7a3ab0b4 --- /dev/null +++ b/src/main/java/use_case/trade/TradeInteractor.java @@ -0,0 +1,62 @@ +package use_case.trade; + +import entity.Trade; + +/** + * Interactor for the trade use case. + * Implements the business logic for connecting to and receiving trade data. + */ +public class TradeInteractor implements TradeInputBoundary { + + private final TradeDataAccessInterface tradeDataAccess; + private final TradeOutputBoundary tradePresenter; + private final TradeListener internalListener; + + public TradeInteractor(TradeDataAccessInterface tradeDataAccess, + TradeOutputBoundary tradePresenter) { + this.tradeDataAccess = tradeDataAccess; + this.tradePresenter = tradePresenter; + + // Create internal listener that converts TradeListener callbacks to OutputBoundary calls + this.internalListener = new TradeListener() { + @Override + public void onTrade(Trade trade) { + TradeResponseModel responseModel = new TradeResponseModel(trade, null, false); + tradePresenter.prepareSuccessView(responseModel); + } + + @Override + public void onStatusChanged(String statusText, boolean isError) { + tradePresenter.prepareStatusView(statusText, isError); + } + }; + } + + @Override + public void execute(TradeRequestModel requestModel) { + String symbol = requestModel.getSymbol(); + + if (symbol == null || symbol.trim().isEmpty()) { + tradePresenter.prepareFailView("Please enter a symbol."); + return; + } + + symbol = symbol.trim(); + + try { + tradeDataAccess.connect(symbol, internalListener); + } catch (Exception e) { + tradePresenter.prepareFailView("Unable to connect to trade feed. " + e.getMessage()); + } + } + + @Override + public void disconnect() { + try { + tradeDataAccess.disconnect(); + } catch (Exception e) { + tradePresenter.prepareFailView("Unable to disconnect. " + e.getMessage()); + } + } +} + diff --git a/src/main/java/use_case/trade/TradeOutputBoundary.java b/src/main/java/use_case/trade/TradeOutputBoundary.java new file mode 100644 index 00000000..d53a7fd4 --- /dev/null +++ b/src/main/java/use_case/trade/TradeOutputBoundary.java @@ -0,0 +1,26 @@ +package use_case.trade; + +/** + * Output boundary for the trade use case. + */ +public interface TradeOutputBoundary { + /** + * Prepares the success view when a trade is received. + * @param responseModel The trade response model + */ + void prepareSuccessView(TradeResponseModel responseModel); + + /** + * Prepares the fail view when an error occurs. + * @param errorMessage The error message + */ + void prepareFailView(String errorMessage); + + /** + * Prepares the status update view. + * @param statusText The status text + * @param isError Whether this is an error status + */ + void prepareStatusView(String statusText, boolean isError); +} + diff --git a/src/main/java/use_case/trade/TradeRequestModel.java b/src/main/java/use_case/trade/TradeRequestModel.java new file mode 100644 index 00000000..fa4dc231 --- /dev/null +++ b/src/main/java/use_case/trade/TradeRequestModel.java @@ -0,0 +1,17 @@ +package use_case.trade; + +/** + * Request model for trade feed connection. + */ +public class TradeRequestModel { + private final String symbol; + + public TradeRequestModel(String symbol) { + this.symbol = symbol; + } + + public String getSymbol() { + return symbol; + } +} + diff --git a/src/main/java/use_case/trade/TradeResponseModel.java b/src/main/java/use_case/trade/TradeResponseModel.java new file mode 100644 index 00000000..f27d26c9 --- /dev/null +++ b/src/main/java/use_case/trade/TradeResponseModel.java @@ -0,0 +1,31 @@ +package use_case.trade; + +import entity.Trade; + +/** + * Response model for trade updates. + */ +public class TradeResponseModel { + private final Trade trade; + private final String statusText; + private final boolean isError; + + public TradeResponseModel(Trade trade, String statusText, boolean isError) { + this.trade = trade; + this.statusText = statusText; + this.isError = isError; + } + + public Trade getTrade() { + return trade; + } + + public String getStatusText() { + return statusText; + } + + public boolean isError() { + return isError; + } +} + diff --git a/src/main/java/view/TradeView.java b/src/main/java/view/TradeView.java index 9b75d1bd..444a0193 100644 --- a/src/main/java/view/TradeView.java +++ b/src/main/java/view/TradeView.java @@ -2,26 +2,30 @@ import entity.Trade; import interface_adapter.ViewManagerModel; -import use_case.trade.TradeFeed; -import use_case.trade.TradeListener; +import interface_adapter.trade.TradeController; +import interface_adapter.trade.TradeState; +import interface_adapter.trade.TradeViewModel; import javax.swing.*; import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; /** * Swing view for displaying real-time trade data. - * Depends only on the TradeFeed abstraction and domain Trade entity. + * Follows Clean Architecture pattern using Controller and ViewModel. */ -public class TradeView extends JPanel { +public class TradeView extends JPanel implements PropertyChangeListener { private final String viewName = "trade"; private ViewManagerModel viewManagerModel; private String homeViewName; - private final TradeFeed tradeFeed; + private final TradeController tradeController; + private final TradeViewModel tradeViewModel; // Trade data labels private final JLabel symbolLabel = new JLabel("---"); @@ -42,11 +46,14 @@ public class TradeView extends JPanel { /** * Initializes the GUI components. */ - public TradeView(TradeFeed tradeFeed) { - this.tradeFeed = tradeFeed; + public TradeView(TradeController tradeController, TradeViewModel tradeViewModel) { + this.tradeController = tradeController; + this.tradeViewModel = tradeViewModel; + + tradeViewModel.addPropertyChangeListener(this); + setLayout(new BorderLayout()); - // --- Top: Back button + status --- JPanel topPanel = new JPanel(new BorderLayout()); JPanel backPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); @@ -127,150 +134,77 @@ private void styleLabel(JLabel label) { } /** - * Handles the Connect button click by delegating to the TradeFeed. + * Handles the Connect button click by delegating to the Controller. */ private void onConnectClicked() { - if (tradeFeed != null) { - // Check if already connected - user must disconnect first - if (isConnected) { - JOptionPane.showMessageDialog( - this, - "Please disconnect from the current trade before connecting to a new one.", - "Already Connected", - JOptionPane.WARNING_MESSAGE - ); - return; - } - - String symbol = symbolInputField.getText().trim(); - if (symbol.isEmpty()) { - statusLabel.setText("Status: Please enter a symbol"); - statusLabel.setForeground(Color.RED); - return; - } - - // Check rate limiting - prevent too frequent connection attempts - long currentTime = System.currentTimeMillis(); - long timeSinceLastAttempt = currentTime - lastConnectionAttemptTime; - - if (timeSinceLastAttempt < CONNECTION_COOLDOWN_MS) { - long remainingTime = (CONNECTION_COOLDOWN_MS - timeSinceLastAttempt) / 1000; - JOptionPane.showMessageDialog( - this, - "Please wait " + remainingTime + " second(s) before connecting again.\nToo many requests may result in rate limiting.", - "Rate Limit", - JOptionPane.WARNING_MESSAGE - ); - return; - } - - // Update last connection attempt time - lastConnectionAttemptTime = currentTime; - - // Disconnect previous connection if any - tradeFeed.disconnect(); - - // Reset state - currentSymbol = symbol; - connectionStartTime = System.currentTimeMillis(); - symbolLabel.setText("---"); - priceLabel.setText("---"); - volumeLabel.setText("---"); - timestampLabel.setText("---"); - - // Disable connect button during connection attempt, enable disconnect button - connectButton.setEnabled(false); - disconnectButton.setEnabled(true); - - // Immediately show "Connecting..." while we wait for the first trade. - statusLabel.setText("Status: Connecting..."); - statusLabel.setForeground(Color.ORANGE); - - tradeFeed.connect(symbol, new TradeListener() { - @Override - public void onTrade(Trade trade) { - connectionStartTime = 0; // Reset timeout on first trade - isConnected = true; // Mark as connected on first trade - SwingUtilities.invokeLater(() -> { - connectButton.setEnabled(false); // Disable connect when connected - disconnectButton.setEnabled(true); // Enable disconnect when connected - }); - updateTradeOnUi(trade); - } - - @Override - public void onStatusChanged(String statusText, boolean isError) { - SwingUtilities.invokeLater(() -> { - if (isError) { - // On error, re-enable connect button and disable disconnect - connectButton.setEnabled(true); - disconnectButton.setEnabled(false); - isConnected = false; - } else if (statusText.contains("Disconnected")) { - // On disconnect, reset button states - connectButton.setEnabled(true); - disconnectButton.setEnabled(false); - isConnected = false; - } - }); - updateStatusOnUi(statusText, isError); - - // Combined error handling for rate limiting and symbol not found - if (isError) { - boolean isRateLimit = statusText.contains("429") || statusText.contains("Too Many Requests") || statusText.contains("Rate limit"); - boolean isOtherError = statusText.contains("Error") || statusText.contains("Failure"); - - if (isRateLimit || isOtherError) { - SwingUtilities.invokeLater(() -> { - String message; - if (isRateLimit) { - message = "Connection failed: Too Many Requests (429).\n" + - "This may also occur if the symbol '" + currentSymbol + "' is invalid.\n\n" + - "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + - " seconds before trying again.\n" + - "If the problem persists, please verify the symbol is correct."; - // Enforce cooldown after 429 error - lastConnectionAttemptTime = System.currentTimeMillis(); - startCooldownTimer(); - } else { - message = "Connection failed: Symbol '" + currentSymbol + "' not found or invalid.\n" + - "This may also occur if requests are made too frequently.\n\n" + - "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + - " seconds before trying again.\n" + - "If the problem persists, please verify the symbol is correct."; - // Enforce cooldown for other errors too - lastConnectionAttemptTime = System.currentTimeMillis(); - startCooldownTimer(); - } - - JOptionPane.showMessageDialog( - TradeView.this, - message, - "Connection Error", - JOptionPane.ERROR_MESSAGE - ); - }); - } - } - } - }); - - // Start a timeout check for symbol not found - startSymbolNotFoundCheck(); + // Check if already connected - user must disconnect first + if (isConnected) { + JOptionPane.showMessageDialog( + this, + "Please disconnect from the current trade before connecting to a new one.", + "Already Connected", + JOptionPane.WARNING_MESSAGE + ); + return; + } + + String symbol = symbolInputField.getText().trim(); + if (symbol.isEmpty()) { + statusLabel.setText("Status: Please enter a symbol"); + statusLabel.setForeground(Color.RED); + return; } + + // Check rate limiting - prevent too frequent connection attempts + long currentTime = System.currentTimeMillis(); + long timeSinceLastAttempt = currentTime - lastConnectionAttemptTime; + + if (timeSinceLastAttempt < CONNECTION_COOLDOWN_MS) { + long remainingTime = (CONNECTION_COOLDOWN_MS - timeSinceLastAttempt) / 1000; + JOptionPane.showMessageDialog( + this, + "Please wait " + remainingTime + " second(s) before connecting again.\nToo many requests may result in rate limiting.", + "Rate Limit", + JOptionPane.WARNING_MESSAGE + ); + return; + } + + // Update last connection attempt time + lastConnectionAttemptTime = currentTime; + + // Reset state + currentSymbol = symbol; + connectionStartTime = System.currentTimeMillis(); + symbolLabel.setText("---"); + priceLabel.setText("---"); + volumeLabel.setText("---"); + timestampLabel.setText("---"); + + // Disable connect button during connection attempt, enable disconnect button + connectButton.setEnabled(false); + disconnectButton.setEnabled(true); + + // Immediately show "Connecting..." while we wait for the first trade. + statusLabel.setText("Status: Connecting..."); + statusLabel.setForeground(Color.ORANGE); + + // Execute through controller + tradeController.execute(symbol); + + // Start a timeout check for symbol not found + startSymbolNotFoundCheck(); } /** * Handles the Disconnect button click. */ private void onDisconnectClicked() { - if (tradeFeed != null) { - // Disconnect from the trade feed - tradeFeed.disconnect(); - - // Reset all info to default - resetToDefault(); - } + // Disconnect through controller + tradeController.disconnect(); + + // Reset all info to default + resetToDefault(); } /** @@ -322,7 +256,7 @@ private void startSymbolNotFoundCheck() { ); statusLabel.setText("Status: Connection failed"); statusLabel.setForeground(Color.RED); - tradeFeed.disconnect(); + tradeController.disconnect(); resetToDefault(); // Enforce cooldown after timeout lastConnectionAttemptTime = System.currentTimeMillis(); @@ -394,6 +328,12 @@ private void updateTradeOnUi(Trade trade) { } else { timestampLabel.setText("N/A"); } + + // Mark as connected on first trade + connectionStartTime = 0; // Reset timeout on first trade + isConnected = true; + connectButton.setEnabled(false); + disconnectButton.setEnabled(true); }); } @@ -405,15 +345,73 @@ private void updateStatusOnUi(String statusText, boolean isError) { statusLabel.setText(statusText); if (isError) { statusLabel.setForeground(Color.RED); + // On error, re-enable connect button and disable disconnect + connectButton.setEnabled(true); + disconnectButton.setEnabled(false); + isConnected = false; + + // Combined error handling for rate limiting and symbol not found + boolean isRateLimit = statusText.contains("429") || statusText.contains("Too Many Requests") || statusText.contains("Rate limit"); + boolean isOtherError = statusText.contains("Error") || statusText.contains("Failure"); + + if (isRateLimit || isOtherError) { + String message; + if (isRateLimit) { + message = "Connection failed: Too Many Requests (429).\n" + + "This may also occur if the symbol '" + currentSymbol + "' is invalid.\n\n" + + "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + + " seconds before trying again.\n" + + "If the problem persists, please verify the symbol is correct."; + // Enforce cooldown after 429 error + lastConnectionAttemptTime = System.currentTimeMillis(); + startCooldownTimer(); + } else { + message = "Connection failed: Symbol '" + currentSymbol + "' not found or invalid.\n" + + "This may also occur if requests are made too frequently.\n\n" + + "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + + " seconds before trying again.\n" + + "If the problem persists, please verify the symbol is correct."; + // Enforce cooldown for other errors too + lastConnectionAttemptTime = System.currentTimeMillis(); + startCooldownTimer(); + } + + JOptionPane.showMessageDialog( + TradeView.this, + message, + "Connection Error", + JOptionPane.ERROR_MESSAGE + ); + } } else if (statusText != null && statusText.contains("Connected")) { statusLabel.setForeground(new Color(0, 128, 0)); } else if (statusText != null && statusText.contains("Connecting")) { statusLabel.setForeground(Color.ORANGE); + } else if (statusText != null && statusText.contains("Disconnected")) { + statusLabel.setForeground(Color.BLACK); + // On disconnect, reset button states + connectButton.setEnabled(true); + disconnectButton.setEnabled(false); + isConnected = false; } else { statusLabel.setForeground(Color.BLACK); } }); } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + TradeState state = tradeViewModel.getState(); + if (state != null) { + // Update UI based on ViewModel state + if (state.getCurrentTrade() != null) { + updateTradeOnUi(state.getCurrentTrade()); + } + if (state.getStatusText() != null) { + updateStatusOnUi(state.getStatusText(), state.isError()); + } + } + } public String getViewName() { return this.viewName; @@ -424,4 +422,3 @@ public void setBackNavigation(ViewManagerModel viewManagerModel, String homeView this.homeViewName = homeViewName; } } - From 2558b33f6f5734a59fe69740e88b047438520a51 Mon Sep 17 00:00:00 2001 From: lindaaaaen Date: Mon, 1 Dec 2025 15:20:30 -0500 Subject: [PATCH 089/103] delete testing display --- src/test/java/app_LH/AppBuilder.java | 62 ---------------------------- src/test/java/app_LH/Main.java | 12 ------ 2 files changed, 74 deletions(-) delete mode 100644 src/test/java/app_LH/AppBuilder.java delete mode 100644 src/test/java/app_LH/Main.java diff --git a/src/test/java/app_LH/AppBuilder.java b/src/test/java/app_LH/AppBuilder.java deleted file mode 100644 index 32956d58..00000000 --- a/src/test/java/app_LH/AppBuilder.java +++ /dev/null @@ -1,62 +0,0 @@ -//package app_LH; -// -//import data_access.FinnhubEarningsDataAccessObject; -//import interface_adapter.earnings_history.EarningsHistoryController; -//import interface_adapter.earnings_history.EarningsHistoryPresenter; -//import interface_adapter.earnings_history.EarningsHistoryViewModel; -//import use_case.earnings_history.EarningsDataAccessInterface; -//import use_case.earnings_history.GetEarningsHistoryInputBoundary; -//import use_case.earnings_history.GetEarningsHistoryInteractor; -//import use_case.earnings_history.GetEarningsHistoryOutputBoundary; -//import view.EarningsHistoryView; -// -//import javax.swing.*; -//import java.awt.*; -// -///** -// * Builds a JFrame with the EarningsHistoryView wired to its use case. -// */ -//public class AppBuilder { -// -// private final JFrame frame; -// private final CardLayout cardLayout; -// private final JPanel viewsPanel; -// private static final String EARNINGS_HISTORY_VIEW = "earnings history"; -// -// public AppBuilder() { -// frame = new JFrame("Earnings History – Test App"); -// frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); -// -// cardLayout = new CardLayout(); -// viewsPanel = new JPanel(cardLayout); -// frame.setContentPane(viewsPanel); -// } -// -// /** -// * Add the EarningsHistory view, wires controller,interactor, and DAO. -// */ -// public AppBuilder addEarningsHistoryView() { -// EarningsHistoryViewModel earningsVM = new EarningsHistoryViewModel(); -// EarningsDataAccessInterface earningsDAO = new FinnhubEarningsDataAccessObject(); -// GetEarningsHistoryOutputBoundary presenter = new EarningsHistoryPresenter(earningsVM); -// GetEarningsHistoryInputBoundary interactor = new GetEarningsHistoryInteractor(earningsDAO, presenter); -// EarningsHistoryController controller = new EarningsHistoryController(interactor, earningsVM); -// EarningsHistoryView earningsView = new EarningsHistoryView(controller, earningsVM); -// -// viewsPanel.add(earningsView, EARNINGS_HISTORY_VIEW); -// -// return this; -// } -// -// /** -// * Finalizes the window and returns the JFrame. -// */ -// public JFrame build() { -// frame.pack(); -// frame.setLocationRelativeTo(null); -// frame.setVisible(true); -// cardLayout.show(viewsPanel, EARNINGS_HISTORY_VIEW); //show the earnings view by default -// -// return frame; -// } -//} diff --git a/src/test/java/app_LH/Main.java b/src/test/java/app_LH/Main.java deleted file mode 100644 index 12eb7b62..00000000 --- a/src/test/java/app_LH/Main.java +++ /dev/null @@ -1,12 +0,0 @@ -//package app_LH; -// -//import javax.swing.*; -// -//public class Main { -// public static void main(String[] args) { -// SwingUtilities.invokeLater(() -> { -// AppBuilder builder = new AppBuilder(); -// builder.addEarningsHistoryView().build(); -// }); -// } -//} From cec302b046e51efef2c582c104d051288f809637 Mon Sep 17 00:00:00 2001 From: zoe Date: Mon, 1 Dec 2025 16:01:11 -0500 Subject: [PATCH 090/103] fixed something really quick --- src/main/java/view/AccountView.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/view/AccountView.java b/src/main/java/view/AccountView.java index b847c4aa..113c10ec 100644 --- a/src/main/java/view/AccountView.java +++ b/src/main/java/view/AccountView.java @@ -39,7 +39,7 @@ public AccountView(AccountViewModel accountViewModel) { } }); - usernameLabel = new JLabel("Username: "); + usernameLabel = new JLabel("Watchlist"); usernameLabel.setHorizontalAlignment(SwingConstants.CENTER); topPanel.add(usernameLabel, BorderLayout.CENTER); @@ -55,9 +55,6 @@ public AccountView(AccountViewModel accountViewModel) { public void propertyChange(PropertyChangeEvent evt) { - String username = viewModel.getState().getUsername(); - usernameLabel.setText("Username: " + username); - watchlistPanel.removeAll(); ArrayList watchlist = viewModel.getState().getWatchlist(); From 89de9df4bedfe329cb4d710a2582adcafdcf6218 Mon Sep 17 00:00:00 2001 From: Ale Benavides Loo Date: Mon, 1 Dec 2025 16:16:22 -0500 Subject: [PATCH 091/103] Minor Bug fix --- .../use_case/filter_search/FilterSearchInputBoundary.java | 6 +++--- src/main/java/view/FilterSearchView.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java b/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java index 5964f2cd..922d666f 100644 --- a/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java +++ b/src/main/java/use_case/filter_search/FilterSearchInputBoundary.java @@ -9,9 +9,9 @@ public interface FilterSearchInputBoundary { * Executes the filter search use case. * @param request the input data */ - public final String[] EXCHANGE_OPTIONS = {"US"}; + String[] EXCHANGE_OPTIONS = {"US"}; - public final String[] MIC_OPTIONS = {null, "XADS", "XAMS", "ASEX", "XASX", "XBUE", "XBOG", "XBUD", "XBER", "XBAH", "XBKK", + String[] MIC_OPTIONS = {null, "XADS", "XAMS", "ASEX", "XASX", "XBUE", "XBOG", "XBUD", "XBER", "XBAH", "XBKK", "XBOM", "XBRU", "XCAI", "XCNQ", "XCSE", "BVCA", "XCAS", "XDFM", "XETR", "XDUS", "XFRA", "XHEL", "XHKG", "XHAM", "XICE", "XDUB", "XIST", "XIDX", "XJSE", "XKLS", "XKOS", "XKRX", "XKUW", "XLON", "XLIS", "XMAD", "MISX", "XMIL", "XMAL", "XMUN", "XMEX", "NEOE", "XNSA", "XNSE", "XNZE", "XOSL", "XMUS", "XPAR", "XPHS", @@ -20,7 +20,7 @@ public interface FilterSearchInputBoundary { "XNYS", "XGAT", "XASE", "BATS", "ARCX", "XNMS", "XNCM", "XNGS", "IEXG", "XNAS", "OTCM", "OOTC", "XSTC", "XHNX", "FNIS", "FNDK", "NFI"}; - final String[] SECURITY_OPTIONS = {null, "ABS Auto", "ABS Card", "ABS Home", "ABS Other", "ACCEPT BANCARIA", + String[] SECURITY_OPTIONS = {null, "ABS Auto", "ABS Card", "ABS Home", "ABS Other", "ACCEPT BANCARIA", "ADJ CONV. TO FIXED", "ADJ CONV. TO FIXED, OID", "ADJUSTABLE", "ADJUSTABLE, OID", "ADR", "Agncy ABS Home", "Agncy ABS Other", "Agncy CMBS", "Agncy CMO FLT", "Agncy CMO INV", "Agncy CMO IO", "Agncy CMO Other", "Agncy CMO PO", "Agncy CMO Z", "Asset-Based", "ASSET-BASED BRIDGE", "ASSET-BASED BRIDGE REV", diff --git a/src/main/java/view/FilterSearchView.java b/src/main/java/view/FilterSearchView.java index 08d53180..0c2f67d0 100644 --- a/src/main/java/view/FilterSearchView.java +++ b/src/main/java/view/FilterSearchView.java @@ -94,8 +94,8 @@ public FilterSearchView(FilterSearchViewModel filterSearchViewModel) { topPanel.add(filtersPanel, BorderLayout.CENTER); - String[] columnNames = {"Symbol", "Description", "Currency", - "Display Symbol", "FIGI", "MIC", "Security Type"}; + String[] columnNames = {"MIC", "Description", "Currency", + "Display Symbol", "FIGI", "Symbol", "Security Type"}; tableModel = new DefaultTableModel(columnNames, 0) { @Override From 23d497694a442f7b9731b04e39feb9219f2e2f74 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Mon, 1 Dec 2025 16:28:40 -0500 Subject: [PATCH 092/103] architecture updated with fixed bugs --- src/main/java/app/Main.java | 2 ++ src/main/java/view/LoggedInView.java | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index a921fb06..34edebf0 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -16,6 +16,7 @@ public static void main(String[] args) { .addFilterSearchUseCase() .addNewsView() .addEarningsHistoryView() + .addTradeView() .addStockSearchUseCase() .addSignupUseCase() .addLoginUseCase() @@ -24,6 +25,7 @@ public static void main(String[] args) { .addEarningsHistoryUseCase() .addAccount() .addMarketStatusUseCase() + .addRealtimeTradeUseCase() .build(); application.pack(); diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 0923934e..9de57ef4 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -99,6 +99,18 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { } }); + final JButton realtimeTradeButton = new JButton("Realtime Trade"); + realtimeTradeButton.addActionListener(e -> { + System.out.println("Realtime Trade button clicked"); // debug + System.out.println("viewManagerModel: " + viewManagerModel); // debug + System.out.println("realtimeTradeViewName: " + realtimeTradeViewName); // debug + if (viewManagerModel != null && realtimeTradeViewName != null) { + viewManagerModel.setState(realtimeTradeViewName); + viewManagerModel.firePropertyChange(); + } else { + System.out.println("ERROR: Cannot navigate - viewManagerModel or realtimeTradeViewName is null"); // debug + } + }); final JButton accountButton = new JButton("Account"); accountButton.addActionListener(e -> { @@ -112,6 +124,7 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { topToolbar.add(newsButton); topToolbar.add(filterSearchButton); topToolbar.add(historyButton); + topToolbar.add(realtimeTradeButton); topToolbar.add(accountButton); marketStatusLabel = new JLabel("Loading market status..."); @@ -297,6 +310,11 @@ public void setAccountNavigation(ViewManagerModel viewManagerModel, this.accountViewName = accountViewName; } + public void setRealtimeTradeNavigation(ViewManagerModel viewManagerModel, + String tradeViewName) { + this.viewManagerModel = viewManagerModel; + this.realtimeTradeViewName = tradeViewName; + } public void setMarketStatusViewModel(MarketStatusViewModel viewModel) { this.marketStatusViewModel = viewModel; From 8bf5a8acdacec1effd59b26a386415283c67db41 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Mon, 1 Dec 2025 23:01:50 -0500 Subject: [PATCH 093/103] Unnecessary code deletion and simplification --- src/main/java/app/AppBuilder.java | 13 +- .../FinnhubTradeDataAccessObject.java | 212 +++++++++++++++++- .../java/data_access/FinnhubTradeFeed.java | 192 ---------------- .../trade/TradeController.java | 1 + .../java/use_case/trade/TradeInteractor.java | 4 + src/main/java/view/TradeView.java | 92 ++++++-- 6 files changed, 279 insertions(+), 235 deletions(-) delete mode 100644 src/main/java/data_access/FinnhubTradeFeed.java diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index ab30af15..1dab2f06 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -204,18 +204,14 @@ public AppBuilder addEarningsHistoryView() { } public AppBuilder addTradeView() { - // ViewModel tradeViewModel = new TradeViewModel(); - - // Presenter + Interactor + TradeOutputBoundary tradeOutputBoundary = new TradePresenter(tradeViewModel); TradeDataAccessInterface tradeDataAccess = new FinnhubTradeDataAccessObject(); TradeInputBoundary tradeInteractor = new TradeInteractor(tradeDataAccess, tradeOutputBoundary); - - // Controller TradeController tradeController = new TradeController(tradeInteractor); - // Swing view + tradeView = new TradeView(tradeController, tradeViewModel); cardPanel.add(tradeView, tradeView.getViewName()); return this; @@ -358,14 +354,9 @@ public AppBuilder addMarketStatusUseCase() { } public AppBuilder addRealtimeTradeUseCase() { - // Logged-in page: Realtime Trade button → Trade view String tradeViewName = tradeView.getViewName(); - System.out.println("Setting up Realtime Trade navigation to view: " + tradeViewName); // debug loggedInView.setRealtimeTradeNavigation(viewManagerModel, tradeViewName); - - // Trade page: Back button → Logged-in view tradeView.setBackNavigation(viewManagerModel, loggedInView.getViewName()); - return this; } diff --git a/src/main/java/data_access/FinnhubTradeDataAccessObject.java b/src/main/java/data_access/FinnhubTradeDataAccessObject.java index ab995e32..1261fd79 100644 --- a/src/main/java/data_access/FinnhubTradeDataAccessObject.java +++ b/src/main/java/data_access/FinnhubTradeDataAccessObject.java @@ -29,19 +29,55 @@ public class FinnhubTradeDataAccessObject implements TradeDataAccessInterface { private OkHttpClient client; private TradeListener listener; private String currentSymbol; + private boolean hasReceivedPing = false; @Override public void connect(String symbol, TradeListener listener) { this.listener = listener; - this.currentSymbol = symbol; - + + // Normalize the symbol input (trim whitespace) + if (symbol != null) { + symbol = symbol.trim(); + } + + // If we have an existing connection, unsubscribe and close it first if (webSocket != null) { LOGGER.info("Closing existing Finnhub connection..."); + // Unsubscribe from old symbol if we have one + if (currentSymbol != null && !currentSymbol.isEmpty()) { + try { + String unsubscribeMsg = String.format("{\"type\":\"unsubscribe\",\"symbol\":\"%s\"}", currentSymbol); + webSocket.send(unsubscribeMsg); + LOGGER.info("Unsubscribed from " + currentSymbol); + // Give a small delay for unsubscribe to be processed + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error unsubscribing: " + e.getMessage()); + } + } webSocket.close(1000, "Reconnecting"); + webSocket = null; + // Wait a bit for the connection to close + try { + Thread.sleep(200); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } } + // Set the new symbol (trimmed and normalized) and reset ping tracking + this.currentSymbol = (symbol != null) ? symbol.trim() : null; + this.hasReceivedPing = false; + LOGGER.info("Setting currentSymbol to: '" + this.currentSymbol + "'"); + client = new OkHttpClient.Builder() .readTimeout(0, TimeUnit.MILLISECONDS) + .connectTimeout(10, TimeUnit.SECONDS) + .pingInterval(25, TimeUnit.SECONDS) // Finnhub often sends pings - keep connection alive .build(); Request request = new Request.Builder().url(WEB_SOCKET_URL).build(); @@ -53,13 +89,19 @@ public void connect(String symbol, TradeListener listener) { webSocket = client.newWebSocket(request, new WebSocketListener() { @Override public void onOpen(WebSocket ws, Response response) { - LOGGER.info("WebSocket Opened. Subscribing to " + currentSymbol); + LOGGER.info("WebSocket Opened. Subscribing to symbol: '" + currentSymbol + "'"); String subscribeMsg = String.format("{\"type\":\"subscribe\",\"symbol\":\"%s\"}", currentSymbol); ws.send(subscribeMsg); + LOGGER.info("Sent subscribe message: " + subscribeMsg); } @Override public void onMessage(WebSocket ws, String text) { + LOGGER.info("Received raw message from WebSocket: " + text); + LOGGER.info("Message length: " + text.length() + " characters"); + if (text != null && !text.isEmpty()) { + LOGGER.info("First 200 chars of message: " + text.substring(0, Math.min(200, text.length()))); + } processMessage(text); } @@ -91,13 +133,29 @@ public void onClosed(WebSocket ws, int code, String reason) { @Override public void disconnect() { if (webSocket != null) { + // Unsubscribe from current symbol before closing + if (currentSymbol != null && !currentSymbol.isEmpty()) { + try { + String unsubscribeMsg = String.format("{\"type\":\"unsubscribe\",\"symbol\":\"%s\"}", currentSymbol); + webSocket.send(unsubscribeMsg); + LOGGER.info("Unsubscribed from " + currentSymbol); + // Give a small delay for unsubscribe to be processed + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error unsubscribing: " + e.getMessage()); + } + } webSocket.close(1000, "User disconnect"); webSocket = null; } - if (client != null) { - client.dispatcher().executorService().shutdown(); - client = null; - } + // Don't shut down executor service - we might reconnect + // Just set client to null, a new one will be created on next connect + client = null; + currentSymbol = null; } /** @@ -140,13 +198,138 @@ private String extractValue(String json, String key) { return null; } + /** + * Processes trade data that comes in array format from Finnhub. + * Format: {"type":"trade","data":[{"s":"SYMBOL","p":price,"v":volume,"t":timestamp},...]} + * Or: [{"s":"SYMBOL","p":price,"v":volume,"t":timestamp},...] + */ + private void processTradeArray(String jsonMessage) { + try { + LOGGER.info("Processing trade array message"); + + // Extract the data array part + String dataArray = null; + if (jsonMessage.contains("\"data\":[")) { + // Format: {"type":"trade","data":[...]} + int dataStart = jsonMessage.indexOf("\"data\":[") + 8; + int bracketCount = 1; + int dataEnd = dataStart; + while (dataEnd < jsonMessage.length() && bracketCount > 0) { + if (jsonMessage.charAt(dataEnd) == '[') bracketCount++; + if (jsonMessage.charAt(dataEnd) == ']') bracketCount--; + dataEnd++; + } + if (bracketCount == 0) { + dataArray = jsonMessage.substring(dataStart, dataEnd - 1); + } + } else if (jsonMessage.trim().startsWith("[")) { + // Format: [{...}] + dataArray = jsonMessage.trim().substring(1, jsonMessage.trim().length() - 1); + } + + if (dataArray == null || dataArray.isEmpty()) { + LOGGER.warning("Could not extract data array from message: " + jsonMessage); + return; + } + + LOGGER.info("Extracted data array: " + dataArray); + + // Split by },{ to get individual trade objects + String[] tradeObjects = dataArray.split("\\},\\{"); + LOGGER.info("Found " + tradeObjects.length + " trade object(s) in array"); + + for (int i = 0; i < tradeObjects.length; i++) { + String tradeObj = tradeObjects[i]; + // Clean up brackets and braces + tradeObj = tradeObj.replaceFirst("^\\{?", "{").replaceFirst("\\}?$", "}"); + if (!tradeObj.startsWith("{")) { + tradeObj = "{" + tradeObj; + } + if (!tradeObj.endsWith("}")) { + tradeObj = tradeObj + "}"; + } + + LOGGER.info("Processing trade object " + (i + 1) + ": " + tradeObj); + + // Process as a single trade message + String symbol = extractValue(tradeObj, "s"); + String normalizedReceivedSymbol = (symbol != null) ? symbol.trim() : null; + String normalizedCurrentSymbol = (currentSymbol != null) ? currentSymbol.trim() : null; + + if (normalizedCurrentSymbol != null && normalizedReceivedSymbol != null && + normalizedCurrentSymbol.equalsIgnoreCase(normalizedReceivedSymbol)) { + LOGGER.info("Processing trade message for symbol: " + normalizedReceivedSymbol); + + String priceStr = extractValue(tradeObj, "p"); + double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; + + String volumeStr = extractValue(tradeObj, "v"); + double volume = (volumeStr != null) ? Double.parseDouble(volumeStr) : 0.0; + + String timestampStr = extractValue(tradeObj, "t"); + long timestamp = (timestampStr != null) ? Long.parseLong(timestampStr) : 0L; + + Instant ts = timestamp > 0 ? Instant.ofEpochMilli(timestamp) : null; + + if (listener != null) { + listener.onStatusChanged("Status: Connected to " + currentSymbol, false); + Trade trade = new Trade(symbol, price, volume, ts); + listener.onTrade(trade); + } + } else { + LOGGER.info("Ignoring trade in array for symbol '" + normalizedReceivedSymbol + + "' (current: '" + normalizedCurrentSymbol + "')"); + } + } + } catch (Exception e) { + LOGGER.log(Level.WARNING, "Error processing trade array: " + e.getMessage(), e); + } + } + /** * Parses the incoming JSON message using manual string manipulation and dispatches Trade domain objects. + * Based on working Finnhub WebSocket example. */ private void processMessage(String jsonMessage) { try { + // Handle ping messages (connection keep-alive) + if (jsonMessage.contains("\"type\":\"ping\"")) { + hasReceivedPing = true; + LOGGER.info("Received PING: Connection is alive."); + return; + } + + // Handle trade messages - this is the main format Finnhub uses if (jsonMessage.contains("\"type\":\"trade\"")) { + LOGGER.info("💵 RECEIVED TRADE DATA: " + jsonMessage); + // Parse the trade data - Finnhub sends: {"type":"trade","data":[{"s":"SYMBOL","p":price,"v":volume,"t":timestamp}]} + // Or sometimes: {"type":"trade","s":"SYMBOL","p":price,"v":volume,"t":timestamp} + + // Check if it's an array format with "data" field + if (jsonMessage.contains("\"data\":[")) { + LOGGER.info("Trade message contains data array, processing as array"); + processTradeArray(jsonMessage); + return; + } + + // Otherwise, try to extract trade data directly from the message String symbol = extractValue(jsonMessage, "s"); + + // Normalize both symbols for comparison (trim and case-insensitive) + String normalizedReceivedSymbol = (symbol != null) ? symbol.trim() : null; + String normalizedCurrentSymbol = (currentSymbol != null) ? currentSymbol.trim() : null; + + // Only process messages for the current symbol to avoid processing old messages + // Use case-insensitive comparison to handle API format differences + if (normalizedCurrentSymbol == null || + normalizedReceivedSymbol == null || + !normalizedCurrentSymbol.equalsIgnoreCase(normalizedReceivedSymbol)) { + LOGGER.info("Ignoring trade message for symbol '" + normalizedReceivedSymbol + + "' (current: '" + normalizedCurrentSymbol + "')"); + return; + } + + LOGGER.info("Processing trade message for symbol: " + normalizedReceivedSymbol); String priceStr = extractValue(jsonMessage, "p"); double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; @@ -166,15 +349,24 @@ private void processMessage(String jsonMessage) { Trade trade = new Trade(symbol, price, volume, ts); listener.onTrade(trade); } - } else if (jsonMessage.contains("\"type\":\"ping\"")) { - // No-op: connection keep-alive - } else if (jsonMessage.contains("\"type\":\"error\"")) { + } + // Handle error messages + else if (jsonMessage.contains("\"type\":\"error\"")) { // Handle error messages from the API String errorMsg = extractValue(jsonMessage, "msg"); + LOGGER.warning("Received error message: " + errorMsg); if (listener != null) { String errorText = errorMsg != null ? errorMsg : "Unknown error"; listener.onStatusChanged("Status: Error - " + errorText, true); } + } else { + // Log any other messages (like subscription confirmations) + LOGGER.info("Received TEXT: " + jsonMessage); + // Check if it might be an array format we missed + if (jsonMessage.trim().startsWith("[") && jsonMessage.contains("\"s\"") && jsonMessage.contains("\"p\"")) { + LOGGER.info("Message appears to be an array format, trying array processing"); + processTradeArray(jsonMessage); + } } } catch (Exception e) { LOGGER.log(Level.WARNING, "Error processing JSON message with manual parsing: " + e.getMessage() diff --git a/src/main/java/data_access/FinnhubTradeFeed.java b/src/main/java/data_access/FinnhubTradeFeed.java deleted file mode 100644 index a9480177..00000000 --- a/src/main/java/data_access/FinnhubTradeFeed.java +++ /dev/null @@ -1,192 +0,0 @@ -package data_access; - -import entity.Trade; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.WebSocket; -import okhttp3.WebSocketListener; -import use_case.trade.TradeDataAccessInterface; -import use_case.trade.TradeListener; - -import java.time.Instant; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Finnhub-specific implementation of the TradeDataAccessInterface. - * This class belongs to the data_access / infrastructure layer and knows about WebSockets and JSON format. - */ -public class FinnhubTradeFeed implements TradeDataAccessInterface { - - private static final Logger LOGGER = Logger.getLogger(FinnhubTradeFeed.class.getName()); - - private static final String API_KEY = "d4977ehr01qshn3kvpt0d4977ehr01qshn3kvptg"; - private static final String WEB_SOCKET_URL = "wss://ws.finnhub.io?token=" + API_KEY; - - private WebSocket webSocket; - private OkHttpClient client; - private TradeListener listener; - private String currentSymbol; - - @Override - public void connect(String symbol, TradeListener listener) { - this.listener = listener; - this.currentSymbol = symbol; - - if (webSocket != null) { - LOGGER.info("Closing existing Finnhub connection..."); - webSocket.close(1000, "Reconnecting"); - } - - client = new OkHttpClient.Builder() - .readTimeout(0, TimeUnit.MILLISECONDS) - .build(); - - Request request = new Request.Builder().url(WEB_SOCKET_URL).build(); - - if (listener != null) { - listener.onStatusChanged("Status: Connecting...", false); - } - - webSocket = client.newWebSocket(request, new WebSocketListener() { - @Override - public void onOpen(WebSocket ws, Response response) { - LOGGER.info("WebSocket Opened. Subscribing to " + currentSymbol); - String subscribeMsg = String.format("{\"type\":\"subscribe\",\"symbol\":\"%s\"}", currentSymbol); - ws.send(subscribeMsg); - } - - @Override - public void onMessage(WebSocket ws, String text) { - processMessage(text); - } - - @Override - public void onClosing(WebSocket ws, int code, String reason) { - LOGGER.info("WebSocket Closing: Code " + code + ", Reason: " + reason); - ws.close(1000, null); - } - - @Override - public void onFailure(WebSocket ws, Throwable t, Response response) { - LOGGER.log(Level.SEVERE, "WebSocket Failure: " + t.getMessage(), t); - if (listener != null) { - String errorMessage = "Status: Failure! See console."; - // Check for 429 rate limiting error - if (t.getMessage() != null && t.getMessage().contains("429")) { - errorMessage = "Status: 429 Too Many Requests - Rate limit exceeded"; - } else if (response != null && response.code() == 429) { - errorMessage = "Status: 429 Too Many Requests - Rate limit exceeded"; - } - listener.onStatusChanged(errorMessage, true); - } - } - - @Override - public void onClosed(WebSocket ws, int code, String reason) { - LOGGER.info("WebSocket Closed."); - webSocket = null; - if (listener != null) { - listener.onStatusChanged("Status: Disconnected", false); - } - } - }); - } - - @Override - public void disconnect() { - if (webSocket != null) { - webSocket.close(1000, "User disconnect"); - webSocket = null; - } - if (client != null) { - client.dispatcher().executorService().shutdown(); - client = null; - } - } - - /** - * Small internal JSON parsing helper: extracts a value from a JSON string using basic String methods. - * Kept here in the infrastructure layer so UI and domain remain free of parsing details. - */ - private String extractValue(String json, String key) { - String keySearch = "\"" + key + "\":"; - int keyIndex = json.indexOf(keySearch); - - if (keyIndex == -1) { - return null; - } - - int valueStart = keyIndex + keySearch.length(); - char firstChar = json.charAt(valueStart); - - if (firstChar == '"') { - valueStart++; - int valueEnd = json.indexOf('"', valueStart); - if (valueEnd != -1) { - return json.substring(valueStart, valueEnd); - } - } else { - int valueEnd = valueStart; - while (valueEnd < json.length() && (Character.isDigit(json.charAt(valueEnd)) || json.charAt(valueEnd) == '.' || json.charAt(valueEnd) == '-')) { - valueEnd++; - } - int commaIndex = json.indexOf(',', valueStart); - int braceIndex = json.indexOf('}', valueStart); - - int terminationIndex = json.length(); - if (commaIndex != -1) terminationIndex = Math.min(terminationIndex, commaIndex); - if (braceIndex != -1) terminationIndex = Math.min(terminationIndex, braceIndex); - - if (terminationIndex > valueStart) { - return json.substring(valueStart, terminationIndex).trim(); - } - } - return null; - } - - /** - * Parses the incoming JSON message using manual string manipulation and dispatches Trade domain objects. - */ - private void processMessage(String jsonMessage) { - try { - if (jsonMessage.contains("\"type\":\"trade\"")) { - String symbol = extractValue(jsonMessage, "s"); - - String priceStr = extractValue(jsonMessage, "p"); - double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; - - String volumeStr = extractValue(jsonMessage, "v"); - double volume = (volumeStr != null) ? Double.parseDouble(volumeStr) : 0.0; - - String timestampStr = extractValue(jsonMessage, "t"); - long timestamp = (timestampStr != null) ? Long.parseLong(timestampStr) : 0L; - - Instant ts = timestamp > 0 ? Instant.ofEpochMilli(timestamp) : null; - - if (listener != null) { - // First successful trade means we are effectively connected for this symbol. - listener.onStatusChanged("Status: Connected to " + currentSymbol, false); - - Trade trade = new Trade(symbol, price, volume, ts); - listener.onTrade(trade); - } - } else if (jsonMessage.contains("\"type\":\"ping\"")) { - // No-op: connection keep-alive - } else if (jsonMessage.contains("\"type\":\"error\"")) { - // Handle error messages from the API - String errorMsg = extractValue(jsonMessage, "msg"); - if (listener != null) { - String errorText = errorMsg != null ? errorMsg : "Unknown error"; - listener.onStatusChanged("Status: Error - " + errorText, true); - } - } - } catch (Exception e) { - LOGGER.log(Level.WARNING, "Error processing JSON message with manual parsing: " + e.getMessage() - + "\nMessage: " + jsonMessage); - } - } -} - diff --git a/src/main/java/interface_adapter/trade/TradeController.java b/src/main/java/interface_adapter/trade/TradeController.java index 8284a3cc..5cc0b15a 100644 --- a/src/main/java/interface_adapter/trade/TradeController.java +++ b/src/main/java/interface_adapter/trade/TradeController.java @@ -15,6 +15,7 @@ public TradeController(TradeInputBoundary tradeInteractor) { } public void execute(String symbol) { + System.out.println("TradeController: Received symbol: '" + symbol + "'"); TradeRequestModel requestModel = new TradeRequestModel(symbol); tradeInteractor.execute(requestModel); } diff --git a/src/main/java/use_case/trade/TradeInteractor.java b/src/main/java/use_case/trade/TradeInteractor.java index 7a3ab0b4..df5b6ecb 100644 --- a/src/main/java/use_case/trade/TradeInteractor.java +++ b/src/main/java/use_case/trade/TradeInteractor.java @@ -35,6 +35,7 @@ public void onStatusChanged(String statusText, boolean isError) { @Override public void execute(TradeRequestModel requestModel) { String symbol = requestModel.getSymbol(); + System.out.println("TradeInteractor: Received symbol from request model: '" + symbol + "'"); if (symbol == null || symbol.trim().isEmpty()) { tradePresenter.prepareFailView("Please enter a symbol."); @@ -42,10 +43,13 @@ public void execute(TradeRequestModel requestModel) { } symbol = symbol.trim(); + System.out.println("TradeInteractor: Calling tradeDataAccess.connect with symbol: '" + symbol + "'"); try { tradeDataAccess.connect(symbol, internalListener); } catch (Exception e) { + System.err.println("TradeInteractor: Error connecting: " + e.getMessage()); + e.printStackTrace(); tradePresenter.prepareFailView("Unable to connect to trade feed. " + e.getMessage()); } } diff --git a/src/main/java/view/TradeView.java b/src/main/java/view/TradeView.java index 444a0193..033de03b 100644 --- a/src/main/java/view/TradeView.java +++ b/src/main/java/view/TradeView.java @@ -89,7 +89,12 @@ private JPanel createMainDashboardCard() { JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); searchPanel.add(new JLabel("Symbol:")); symbolInputField.setText("BINANCE:BTCUSDT"); // Default value + symbolInputField.setToolTipText("Enter crypto pair (e.g., BINANCE:BTCUSDT). Stock symbols may not have real-time trade data."); searchPanel.add(symbolInputField); + + // Add a help label to guide users + JLabel helpLabel = new JLabel("Note: Crypto pairs work best (e.g., BINANCE:BTCUSDT)"); + searchPanel.add(helpLabel); connectButton = new JButton("Connect"); connectButton.addActionListener(e -> onConnectClicked()); searchPanel.add(connectButton); @@ -137,24 +142,30 @@ private void styleLabel(JLabel label) { * Handles the Connect button click by delegating to the Controller. */ private void onConnectClicked() { - // Check if already connected - user must disconnect first - if (isConnected) { - JOptionPane.showMessageDialog( - this, - "Please disconnect from the current trade before connecting to a new one.", - "Already Connected", - JOptionPane.WARNING_MESSAGE - ); - return; - } - String symbol = symbolInputField.getText().trim(); + System.out.println("TradeView: User entered symbol: '" + symbol + "'"); if (symbol.isEmpty()) { statusLabel.setText("Status: Please enter a symbol"); statusLabel.setForeground(Color.RED); return; } + // Warn user if they're using a stock symbol (not crypto) + if (!symbol.contains(":") || !symbol.toUpperCase().startsWith("BINANCE:")) { + int result = JOptionPane.showConfirmDialog( + this, + "Stock symbols (like AAPL, TSLA) may not have real-time trade data available.\n\n" + + "Crypto pairs (like BINANCE:BTCUSDT) work best for real-time trades.\n\n" + + "Do you want to continue with '" + symbol + "'?", + "Symbol Warning", + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE + ); + if (result == JOptionPane.NO_OPTION) { + return; + } + } + // Check rate limiting - prevent too frequent connection attempts long currentTime = System.currentTimeMillis(); long timeSinceLastAttempt = currentTime - lastConnectionAttemptTime; @@ -170,10 +181,24 @@ private void onConnectClicked() { return; } + // Always disconnect first to ensure clean state + if (isConnected || connectionStartTime > 0) { + tradeController.disconnect(); + // Reset state immediately + isConnected = false; + connectionStartTime = 0; + // Give a delay for disconnect to complete + try { + Thread.sleep(300); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + // Update last connection attempt time lastConnectionAttemptTime = currentTime; - // Reset state + // Reset state and set new connection start time currentSymbol = symbol; connectionStartTime = System.currentTimeMillis(); symbolLabel.setText("---"); @@ -190,6 +215,7 @@ private void onConnectClicked() { statusLabel.setForeground(Color.ORANGE); // Execute through controller + System.out.println("TradeView: Calling tradeController.execute with symbol: '" + symbol + "'"); tradeController.execute(symbol); // Start a timeout check for symbol not found @@ -234,19 +260,42 @@ private void resetToDefault() { * Starts a background thread to check if symbol is not found after timeout. */ private void startSymbolNotFoundCheck() { + // Capture the connection start time and symbol for this specific connection attempt + final long checkStartTime = connectionStartTime; + final String checkSymbol = currentSymbol; + new Thread(() -> { try { Thread.sleep(SYMBOL_NOT_FOUND_TIMEOUT); - // If we're still connecting after timeout and no trades received - if (connectionStartTime > 0 && System.currentTimeMillis() - connectionStartTime >= SYMBOL_NOT_FOUND_TIMEOUT) { - SwingUtilities.invokeLater(() -> { - if (symbolLabel.getText().equals("---")) { + SwingUtilities.invokeLater(() -> { + // Only trigger timeout if: + // 1. We're still checking the same symbol + // 2. The connection start time hasn't changed (no new connection started) + // 3. Enough time has passed since connection started + // 4. No trades have been received (symbol label still shows "---") + long currentTime = System.currentTimeMillis(); + if (checkSymbol.equals(currentSymbol) && + connectionStartTime == checkStartTime && + connectionStartTime > 0 && + currentTime - connectionStartTime >= SYMBOL_NOT_FOUND_TIMEOUT && + symbolLabel.getText().equals("---")) { // No trades received - could be symbol not found or rate limiting - String message = "Connection timeout: Symbol '" + currentSymbol + "' not found or no trades available.\n" + - "This may also occur if requests are made too frequently.\n\n" + - "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + - " seconds before trying again.\n" + - "If the problem persists, please verify the symbol is correct."; + boolean isCrypto = currentSymbol.contains(":") && currentSymbol.toUpperCase().startsWith("BINANCE:"); + String message = "Connection timeout: Symbol '" + currentSymbol + "' not found or no trades available.\n\n"; + if (!isCrypto) { + message += "⚠️ IMPORTANT: Stock symbols (like AAPL, TSLA) typically do NOT have real-time trade data on Finnhub.\n" + + "The WebSocket trade feed primarily supports CRYPTO pairs.\n\n"; + } + message += "This may also occur if:\n" + + "• Requests are made too frequently (rate limiting)\n" + + "• The symbol format is incorrect\n" + + "• The market is closed\n\n" + + "✅ RECOMMENDED: Use crypto pairs like:\n" + + " • BINANCE:BTCUSDT\n" + + " • BINANCE:ETHUSDT\n" + + " • BINANCE:BNBUSDT\n\n" + + "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + + " seconds before trying again."; JOptionPane.showMessageDialog( TradeView.this, @@ -263,7 +312,6 @@ private void startSymbolNotFoundCheck() { startCooldownTimer(); } }); - } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } From cb3423b9db71a7ff17b06f3b14550c28bdb5def7 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Tue, 2 Dec 2025 16:46:22 -0500 Subject: [PATCH 094/103] minor simplification and formatting --- .../FinnhubTradeDataAccessObject.java | 147 ++++++------------ 1 file changed, 46 insertions(+), 101 deletions(-) diff --git a/src/main/java/data_access/FinnhubTradeDataAccessObject.java b/src/main/java/data_access/FinnhubTradeDataAccessObject.java index 1261fd79..dac6a55b 100644 --- a/src/main/java/data_access/FinnhubTradeDataAccessObject.java +++ b/src/main/java/data_access/FinnhubTradeDataAccessObject.java @@ -34,22 +34,18 @@ public class FinnhubTradeDataAccessObject implements TradeDataAccessInterface { @Override public void connect(String symbol, TradeListener listener) { this.listener = listener; - - // Normalize the symbol input (trim whitespace) + if (symbol != null) { - symbol = symbol.trim(); + symbol = symbol.trim().toUpperCase(); } - - // If we have an existing connection, unsubscribe and close it first + if (webSocket != null) { LOGGER.info("Closing existing Finnhub connection..."); - // Unsubscribe from old symbol if we have one if (currentSymbol != null && !currentSymbol.isEmpty()) { try { String unsubscribeMsg = String.format("{\"type\":\"unsubscribe\",\"symbol\":\"%s\"}", currentSymbol); webSocket.send(unsubscribeMsg); LOGGER.info("Unsubscribed from " + currentSymbol); - // Give a small delay for unsubscribe to be processed try { Thread.sleep(100); } catch (InterruptedException e) { @@ -61,7 +57,6 @@ public void connect(String symbol, TradeListener listener) { } webSocket.close(1000, "Reconnecting"); webSocket = null; - // Wait a bit for the connection to close try { Thread.sleep(200); } catch (InterruptedException e) { @@ -69,8 +64,11 @@ public void connect(String symbol, TradeListener listener) { } } - // Set the new symbol (trimmed and normalized) and reset ping tracking - this.currentSymbol = (symbol != null) ? symbol.trim() : null; + if (symbol != null) { + this.currentSymbol = symbol.trim(); + } else { + this.currentSymbol = null; + } this.hasReceivedPing = false; LOGGER.info("Setting currentSymbol to: '" + this.currentSymbol + "'"); @@ -104,42 +102,16 @@ public void onMessage(WebSocket ws, String text) { } processMessage(text); } - - @Override - public void onClosing(WebSocket ws, int code, String reason) { - LOGGER.info("WebSocket Closing: Code " + code + ", Reason: " + reason); - ws.close(1000, null); - } - - @Override - public void onFailure(WebSocket ws, Throwable t, Response response) { - LOGGER.log(Level.SEVERE, "WebSocket Failure: " + t.getMessage(), t); - if (listener != null) { - listener.onStatusChanged("Status: Failure! See console.", true); - } - } - - @Override - public void onClosed(WebSocket ws, int code, String reason) { - LOGGER.info("WebSocket Closed."); - webSocket = null; - if (listener != null) { - listener.onStatusChanged("Status: Disconnected", false); - } - } }); } @Override public void disconnect() { if (webSocket != null) { - // Unsubscribe from current symbol before closing if (currentSymbol != null && !currentSymbol.isEmpty()) { try { String unsubscribeMsg = String.format("{\"type\":\"unsubscribe\",\"symbol\":\"%s\"}", currentSymbol); webSocket.send(unsubscribeMsg); - LOGGER.info("Unsubscribed from " + currentSymbol); - // Give a small delay for unsubscribe to be processed try { Thread.sleep(100); } catch (InterruptedException e) { @@ -152,8 +124,6 @@ public void disconnect() { webSocket.close(1000, "User disconnect"); webSocket = null; } - // Don't shut down executor service - we might reconnect - // Just set client to null, a new one will be created on next connect client = null; currentSymbol = null; } @@ -205,12 +175,8 @@ private String extractValue(String json, String key) { */ private void processTradeArray(String jsonMessage) { try { - LOGGER.info("Processing trade array message"); - - // Extract the data array part String dataArray = null; if (jsonMessage.contains("\"data\":[")) { - // Format: {"type":"trade","data":[...]} int dataStart = jsonMessage.indexOf("\"data\":[") + 8; int bracketCount = 1; int dataEnd = dataStart; @@ -223,24 +189,15 @@ private void processTradeArray(String jsonMessage) { dataArray = jsonMessage.substring(dataStart, dataEnd - 1); } } else if (jsonMessage.trim().startsWith("[")) { - // Format: [{...}] dataArray = jsonMessage.trim().substring(1, jsonMessage.trim().length() - 1); } - + if (dataArray == null || dataArray.isEmpty()) { - LOGGER.warning("Could not extract data array from message: " + jsonMessage); return; } - - LOGGER.info("Extracted data array: " + dataArray); - - // Split by },{ to get individual trade objects String[] tradeObjects = dataArray.split("\\},\\{"); - LOGGER.info("Found " + tradeObjects.length + " trade object(s) in array"); - for (int i = 0; i < tradeObjects.length; i++) { String tradeObj = tradeObjects[i]; - // Clean up brackets and braces tradeObj = tradeObj.replaceFirst("^\\{?", "{").replaceFirst("\\}?$", "}"); if (!tradeObj.startsWith("{")) { tradeObj = "{" + tradeObj; @@ -248,36 +205,32 @@ private void processTradeArray(String jsonMessage) { if (!tradeObj.endsWith("}")) { tradeObj = tradeObj + "}"; } - - LOGGER.info("Processing trade object " + (i + 1) + ": " + tradeObj); - - // Process as a single trade message + String symbol = extractValue(tradeObj, "s"); String normalizedReceivedSymbol = (symbol != null) ? symbol.trim() : null; String normalizedCurrentSymbol = (currentSymbol != null) ? currentSymbol.trim() : null; - + if (normalizedCurrentSymbol != null && normalizedReceivedSymbol != null && normalizedCurrentSymbol.equalsIgnoreCase(normalizedReceivedSymbol)) { - LOGGER.info("Processing trade message for symbol: " + normalizedReceivedSymbol); - + String priceStr = extractValue(tradeObj, "p"); double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; - + String volumeStr = extractValue(tradeObj, "v"); double volume = (volumeStr != null) ? Double.parseDouble(volumeStr) : 0.0; - + String timestampStr = extractValue(tradeObj, "t"); long timestamp = (timestampStr != null) ? Long.parseLong(timestampStr) : 0L; - + Instant ts = timestamp > 0 ? Instant.ofEpochMilli(timestamp) : null; - + if (listener != null) { listener.onStatusChanged("Status: Connected to " + currentSymbol, false); Trade trade = new Trade(symbol, price, volume, ts); listener.onTrade(trade); } } else { - LOGGER.info("Ignoring trade in array for symbol '" + normalizedReceivedSymbol + + LOGGER.info("Ignoring trade in array for symbol '" + normalizedReceivedSymbol + "' (current: '" + normalizedCurrentSymbol + "')"); } } @@ -285,74 +238,67 @@ private void processTradeArray(String jsonMessage) { LOGGER.log(Level.WARNING, "Error processing trade array: " + e.getMessage(), e); } } - + /** * Parses the incoming JSON message using manual string manipulation and dispatches Trade domain objects. * Based on working Finnhub WebSocket example. */ private void processMessage(String jsonMessage) { try { - // Handle ping messages (connection keep-alive) if (jsonMessage.contains("\"type\":\"ping\"")) { hasReceivedPing = true; LOGGER.info("Received PING: Connection is alive."); return; } - - // Handle trade messages - this is the main format Finnhub uses + if (jsonMessage.contains("\"type\":\"trade\"")) { - LOGGER.info("💵 RECEIVED TRADE DATA: " + jsonMessage); - // Parse the trade data - Finnhub sends: {"type":"trade","data":[{"s":"SYMBOL","p":price,"v":volume,"t":timestamp}]} - // Or sometimes: {"type":"trade","s":"SYMBOL","p":price,"v":volume,"t":timestamp} - - // Check if it's an array format with "data" field if (jsonMessage.contains("\"data\":[")) { - LOGGER.info("Trade message contains data array, processing as array"); processTradeArray(jsonMessage); return; } - - // Otherwise, try to extract trade data directly from the message + String symbol = extractValue(jsonMessage, "s"); - - // Normalize both symbols for comparison (trim and case-insensitive) - String normalizedReceivedSymbol = (symbol != null) ? symbol.trim() : null; - String normalizedCurrentSymbol = (currentSymbol != null) ? currentSymbol.trim() : null; - - // Only process messages for the current symbol to avoid processing old messages - // Use case-insensitive comparison to handle API format differences - if (normalizedCurrentSymbol == null || - normalizedReceivedSymbol == null || - !normalizedCurrentSymbol.equalsIgnoreCase(normalizedReceivedSymbol)) { - LOGGER.info("Ignoring trade message for symbol '" + normalizedReceivedSymbol + - "' (current: '" + normalizedCurrentSymbol + "')"); - return; - } - - LOGGER.info("Processing trade message for symbol: " + normalizedReceivedSymbol); String priceStr = extractValue(jsonMessage, "p"); - double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; + double price; + if (priceStr != null) { + price = Double.parseDouble(priceStr); + } else { + price = 0.0; + } String volumeStr = extractValue(jsonMessage, "v"); - double volume = (volumeStr != null) ? Double.parseDouble(volumeStr) : 0.0; + double volume; + if (volumeStr != null) { + volume = Double.parseDouble(volumeStr); + } else { + volume = 0.0; + } String timestampStr = extractValue(jsonMessage, "t"); - long timestamp = (timestampStr != null) ? Long.parseLong(timestampStr) : 0L; + long timestamp; + if (timestampStr != null) { + timestamp = Long.parseLong(timestampStr); + } else { + timestamp = 0L; + } + + Instant ts; + if (timestamp > 0) { + ts = Instant.ofEpochMilli(timestamp); + } else { + ts = null; + } - Instant ts = timestamp > 0 ? Instant.ofEpochMilli(timestamp) : null; if (listener != null) { - // First successful trade means we are effectively connected for this symbol. listener.onStatusChanged("Status: Connected to " + currentSymbol, false); - Trade trade = new Trade(symbol, price, volume, ts); listener.onTrade(trade); } } - // Handle error messages + else if (jsonMessage.contains("\"type\":\"error\"")) { - // Handle error messages from the API String errorMsg = extractValue(jsonMessage, "msg"); LOGGER.warning("Received error message: " + errorMsg); if (listener != null) { @@ -360,7 +306,6 @@ else if (jsonMessage.contains("\"type\":\"error\"")) { listener.onStatusChanged("Status: Error - " + errorText, true); } } else { - // Log any other messages (like subscription confirmations) LOGGER.info("Received TEXT: " + jsonMessage); // Check if it might be an array format we missed if (jsonMessage.trim().startsWith("[") && jsonMessage.contains("\"s\"") && jsonMessage.contains("\"p\"")) { From b8ee464c3aff9cc48cb693fd8f2f1a615164c445 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Tue, 2 Dec 2025 17:43:28 -0500 Subject: [PATCH 095/103] deletion of unused code for data accessing --- src/main/java/use_case/trade/TradeFeed.java | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 src/main/java/use_case/trade/TradeFeed.java diff --git a/src/main/java/use_case/trade/TradeFeed.java b/src/main/java/use_case/trade/TradeFeed.java deleted file mode 100644 index afc588b0..00000000 --- a/src/main/java/use_case/trade/TradeFeed.java +++ /dev/null @@ -1,21 +0,0 @@ -package use_case.trade; - -/** - * Abstraction for any real-time trade data source. - * Infrastructure implementations (e.g., Finnhub WebSocket) should implement this. - */ -public interface TradeFeed { - - /** - * Starts or (re)connects the feed and begins streaming trades to the listener. - * @param symbol The trading symbol to subscribe to (e.g., "BINANCE:BTCUSDT", "AAPL") - * @param listener The listener to receive trade updates - */ - void connect(String symbol, TradeListener listener); - - /** - * Gracefully disconnects the feed. - */ - void disconnect(); -} - From dd7e68c01670fa7dce4f6ec42ae08f82527a8514 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Tue, 2 Dec 2025 18:08:53 -0500 Subject: [PATCH 096/103] deletion of debugging logger terminal prints --- .../FinnhubTradeDataAccessObject.java | 30 +++---------------- .../trade/TradeController.java | 1 - .../java/use_case/trade/TradeInteractor.java | 4 --- src/main/java/view/TradeView.java | 2 -- 4 files changed, 4 insertions(+), 33 deletions(-) diff --git a/src/main/java/data_access/FinnhubTradeDataAccessObject.java b/src/main/java/data_access/FinnhubTradeDataAccessObject.java index dac6a55b..9f9c8bce 100644 --- a/src/main/java/data_access/FinnhubTradeDataAccessObject.java +++ b/src/main/java/data_access/FinnhubTradeDataAccessObject.java @@ -11,8 +11,6 @@ import java.time.Instant; import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; /** * Finnhub-specific implementation of the TradeDataAccessInterface. @@ -20,8 +18,6 @@ */ public class FinnhubTradeDataAccessObject implements TradeDataAccessInterface { - private static final Logger LOGGER = Logger.getLogger(FinnhubTradeDataAccessObject.class.getName()); - private static final String API_KEY = "d4977ehr01qshn3kvpt0d4977ehr01qshn3kvptg"; private static final String WEB_SOCKET_URL = "wss://ws.finnhub.io?token=" + API_KEY; @@ -40,19 +36,17 @@ public void connect(String symbol, TradeListener listener) { } if (webSocket != null) { - LOGGER.info("Closing existing Finnhub connection..."); if (currentSymbol != null && !currentSymbol.isEmpty()) { try { String unsubscribeMsg = String.format("{\"type\":\"unsubscribe\",\"symbol\":\"%s\"}", currentSymbol); webSocket.send(unsubscribeMsg); - LOGGER.info("Unsubscribed from " + currentSymbol); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } catch (Exception e) { - LOGGER.log(Level.WARNING, "Error unsubscribing: " + e.getMessage()); + // Error unsubscribing } } webSocket.close(1000, "Reconnecting"); @@ -70,7 +64,6 @@ public void connect(String symbol, TradeListener listener) { this.currentSymbol = null; } this.hasReceivedPing = false; - LOGGER.info("Setting currentSymbol to: '" + this.currentSymbol + "'"); client = new OkHttpClient.Builder() .readTimeout(0, TimeUnit.MILLISECONDS) @@ -87,19 +80,12 @@ public void connect(String symbol, TradeListener listener) { webSocket = client.newWebSocket(request, new WebSocketListener() { @Override public void onOpen(WebSocket ws, Response response) { - LOGGER.info("WebSocket Opened. Subscribing to symbol: '" + currentSymbol + "'"); String subscribeMsg = String.format("{\"type\":\"subscribe\",\"symbol\":\"%s\"}", currentSymbol); ws.send(subscribeMsg); - LOGGER.info("Sent subscribe message: " + subscribeMsg); } @Override public void onMessage(WebSocket ws, String text) { - LOGGER.info("Received raw message from WebSocket: " + text); - LOGGER.info("Message length: " + text.length() + " characters"); - if (text != null && !text.isEmpty()) { - LOGGER.info("First 200 chars of message: " + text.substring(0, Math.min(200, text.length()))); - } processMessage(text); } }); @@ -118,7 +104,7 @@ public void disconnect() { Thread.currentThread().interrupt(); } } catch (Exception e) { - LOGGER.log(Level.WARNING, "Error unsubscribing: " + e.getMessage()); + // Error unsubscribing } } webSocket.close(1000, "User disconnect"); @@ -229,13 +215,10 @@ private void processTradeArray(String jsonMessage) { Trade trade = new Trade(symbol, price, volume, ts); listener.onTrade(trade); } - } else { - LOGGER.info("Ignoring trade in array for symbol '" + normalizedReceivedSymbol + - "' (current: '" + normalizedCurrentSymbol + "')"); } } } catch (Exception e) { - LOGGER.log(Level.WARNING, "Error processing trade array: " + e.getMessage(), e); + // Error processing trade array } } @@ -247,7 +230,6 @@ private void processMessage(String jsonMessage) { try { if (jsonMessage.contains("\"type\":\"ping\"")) { hasReceivedPing = true; - LOGGER.info("Received PING: Connection is alive."); return; } @@ -300,22 +282,18 @@ private void processMessage(String jsonMessage) { else if (jsonMessage.contains("\"type\":\"error\"")) { String errorMsg = extractValue(jsonMessage, "msg"); - LOGGER.warning("Received error message: " + errorMsg); if (listener != null) { String errorText = errorMsg != null ? errorMsg : "Unknown error"; listener.onStatusChanged("Status: Error - " + errorText, true); } } else { - LOGGER.info("Received TEXT: " + jsonMessage); // Check if it might be an array format we missed if (jsonMessage.trim().startsWith("[") && jsonMessage.contains("\"s\"") && jsonMessage.contains("\"p\"")) { - LOGGER.info("Message appears to be an array format, trying array processing"); processTradeArray(jsonMessage); } } } catch (Exception e) { - LOGGER.log(Level.WARNING, "Error processing JSON message with manual parsing: " + e.getMessage() - + "\nMessage: " + jsonMessage); + // Error processing JSON message } } } diff --git a/src/main/java/interface_adapter/trade/TradeController.java b/src/main/java/interface_adapter/trade/TradeController.java index 5cc0b15a..8284a3cc 100644 --- a/src/main/java/interface_adapter/trade/TradeController.java +++ b/src/main/java/interface_adapter/trade/TradeController.java @@ -15,7 +15,6 @@ public TradeController(TradeInputBoundary tradeInteractor) { } public void execute(String symbol) { - System.out.println("TradeController: Received symbol: '" + symbol + "'"); TradeRequestModel requestModel = new TradeRequestModel(symbol); tradeInteractor.execute(requestModel); } diff --git a/src/main/java/use_case/trade/TradeInteractor.java b/src/main/java/use_case/trade/TradeInteractor.java index df5b6ecb..7a3ab0b4 100644 --- a/src/main/java/use_case/trade/TradeInteractor.java +++ b/src/main/java/use_case/trade/TradeInteractor.java @@ -35,7 +35,6 @@ public void onStatusChanged(String statusText, boolean isError) { @Override public void execute(TradeRequestModel requestModel) { String symbol = requestModel.getSymbol(); - System.out.println("TradeInteractor: Received symbol from request model: '" + symbol + "'"); if (symbol == null || symbol.trim().isEmpty()) { tradePresenter.prepareFailView("Please enter a symbol."); @@ -43,13 +42,10 @@ public void execute(TradeRequestModel requestModel) { } symbol = symbol.trim(); - System.out.println("TradeInteractor: Calling tradeDataAccess.connect with symbol: '" + symbol + "'"); try { tradeDataAccess.connect(symbol, internalListener); } catch (Exception e) { - System.err.println("TradeInteractor: Error connecting: " + e.getMessage()); - e.printStackTrace(); tradePresenter.prepareFailView("Unable to connect to trade feed. " + e.getMessage()); } } diff --git a/src/main/java/view/TradeView.java b/src/main/java/view/TradeView.java index 033de03b..def5a553 100644 --- a/src/main/java/view/TradeView.java +++ b/src/main/java/view/TradeView.java @@ -143,7 +143,6 @@ private void styleLabel(JLabel label) { */ private void onConnectClicked() { String symbol = symbolInputField.getText().trim(); - System.out.println("TradeView: User entered symbol: '" + symbol + "'"); if (symbol.isEmpty()) { statusLabel.setText("Status: Please enter a symbol"); statusLabel.setForeground(Color.RED); @@ -215,7 +214,6 @@ private void onConnectClicked() { statusLabel.setForeground(Color.ORANGE); // Execute through controller - System.out.println("TradeView: Calling tradeController.execute with symbol: '" + symbol + "'"); tradeController.execute(symbol); // Start a timeout check for symbol not found From 8250617e13426f55f1f8e2220ca52ee76f097aea Mon Sep 17 00:00:00 2001 From: lyzsa Date: Tue, 2 Dec 2025 19:52:16 -0500 Subject: [PATCH 097/103] Trade test --- .../use_case/trade/TradeInteractorTest.java | 288 ++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 src/test/java/use_case/trade/TradeInteractorTest.java diff --git a/src/test/java/use_case/trade/TradeInteractorTest.java b/src/test/java/use_case/trade/TradeInteractorTest.java new file mode 100644 index 00000000..f0d3fe74 --- /dev/null +++ b/src/test/java/use_case/trade/TradeInteractorTest.java @@ -0,0 +1,288 @@ +package use_case.trade; + +import entity.Trade; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for TradeInteractor. + */ +class TradeInteractorTest { + + /** + * Simple in-memory stub for TradeDataAccessInterface. + */ + private static class InMemoryTradeDataAccess implements TradeDataAccessInterface { + + TradeListener storedListener; + String lastSymbol; + boolean throwOnConnect = false; + boolean throwOnDisconnect = false; + int connectCalls = 0; + int disconnectCalls = 0; + + @Override + public void connect(String symbol, TradeListener listener) { + connectCalls++; + lastSymbol = symbol; + storedListener = listener; + if (throwOnConnect) { + throw new RuntimeException("Connection error"); + } + } + + @Override + public void disconnect() { + disconnectCalls++; + if (throwOnDisconnect) { + throw new RuntimeException("Disconnection error"); + } + } + } + + /** + * Recording presenter that captures the last success, error, or status update. + */ + private static class RecordingPresenter implements TradeOutputBoundary { + + TradeResponseModel lastSuccess; + String lastError; + String lastStatusText; + Boolean lastStatusIsError; + + @Override + public void prepareSuccessView(TradeResponseModel responseModel) { + lastSuccess = responseModel; + } + + @Override + public void prepareFailView(String errorMessage) { + lastError = errorMessage; + } + + @Override + public void prepareStatusView(String statusText, boolean isError) { + lastStatusText = statusText; + lastStatusIsError = isError; + } + } + + @Test + void execute_success_validSymbol() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + TradeRequestModel requestModel = new TradeRequestModel("AAPL"); + + // Act + interactor.execute(requestModel); + + // Assert + assertEquals(1, dataAccess.connectCalls, "DAO should be called exactly once"); + assertEquals("AAPL", dataAccess.lastSymbol, "DAO should receive the correct symbol"); + assertNotNull(dataAccess.storedListener, "Listener should be stored"); + assertNull(presenter.lastError, "No error should be reported"); + } + + @Test + void execute_failure_emptySymbol() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + TradeRequestModel requestModel = new TradeRequestModel(""); + + // Act + interactor.execute(requestModel); + + // Assert + assertEquals(0, dataAccess.connectCalls, "DAO should not be called"); + assertNotNull(presenter.lastError, "Error should be reported"); + assertEquals("Please enter a symbol.", presenter.lastError); + assertNull(presenter.lastSuccess, "No success should be sent"); + } + + @Test + void execute_failure_nullSymbol() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + TradeRequestModel requestModel = new TradeRequestModel(null); + + // Act + interactor.execute(requestModel); + + // Assert + assertEquals(0, dataAccess.connectCalls, "DAO should not be called"); + assertNotNull(presenter.lastError, "Error should be reported"); + assertEquals("Please enter a symbol.", presenter.lastError); + } + + @Test + void execute_failure_whitespaceOnlySymbol() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + TradeRequestModel requestModel = new TradeRequestModel(" "); + + // Act + interactor.execute(requestModel); + + // Assert + assertEquals(0, dataAccess.connectCalls, "DAO should not be called"); + assertNotNull(presenter.lastError, "Error should be reported"); + assertEquals("Please enter a symbol.", presenter.lastError); + } + + @Test + void execute_success_trimsWhitespace() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + TradeRequestModel requestModel = new TradeRequestModel(" AAPL "); + + // Act + interactor.execute(requestModel); + + // Assert + assertEquals(1, dataAccess.connectCalls); + assertEquals("AAPL", dataAccess.lastSymbol, "Symbol should be trimmed"); + } + + @Test + void execute_failure_connectionException() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + dataAccess.throwOnConnect = true; + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + TradeRequestModel requestModel = new TradeRequestModel("AAPL"); + + // Act + interactor.execute(requestModel); + + // Assert + assertEquals(1, dataAccess.connectCalls); + assertNotNull(presenter.lastError, "Error should be reported"); + assertTrue(presenter.lastError.contains("Unable to connect to trade feed"), + "Error message should mention connection failure"); + assertTrue(presenter.lastError.contains("Connection error"), + "Error message should include exception message"); + } + + @Test + void execute_success_listenerOnTradeCalled() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + TradeRequestModel requestModel = new TradeRequestModel("BINANCE:BTCUSDT"); + + // Act + interactor.execute(requestModel); + + // Simulate trade data arriving via listener callback + Trade trade = new Trade("BINANCE:BTCUSDT", 50000.0, 1.5, Instant.now()); + dataAccess.storedListener.onTrade(trade); + + // Assert + assertNotNull(presenter.lastSuccess, "Success view should be prepared"); + assertNotNull(presenter.lastSuccess.getTrade(), "Trade should be in response model"); + assertEquals("BINANCE:BTCUSDT", presenter.lastSuccess.getTrade().getSymbol()); + assertEquals(50000.0, presenter.lastSuccess.getTrade().getPrice()); + assertEquals(1.5, presenter.lastSuccess.getTrade().getVolume()); + } + + @Test + void execute_success_listenerOnStatusChangedCalled() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + TradeRequestModel requestModel = new TradeRequestModel("AAPL"); + + // Act + interactor.execute(requestModel); + + // Simulate status update via listener callback + dataAccess.storedListener.onStatusChanged("Status: Connected", false); + + // Assert + assertNotNull(presenter.lastStatusText, "Status should be recorded"); + assertEquals("Status: Connected", presenter.lastStatusText); + assertFalse(presenter.lastStatusIsError, "Should not be an error status"); + } + + @Test + void execute_success_listenerOnStatusChangedError() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + TradeRequestModel requestModel = new TradeRequestModel("AAPL"); + + // Act + interactor.execute(requestModel); + + // Simulate error status via listener callback + dataAccess.storedListener.onStatusChanged("Status: Error - Symbol not found", true); + + // Assert + assertEquals("Status: Error - Symbol not found", presenter.lastStatusText); + assertTrue(presenter.lastStatusIsError, "Should be an error status"); + } + + @Test + void disconnect_success() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + + // Act + interactor.disconnect(); + + // Assert + assertEquals(1, dataAccess.disconnectCalls, "DAO disconnect should be called"); + assertNull(presenter.lastError, "No error should be reported"); + } + + @Test + void disconnect_failure_exception() { + // Arrange + InMemoryTradeDataAccess dataAccess = new InMemoryTradeDataAccess(); + RecordingPresenter presenter = new RecordingPresenter(); + dataAccess.throwOnDisconnect = true; + + TradeInteractor interactor = new TradeInteractor(dataAccess, presenter); + + // Act + interactor.disconnect(); + + // Assert + assertEquals(1, dataAccess.disconnectCalls); + assertNotNull(presenter.lastError, "Error should be reported"); + assertTrue(presenter.lastError.contains("Unable to disconnect"), + "Error message should mention disconnection failure"); + assertTrue(presenter.lastError.contains("Disconnection error"), + "Error message should include exception message"); + } +} + From 405d69ebd44eda12990a8c213536a73d14a84f86 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Tue, 2 Dec 2025 20:05:03 -0500 Subject: [PATCH 098/103] README update with watchlist functionality --- README.md | 199 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 180 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index ef0260a8..c3b25b97 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,15 @@ src/main/java/ ### Key Components -- **Entity Layer**: Domain models (`User`, `Stock`, `Trade`, `NewsArticle`, etc.) +- **Entity Layer**: Domain models (`User`, `Stock`, `StockQuote`, `Trade`, `NewsArticle`, `EarningsRecord`, `MarketStatus`) - **Use Case Layer**: Business logic and application rules + - User management: Login, Signup, Logout, Change Password + - Stock operations: Stock Search, Filter Search + - Market data: News, Earnings History, Market Status + - Real-time data: Trade Feed + - User features: Watchlist management - **Interface Adapter Layer**: Controllers, Presenters, and ViewModels -- **Framework Layer**: Data access objects and UI components +- **Framework Layer**: Data access objects and UI components (Swing views) ## Getting Started @@ -52,18 +57,87 @@ src/main/java/ - Maven 3.6+ - Internet connection (for API access) +### Installation & Running + +1. **Clone the repository**: + ```bash + git clone + cd CSC207_Stock_Project + ``` + +2. **Build the project**: + ```bash + mvn clean compile + ``` + +3. **Run the application**: + ```bash + mvn exec:java -Dexec.mainClass="app.Main" + ``` + Or run `Main.java` directly from your IDE + +4. **Run tests**: + ```bash + mvn test + ``` + ### First Time Setup -1. **Create an Account**: Click "Sign Up" to create a new user account -2. **Login**: Use your credentials to log in -3. **Explore Features**: Navigate using the top toolbar buttons +1. **Create an Account**: + - Launch the application + - Click "Sign Up" to create a new user account + - Enter a username and password + +2. **Login**: + - Use your credentials to log in + +3. **Explore Features**: + - Navigate using the top toolbar buttons + - Try searching for stocks, viewing news, or connecting to real-time trades ## Usage Guide -### Real-Time Trade Feed +### User Management +#### Sign Up +1. Launch the application +2. Enter a username and password +3. Click "Sign Up" to create your account +4. You'll be automatically redirected to the login page + +#### Login +1. Enter your username and password +2. Click "Log In" to access the application +3. You'll be taken to the main dashboard (Logged In View) + +#### Change Password +1. After logging in, enter your new password in the password field +2. Click "Change Password" to update your credentials +3. You'll receive a confirmation message upon success + +#### Logout +1. Click "Log Out" to securely end your session +2. You'll be redirected to the login page + +### Main Dashboard Features + +Once logged in, you'll see the main dashboard with the following features: + +#### Stock Search & Watchlist +1. Enter a stock symbol (e.g., `AAPL`, `TSLA`, `GOOGL`) in the search field on the main dashboard +2. Click "Search" to retrieve detailed stock information +3. View stock details in the "Stock Information" panel including: + - Company name and sector + - Current price and market data + - Price changes and volume +4. **Add to Watchlist**: Click the "Add to Watchlist" button at the bottom of the main dashboard to save the currently displayed stock information to your watchlist +5. **View Watchlist**: Click the "Account" button in the top toolbar to view all your saved watchlist items + +#### Real-Time Trade Feed 1. Click the **"Realtime Trade"** button in the top toolbar 2. Enter a trading symbol in the input field (default: `BINANCE:BTCUSDT`) + - **Crypto pairs work best**: `BINANCE:BTCUSDT`, `BINANCE:ETHUSDT` + - **Stock symbols**: `AAPL`, `TSLA` (only active during market hours) 3. Click **"Connect"** to start receiving real-time trade data 4. View live updates for: - Symbol @@ -77,13 +151,40 @@ src/main/java/ - Rate limiting is enforced (5-second cooldown between connections) - Invalid symbols or connection errors will display appropriate error messages - The application automatically handles API rate limits (429 errors) - -### Other Features - -- **News**: View market and company news with filtering options -- **History**: Access earnings history for companies -- **Filter Search**: Search and filter stocks by various criteria -- **Market Status**: View current market status (open/closed) +- Crypto pairs (e.g., `BINANCE:BTCUSDT`) work 24/7, while stock symbols only work during market hours + +#### Filter Search +1. Click the **"Filter Search"** button in the top toolbar +2. Use the search and filter options to find stocks by: + - Symbol pattern + - Market criteria + - Other filtering options +3. View filtered results in the table +4. Click "Back" to return to the main dashboard + +#### News +1. Click the **"News"** button in the top toolbar +2. View latest market news and company-specific articles +3. Filter news by company symbol if needed +4. Click "Back" to return to the main dashboard + +#### Earnings History +1. Click the **"History"** button in the top toolbar +2. Enter a stock symbol to view historical earnings data +3. Browse through past earnings records and dates +4. Click "Back" to return to the main dashboard + +#### Account & Watchlist +1. Click the **"Account"** button in the top toolbar +2. View all your saved watchlist items that you've added from the main dashboard +3. Your watchlist displays all the stock information you've saved using the "Add to Watchlist" button +4. Click "Back" to return to the main dashboard + +#### Market Status +- The market status indicator is displayed in the top toolbar +- Shows current market status (Open/Closed) in real-time +- Updates automatically when market status changes +- Color-coded: Green for open, Gray for closed, Red for errors ## Technical Details @@ -120,18 +221,78 @@ CSC207_Stock_Project/ ## Error Handling -The application includes comprehensive error handling: -- **Rate Limiting**: Automatic cooldown periods to prevent API throttling -- **Connection Errors**: User-friendly error messages for connection issues -- **Invalid Symbols**: Clear feedback when symbols are not found -- **Network Issues**: Graceful handling of network failures +The application includes comprehensive error handling across all features: +- **User Authentication**: Clear error messages for invalid credentials or existing usernames +- **Password Management**: Validation for password changes and mismatches +- **Stock Search**: Error messages for invalid symbols or search failures +- **Rate Limiting**: Automatic cooldown periods to prevent API throttling (trade feed) +- **Connection Errors**: User-friendly error messages for WebSocket connection issues +- **Invalid Symbols**: Clear feedback when symbols are not found or unavailable +- **Network Issues**: Graceful handling of network failures and API unavailability +- **Data Loading**: Error handling for failed data fetches (news, earnings, market status) + +## Use Cases Implemented + +The application implements the following use cases, each following Clean Architecture: + +### User Management +- **Signup**: Create new user accounts with validation +- **Login**: Authenticate users and manage sessions +- **Logout**: Terminate user sessions securely +- **Change Password**: Update user passwords with validation + +### Stock Operations +- **Stock Search**: Search for stocks by symbol and display detailed information +- **Filter Search**: Advanced filtering and search capabilities for stock discovery + +### Market Data +- **News**: Fetch and display market and company-specific news articles +- **Earnings History**: Retrieve and display historical earnings data for companies +- **Market Status**: Real-time market open/closed status with automatic updates + +### Real-Time Features +- **Trade Feed**: WebSocket-based real-time trade data streaming + - Supports both crypto pairs and stock symbols + - Automatic reconnection and error handling + - Rate limiting protection + +### User Features +- **Watchlist**: Save and manage favorite stocks in user accounts +- **Account Management**: View and manage user account information + +## Design Patterns & Principles + +### SOLID Principles +- **Single Responsibility**: Each class has one clear purpose +- **Open/Closed**: System open for extension (new implementations) but closed for modification +- **Liskov Substitution**: All interface implementations are interchangeable +- **Interface Segregation**: Focused interfaces (e.g., separate interfaces for Signup, Login, ChangePassword) +- **Dependency Inversion**: High-level components depend on abstractions (interfaces), not concrete implementations + +### Design Patterns +- **Observer Pattern**: PropertyChangeListener for view updates +- **Adapter Pattern**: TradeListener adapts async callbacks to synchronous presenter calls +- **Builder Pattern**: AppBuilder for dependency injection and application setup +- **Strategy Pattern**: Swappable data access implementations (FileUserDataAccessObject, DBUserDataAccessObject) + +## Testing + +The project includes unit tests for use cases: +- Login, Signup, Logout tests +- Stock Search, Filter Search tests +- News, Earnings History, Market Status tests +- Additional tests can be run with `mvn test` ## Contributing This is a course project following Clean Architecture principles. When contributing: -- Maintain separation of concerns +- Maintain separation of concerns across layers - Follow the existing architecture patterns - Add appropriate error handling - Include unit tests for new features +- Respect interface boundaries and dependency inversion + +## License +[Add your license information here if applicable] From e0437604602b3c2dbb62ef3cb72b0cab15faf097 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Tue, 2 Dec 2025 20:24:08 -0500 Subject: [PATCH 099/103] Error Handling for closed market --- .../login_presenter_sequence_diagram.puml | 39 ----- src/main/java/app/AppBuilder.java | 4 + src/main/java/view/TradeView.java | 136 +++++++++++++++++- 3 files changed, 134 insertions(+), 45 deletions(-) delete mode 100644 plantuml/login_presenter_sequence_diagram.puml diff --git a/plantuml/login_presenter_sequence_diagram.puml b/plantuml/login_presenter_sequence_diagram.puml deleted file mode 100644 index 0dd0b200..00000000 --- a/plantuml/login_presenter_sequence_diagram.puml +++ /dev/null @@ -1,39 +0,0 @@ -@startuml -skinparam dpi 300 -actor User -participant LoginPresenter as LP -participant LoggedInViewModel as LVM -participant LoggedInState as LS -participant PropertyChangeSupport as PCS -participant LoggedInView as LIV - -activate LP -User -> LP : prepareSuccessView(LoginOutputData) -LP -> LVM : getState() -activate LVM -LVM --> LP : LoggedInState -deactivate LVM - -LP -> LS : setUsername(response.getUsername()) -activate LS -LS --> LP -deactivate LS - -LP -> PCS : firePropertyChange() -activate PCS - - -PCS -> LIV : propertyChange(evt) -activate LIV - -LIV -> LIV : update UI with LoggedInState - -LIV --> PCS -deactivate LIV -PCS --> LP -deactivate PCS - -... Diagram continues with LoginViewModel and ViewManager updates... - - -@enduml \ No newline at end of file diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 1dab2f06..6746c549 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -349,6 +349,10 @@ public AppBuilder addMarketStatusUseCase() { MarketStatusInputBoundary msInteractor = new MarketStatusInteractor(msDao, msPresenter); marketStatusController = new MarketStatusController(msInteractor); loggedInView.setMarketStatusViewModel(marketStatusViewModel); + // Also set market status view model on trade view if it exists + if (tradeView != null) { + tradeView.setMarketStatusViewModel(marketStatusViewModel); + } marketStatusController.updateStatus(); return this; } diff --git a/src/main/java/view/TradeView.java b/src/main/java/view/TradeView.java index def5a553..389963ae 100644 --- a/src/main/java/view/TradeView.java +++ b/src/main/java/view/TradeView.java @@ -2,6 +2,7 @@ import entity.Trade; import interface_adapter.ViewManagerModel; +import interface_adapter.market_status.MarketStatusViewModel; import interface_adapter.trade.TradeController; import interface_adapter.trade.TradeState; import interface_adapter.trade.TradeViewModel; @@ -26,6 +27,7 @@ public class TradeView extends JPanel implements PropertyChangeListener { private final TradeController tradeController; private final TradeViewModel tradeViewModel; + private MarketStatusViewModel marketStatusViewModel; // Trade data labels private final JLabel symbolLabel = new JLabel("---"); @@ -34,6 +36,7 @@ public class TradeView extends JPanel implements PropertyChangeListener { private final JLabel timestampLabel = new JLabel("---"); private final JLabel statusLabel = new JLabel("Status: Disconnected", SwingConstants.CENTER); private final JTextField symbolInputField = new JTextField(20); + private JLabel symbolTypeLabel; // Label to show symbol type (Stock/Crypto) private JButton connectButton; private JButton disconnectButton; private String currentSymbol = ""; @@ -89,12 +92,37 @@ private JPanel createMainDashboardCard() { JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); searchPanel.add(new JLabel("Symbol:")); symbolInputField.setText("BINANCE:BTCUSDT"); // Default value - symbolInputField.setToolTipText("Enter crypto pair (e.g., BINANCE:BTCUSDT). Stock symbols may not have real-time trade data."); + symbolInputField.setToolTipText("Enter crypto pair (e.g., BINANCE:BTCUSDT) or stock symbol (e.g., AAPL)"); + + // Add listener to detect symbol type as user types + symbolInputField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { + @Override + public void insertUpdate(javax.swing.event.DocumentEvent e) { + updateSymbolTypeIndicator(); + } + + @Override + public void removeUpdate(javax.swing.event.DocumentEvent e) { + updateSymbolTypeIndicator(); + } + + @Override + public void changedUpdate(javax.swing.event.DocumentEvent e) { + updateSymbolTypeIndicator(); + } + }); + searchPanel.add(symbolInputField); - // Add a help label to guide users - JLabel helpLabel = new JLabel("Note: Crypto pairs work best (e.g., BINANCE:BTCUSDT)"); - searchPanel.add(helpLabel); + // Symbol type indicator label + JLabel symbolTypeLabel = new JLabel(""); + symbolTypeLabel.setFont(new Font("Arial", Font.ITALIC, 11)); + symbolTypeLabel.setForeground(Color.GRAY); + searchPanel.add(symbolTypeLabel); + this.symbolTypeLabel = symbolTypeLabel; // Store reference for updates + + // Initialize symbol type indicator + updateSymbolTypeIndicator(); connectButton = new JButton("Connect"); connectButton.addActionListener(e -> onConnectClicked()); searchPanel.add(connectButton); @@ -137,6 +165,73 @@ private void styleLabel(JLabel label) { label.setFont(new Font("Monospaced", Font.BOLD, 18)); label.setForeground(new Color(0, 128, 0)); // Green color for data } + + /** + * Determines if a symbol is a cryptocurrency pair or a stock symbol. + * @param symbol The symbol to check + * @return "Crypto" if it's a crypto pair, "Stock" if it's a stock, "Unknown" if unclear + */ + private String detectSymbolType(String symbol) { + if (symbol == null || symbol.trim().isEmpty()) { + return "Unknown"; + } + + String upperSymbol = symbol.trim().toUpperCase(); + + // Check for crypto pair format: EXCHANGE:PAIR (e.g., BINANCE:BTCUSDT, COINBASE:BTCUSD) + if (upperSymbol.contains(":")) { + String[] parts = upperSymbol.split(":", 2); + if (parts.length == 2) { + String exchange = parts[0]; + String pair = parts[1]; + + // Common crypto exchanges + if (exchange.equals("BINANCE") || exchange.equals("COINBASE") || + exchange.equals("KRAKEN") || exchange.equals("BITSTAMP") || + exchange.equals("BITFINEX") || exchange.equals("GEMINI")) { + // Check if pair looks like a crypto pair (usually ends with USD, USDT, EUR, etc.) + if (pair.endsWith("USD") || pair.endsWith("USDT") || pair.endsWith("EUR") || + pair.endsWith("BTC") || pair.endsWith("ETH") || pair.length() >= 6) { + return "Crypto"; + } + } + } + } + + // Stock symbols are typically 1-5 uppercase letters/numbers, no special characters + // Common patterns: AAPL, TSLA, GOOGL, BRK.B, 005930 (Korean stocks) + if (upperSymbol.matches("^[A-Z0-9]{1,5}$") || upperSymbol.matches("^[A-Z0-9]+\\.[A-Z]$")) { + return "Stock"; + } + + return "Unknown"; + } + + /** + * Updates the symbol type indicator label based on current input. + */ + private void updateSymbolTypeIndicator() { + if (symbolTypeLabel == null) { + return; + } + + String symbol = symbolInputField.getText().trim(); + String symbolType = detectSymbolType(symbol); + + switch (symbolType) { + case "Crypto": + symbolTypeLabel.setText("(Crypto)"); + symbolTypeLabel.setForeground(new Color(0, 150, 0)); // Green + break; + case "Stock": + symbolTypeLabel.setText("(Stock)"); + symbolTypeLabel.setForeground(new Color(200, 100, 0)); // Orange + break; + default: + symbolTypeLabel.setText(""); + break; + } + } /** * Handles the Connect button click by delegating to the Controller. @@ -149,8 +244,30 @@ private void onConnectClicked() { return; } - // Warn user if they're using a stock symbol (not crypto) - if (!symbol.contains(":") || !symbol.toUpperCase().startsWith("BINANCE:")) { + // Detect symbol type + String symbolType = detectSymbolType(symbol); + + // If it's a stock symbol, check if market is closed + if (symbolType.equals("Stock")) { + if (marketStatusViewModel != null && !marketStatusViewModel.isOpen()) { + String marketStatusText = marketStatusViewModel.getStatusText(); + if (marketStatusText == null) { + marketStatusText = "Market is closed"; + } + JOptionPane.showMessageDialog( + this, + "Cannot connect to stock symbol '" + symbol + "' because the market is closed.\n\n" + + "Market Status: " + marketStatusText + "\n\n" + + "Please try:\n" + + "• Wait until the market opens\n" + + "• Use a cryptocurrency pair instead (e.g., BINANCE:BTCUSDT)", + "Market Closed", + JOptionPane.WARNING_MESSAGE + ); + return; + } + + // Warn user if they're using a stock symbol (not crypto) int result = JOptionPane.showConfirmDialog( this, "Stock symbols (like AAPL, TSLA) may not have real-time trade data available.\n\n" + @@ -467,4 +584,11 @@ public void setBackNavigation(ViewManagerModel viewManagerModel, String homeView this.viewManagerModel = viewManagerModel; this.homeViewName = homeViewName; } + + /** + * Sets the market status view model to check if market is open/closed. + */ + public void setMarketStatusViewModel(MarketStatusViewModel marketStatusViewModel) { + this.marketStatusViewModel = marketStatusViewModel; + } } From 1dd18551735bb3fa1c1877d1bb2090841df33100 Mon Sep 17 00:00:00 2001 From: lyzsa Date: Tue, 2 Dec 2025 20:28:55 -0500 Subject: [PATCH 100/103] Error Handling for closed market --- src/main/java/app/AppBuilder.java | 1 - src/main/java/view/LoggedInView.java | 5 ----- 2 files changed, 6 deletions(-) diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 6746c549..d57b15fd 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -349,7 +349,6 @@ public AppBuilder addMarketStatusUseCase() { MarketStatusInputBoundary msInteractor = new MarketStatusInteractor(msDao, msPresenter); marketStatusController = new MarketStatusController(msInteractor); loggedInView.setMarketStatusViewModel(marketStatusViewModel); - // Also set market status view model on trade view if it exists if (tradeView != null) { tradeView.setMarketStatusViewModel(marketStatusViewModel); } diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 9de57ef4..442e1060 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -101,14 +101,9 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { final JButton realtimeTradeButton = new JButton("Realtime Trade"); realtimeTradeButton.addActionListener(e -> { - System.out.println("Realtime Trade button clicked"); // debug - System.out.println("viewManagerModel: " + viewManagerModel); // debug - System.out.println("realtimeTradeViewName: " + realtimeTradeViewName); // debug if (viewManagerModel != null && realtimeTradeViewName != null) { viewManagerModel.setState(realtimeTradeViewName); viewManagerModel.firePropertyChange(); - } else { - System.out.println("ERROR: Cannot navigate - viewManagerModel or realtimeTradeViewName is null"); // debug } }); From cccada76d77f6be4956ff0cd315a6c27915ee95d Mon Sep 17 00:00:00 2001 From: lyzsa Date: Tue, 2 Dec 2025 20:35:18 -0500 Subject: [PATCH 101/103] Minor changes for detecting symbol type --- src/main/java/view/TradeView.java | 57 ------------------------------- 1 file changed, 57 deletions(-) diff --git a/src/main/java/view/TradeView.java b/src/main/java/view/TradeView.java index 389963ae..c3fd6ce5 100644 --- a/src/main/java/view/TradeView.java +++ b/src/main/java/view/TradeView.java @@ -36,7 +36,6 @@ public class TradeView extends JPanel implements PropertyChangeListener { private final JLabel timestampLabel = new JLabel("---"); private final JLabel statusLabel = new JLabel("Status: Disconnected", SwingConstants.CENTER); private final JTextField symbolInputField = new JTextField(20); - private JLabel symbolTypeLabel; // Label to show symbol type (Stock/Crypto) private JButton connectButton; private JButton disconnectButton; private String currentSymbol = ""; @@ -93,36 +92,7 @@ private JPanel createMainDashboardCard() { searchPanel.add(new JLabel("Symbol:")); symbolInputField.setText("BINANCE:BTCUSDT"); // Default value symbolInputField.setToolTipText("Enter crypto pair (e.g., BINANCE:BTCUSDT) or stock symbol (e.g., AAPL)"); - - // Add listener to detect symbol type as user types - symbolInputField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { - @Override - public void insertUpdate(javax.swing.event.DocumentEvent e) { - updateSymbolTypeIndicator(); - } - - @Override - public void removeUpdate(javax.swing.event.DocumentEvent e) { - updateSymbolTypeIndicator(); - } - - @Override - public void changedUpdate(javax.swing.event.DocumentEvent e) { - updateSymbolTypeIndicator(); - } - }); - searchPanel.add(symbolInputField); - - // Symbol type indicator label - JLabel symbolTypeLabel = new JLabel(""); - symbolTypeLabel.setFont(new Font("Arial", Font.ITALIC, 11)); - symbolTypeLabel.setForeground(Color.GRAY); - searchPanel.add(symbolTypeLabel); - this.symbolTypeLabel = symbolTypeLabel; // Store reference for updates - - // Initialize symbol type indicator - updateSymbolTypeIndicator(); connectButton = new JButton("Connect"); connectButton.addActionListener(e -> onConnectClicked()); searchPanel.add(connectButton); @@ -207,32 +177,6 @@ private String detectSymbolType(String symbol) { return "Unknown"; } - /** - * Updates the symbol type indicator label based on current input. - */ - private void updateSymbolTypeIndicator() { - if (symbolTypeLabel == null) { - return; - } - - String symbol = symbolInputField.getText().trim(); - String symbolType = detectSymbolType(symbol); - - switch (symbolType) { - case "Crypto": - symbolTypeLabel.setText("(Crypto)"); - symbolTypeLabel.setForeground(new Color(0, 150, 0)); // Green - break; - case "Stock": - symbolTypeLabel.setText("(Stock)"); - symbolTypeLabel.setForeground(new Color(200, 100, 0)); // Orange - break; - default: - symbolTypeLabel.setText(""); - break; - } - } - /** * Handles the Connect button click by delegating to the Controller. */ @@ -271,7 +215,6 @@ private void onConnectClicked() { int result = JOptionPane.showConfirmDialog( this, "Stock symbols (like AAPL, TSLA) may not have real-time trade data available.\n\n" + - "Crypto pairs (like BINANCE:BTCUSDT) work best for real-time trades.\n\n" + "Do you want to continue with '" + symbol + "'?", "Symbol Warning", JOptionPane.YES_NO_OPTION, From c6268683b85832afe6504dcb9339dc2bdd4ea93f Mon Sep 17 00:00:00 2001 From: lyzsa Date: Tue, 2 Dec 2025 20:55:32 -0500 Subject: [PATCH 102/103] Refactor tradecode to meet Checkstyle --- .../FinnhubTradeDataAccessObject.java | 83 ++++-- src/main/java/view/TradeView.java | 246 ++++++++++-------- 2 files changed, 191 insertions(+), 138 deletions(-) diff --git a/src/main/java/data_access/FinnhubTradeDataAccessObject.java b/src/main/java/data_access/FinnhubTradeDataAccessObject.java index 9f9c8bce..c9a3b594 100644 --- a/src/main/java/data_access/FinnhubTradeDataAccessObject.java +++ b/src/main/java/data_access/FinnhubTradeDataAccessObject.java @@ -19,7 +19,14 @@ public class FinnhubTradeDataAccessObject implements TradeDataAccessInterface { private static final String API_KEY = "d4977ehr01qshn3kvpt0d4977ehr01qshn3kvptg"; - private static final String WEB_SOCKET_URL = "wss://ws.finnhub.io?token=" + API_KEY; + private static final String WEB_SOCKET_URL = "wss://ws.finnhub.io?token=" + + API_KEY; + private static final int UNSUBSCRIBE_DELAY_MS = 100; + private static final int RECONNECT_DELAY_MS = 200; + private static final int CLOSE_CODE_NORMAL = 1000; + private static final int CONNECT_TIMEOUT_SECONDS = 10; + private static final int PING_INTERVAL_SECONDS = 25; + private static final int DATA_ARRAY_OFFSET = 8; private WebSocket webSocket; private OkHttpClient client; @@ -38,10 +45,12 @@ public void connect(String symbol, TradeListener listener) { if (webSocket != null) { if (currentSymbol != null && !currentSymbol.isEmpty()) { try { - String unsubscribeMsg = String.format("{\"type\":\"unsubscribe\",\"symbol\":\"%s\"}", currentSymbol); + String unsubscribeMsg = String.format( + "{\"type\":\"unsubscribe\",\"symbol\":\"%s\"}", + currentSymbol); webSocket.send(unsubscribeMsg); try { - Thread.sleep(100); + Thread.sleep(UNSUBSCRIBE_DELAY_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -49,10 +58,10 @@ public void connect(String symbol, TradeListener listener) { // Error unsubscribing } } - webSocket.close(1000, "Reconnecting"); + webSocket.close(CLOSE_CODE_NORMAL, "Reconnecting"); webSocket = null; try { - Thread.sleep(200); + Thread.sleep(RECONNECT_DELAY_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -67,8 +76,8 @@ public void connect(String symbol, TradeListener listener) { client = new OkHttpClient.Builder() .readTimeout(0, TimeUnit.MILLISECONDS) - .connectTimeout(10, TimeUnit.SECONDS) - .pingInterval(25, TimeUnit.SECONDS) // Finnhub often sends pings - keep connection alive + .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .pingInterval(PING_INTERVAL_SECONDS, TimeUnit.SECONDS) .build(); Request request = new Request.Builder().url(WEB_SOCKET_URL).build(); @@ -80,7 +89,9 @@ public void connect(String symbol, TradeListener listener) { webSocket = client.newWebSocket(request, new WebSocketListener() { @Override public void onOpen(WebSocket ws, Response response) { - String subscribeMsg = String.format("{\"type\":\"subscribe\",\"symbol\":\"%s\"}", currentSymbol); + String subscribeMsg = String.format( + "{\"type\":\"subscribe\",\"symbol\":\"%s\"}", + currentSymbol); ws.send(subscribeMsg); } @@ -96,10 +107,12 @@ public void disconnect() { if (webSocket != null) { if (currentSymbol != null && !currentSymbol.isEmpty()) { try { - String unsubscribeMsg = String.format("{\"type\":\"unsubscribe\",\"symbol\":\"%s\"}", currentSymbol); + String unsubscribeMsg = String.format( + "{\"type\":\"unsubscribe\",\"symbol\":\"%s\"}", + currentSymbol); webSocket.send(unsubscribeMsg); try { - Thread.sleep(100); + Thread.sleep(UNSUBSCRIBE_DELAY_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } @@ -107,7 +120,7 @@ public void disconnect() { // Error unsubscribing } } - webSocket.close(1000, "User disconnect"); + webSocket.close(CLOSE_CODE_NORMAL, "User disconnect"); webSocket = null; } client = null; @@ -137,15 +150,22 @@ private String extractValue(String json, String key) { } } else { int valueEnd = valueStart; - while (valueEnd < json.length() && (Character.isDigit(json.charAt(valueEnd)) || json.charAt(valueEnd) == '.' || json.charAt(valueEnd) == '-')) { + while (valueEnd < json.length() + && (Character.isDigit(json.charAt(valueEnd)) + || json.charAt(valueEnd) == '.' + || json.charAt(valueEnd) == '-')) { valueEnd++; } int commaIndex = json.indexOf(',', valueStart); int braceIndex = json.indexOf('}', valueStart); int terminationIndex = json.length(); - if (commaIndex != -1) terminationIndex = Math.min(terminationIndex, commaIndex); - if (braceIndex != -1) terminationIndex = Math.min(terminationIndex, braceIndex); + if (commaIndex != -1) { + terminationIndex = Math.min(terminationIndex, commaIndex); + } + if (braceIndex != -1) { + terminationIndex = Math.min(terminationIndex, braceIndex); + } if (terminationIndex > valueStart) { return json.substring(valueStart, terminationIndex).trim(); @@ -163,19 +183,26 @@ private void processTradeArray(String jsonMessage) { try { String dataArray = null; if (jsonMessage.contains("\"data\":[")) { - int dataStart = jsonMessage.indexOf("\"data\":[") + 8; + int dataStart = jsonMessage.indexOf("\"data\":[") + + DATA_ARRAY_OFFSET; int bracketCount = 1; int dataEnd = dataStart; while (dataEnd < jsonMessage.length() && bracketCount > 0) { - if (jsonMessage.charAt(dataEnd) == '[') bracketCount++; - if (jsonMessage.charAt(dataEnd) == ']') bracketCount--; + if (jsonMessage.charAt(dataEnd) == '[') { + bracketCount++; + } + if (jsonMessage.charAt(dataEnd) == ']') { + bracketCount--; + } dataEnd++; } if (bracketCount == 0) { dataArray = jsonMessage.substring(dataStart, dataEnd - 1); } } else if (jsonMessage.trim().startsWith("[")) { - dataArray = jsonMessage.trim().substring(1, jsonMessage.trim().length() - 1); + String trimmedMessage = jsonMessage.trim(); + dataArray = trimmedMessage.substring(1, + trimmedMessage.length() - 1); } if (dataArray == null || dataArray.isEmpty()) { @@ -196,8 +223,10 @@ private void processTradeArray(String jsonMessage) { String normalizedReceivedSymbol = (symbol != null) ? symbol.trim() : null; String normalizedCurrentSymbol = (currentSymbol != null) ? currentSymbol.trim() : null; - if (normalizedCurrentSymbol != null && normalizedReceivedSymbol != null && - normalizedCurrentSymbol.equalsIgnoreCase(normalizedReceivedSymbol)) { + if (normalizedCurrentSymbol != null + && normalizedReceivedSymbol != null + && normalizedCurrentSymbol + .equalsIgnoreCase(normalizedReceivedSymbol)) { String priceStr = extractValue(tradeObj, "p"); double price = (priceStr != null) ? Double.parseDouble(priceStr) : 0.0; @@ -211,7 +240,8 @@ private void processTradeArray(String jsonMessage) { Instant ts = timestamp > 0 ? Instant.ofEpochMilli(timestamp) : null; if (listener != null) { - listener.onStatusChanged("Status: Connected to " + currentSymbol, false); + listener.onStatusChanged("Status: Connected to " + + currentSymbol, false); Trade trade = new Trade(symbol, price, volume, ts); listener.onTrade(trade); } @@ -274,13 +304,12 @@ private void processMessage(String jsonMessage) { if (listener != null) { - listener.onStatusChanged("Status: Connected to " + currentSymbol, false); + listener.onStatusChanged("Status: Connected to " + + currentSymbol, false); Trade trade = new Trade(symbol, price, volume, ts); listener.onTrade(trade); } - } - - else if (jsonMessage.contains("\"type\":\"error\"")) { + } else if (jsonMessage.contains("\"type\":\"error\"")) { String errorMsg = extractValue(jsonMessage, "msg"); if (listener != null) { String errorText = errorMsg != null ? errorMsg : "Unknown error"; @@ -288,7 +317,9 @@ else if (jsonMessage.contains("\"type\":\"error\"")) { } } else { // Check if it might be an array format we missed - if (jsonMessage.trim().startsWith("[") && jsonMessage.contains("\"s\"") && jsonMessage.contains("\"p\"")) { + if (jsonMessage.trim().startsWith("[") + && jsonMessage.contains("\"s\"") + && jsonMessage.contains("\"p\"")) { processTradeArray(jsonMessage); } } diff --git a/src/main/java/view/TradeView.java b/src/main/java/view/TradeView.java index c3fd6ce5..b830a549 100644 --- a/src/main/java/view/TradeView.java +++ b/src/main/java/view/TradeView.java @@ -43,7 +43,14 @@ public class TradeView extends JPanel implements PropertyChangeListener { private long lastConnectionAttemptTime = 0; private boolean isConnected = false; private static final long SYMBOL_NOT_FOUND_TIMEOUT = 10000; // 10 seconds - private static final long CONNECTION_COOLDOWN_MS = 5000; // 5 seconds between connection attempts + private static final long CONNECTION_COOLDOWN_MS = 5000; // 5 seconds + private static final int DISCONNECT_DELAY_MS = 300; + private static final int ONE_SECOND_MS = 1000; + private static final int MILLISECONDS_PER_SECOND = 1000; + private static final String STATUS_CONNECTED = "Connected"; + private static final String STATUS_CONNECTING = "Connecting"; + private static final String STATUS_DISCONNECTED = "Disconnected"; + private static final String CRYPTO_PAIR_EXAMPLE = "BINANCE:BTCUSDT"; /** * Initializes the GUI components. @@ -90,8 +97,9 @@ private JPanel createMainDashboardCard() { // Search panel at top with Connect and Disconnect buttons JPanel searchPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); searchPanel.add(new JLabel("Symbol:")); - symbolInputField.setText("BINANCE:BTCUSDT"); // Default value - symbolInputField.setToolTipText("Enter crypto pair (e.g., BINANCE:BTCUSDT) or stock symbol (e.g., AAPL)"); + symbolInputField.setText(CRYPTO_PAIR_EXAMPLE); + symbolInputField.setToolTipText("Enter crypto pair (e.g., " + + CRYPTO_PAIR_EXAMPLE + ") or stock symbol (e.g., AAPL)"); searchPanel.add(symbolInputField); connectButton = new JButton("Connect"); connectButton.addActionListener(e -> onConnectClicked()); @@ -156,12 +164,14 @@ private String detectSymbolType(String symbol) { String pair = parts[1]; // Common crypto exchanges - if (exchange.equals("BINANCE") || exchange.equals("COINBASE") || - exchange.equals("KRAKEN") || exchange.equals("BITSTAMP") || - exchange.equals("BITFINEX") || exchange.equals("GEMINI")) { - // Check if pair looks like a crypto pair (usually ends with USD, USDT, EUR, etc.) - if (pair.endsWith("USD") || pair.endsWith("USDT") || pair.endsWith("EUR") || - pair.endsWith("BTC") || pair.endsWith("ETH") || pair.length() >= 6) { + if (exchange.equals("BINANCE") || exchange.equals("COINBASE") + || exchange.equals("KRAKEN") || exchange.equals("BITSTAMP") + || exchange.equals("BITFINEX") || exchange.equals("GEMINI")) { + // Check if pair looks like a crypto pair + // (usually ends with USD, USDT, EUR, etc.) + if (pair.endsWith("USD") || pair.endsWith("USDT") + || pair.endsWith("EUR") || pair.endsWith("BTC") + || pair.endsWith("ETH") || pair.length() >= 6) { return "Crypto"; } } @@ -198,28 +208,31 @@ private void onConnectClicked() { if (marketStatusText == null) { marketStatusText = "Market is closed"; } + String message = "Cannot connect to stock symbol '" + + symbol + "' because the market is closed.\n\n" + + "Market Status: " + marketStatusText + "\n\n" + + "Please try:\n" + + "• Wait until the market opens\n" + + "• Use a cryptocurrency pair instead (e.g., " + + CRYPTO_PAIR_EXAMPLE + ")"; JOptionPane.showMessageDialog( - this, - "Cannot connect to stock symbol '" + symbol + "' because the market is closed.\n\n" + - "Market Status: " + marketStatusText + "\n\n" + - "Please try:\n" + - "• Wait until the market opens\n" + - "• Use a cryptocurrency pair instead (e.g., BINANCE:BTCUSDT)", - "Market Closed", - JOptionPane.WARNING_MESSAGE - ); + this, + message, + "Market Closed", + JOptionPane.WARNING_MESSAGE); return; } // Warn user if they're using a stock symbol (not crypto) + String warningMessage = "Stock symbols (like AAPL, TSLA) " + + "may not have real-time trade data available.\n\n" + + "Do you want to continue with '" + symbol + "'?"; int result = JOptionPane.showConfirmDialog( - this, - "Stock symbols (like AAPL, TSLA) may not have real-time trade data available.\n\n" + - "Do you want to continue with '" + symbol + "'?", - "Symbol Warning", - JOptionPane.YES_NO_OPTION, - JOptionPane.WARNING_MESSAGE - ); + this, + warningMessage, + "Symbol Warning", + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE); if (result == JOptionPane.NO_OPTION) { return; } @@ -230,52 +243,47 @@ private void onConnectClicked() { long timeSinceLastAttempt = currentTime - lastConnectionAttemptTime; if (timeSinceLastAttempt < CONNECTION_COOLDOWN_MS) { - long remainingTime = (CONNECTION_COOLDOWN_MS - timeSinceLastAttempt) / 1000; + long remainingTime = (CONNECTION_COOLDOWN_MS - timeSinceLastAttempt) + / MILLISECONDS_PER_SECOND; + String rateLimitMessage = "Please wait " + remainingTime + + " second(s) before connecting again.\n" + + "Too many requests may result in rate limiting."; JOptionPane.showMessageDialog( - this, - "Please wait " + remainingTime + " second(s) before connecting again.\nToo many requests may result in rate limiting.", - "Rate Limit", - JOptionPane.WARNING_MESSAGE - ); + this, + rateLimitMessage, + "Rate Limit", + JOptionPane.WARNING_MESSAGE); return; } - - // Always disconnect first to ensure clean state + if (isConnected || connectionStartTime > 0) { tradeController.disconnect(); - // Reset state immediately isConnected = false; connectionStartTime = 0; - // Give a delay for disconnect to complete try { - Thread.sleep(300); + Thread.sleep(DISCONNECT_DELAY_MS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } - - // Update last connection attempt time + lastConnectionAttemptTime = currentTime; - - // Reset state and set new connection start time + currentSymbol = symbol; connectionStartTime = System.currentTimeMillis(); symbolLabel.setText("---"); priceLabel.setText("---"); volumeLabel.setText("---"); timestampLabel.setText("---"); - - // Disable connect button during connection attempt, enable disconnect button + connectButton.setEnabled(false); disconnectButton.setEnabled(true); - - // Immediately show "Connecting..." while we wait for the first trade. + statusLabel.setText("Status: Connecting..."); statusLabel.setForeground(Color.ORANGE); - - // Execute through controller + tradeController.execute(symbol); - + // Start a timeout check for symbol not found startSymbolNotFoundCheck(); } @@ -284,10 +292,7 @@ private void onConnectClicked() { * Handles the Disconnect button click. */ private void onDisconnectClicked() { - // Disconnect through controller tradeController.disconnect(); - - // Reset all info to default resetToDefault(); } @@ -295,20 +300,17 @@ private void onDisconnectClicked() { * Resets all information to default values. */ private void resetToDefault() { - // Reset connection state isConnected = false; connectionStartTime = 0; currentSymbol = ""; - - // Reset UI labels to default + symbolLabel.setText("---"); priceLabel.setText("---"); volumeLabel.setText("---"); timestampLabel.setText("---"); statusLabel.setText("Status: Disconnected"); statusLabel.setForeground(Color.BLACK); - - // Reset button states + connectButton.setEnabled(true); disconnectButton.setEnabled(false); connectButton.setText("Connect"); @@ -318,7 +320,6 @@ private void resetToDefault() { * Starts a background thread to check if symbol is not found after timeout. */ private void startSymbolNotFoundCheck() { - // Capture the connection start time and symbol for this specific connection attempt final long checkStartTime = connectionStartTime; final String checkSymbol = currentSymbol; @@ -326,41 +327,46 @@ private void startSymbolNotFoundCheck() { try { Thread.sleep(SYMBOL_NOT_FOUND_TIMEOUT); SwingUtilities.invokeLater(() -> { - // Only trigger timeout if: - // 1. We're still checking the same symbol - // 2. The connection start time hasn't changed (no new connection started) - // 3. Enough time has passed since connection started - // 4. No trades have been received (symbol label still shows "---") long currentTime = System.currentTimeMillis(); - if (checkSymbol.equals(currentSymbol) && - connectionStartTime == checkStartTime && - connectionStartTime > 0 && - currentTime - connectionStartTime >= SYMBOL_NOT_FOUND_TIMEOUT && - symbolLabel.getText().equals("---")) { - // No trades received - could be symbol not found or rate limiting - boolean isCrypto = currentSymbol.contains(":") && currentSymbol.toUpperCase().startsWith("BINANCE:"); - String message = "Connection timeout: Symbol '" + currentSymbol + "' not found or no trades available.\n\n"; + if (checkSymbol.equals(currentSymbol) + && connectionStartTime == checkStartTime + && connectionStartTime > 0 + && currentTime - connectionStartTime + >= SYMBOL_NOT_FOUND_TIMEOUT + && symbolLabel.getText().equals("---")) { + // No trades received - could be symbol not found + // or rate limiting + boolean isCrypto = currentSymbol.contains(":") + && currentSymbol.toUpperCase() + .startsWith("BINANCE:"); + String message = "Connection timeout: Symbol '" + + currentSymbol + + "' not found or no trades available.\n\n"; if (!isCrypto) { - message += "⚠️ IMPORTANT: Stock symbols (like AAPL, TSLA) typically do NOT have real-time trade data on Finnhub.\n" + - "The WebSocket trade feed primarily supports CRYPTO pairs.\n\n"; + message += "⚠️ IMPORTANT: Stock symbols " + + "(like AAPL, TSLA) typically do NOT " + + "have real-time trade data on Finnhub.\n" + + "The WebSocket trade feed primarily " + + "supports CRYPTO pairs.\n\n"; } - message += "This may also occur if:\n" + - "• Requests are made too frequently (rate limiting)\n" + - "• The symbol format is incorrect\n" + - "• The market is closed\n\n" + - "✅ RECOMMENDED: Use crypto pairs like:\n" + - " • BINANCE:BTCUSDT\n" + - " • BINANCE:ETHUSDT\n" + - " • BINANCE:BNBUSDT\n\n" + - "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + - " seconds before trying again."; - + message += "This may also occur if:\n" + + "• Requests are made too frequently " + + "(rate limiting)\n" + + "• The symbol format is incorrect\n" + + "• The market is closed\n\n" + + "✅ RECOMMENDED: Use crypto pairs like:\n" + + " • BINANCE:BTCUSDT\n" + + " • BINANCE:ETHUSDT\n" + + " • BINANCE:BNBUSDT\n\n" + + "Please wait " + + (CONNECTION_COOLDOWN_MS / MILLISECONDS_PER_SECOND) + + " seconds before trying again."; + JOptionPane.showMessageDialog( - TradeView.this, - message, - "Connection Error", - JOptionPane.ERROR_MESSAGE - ); + TradeView.this, + message, + "Connection Error", + JOptionPane.ERROR_MESSAGE); statusLabel.setText("Status: Connection failed"); statusLabel.setForeground(Color.RED); tradeController.disconnect(); @@ -383,14 +389,17 @@ private void startCooldownTimer() { connectButton.setEnabled(false); new Thread(() -> { try { - for (int remaining = (int)(CONNECTION_COOLDOWN_MS / 1000); remaining > 0; remaining--) { + int cooldownSeconds = (int) (CONNECTION_COOLDOWN_MS + / MILLISECONDS_PER_SECOND); + for (int remaining = cooldownSeconds; remaining > 0; remaining--) { final int seconds = remaining; SwingUtilities.invokeLater(() -> { connectButton.setText("Wait " + seconds + "s"); - statusLabel.setText("Status: Rate limited - wait " + seconds + " second(s)"); + statusLabel.setText("Status: Rate limited - wait " + + seconds + " second(s)"); statusLabel.setForeground(Color.ORANGE); }); - Thread.sleep(1000); + Thread.sleep(ONE_SECOND_MS); } SwingUtilities.invokeLater(() -> { connectButton.setText("Connect"); @@ -457,43 +466,56 @@ private void updateStatusOnUi(String statusText, boolean isError) { isConnected = false; // Combined error handling for rate limiting and symbol not found - boolean isRateLimit = statusText.contains("429") || statusText.contains("Too Many Requests") || statusText.contains("Rate limit"); - boolean isOtherError = statusText.contains("Error") || statusText.contains("Failure"); - + boolean isRateLimit = statusText.contains("429") + || statusText.contains("Too Many Requests") + || statusText.contains("Rate limit"); + boolean isOtherError = statusText.contains("Error") + || statusText.contains("Failure"); + if (isRateLimit || isOtherError) { String message; + long cooldownSeconds = CONNECTION_COOLDOWN_MS + / MILLISECONDS_PER_SECOND; if (isRateLimit) { - message = "Connection failed: Too Many Requests (429).\n" + - "This may also occur if the symbol '" + currentSymbol + "' is invalid.\n\n" + - "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + - " seconds before trying again.\n" + - "If the problem persists, please verify the symbol is correct."; + message = "Connection failed: Too Many Requests (429).\n" + + "This may also occur if the symbol '" + + currentSymbol + "' is invalid.\n\n" + + "Please wait " + cooldownSeconds + + " seconds before trying again.\n" + + "If the problem persists, please verify " + + "the symbol is correct."; // Enforce cooldown after 429 error lastConnectionAttemptTime = System.currentTimeMillis(); startCooldownTimer(); } else { - message = "Connection failed: Symbol '" + currentSymbol + "' not found or invalid.\n" + - "This may also occur if requests are made too frequently.\n\n" + - "Please wait " + (CONNECTION_COOLDOWN_MS / 1000) + - " seconds before trying again.\n" + - "If the problem persists, please verify the symbol is correct."; + message = "Connection failed: Symbol '" + + currentSymbol + + "' not found or invalid.\n" + + "This may also occur if requests are made " + + "too frequently.\n\n" + + "Please wait " + cooldownSeconds + + " seconds before trying again.\n" + + "If the problem persists, please verify " + + "the symbol is correct."; // Enforce cooldown for other errors too lastConnectionAttemptTime = System.currentTimeMillis(); startCooldownTimer(); } - + JOptionPane.showMessageDialog( - TradeView.this, - message, - "Connection Error", - JOptionPane.ERROR_MESSAGE - ); + TradeView.this, + message, + "Connection Error", + JOptionPane.ERROR_MESSAGE); } - } else if (statusText != null && statusText.contains("Connected")) { + } else if (statusText != null + && statusText.contains(STATUS_CONNECTED)) { statusLabel.setForeground(new Color(0, 128, 0)); - } else if (statusText != null && statusText.contains("Connecting")) { + } else if (statusText != null + && statusText.contains(STATUS_CONNECTING)) { statusLabel.setForeground(Color.ORANGE); - } else if (statusText != null && statusText.contains("Disconnected")) { + } else if (statusText != null + && statusText.contains(STATUS_DISCONNECTED)) { statusLabel.setForeground(Color.BLACK); // On disconnect, reset button states connectButton.setEnabled(true); From 0c1f28fe780c2a27aedfbfa5bc75447b016af74f Mon Sep 17 00:00:00 2001 From: lyzsa Date: Tue, 2 Dec 2025 20:56:31 -0500 Subject: [PATCH 103/103] minor style updates --- src/main/java/data_access/FinnhubTradeDataAccessObject.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/data_access/FinnhubTradeDataAccessObject.java b/src/main/java/data_access/FinnhubTradeDataAccessObject.java index c9a3b594..e7b7e080 100644 --- a/src/main/java/data_access/FinnhubTradeDataAccessObject.java +++ b/src/main/java/data_access/FinnhubTradeDataAccessObject.java @@ -316,7 +316,6 @@ private void processMessage(String jsonMessage) { listener.onStatusChanged("Status: Error - " + errorText, true); } } else { - // Check if it might be an array format we missed if (jsonMessage.trim().startsWith("[") && jsonMessage.contains("\"s\"") && jsonMessage.contains("\"p\"")) {