diff --git a/.gitignore b/.gitignore index 275ff638..62d8dd17 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,14 @@ build/ .DS_Store ### other files ### -users.csv \ No newline at end of file +users.csv + +# Environment variables +.env +.env.local +.env.development +.env.production + +# Configuration files with secrets +config.properties +*.config \ No newline at end of file diff --git a/README.md b/README.md index 9487008b..85e9832f 100644 --- a/README.md +++ b/README.md @@ -1,252 +1,10 @@ -# Clean Architecture Team Lab Activity: Login and Logout +Cleaned out this readme. Add stuff here -In this team lab activity, your team will: -- explore an existing use case (login) -- add a new use case (logout). +Original readme from here: +https://github.com/CSC207-2025F-UofT/ca-lab/blob/main/README.md -To earn credit: -- your team must demo your working `logout` use case. +Also, Spotify WebAPi Java wrapper in case we want to use it. +https://github.com/spotify-web-api-java/spotify-web-api-java?tab=readme-ov-file -Your demo should be similar to the below example: +> import se.michaelthelin.spotify.SpotifyApi; to access the api -![](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? - ---- diff --git a/loyaltyscore_1203.json b/loyaltyscore_1203.json new file mode 100644 index 00000000..c346b359 --- /dev/null +++ b/loyaltyscore_1203.json @@ -0,0 +1,272 @@ +{"loyalty_scores": [ + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + } +]} \ No newline at end of file diff --git a/loyaltyscore_520.json b/loyaltyscore_520.json new file mode 100644 index 00000000..01d08aed --- /dev/null +++ b/loyaltyscore_520.json @@ -0,0 +1,12 @@ +{"loyalty_scores": [ + { + "date": "2023-11-23", + "score": 85, + "artist_name": "Artist1" + }, + { + "date": "2024-11-25", + "score": 150, + "artist_name": "Artist1" + } +]} \ No newline at end of file diff --git a/loyaltyscore_kh1203.json b/loyaltyscore_kh1203.json new file mode 100644 index 00000000..271f2fd1 --- /dev/null +++ b/loyaltyscore_kh1203.json @@ -0,0 +1,92 @@ +{"loyalty_scores": [ + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + }, + { + "date": "2025-04-12", + "score": 90, + "artist_name": "Artist1" + }, + { + "date": "2025-04-13", + "score": 590, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 620, + "artist_name": "Artist1" + }, + { + "date": "2025-11-24", + "score": 1130, + "artist_name": "Artist2" + } +]} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5875d5ae..4565a785 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ okhttp 4.12.0 + org.junit.jupiter junit-jupiter @@ -28,6 +29,19 @@ test + + se.michaelthelin.spotify + spotify-web-api-java + 9.4.0 + + + + org.mockito + mockito-core + 4.5.1 + test + + @@ -49,4 +63,4 @@ UTF-8 - \ No newline at end of file + diff --git a/src/main/java/app/AppBuilder.java b/src/main/java/app/AppBuilder.java index 3c654cad..0c46832f 100644 --- a/src/main/java/app/AppBuilder.java +++ b/src/main/java/app/AppBuilder.java @@ -1,34 +1,35 @@ package app; import data_access.FileUserDataAccessObject; +import data_access.SpotifyDataAccessObject; import entity.UserFactory; import interface_adapter.ViewManagerModel; -import interface_adapter.logged_in.ChangePasswordController; -import interface_adapter.logged_in.ChangePasswordPresenter; import interface_adapter.logged_in.LoggedInViewModel; -import interface_adapter.login.LoginController; import interface_adapter.login.LoginPresenter; import interface_adapter.login.LoginViewModel; -import interface_adapter.logout.LogoutController; -import interface_adapter.logout.LogoutPresenter; -import interface_adapter.signup.SignupController; -import interface_adapter.signup.SignupPresenter; -import interface_adapter.signup.SignupViewModel; +import interface_adapter.spotify_auth.SpotifyAuthController; +import interface_adapter.spotify_auth.SpotifyAuthPresenter; +import interface_adapter.spotify_auth.SpotifyAuthViewModel; +import interface_adapter.daily_mix.DailyMixViewModel; +import interface_adapter.daily_mix.DailyMixController; +import interface_adapter.daily_mix.DailyMixPresenter; import use_case.change_password.ChangePasswordInputBoundary; import use_case.change_password.ChangePasswordInteractor; import use_case.change_password.ChangePasswordOutputBoundary; import use_case.login.LoginInputBoundary; import use_case.login.LoginInteractor; import use_case.login.LoginOutputBoundary; -import use_case.logout.LogoutInputBoundary; -import use_case.logout.LogoutInteractor; -import use_case.logout.LogoutOutputBoundary; -import use_case.signup.SignupInputBoundary; -import use_case.signup.SignupInteractor; -import use_case.signup.SignupOutputBoundary; +import use_case.spotify_auth.SpotifyAuthInputBoundary; +import use_case.spotify_auth.SpotifyAuthInteractor; +import use_case.spotify_auth.SpotifyAuthOutputBoundary; +import use_case.daily_mix.DailyMixInputBoundary; +import use_case.daily_mix.DailyMixInputData; +import use_case.daily_mix.DailyMixInteractor; +import use_case.daily_mix.DailyMixOutputBoundary; +import use_case.daily_mix.DailyMixOutputData; import view.LoggedInView; import view.LoginView; -import view.SignupView; +import view.SpotifyAuthView; import view.ViewManager; import javax.swing.*; @@ -37,112 +38,120 @@ public class AppBuilder { private final JPanel cardPanel = new JPanel(); private final CardLayout cardLayout = new CardLayout(); - final UserFactory userFactory = new UserFactory(); final ViewManagerModel viewManagerModel = new ViewManagerModel(); ViewManager viewManager = new ViewManager(cardPanel, cardLayout, viewManagerModel); - // set which data access implementation to use, can be any - // of the classes from the data_access package - - // DAO version using local file storage - final FileUserDataAccessObject userDataAccessObject = new FileUserDataAccessObject("users.csv", userFactory); - - // DAO version using a shared external database - // final DBUserDataAccessObject userDataAccessObject = new DBUserDataAccessObject(userFactory); - - private SignupView signupView; - private SignupViewModel signupViewModel; private LoginViewModel loginViewModel; private LoggedInViewModel loggedInViewModel; private LoggedInView loggedInView; private LoginView loginView; + private SpotifyAuthView spotifyAuthView; + private SpotifyAuthViewModel spotifyAuthViewModel; + private DailyMixViewModel dailyMixViewModel; public AppBuilder() { cardPanel.setLayout(cardLayout); } - public AppBuilder addSignupView() { - signupViewModel = new SignupViewModel(); - signupView = new SignupView(signupViewModel); - cardPanel.add(signupView, signupView.getViewName()); + public AppBuilder addViewModels() { + loginViewModel = new LoginViewModel(); + loggedInViewModel = new LoggedInViewModel(); + spotifyAuthViewModel = new SpotifyAuthViewModel(); + dailyMixViewModel = new DailyMixViewModel(); return this; } public AppBuilder addLoginView() { - loginViewModel = new LoginViewModel(); - loginView = new LoginView(loginViewModel); + loginView = new LoginView(loginViewModel, viewManagerModel); cardPanel.add(loginView, loginView.getViewName()); return this; } public AppBuilder addLoggedInView() { - loggedInViewModel = new LoggedInViewModel(); - loggedInView = new LoggedInView(loggedInViewModel); + loggedInView = new LoggedInView(loggedInViewModel, viewManagerModel, spotifyAuthViewModel,dailyMixViewModel); cardPanel.add(loggedInView, loggedInView.getViewName()); return this; } - public AppBuilder addSignupUseCase() { - final SignupOutputBoundary signupOutputBoundary = new SignupPresenter(viewManagerModel, - signupViewModel, loginViewModel); - final SignupInputBoundary userSignupInteractor = new SignupInteractor( - userDataAccessObject, signupOutputBoundary, userFactory); + public AppBuilder addSpotifyAuthView() { + spotifyAuthView = new SpotifyAuthView(spotifyAuthViewModel, loggedInViewModel); // Pass loggedInViewModel + spotifyAuthView.setViewManagerModel(viewManagerModel); + cardPanel.add(spotifyAuthView, spotifyAuthView.getViewName()); + return this; + } - SignupController controller = new SignupController(userSignupInteractor); - signupView.setSignupController(controller); + public AppBuilder addSignupUseCase() { + // emptied; do not need return this; } public AppBuilder addLoginUseCase() { final LoginOutputBoundary loginOutputBoundary = new LoginPresenter(viewManagerModel, loggedInViewModel, loginViewModel); - final LoginInputBoundary loginInteractor = new LoginInteractor( - userDataAccessObject, loginOutputBoundary); + // final LoginInputBoundary loginInteractor = new LoginInteractor( + // userDataAccessObject, loginOutputBoundary); + // TODO: add back in LoginInputBoundary loginInteractor variable - LoginController loginController = new LoginController(loginInteractor); - loginView.setLoginController(loginController); + + // LoginController loginController = new LoginController(loginInteractor); + // TODO: Add back in LoginController + // loginView.setLoginController(loginController); return this; } public AppBuilder addChangePasswordUseCase() { - final ChangePasswordOutputBoundary changePasswordOutputBoundary = new ChangePasswordPresenter(viewManagerModel, - loggedInViewModel); + // emptied, do not need. + return this; + } - final ChangePasswordInputBoundary changePasswordInteractor = - new ChangePasswordInteractor(userDataAccessObject, changePasswordOutputBoundary, userFactory); + public AppBuilder addLogoutUseCase() { + // emptied; not necessary. + return this; + } + + public AppBuilder addSpotifyAuthUseCase() { + // PKCE doesn't need client secret! + data_access.SpotifyDataAccessObject spotifyDAO = new data_access.SpotifyDataAccessObject(); + + final SpotifyAuthOutputBoundary spotifyAuthOutputBoundary = + new SpotifyAuthPresenter(viewManagerModel, spotifyAuthViewModel, loggedInViewModel); + + if (spotifyAuthOutputBoundary instanceof SpotifyAuthPresenter) { + ((SpotifyAuthPresenter) spotifyAuthOutputBoundary).setLoggedInView(loggedInView); + } + + final SpotifyAuthInputBoundary spotifyAuthInteractor = + new SpotifyAuthInteractor(spotifyDAO, spotifyAuthOutputBoundary); + + SpotifyAuthController controller = new SpotifyAuthController(spotifyAuthInteractor); + spotifyAuthView.setSpotifyAuthController(controller); - ChangePasswordController changePasswordController = new ChangePasswordController(changePasswordInteractor); - loggedInView.setChangePasswordController(changePasswordController); return this; } - /** - * Adds the Logout Use Case to the application. - * @return this builder - */ - public AppBuilder addLogoutUseCase() { - final LogoutOutputBoundary logoutOutputBoundary = new LogoutPresenter(viewManagerModel, - loggedInViewModel, loginViewModel); + public AppBuilder addDailyMixUseCase() { + // use new DailyMixPresenter and Interactor + final DailyMixOutputBoundary dailyMixOutputBoundary = + new DailyMixPresenter(dailyMixViewModel); - final LogoutInputBoundary logoutInteractor = - new LogoutInteractor(userDataAccessObject, logoutOutputBoundary); + final DailyMixInputBoundary dailyMixInteractor = + new DailyMixInteractor(new SpotifyDataAccessObject(), dailyMixOutputBoundary); + + DailyMixController dailyMixController = new DailyMixController(dailyMixInteractor); + loggedInView.setDailyMixController(dailyMixController); - final LogoutController logoutController = new LogoutController(logoutInteractor); - loggedInView.setLogoutController(logoutController); return this; } public JFrame build() { - final JFrame application = new JFrame("User Login Example"); + final JFrame application = new JFrame("Better Wrapped - Spotify Analysis"); application.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); application.add(cardPanel); - viewManagerModel.setState(signupView.getViewName()); + viewManagerModel.setState(loginView.getViewName()); viewManagerModel.firePropertyChange(); return application; } - - } diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java index 424404fb..06df2fcb 100644 --- a/src/main/java/app/Main.java +++ b/src/main/java/app/Main.java @@ -3,19 +3,36 @@ import javax.swing.*; public class Main { + // VARIABLES CLIENT_ID AND CLIENT_SECRET MUST BE SET: + + String CLIENT_ID; + String CLIENT_SECRET; + public static void main(String[] args) { + String clientId = System.getenv("SPOTIFY_CLIENT_ID"); + + if (clientId == null) { + System.out.println("Please set SPOTIFY_CLIENT_ID environment variable"); + System.out.println("You can get this from: https://developer.spotify.com/dashboard"); + System.out.println("Note: PKCE flow doesn't require a client secret!"); + System.out.println("Running without Spotify integration..."); + } + AppBuilder appBuilder = new AppBuilder(); JFrame application = appBuilder + .addViewModels() // ✅ Create ALL view models FIRST .addLoginView() - .addSignupView() .addLoggedInView() + .addSpotifyAuthView() .addSignupUseCase() .addLoginUseCase() .addChangePasswordUseCase() + .addLogoutUseCase() + .addSpotifyAuthUseCase() + .addDailyMixUseCase() .build(); - application.pack(); application.setLocationRelativeTo(null); application.setVisible(true); } -} +} \ No newline at end of file diff --git a/src/main/java/data_access/DBUserDataAccessObject.java b/src/main/java/data_access/DBUserDataAccessObject.java deleted file mode 100644 index 08cc04ad..00000000 --- a/src/main/java/data_access/DBUserDataAccessObject.java +++ /dev/null @@ -1,162 +0,0 @@ -package data_access; - -import entity.User; -import entity.UserFactory; -import okhttp3.*; -import org.json.JSONException; -import org.json.JSONObject; -import use_case.change_password.ChangePasswordUserDataAccessInterface; -import use_case.login.LoginUserDataAccessInterface; -import use_case.logout.LogoutUserDataAccessInterface; -import use_case.signup.SignupUserDataAccessInterface; - -import java.io.IOException; - -/** - * The DAO for user data. - */ -public class DBUserDataAccessObject implements SignupUserDataAccessInterface, - LoginUserDataAccessInterface, - ChangePasswordUserDataAccessInterface, - LogoutUserDataAccessInterface { - private static final int SUCCESS_CODE = 200; - private static final String CONTENT_TYPE_LABEL = "Content-Type"; - private static final String CONTENT_TYPE_JSON = "application/json"; - private static final String STATUS_CODE_LABEL = "status_code"; - private static final String USERNAME = "username"; - private static final String PASSWORD = "password"; - private static final String MESSAGE = "message"; - private final UserFactory userFactory; - - private String currentUsername; - - public DBUserDataAccessObject(UserFactory userFactory) { - this.userFactory = userFactory; - } - - @Override - public User get(String username) { - // Make an API call to get the user object. - final OkHttpClient client = new OkHttpClient().newBuilder().build(); - final Request request = new Request.Builder() - .url(String.format("http://vm003.teach.cs.toronto.edu:20112/user?username=%s", username)) - .addHeader("Content-Type", CONTENT_TYPE_JSON) - .build(); - try { - final Response response = client.newCall(request).execute(); - - final JSONObject responseBody = new JSONObject(response.body().string()); - - if (responseBody.getInt(STATUS_CODE_LABEL) == SUCCESS_CODE) { - final JSONObject userJSONObject = responseBody.getJSONObject("user"); - final String name = userJSONObject.getString(USERNAME); - final String password = userJSONObject.getString(PASSWORD); - - return userFactory.create(name, password); - } - else { - throw new RuntimeException(responseBody.getString(MESSAGE)); - } - } - catch (IOException | JSONException ex) { - throw new RuntimeException(ex); - } - } - - @Override - public void setCurrentUsername(String name) { - currentUsername = name; - } - - @Override - public String getCurrentUsername() { - return currentUsername; - } - - @Override - public boolean existsByName(String username) { - final OkHttpClient client = new OkHttpClient().newBuilder() - .build(); - final Request request = new Request.Builder() - .url(String.format("http://vm003.teach.cs.toronto.edu:20112/checkIfUserExists?username=%s", username)) - .addHeader(CONTENT_TYPE_LABEL, CONTENT_TYPE_JSON) - .build(); - try { - final Response response = client.newCall(request).execute(); - - final JSONObject responseBody = new JSONObject(response.body().string()); - - // throw new RuntimeException(responseBody.getString("message")); - return responseBody.getInt(STATUS_CODE_LABEL) == SUCCESS_CODE; - } - catch (IOException | JSONException ex) { - throw new RuntimeException(ex); - } - } - - @Override - public void save(User user) { - final OkHttpClient client = new OkHttpClient().newBuilder() - .build(); - - // POST METHOD - final MediaType mediaType = MediaType.parse(CONTENT_TYPE_JSON); - final JSONObject requestBody = new JSONObject(); - requestBody.put(USERNAME, user.getName()); - requestBody.put(PASSWORD, user.getPassword()); - final RequestBody body = RequestBody.create(requestBody.toString(), mediaType); - final Request request = new Request.Builder() - .url("http://vm003.teach.cs.toronto.edu:20112/user") - .method("POST", body) - .addHeader(CONTENT_TYPE_LABEL, CONTENT_TYPE_JSON) - .build(); - try { - final Response response = client.newCall(request).execute(); - - final JSONObject responseBody = new JSONObject(response.body().string()); - - if (responseBody.getInt(STATUS_CODE_LABEL) == SUCCESS_CODE) { - // success! - } - else { - throw new RuntimeException(responseBody.getString(MESSAGE)); - } - } - catch (IOException | JSONException ex) { - throw new RuntimeException(ex); - } - } - - @Override - public void changePassword(User user) { - final OkHttpClient client = new OkHttpClient().newBuilder() - .build(); - - // POST METHOD - final MediaType mediaType = MediaType.parse(CONTENT_TYPE_JSON); - final JSONObject requestBody = new JSONObject(); - requestBody.put(USERNAME, user.getName()); - requestBody.put(PASSWORD, user.getPassword()); - final RequestBody body = RequestBody.create(requestBody.toString(), mediaType); - final Request request = new Request.Builder() - .url("http://vm003.teach.cs.toronto.edu:20112/user") - .method("PUT", body) - .addHeader(CONTENT_TYPE_LABEL, CONTENT_TYPE_JSON) - .build(); - try { - final Response response = client.newCall(request).execute(); - - final JSONObject responseBody = new JSONObject(response.body().string()); - - if (responseBody.getInt(STATUS_CODE_LABEL) == SUCCESS_CODE) { - // success! - } - else { - throw new RuntimeException(responseBody.getString(MESSAGE)); - } - } - catch (IOException | JSONException ex) { - throw new RuntimeException(ex); - } - } -} diff --git a/src/main/java/data_access/FileUserDataAccessObject.java b/src/main/java/data_access/FileUserDataAccessObject.java deleted file mode 100644 index 4cae88c5..00000000 --- a/src/main/java/data_access/FileUserDataAccessObject.java +++ /dev/null @@ -1,124 +0,0 @@ -package data_access; - -import entity.User; -import entity.UserFactory; -import use_case.change_password.ChangePasswordUserDataAccessInterface; -import use_case.login.LoginUserDataAccessInterface; -import use_case.logout.LogoutUserDataAccessInterface; -import use_case.signup.SignupUserDataAccessInterface; - -import java.io.*; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * DAO for user data implemented using a File to persist the data. - */ -public class FileUserDataAccessObject implements SignupUserDataAccessInterface, - LoginUserDataAccessInterface, - ChangePasswordUserDataAccessInterface, - LogoutUserDataAccessInterface { - - private static final String HEADER = "username,password"; - - private final File csvFile; - private final Map headers = new LinkedHashMap<>(); - private final Map accounts = new HashMap<>(); - - private String currentUsername; - - /** - * Construct this DAO for saving to and reading from a local file. - * @param csvPath the path of the file to save to - * @param userFactory factory for creating user objects - * @throws RuntimeException if there is an IOException when accessing the file - */ - public FileUserDataAccessObject(String csvPath, UserFactory userFactory) { - - csvFile = new File(csvPath); - headers.put("username", 0); - headers.put("password", 1); - - if (csvFile.length() == 0) { - save(); - } - else { - - try (BufferedReader reader = new BufferedReader(new FileReader(csvFile))) { - final String header = reader.readLine(); - - if (!header.equals(HEADER)) { - throw new RuntimeException(String.format("header should be%n: %s%n but was:%n%s", HEADER, header)); - } - - String row; - while ((row = reader.readLine()) != null) { - final String[] col = row.split(","); - final String username = String.valueOf(col[headers.get("username")]); - final String password = String.valueOf(col[headers.get("password")]); - final User user = userFactory.create(username, password); - accounts.put(username, user); - } - } - catch (IOException ex) { - throw new RuntimeException(ex); - } - } - } - - private void save() { - final BufferedWriter writer; - try { - writer = new BufferedWriter(new FileWriter(csvFile)); - writer.write(String.join(",", headers.keySet())); - writer.newLine(); - - for (User user : accounts.values()) { - final String line = String.format("%s,%s", - user.getName(), user.getPassword()); - writer.write(line); - writer.newLine(); - } - - writer.close(); - - } - catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - @Override - public void save(User user) { - accounts.put(user.getName(), user); - this.save(); - } - - @Override - public User get(String username) { - return accounts.get(username); - } - - @Override - public void setCurrentUsername(String name) { - currentUsername = name; - } - - @Override - public String getCurrentUsername() { - return currentUsername; - } - - @Override - public boolean existsByName(String identifier) { - return accounts.containsKey(identifier); - } - - @Override - public void changePassword(User user) { - // Replace the User object in the map - accounts.put(user.getName(), user); - save(); - } -} diff --git a/src/main/java/data_access/InMemoryUserDataAccessObject.java b/src/main/java/data_access/InMemoryUserDataAccessObject.java deleted file mode 100644 index fc5dd2b2..00000000 --- a/src/main/java/data_access/InMemoryUserDataAccessObject.java +++ /dev/null @@ -1,56 +0,0 @@ -package data_access; - -import entity.User; -import use_case.change_password.ChangePasswordUserDataAccessInterface; -import use_case.login.LoginUserDataAccessInterface; -import use_case.logout.LogoutUserDataAccessInterface; -import use_case.signup.SignupUserDataAccessInterface; - -import java.util.HashMap; -import java.util.Map; - -/** - * In-memory implementation of the DAO for storing user data. This implementation does - * NOT persist data between runs of the program. - */ -public class InMemoryUserDataAccessObject implements SignupUserDataAccessInterface, - LoginUserDataAccessInterface, - ChangePasswordUserDataAccessInterface, - LogoutUserDataAccessInterface { - - private final Map users = new HashMap<>(); - - private String currentUsername; - - @Override - public boolean existsByName(String identifier) { - return users.containsKey(identifier); - } - - @Override - public void save(User user) { - users.put(user.getName(), user); - } - - @Override - public User get(String username) { - return users.get(username); - } - - @Override - public void setCurrentUsername(String name) { - currentUsername = name; - } - - @Override - public String getCurrentUsername() { - return currentUsername; - } - - @Override - public void changePassword(User user) { - // Replace the old entry with the new password - users.put(user.getName(), user); - } - -} diff --git a/src/main/java/data_access/LoyaltyScoreDataAccessObject.java b/src/main/java/data_access/LoyaltyScoreDataAccessObject.java new file mode 100644 index 00000000..d6936e61 --- /dev/null +++ b/src/main/java/data_access/LoyaltyScoreDataAccessObject.java @@ -0,0 +1,167 @@ +package data_access; + +import org.json.JSONArray; +import org.json.JSONObject; +import use_case.loyalty_score.LoyaltyScoreDataAccessInterface; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; + +/** + * Implementation of LoyaltyScoreDataAccessInterface + * The file is formatted as: loyaltyscore_userid + * { + * "loyalty_scores": [ + * { + * "date": "2023-11-23", + * "artist_name": "Artist1", + * "score": 85 + * } + */ + +public class LoyaltyScoreDataAccessObject implements LoyaltyScoreDataAccessInterface { + + + @Override + public Map getLoyaltyDate(String userid, String date) { + Map loyaltyScores = new HashMap<>(); + JSONObject rootNode = readUserFile(userid); + if (rootNode == null) { + return loyaltyScores; // Early return if the file doesn't exist + } + + JSONArray loyaltyScoresArray = rootNode.optJSONArray("loyalty_scores"); + if (loyaltyScoresArray == null) { + return loyaltyScores; // Early return if the loyalty_scores array doesn't exist + } + + loyaltyScoresArray.forEach(entry -> { + JSONObject loyaltyScore = (JSONObject) entry; + if (loyaltyScore.getString("date").equals(date)) { + String artistName = loyaltyScore.getString("artist_name"); + int score = loyaltyScore.getInt("score"); + loyaltyScores.put(artistName, score); + } + }); + return loyaltyScores; + } + + @Override + public boolean loyaltyScoreExists(String userid, String date, String artist_name) { + JSONObject rootNode = readUserFile(userid); + if (rootNode == null) { + return false; // Early return if the file doesn't exist + } + + JSONArray loyaltyScoresArray = rootNode.optJSONArray("loyalty_scores"); + if (loyaltyScoresArray == null) { + return false; // Early return if the loyalty_scores array doesn't exist + } + + for (Object entry : loyaltyScoresArray) { + JSONObject loyaltyScore = (JSONObject) entry; + if (loyaltyScore.getString("date").equals(date) && + loyaltyScore.getString("artist_name").equals(artist_name)) { + return true; // Return true as soon as we find a match + } + } + return false; // No match found + } + + @Override + public void saveLoyalty(String userid, String date, String artist_name, int score) { + JSONObject rootNode = readUserFile(userid); + + // Ensure rootNode is not null + if (rootNode == null) { + rootNode = new JSONObject(); // Initialize rootNode if it's null + } + + // Ensure loyalty_scores array exists + JSONArray loyaltyScoresArray = rootNode.optJSONArray("loyalty_scores"); + if (loyaltyScoresArray == null) { + loyaltyScoresArray = new JSONArray(); // Create a new empty array if it's null + rootNode.put("loyalty_scores", loyaltyScoresArray); // Add the array to rootNode + } + + // Create new loyalty score entry + JSONObject newLoyaltyScore = new JSONObject(); + newLoyaltyScore.put("date", date); + newLoyaltyScore.put("artist_name", artist_name); + newLoyaltyScore.put("score", score); + + // Add the new loyalty score to the array + loyaltyScoresArray.put(newLoyaltyScore); + + // Write the updated data back to the file + writeUserFile(userid, rootNode, loyaltyScoresArray); + } + + @Override + public Map getLoyaltyArtist(String userid, String artist_name) { + Map loyaltyScores = new HashMap<>(); + JSONObject rootNode = readUserFile(userid); + if (rootNode == null) { + return loyaltyScores; // Early return if the file doesn't exist + } + + JSONArray loyaltyScoresArray = rootNode.optJSONArray("loyalty_scores"); + if (loyaltyScoresArray == null) { + return loyaltyScores; // Early return if the loyalty_scores array doesn't exist + } + + loyaltyScoresArray.forEach(entry -> { + JSONObject loyaltyScore = (JSONObject) entry; + if (loyaltyScore.getString("artist_name").equals(artist_name)) { + String date = loyaltyScore.getString("date"); + int score = loyaltyScore.getInt("score"); + loyaltyScores.put(date, score); + } + }); + return loyaltyScores; + } + + // Helper method to get the file for a specific user + private File getUserFile(String userid) { + return new File("loyaltyscore_" + userid + ".json"); + } + + // Helper method to read the user's file and return a JSONObject + private JSONObject readUserFile(String userid) { + File userFile = getUserFile(userid); + if (!userFile.exists()) { + return null; // Return null if the file doesn't exist + } + + try (BufferedReader br = new BufferedReader(new FileReader(userFile))) { + StringBuilder jsonContent = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + jsonContent.append(line); + } + + // If the file is empty, return a new JSONObject with an empty "loyalty_scores" array + if (jsonContent.length() == 0) { + JSONObject emptyRoot = new JSONObject(); + emptyRoot.put("loyalty_scores", new JSONArray()); + return emptyRoot; + } + + return new JSONObject(jsonContent.toString()); + } catch (IOException e) { + e.printStackTrace(); + return null; // Return null in case of an error reading the file + } + } + + // Helper method to write the JSONObject back to the user's file + private void writeUserFile(String userid, JSONObject rootNode, JSONArray loyaltyScoresArray) { + rootNode.put("loyalty_scores", loyaltyScoresArray); + try (FileWriter fileWriter = new FileWriter(getUserFile(userid))) { + fileWriter.write(rootNode.toString(4)); // Pretty print with indentation level 4 + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/data_access/SpotifyDataAccessObject.java b/src/main/java/data_access/SpotifyDataAccessObject.java new file mode 100644 index 00000000..f4a28bd9 --- /dev/null +++ b/src/main/java/data_access/SpotifyDataAccessObject.java @@ -0,0 +1,363 @@ +package data_access; + +import entity.ArtistLoyaltyScore; +import entity.SpotifyUser; +import okhttp3.*; +import org.json.JSONObject; +import se.michaelthelin.spotify.SpotifyApi; +import se.michaelthelin.spotify.model_objects.specification.*; +import util.ConfigManager; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.*; + +public class SpotifyDataAccessObject { + private final SpotifyApi spotifyApi; + private final String clientId; + private final String redirectUri; + private String codeVerifier; + private final OkHttpClient httpClient; + + public SpotifyDataAccessObject() { + this.clientId = ConfigManager.getProperty("spotify.client.id"); + this.redirectUri = ConfigManager.getProperty("spotify.redirect.uri"); + this.httpClient = new OkHttpClient(); + + if ("default_client_id".equals(this.clientId)) { + System.err.println("ERROR: Please set SPOTIFY_CLIENT_ID environment variable"); + System.err.println("Get it from: https://developer.spotify.com/dashboard"); + } + + this.spotifyApi = new SpotifyApi.Builder() + .setClientId(this.clientId) + .setRedirectUri(URI.create(this.redirectUri)) + .build(); + } + + private String generateCodeVerifier() { + SecureRandom secureRandom = new SecureRandom(); + byte[] codeVerifierBytes = new byte[32]; + secureRandom.nextBytes(codeVerifierBytes); + return Base64.getUrlEncoder().withoutPadding() + .encodeToString(codeVerifierBytes); + } + + private String generateCodeChallenge(String codeVerifier) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().withoutPadding() + .encodeToString(hash); + } catch (Exception e) { + throw new RuntimeException("Failed to generate code challenge", e); + } + } + + public String getAuthorizationUrl() { + try { + this.codeVerifier = generateCodeVerifier(); + String codeChallenge = generateCodeChallenge(codeVerifier); + + System.out.println("Generated code verifier (length: " + codeVerifier.length() + ")"); + System.out.println("Generated code challenge: " + codeChallenge); + + // Build authorization URL manually with PKCE parameters + String scope = "user-library-read user-read-recently-played user-top-read"; + String authUrl = String.format( + "https://accounts.spotify.com/authorize?" + + "client_id=%s&" + + "response_type=code&" + + "redirect_uri=%s&" + + "code_challenge_method=S256&" + + "code_challenge=%s&" + + "scope=%s", + URLEncoder.encode(clientId, StandardCharsets.UTF_8), + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8), + URLEncoder.encode(codeChallenge, StandardCharsets.UTF_8), + URLEncoder.encode(scope, StandardCharsets.UTF_8) + ); + + System.out.println("Authorization URL: " + authUrl); + return authUrl; + + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public SpotifyUser exchangeCodeForTokens(String authorizationCode, String username) { + try { + if (codeVerifier == null) { + throw new RuntimeException("Code verifier not generated. Call getAuthorizationUrl() first."); + } + + System.out.println("Exchanging code for tokens with PKCE..."); + System.out.println("Authorization code: " + authorizationCode); + System.out.println("Using code verifier"); + + // Exchange code for tokens using PKCE via HTTP request + RequestBody formBody = new FormBody.Builder() + .add("grant_type", "authorization_code") + .add("code", authorizationCode) + .add("redirect_uri", redirectUri) + .add("client_id", clientId) + .add("code_verifier", codeVerifier) + .build(); + + Request request = new Request.Builder() + .url("https://accounts.spotify.com/api/token") + .post(formBody) + .addHeader("Content-Type", "application/x-www-form-urlencoded") + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body().string(); + + if (!response.isSuccessful()) { + System.err.println("Token exchange failed: " + responseBody); + throw new RuntimeException("Token exchange failed: " + responseBody); + } + + JSONObject json = new JSONObject(responseBody); + String accessToken = json.getString("access_token"); + String refreshToken = json.optString("refresh_token", null); + + System.out.println("Successfully obtained access token"); + + spotifyApi.setAccessToken(accessToken); + if (refreshToken != null) { + spotifyApi.setRefreshToken(refreshToken); + } + + // Get Spotify user profile + var userProfile = spotifyApi.getCurrentUsersProfile().build().execute(); + System.out.println("Got user profile: " + userProfile.getDisplayName()); + + SpotifyUser spotifyUser = new SpotifyUser( + username, + accessToken, + refreshToken, + userProfile.getId() + ); + + // Clear code verifier after use + this.codeVerifier = null; + + return spotifyUser; + } + + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Failed to exchange authorization code: " + e.getMessage(), e); + } + } + + public SpotifyApi getSpotifyApiForUser(SpotifyUser user) { + return new SpotifyApi.Builder() + .setAccessToken(user.getAccessToken()) + .setRefreshToken(user.getRefreshToken()) + .setClientId(clientId) + .build(); + } + + public SpotifyUser refreshAccessToken(SpotifyUser user) { + try { + if (user.getRefreshToken() == null) { + throw new RuntimeException("No refresh token available"); + } + + System.out.println("Refreshing access token..."); + + RequestBody formBody = new FormBody.Builder() + .add("grant_type", "refresh_token") + .add("refresh_token", user.getRefreshToken()) + .add("client_id", clientId) + .build(); + + Request request = new Request.Builder() + .url("https://accounts.spotify.com/api/token") + .post(formBody) + .addHeader("Content-Type", "application/x-www-form-urlencoded") + .build(); + + try (Response response = httpClient.newCall(request).execute()) { + String responseBody = response.body().string(); + + if (!response.isSuccessful()) { + System.err.println("Token refresh failed: " + responseBody); + throw new RuntimeException("Token refresh failed: " + responseBody); + } + + JSONObject json = new JSONObject(responseBody); + String accessToken = json.getString("access_token"); + String refreshToken = json.optString("refresh_token", user.getRefreshToken()); + + System.out.println("Successfully refreshed access token"); + + return new SpotifyUser( + user.getUsername(), + accessToken, + refreshToken, + user.getSpotifyUserId() + ); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Failed to refresh access token: " + e.getMessage(), e); + } + } + + public List getArtistLoyaltyScores(SpotifyUser user) { + try { + SpotifyApi userApi = getSpotifyApiForUser(user); + + System.out.println("Fetching saved tracks..."); + // Get saved tracks + Map artistTrackCount = new HashMap<>(); + Map artistNames = new HashMap<>(); + + Paging savedTracks = userApi.getUsersSavedTracks().limit(50).build().execute(); + System.out.println("Got " + savedTracks.getItems().length + " saved tracks"); + + for (SavedTrack savedTrack : savedTracks.getItems()) { + Track track = savedTrack.getTrack(); + for (ArtistSimplified artist : track.getArtists()) { + String artistId = artist.getId(); + artistTrackCount.put(artistId, artistTrackCount.getOrDefault(artistId, 0) + 1); + artistNames.put(artistId, artist.getName()); + } + } + + System.out.println("Fetching saved albums..."); + // Get saved albums + Map artistAlbumCount = new HashMap<>(); + Paging savedAlbums = userApi.getCurrentUsersSavedAlbums().limit(50).build().execute(); + System.out.println("Got " + savedAlbums.getItems().length + " saved albums"); + + for (SavedAlbum savedAlbum : savedAlbums.getItems()) { + Album album = savedAlbum.getAlbum(); + for (ArtistSimplified artist : album.getArtists()) { + String artistId = artist.getId(); + artistAlbumCount.put(artistId, artistAlbumCount.getOrDefault(artistId, 0) + 1); + artistNames.putIfAbsent(artistId, artist.getName()); + } + } + + System.out.println("Fetching recently played tracks..."); + // Get recently played tracks + Set recentlyPlayedArtists = new HashSet<>(); + PagingCursorbased recentlyPlayed = userApi.getCurrentUsersRecentlyPlayedTracks() + .limit(50).build().execute(); + System.out.println("Got " + recentlyPlayed.getItems().length + " recently played tracks"); + + for (PlayHistory playHistory : recentlyPlayed.getItems()) { + Track track = playHistory.getTrack(); + for (ArtistSimplified artist : track.getArtists()) { + recentlyPlayedArtists.add(artist.getId()); + artistNames.putIfAbsent(artist.getId(), artist.getName()); + } + } + + // Combine all artists + Set allArtists = new HashSet<>(); + allArtists.addAll(artistTrackCount.keySet()); + allArtists.addAll(artistAlbumCount.keySet()); + allArtists.addAll(recentlyPlayedArtists); + + System.out.println("Total unique artists: " + allArtists.size()); + + // Create loyalty scores + List loyaltyScores = new ArrayList<>(); + for (String artistId : allArtists) { + String artistName = artistNames.get(artistId); + int trackCount = artistTrackCount.getOrDefault(artistId, 0); + int albumCount = artistAlbumCount.getOrDefault(artistId, 0); + boolean inRecent = recentlyPlayedArtists.contains(artistId); + + ArtistLoyaltyScore score = new ArtistLoyaltyScore( + artistName, artistId, trackCount, albumCount, inRecent + ); + loyaltyScores.add(score); + } + + // Sort by loyalty score descending + loyaltyScores.sort((a, b) -> Double.compare(b.getLoyaltyScore(), a.getLoyaltyScore())); + + System.out.println("Calculated loyalty scores for " + loyaltyScores.size() + " artists"); + + return loyaltyScores; + + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Failed to get artist loyalty scores: " + e.getMessage(), e); + } + } + + // NEW: Generate a Daily Mix of tracks based on user's saved and recently played tracks + public List generateDailyMix(SpotifyUser user, int mixSize) { + try { + SpotifyApi userApi = getSpotifyApiForUser(user); + + List pool = new ArrayList<>(); + + // 1. Saved tracks + Paging savedTracks = userApi.getUsersSavedTracks() + .limit(50) + .build() + .execute(); + + for (SavedTrack savedTrack : savedTracks.getItems()) { + pool.add(savedTrack.getTrack()); + } + + // 2. Recently played + PagingCursorbased recentlyPlayed = userApi + .getCurrentUsersRecentlyPlayedTracks() + .limit(50) + .build() + .execute(); + + for (PlayHistory history : recentlyPlayed.getItems()) { + pool.add(history.getTrack()); + } + + if (pool.isEmpty()) { + return Collections.emptyList(); + } + + // sample + Collections.shuffle(pool); + int n = Math.min(mixSize, pool.size()); + + List mix = new ArrayList<>(); + for (int i = 0; i < n; i++) { + Track t = pool.get(i); + StringBuilder artists = new StringBuilder(); + ArtistSimplified[] artistArray = t.getArtists(); + for (int j = 0; j < artistArray.length; j++) { + if (j > 0) { + artists.append(", "); + } + artists.append(artistArray[j].getName()); + } + + // Song Name - Artist1, Artist2 + String line = String.format("%s - %s", t.getName(), artists); + mix.add(line); + } + + return mix; + + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("Failed to generate Daily Mix: " + e.getMessage(), e); + } + } + +} \ No newline at end of file diff --git a/src/main/java/entity/ArtistLoyaltyScore.java b/src/main/java/entity/ArtistLoyaltyScore.java new file mode 100644 index 00000000..4f9b0f1f --- /dev/null +++ b/src/main/java/entity/ArtistLoyaltyScore.java @@ -0,0 +1,63 @@ +package entity; + +public class ArtistLoyaltyScore { + private final String artistName; + private final String artistId; + private final int savedTracks; + private final int savedAlbums; + private final boolean inRecentlyPlayed; + private final int loyaltyScore; + + public ArtistLoyaltyScore(String artistName, String artistId, int savedTracks, + int savedAlbums, boolean inRecentlyPlayed) { + this.artistName = artistName; + this.artistId = artistId; + this.savedTracks = savedTracks; + this.savedAlbums = savedAlbums; + this.inRecentlyPlayed = inRecentlyPlayed; + this.loyaltyScore = calculateLoyaltyScore(); + } + + private int calculateLoyaltyScore() { + // Loyalty score formula: + // - Each saved track: 10 points + // - Each saved album: 50 points + // - In recently played: 100 points bonus + int score = (savedTracks * 10) + (savedAlbums * 50); + if (inRecentlyPlayed) { + score += 100; + } + return score; + } + + public String getArtistName() { + return artistName; + } + + public String getArtistId() { + return artistId; + } + + public int getSavedTracks() { + return savedTracks; + } + + public int getSavedAlbums() { + return savedAlbums; + } + + public boolean isInRecentlyPlayed() { + return inRecentlyPlayed; + } + + public int getLoyaltyScore() { + return loyaltyScore; + } + + @Override + public String toString() { + return String.format("%s - Score: %.0f (Tracks: %d, Albums: %d, Recent: %s)", + artistName, loyaltyScore, savedTracks, savedAlbums, + inRecentlyPlayed ? "Yes" : "No"); + } +} \ No newline at end of file diff --git a/src/main/java/entity/Group.java b/src/main/java/entity/Group.java new file mode 100644 index 00000000..1cd9096e --- /dev/null +++ b/src/main/java/entity/Group.java @@ -0,0 +1,77 @@ +package entity; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; + +/** + * A simple entity representing a group. Groups have a group_name, group_playlists, users, + * and date_created + */ +public class Group { + + public static final int MAX_MEMBERS = 7; + private String group_name; + private final List users; + private final List group_playlists; + private final SpotifyUser owner; + private final String groupCode; + + + /** + * Creates a new group given these parameters: + * + * @param group_name the group's name + * @param owner the user who created the group + */ + public Group(String group_name, SpotifyUser owner ) { + this.owner = owner; + this.group_name = group_name; + this.users = new ArrayList<>(); + this.group_playlists = new ArrayList<>(); + this.users.add(owner); + this.groupCode = generateGroupCode(); + } + public SpotifyUser getOwner() { + return owner; + } + + public String getGroup_name() { + return this.group_name; + } + + public String getGroupCode() { + return groupCode; + } + + public void addUser(SpotifyUser user) { + if (user == null) { + throw new IllegalArgumentException("User does not exist."); + } + if (users.size() >= MAX_MEMBERS) { + throw new IllegalStateException("Cannot have more than 7 members in group");} + this.users.add(user); + } + + public void removeUser (SpotifyUser user) { + this.users.remove(user); + } + + public void addPlaylist(Playlist playlist) { this.group_playlists.add(playlist); } + + public void setGroup_name(String group_name) { + if (group_name == null || group_name.isBlank()){ + throw new IllegalArgumentException("Group name cannot be empty."); + } this.group_name = group_name; + } + + public List getUsers() { + return this.users; + } + + private String generateGroupCode() { + SecureRandom rand = new SecureRandom(); + int code = rand.nextInt(900000) + 100000; // random 6-digit codes associated with each group + return String.valueOf(code); + } + +} diff --git a/src/main/java/entity/Playlist.java b/src/main/java/entity/Playlist.java new file mode 100644 index 00000000..aff50864 --- /dev/null +++ b/src/main/java/entity/Playlist.java @@ -0,0 +1,36 @@ +package entity; + +import java.util.ArrayList; +import java.util.List; + +/** + * A simple entity representing a playlist. A playlist has a playlist_name, list_songs, + * and date_created. + * Recall that each song is a string link to the spotify song. + */ +public class Playlist { + + private String playlist_name; + private final List list_songs; + private final String date_created; + + /** + * Creates a new playlist given these parameters: + * @param playlist_name the playlist's name + * @param date the date the playlist was created + */ + public Playlist(String playlist_name, String date ) { + this.playlist_name = playlist_name; + this.date_created = date; + this.list_songs = new ArrayList(); + } + + public String getName() { + return this.playlist_name; + } + public String getDate() { return this.date_created; } + public List getSongs() {return this.list_songs;} + public void addSong(String song) {this.list_songs.add(song);} + public void changeName(String new_name) {this.playlist_name = new_name;} + +} diff --git a/src/main/java/entity/SpotifyUser.java b/src/main/java/entity/SpotifyUser.java new file mode 100644 index 00000000..c762cffc --- /dev/null +++ b/src/main/java/entity/SpotifyUser.java @@ -0,0 +1,21 @@ +package entity; + +public class SpotifyUser { + private final String username; + private final String accessToken; + private final String refreshToken; + private final String spotifyUserId; + + public SpotifyUser(String username, String accessToken, String refreshToken, String spotifyUserId) { + this.username = username; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.spotifyUserId = spotifyUserId; + } + + // Getters + public String getUsername() { return username; } + public String getAccessToken() { return accessToken; } + public String getRefreshToken() { return refreshToken; } + public String getSpotifyUserId() { return spotifyUserId; } +} \ No newline at end of file diff --git a/src/main/java/entity/TopItem.java b/src/main/java/entity/TopItem.java new file mode 100644 index 00000000..4a8edbc2 --- /dev/null +++ b/src/main/java/entity/TopItem.java @@ -0,0 +1,19 @@ +package entity; + +/** + * A simple entity representing a top item for a user. top_items have a name and rank + */ + +public class TopItem { + private final String name; + private final int rank; + + public TopItem(String name, int rank) { + this.name = name; + this.rank = rank; + } + public String getName() { return this.name; } + public int getRank() { return this.rank; } + +} + diff --git a/src/main/java/entity/User.java b/src/main/java/entity/User.java deleted file mode 100644 index bf9783d0..00000000 --- a/src/main/java/entity/User.java +++ /dev/null @@ -1,36 +0,0 @@ -package entity; - -/** - * A simple entity representing a user. Users have a username and password.. - */ -public class User { - - private final String name; - private final String password; - - /** - * Creates a new user with the given non-empty name and non-empty password. - * @param name the username - * @param password the password - * @throws IllegalArgumentException if the password or name are empty - */ - public User(String name, String password) { - if ("".equals(name)) { - throw new IllegalArgumentException("Username cannot be empty"); - } - if ("".equals(password)) { - throw new IllegalArgumentException("Password cannot be empty"); - } - this.name = name; - this.password = password; - } - - public String getName() { - return name; - } - - public String getPassword() { - return password; - } - -} diff --git a/src/main/java/entity/UserFactory.java b/src/main/java/entity/UserFactory.java deleted file mode 100644 index 238578d3..00000000 --- a/src/main/java/entity/UserFactory.java +++ /dev/null @@ -1,11 +0,0 @@ -package entity; - -/** - * Factory for creating CommonUser objects. - */ -public class UserFactory { - - public User create(String name, String password) { - return new User(name, password); - } -} diff --git a/src/main/java/interface_adapter/create_group/CreateGroupController.java b/src/main/java/interface_adapter/create_group/CreateGroupController.java new file mode 100644 index 00000000..21da0b30 --- /dev/null +++ b/src/main/java/interface_adapter/create_group/CreateGroupController.java @@ -0,0 +1,36 @@ +package interface_adapter.create_group; +import entity.SpotifyUser; +import use_case.create_group.CreateGroupInputBoundary; +import use_case.create_group.CreateGroupInputData; + +import java.util.List; + +/** + * Controller for create group use case + */ +public class CreateGroupController { + + private final CreateGroupInputBoundary createGroupUseCase; + + /** + * Constructor for CreateGroupController. + * + * @param createGroupUseCase the input boundary for the Create Group use case + */ + public CreateGroupController(CreateGroupInputBoundary createGroupUseCase) { + this.createGroupUseCase = createGroupUseCase; + } + + /** + * Creates a group using the provided user input. + * + * @param group_name the name of the group + * @param owner the user creating the group + * @param initialMembers list of initial members + */ + public void create(String group_name, SpotifyUser owner, List initialMembers) { + CreateGroupInputData inputData = new CreateGroupInputData(group_name, owner, initialMembers); + createGroupUseCase.execute(inputData); + + } +} diff --git a/src/main/java/interface_adapter/create_group/CreateGroupPresenter.java b/src/main/java/interface_adapter/create_group/CreateGroupPresenter.java new file mode 100644 index 00000000..2f24c10e --- /dev/null +++ b/src/main/java/interface_adapter/create_group/CreateGroupPresenter.java @@ -0,0 +1,39 @@ +package interface_adapter.create_group; + +import use_case.create_group.CreateGroupOutputBoundary; +import use_case.create_group.CreateGroupOutputData; + +/** + * Presenter for the Create Group use case. + * Converts output data into a form suitable for the UI and updates the ViewModel. + */ +public class CreateGroupPresenter implements CreateGroupOutputBoundary { + + private final CreateGroupViewModel viewModel; + + /** + * Constructs a CreateGroupPresenter. + * + * @param viewModel the ViewModel associated with the Create Group screen + */ + public CreateGroupPresenter(CreateGroupViewModel viewModel) { + this.viewModel = viewModel; + } + + /** + * Updates the ViewModel based on output data from the interactor. + * + * @param outputData the output data containing the created group info + */ + @Override + public void present(CreateGroupOutputData outputData) { + if (outputData != null) { + // Correct method names + viewModel.setGroupName(outputData.getGroup_name()); + viewModel.setSuccess(true); + } else { + viewModel.setSuccess(false); + viewModel.setErrorMessage("Failed to create group."); + } + } +} diff --git a/src/main/java/interface_adapter/create_group/CreateGroupViewModel.java b/src/main/java/interface_adapter/create_group/CreateGroupViewModel.java new file mode 100644 index 00000000..05ec7f34 --- /dev/null +++ b/src/main/java/interface_adapter/create_group/CreateGroupViewModel.java @@ -0,0 +1,38 @@ +package interface_adapter.create_group; + + +/** + * ViewModel for the Create Group view. + * Stores data to be observed by the UI layer. + */ + +public class CreateGroupViewModel { + + private String group_name; + private boolean success; + private String errorMessage; + + public String getGroup_name() { + return group_name; + } + + public boolean isSuccess() { + return success; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setGroupName(String group_name) { this.group_name = group_name; } + + + + public void setSuccess(boolean success) { + this.success = success; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/daily_mix/DailyMixController.java b/src/main/java/interface_adapter/daily_mix/DailyMixController.java new file mode 100644 index 00000000..4b29a2da --- /dev/null +++ b/src/main/java/interface_adapter/daily_mix/DailyMixController.java @@ -0,0 +1,20 @@ +package interface_adapter.daily_mix; + +import entity.SpotifyUser; +import use_case.daily_mix.DailyMixInputBoundary; +import use_case.daily_mix.DailyMixInputData; + +public class DailyMixController { + + private final DailyMixInputBoundary dailyMixInteractor; + + public DailyMixController(DailyMixInputBoundary dailyMixInteractor) { + this.dailyMixInteractor = dailyMixInteractor; + } + + public void execute(SpotifyUser spotifyUser, int mixSize) { + DailyMixInputData inputData = new DailyMixInputData(spotifyUser, mixSize); + dailyMixInteractor.execute(inputData); + } +} + diff --git a/src/main/java/interface_adapter/daily_mix/DailyMixPresenter.java b/src/main/java/interface_adapter/daily_mix/DailyMixPresenter.java new file mode 100644 index 00000000..d67ca690 --- /dev/null +++ b/src/main/java/interface_adapter/daily_mix/DailyMixPresenter.java @@ -0,0 +1,34 @@ +package interface_adapter.daily_mix; + +import use_case.daily_mix.DailyMixOutputBoundary; +import use_case.daily_mix.DailyMixOutputData; + +public class DailyMixPresenter implements DailyMixOutputBoundary { + + private final DailyMixViewModel dailyMixViewModel; + + public DailyMixPresenter(DailyMixViewModel dailyMixViewModel) { + this.dailyMixViewModel = dailyMixViewModel; + } + + @Override + public void prepareSuccessView(DailyMixOutputData response) { + DailyMixState state = dailyMixViewModel.getState(); + state.setError(null); + state.setTracks(response.getTracks()); + + dailyMixViewModel.setState(state); + dailyMixViewModel.firePropertyChange(); // propertyName = "state" + } + + @Override + public void prepareFailView(String error) { + DailyMixState state = dailyMixViewModel.getState(); + state.setError(error); + state.setTracks(null); + + dailyMixViewModel.setState(state); + dailyMixViewModel.firePropertyChange(); // LoggedInView 收到后弹错误 + } +} + diff --git a/src/main/java/interface_adapter/daily_mix/DailyMixState.java b/src/main/java/interface_adapter/daily_mix/DailyMixState.java new file mode 100644 index 00000000..e1705bad --- /dev/null +++ b/src/main/java/interface_adapter/daily_mix/DailyMixState.java @@ -0,0 +1,27 @@ +package interface_adapter.daily_mix; + +import java.util.ArrayList; +import java.util.List; + +public class DailyMixState { + + private List tracks = new ArrayList<>(); + private String error; + + public List getTracks() { + return tracks; + } + + public void setTracks(List tracks) { + this.tracks = tracks; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } +} + diff --git a/src/main/java/interface_adapter/daily_mix/DailyMixViewModel.java b/src/main/java/interface_adapter/daily_mix/DailyMixViewModel.java new file mode 100644 index 00000000..8ce2a16d --- /dev/null +++ b/src/main/java/interface_adapter/daily_mix/DailyMixViewModel.java @@ -0,0 +1,14 @@ +package interface_adapter.daily_mix; + +import interface_adapter.ViewModel; + +public class DailyMixViewModel extends ViewModel { + + public static final String VIEW_NAME = "daily mix"; + + public DailyMixViewModel() { + super(VIEW_NAME); + this.setState(new DailyMixState()); + } +} + diff --git a/src/main/java/interface_adapter/join_group/JoinGroupController.java b/src/main/java/interface_adapter/join_group/JoinGroupController.java new file mode 100644 index 00000000..88874593 --- /dev/null +++ b/src/main/java/interface_adapter/join_group/JoinGroupController.java @@ -0,0 +1,19 @@ +package interface_adapter.join_group; + +import entity.SpotifyUser; +import use_case.joingroup.JoinGroupInputBoundary; +import use_case.joingroup.JoinGroupInputData; + +public class JoinGroupController { + + private final JoinGroupInputBoundary joinGroupInteractor; + + public JoinGroupController(JoinGroupInputBoundary joinGroupInteractor) { + this.joinGroupInteractor = joinGroupInteractor; + } + + public void joinGroup(String groupCode, SpotifyUser user) { + JoinGroupInputData inputData = new JoinGroupInputData(groupCode, user); + joinGroupInteractor.joinGroup(inputData); + } +} diff --git a/src/main/java/interface_adapter/join_group/JoinGroupPresenter.java b/src/main/java/interface_adapter/join_group/JoinGroupPresenter.java new file mode 100644 index 00000000..dcdbcd18 --- /dev/null +++ b/src/main/java/interface_adapter/join_group/JoinGroupPresenter.java @@ -0,0 +1,29 @@ +package interface_adapter.join_group; + +import use_case.joingroup.JoinGroupOutputBoundary; +import use_case.joingroup.JoinGroupOutputData; + +public class JoinGroupPresenter implements JoinGroupOutputBoundary { + + private final JoinGroupViewModel viewModel; + + public JoinGroupPresenter(JoinGroupViewModel viewModel) { + this.viewModel = viewModel; + } + + @Override + public void prepareSuccessView(JoinGroupOutputData outputData) { + viewModel.setSuccess(true); + viewModel.setMessage(outputData.getMessage()); + viewModel.setGroupName(outputData.getGroupName()); + viewModel.firePropertyChanged(); // If using property change listeners + } + + @Override + public void prepareFailView(String errorMessage) { + viewModel.setSuccess(false); + viewModel.setMessage(errorMessage); + viewModel.firePropertyChanged(); + } +} + diff --git a/src/main/java/interface_adapter/join_group/JoinGroupViewModel.java b/src/main/java/interface_adapter/join_group/JoinGroupViewModel.java new file mode 100644 index 00000000..c30d90e3 --- /dev/null +++ b/src/main/java/interface_adapter/join_group/JoinGroupViewModel.java @@ -0,0 +1,44 @@ +package interface_adapter.join_group; + + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; + +public class JoinGroupViewModel { + + private boolean success; + private String message; + private String groupName; + + private final PropertyChangeSupport support = new PropertyChangeSupport(this); + + public void addPropertyChangeListener(PropertyChangeListener listener) { + support.addPropertyChangeListener(listener); + } + + public void firePropertyChanged() { + support.firePropertyChange("joinGroup", null, this); + } + + public boolean isSuccess() { + return success; + } + public void setSuccess(boolean success) { + this.success = success; + } + + public String getMessage() { + return message; + } + public void setMessage(String message) { + this.message = message; + } + + public String getGroupName() { + return groupName; + } + public void setGroupName(String groupName) { + this.groupName = groupName; + } +} + diff --git a/src/main/java/interface_adapter/logged_in/ChangePasswordController.java b/src/main/java/interface_adapter/logged_in/ChangePasswordController.java deleted file mode 100644 index 38c3417a..00000000 --- a/src/main/java/interface_adapter/logged_in/ChangePasswordController.java +++ /dev/null @@ -1,26 +0,0 @@ -package interface_adapter.logged_in; - -import use_case.change_password.ChangePasswordInputBoundary; -import use_case.change_password.ChangePasswordInputData; - -/** - * Controller for the Change Password Use Case. - */ -public class ChangePasswordController { - private final ChangePasswordInputBoundary userChangePasswordUseCaseInteractor; - - public ChangePasswordController(ChangePasswordInputBoundary userChangePasswordUseCaseInteractor) { - this.userChangePasswordUseCaseInteractor = userChangePasswordUseCaseInteractor; - } - - /** - * Executes the Change Password Use Case. - * @param password the new password - * @param username the user whose password to change - */ - public void execute(String password, String username) { - final ChangePasswordInputData changePasswordInputData = new ChangePasswordInputData(username, password); - - userChangePasswordUseCaseInteractor.execute(changePasswordInputData); - } -} diff --git a/src/main/java/interface_adapter/logged_in/ChangePasswordPresenter.java b/src/main/java/interface_adapter/logged_in/ChangePasswordPresenter.java deleted file mode 100644 index fea86e12..00000000 --- a/src/main/java/interface_adapter/logged_in/ChangePasswordPresenter.java +++ /dev/null @@ -1,33 +0,0 @@ -package interface_adapter.logged_in; - -import interface_adapter.ViewManagerModel; -import use_case.change_password.ChangePasswordOutputBoundary; -import use_case.change_password.ChangePasswordOutputData; - -/** - * The Presenter for the Change Password Use Case. - */ -public class ChangePasswordPresenter implements ChangePasswordOutputBoundary { - - private final LoggedInViewModel loggedInViewModel; - private final ViewManagerModel viewManagerModel; - - public ChangePasswordPresenter(ViewManagerModel viewManagerModel, - LoggedInViewModel loggedInViewModel) { - this.viewManagerModel = viewManagerModel; - this.loggedInViewModel = loggedInViewModel; - } - - @Override - public void prepareSuccessView(ChangePasswordOutputData outputData) { - loggedInViewModel.getState().setPassword(""); - loggedInViewModel.getState().setPasswordError(null); - loggedInViewModel.firePropertyChange("password"); - } - - @Override - public void prepareFailView(String error) { - loggedInViewModel.getState().setPasswordError(error); - loggedInViewModel.firePropertyChange("password"); - } -} diff --git a/src/main/java/interface_adapter/logged_in/LoggedInState.java b/src/main/java/interface_adapter/logged_in/LoggedInState.java index 9ed98364..a1de2429 100644 --- a/src/main/java/interface_adapter/logged_in/LoggedInState.java +++ b/src/main/java/interface_adapter/logged_in/LoggedInState.java @@ -1,46 +1,33 @@ package interface_adapter.logged_in; -/** - * The State information representing the logged-in user. - */ public class LoggedInState { private String username = ""; - private String password = ""; private String passwordError; + private boolean spotifyAuthenticated = false; // NEW FIELD + private String spotifyUserId = ""; // NEW FIELD public LoggedInState(LoggedInState copy) { username = copy.username; password = copy.password; passwordError = copy.passwordError; - } - - // Because of the previous copy constructor, the default constructor must be explicit. - public LoggedInState() { - - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getPassword() { - return password; - } - - public void setPasswordError(String passwordError) { - this.passwordError = passwordError; - } - - public String getPasswordError() { - return passwordError; - } -} + spotifyAuthenticated = copy.spotifyAuthenticated; // NEW + spotifyUserId = copy.spotifyUserId; // NEW + } + + public LoggedInState() {} + + // Existing getters and setters... + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public void setPassword(String password) { this.password = password; } + public String getPassword() { return password; } + public void setPasswordError(String passwordError) { this.passwordError = passwordError; } + public String getPasswordError() { return passwordError; } + + // NEW getters and setters for Spotify + public boolean isSpotifyAuthenticated() { return spotifyAuthenticated; } + public void setSpotifyAuthenticated(boolean spotifyAuthenticated) { this.spotifyAuthenticated = spotifyAuthenticated; } + public String getSpotifyUserId() { return spotifyUserId; } + public void setSpotifyUserId(String spotifyUserId) { this.spotifyUserId = spotifyUserId; } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/login/LoginPresenter.java b/src/main/java/interface_adapter/login/LoginPresenter.java index a00bc7c7..e7c8832e 100644 --- a/src/main/java/interface_adapter/login/LoginPresenter.java +++ b/src/main/java/interface_adapter/login/LoginPresenter.java @@ -6,9 +6,6 @@ import use_case.login.LoginOutputBoundary; import use_case.login.LoginOutputData; -/** - * The Presenter for the Login Use Case. - */ public class LoginPresenter implements LoginOutputBoundary { private final LoginViewModel loginViewModel; @@ -25,15 +22,13 @@ public LoginPresenter(ViewManagerModel viewManagerModel, @Override public void prepareSuccessView(LoginOutputData response) { - // On success, update the loggedInViewModel's state + // Update the logged in state with the username final LoggedInState loggedInState = loggedInViewModel.getState(); - loggedInState.setUsername(response.getUsername()); + loggedInState.setUsername(response.getUsername()); // CRITICAL: Set username! + this.loggedInViewModel.setState(loggedInState); this.loggedInViewModel.firePropertyChange(); - // and clear everything from the LoginViewModel's state - loginViewModel.setState(new LoginState()); - - // switch to the logged in view + // Switch to logged in view this.viewManagerModel.setState(loggedInViewModel.getViewName()); this.viewManagerModel.firePropertyChange(); } diff --git a/src/main/java/interface_adapter/logout/LogoutController.java b/src/main/java/interface_adapter/logout/LogoutController.java index 49eedf89..60959f7c 100644 --- a/src/main/java/interface_adapter/logout/LogoutController.java +++ b/src/main/java/interface_adapter/logout/LogoutController.java @@ -10,13 +10,13 @@ public class LogoutController { private LogoutInputBoundary logoutUseCaseInteractor; public LogoutController(LogoutInputBoundary logoutUseCaseInteractor) { - // TODO: Save the interactor in the instance variable. + this.logoutUseCaseInteractor = logoutUseCaseInteractor; } /** * Executes the Logout Use Case. */ public void execute() { - // TODO: run the use case interactor for the logout use case + logoutUseCaseInteractor.execute(); } -} +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/logout/LogoutPresenter.java b/src/main/java/interface_adapter/logout/LogoutPresenter.java index 4df31e73..f98bb15e 100644 --- a/src/main/java/interface_adapter/logout/LogoutPresenter.java +++ b/src/main/java/interface_adapter/logout/LogoutPresenter.java @@ -1,7 +1,9 @@ package interface_adapter.logout; import interface_adapter.ViewManagerModel; +import interface_adapter.logged_in.LoggedInState; import interface_adapter.logged_in.LoggedInViewModel; +import interface_adapter.login.LoginState; import interface_adapter.login.LoginViewModel; import use_case.logout.LogoutOutputBoundary; import use_case.logout.LogoutOutputData; @@ -16,30 +18,30 @@ public class LogoutPresenter implements LogoutOutputBoundary { private LoginViewModel loginViewModel; public LogoutPresenter(ViewManagerModel viewManagerModel, - LoggedInViewModel loggedInViewModel, + LoggedInViewModel loggedInViewModel, LoginViewModel loginViewModel) { - // TODO: assign to the three instance variables. + this.viewManagerModel = viewManagerModel; + this.loggedInViewModel = loggedInViewModel; + this.loginViewModel = loginViewModel; } @Override public void prepareSuccessView(LogoutOutputData response) { - // We need to switch to the login view, which should have - // an empty username and password. - - // We also need to set the username in the LoggedInState to - // the empty string. - - // TODO: have prepareSuccessView update the LoggedInState - // 1. get the LoggedInState out of the appropriate View Model, - // 2. set the username in the state to the empty string - // 3. firePropertyChanged so that the View that is listening is updated. - - // TODO: have prepareSuccessView update the LoginState - // 1. get the LoginState out of the appropriate View Model, - // 2. set the username in the state to be the username of the user that just logged out, - // 3. firePropertyChanged so that the View that is listening is updated. - - // This code tells the View Manager to switch to the LoginView. + // Update LoggedInState - set username to empty + final LoggedInState loggedInState = loggedInViewModel.getState(); + loggedInState.setUsername(""); + loggedInState.setSpotifyAuthenticated(false); // Also reset Spotify status + loggedInViewModel.setState(loggedInState); + loggedInViewModel.firePropertyChange(); + + // Update LoginState - set username to the logged out user + final LoginState loginState = loginViewModel.getState(); + loginState.setUsername(response.getUsername()); + loginState.setPassword(""); // Clear password + loginViewModel.setState(loginState); + loginViewModel.firePropertyChange(); + + // Switch to LoginView this.viewManagerModel.setState(loginViewModel.getViewName()); this.viewManagerModel.firePropertyChange(); } diff --git a/src/main/java/interface_adapter/loyalty_score/LoyaltyController.java b/src/main/java/interface_adapter/loyalty_score/LoyaltyController.java new file mode 100644 index 00000000..8a8a74a1 --- /dev/null +++ b/src/main/java/interface_adapter/loyalty_score/LoyaltyController.java @@ -0,0 +1,22 @@ +package interface_adapter.loyalty_score; + +import entity.SpotifyUser; +import use_case.loyalty_score.LoyaltyScoreInputBoundary; +import use_case.loyalty_score.LoyaltyScoreInputData; + +public class LoyaltyController { + + private final LoyaltyScoreInputBoundary loyaltyScoreInteractor; + + public LoyaltyController(LoyaltyScoreInputBoundary loyaltyScoreInteractor) { + this.loyaltyScoreInteractor = loyaltyScoreInteractor; + } + + /** + * Executes the Logout Use Case. + */ + public void execute(SpotifyUser spotifyUser, String artistName) { + LoyaltyScoreInputData inputData = new LoyaltyScoreInputData(spotifyUser, artistName); + loyaltyScoreInteractor.execute(inputData); + } +} diff --git a/src/main/java/interface_adapter/loyalty_score/LoyaltyPresenter.java b/src/main/java/interface_adapter/loyalty_score/LoyaltyPresenter.java new file mode 100644 index 00000000..737a63c2 --- /dev/null +++ b/src/main/java/interface_adapter/loyalty_score/LoyaltyPresenter.java @@ -0,0 +1,13 @@ +package interface_adapter.loyalty_score; + +// TODO: Implement + +import use_case.loyalty_score.LoyaltyScoreOutputBoundary; +import use_case.loyalty_score.LoyaltyScoreOutputData; + +public class LoyaltyPresenter implements LoyaltyScoreOutputBoundary { + @Override + public void prepareView(LoyaltyScoreOutputData outputData) { + + } +} diff --git a/src/main/java/interface_adapter/loyalty_score/LoyaltyState.java b/src/main/java/interface_adapter/loyalty_score/LoyaltyState.java new file mode 100644 index 00000000..34bc4727 --- /dev/null +++ b/src/main/java/interface_adapter/loyalty_score/LoyaltyState.java @@ -0,0 +1,45 @@ +package interface_adapter.loyalty_score; + +import java.util.List; +import java.util.Map; + +public class LoyaltyState { + private Map loyaltyScores; // List to hold the loyalty scores for different artists + private String currentArtist; // Current artist that the user is viewing + private boolean isLoading; // Indicates whether data is being loaded + private boolean isError; // Indicates whether there was an error fetching the data + + // Getters and Setters + + public Map getLoyaltyScores() { + return this.loyaltyScores; + } + + public void setLoyaltyScores(Map loyaltyScores) { + this.loyaltyScores = loyaltyScores; + } + + public String getCurrentArtist() { + return this.currentArtist; + } + + public void setCurrentArtist(String currentArtist) { + this.currentArtist = currentArtist; + } + + public boolean isLoading() { + return this.isLoading; + } + + public void setLoading(boolean loading) { + this.isLoading = loading; + } + + public boolean isError() { + return this.isError; + } + + public void setError(boolean error) { + this.isError = error; + } +} diff --git a/src/main/java/interface_adapter/loyalty_score/LoyaltyViewModel.java b/src/main/java/interface_adapter/loyalty_score/LoyaltyViewModel.java new file mode 100644 index 00000000..0443ab5c --- /dev/null +++ b/src/main/java/interface_adapter/loyalty_score/LoyaltyViewModel.java @@ -0,0 +1,12 @@ +package interface_adapter.loyalty_score; + +import interface_adapter.ViewModel; + + + +public class LoyaltyViewModel extends ViewModel { + public LoyaltyViewModel() { + super("loyalty"); + setState(new LoyaltyState()); + } +} diff --git a/src/main/java/interface_adapter/sharedsong/SharedSongController.java b/src/main/java/interface_adapter/sharedsong/SharedSongController.java new file mode 100644 index 00000000..2afff2fa --- /dev/null +++ b/src/main/java/interface_adapter/sharedsong/SharedSongController.java @@ -0,0 +1,30 @@ +package interface_adapter.sharedsong; + +import entity.Group; +import entity.SpotifyUser; +import use_case.sharedsong.SharedSongInputBoundary; +import use_case.sharedsong.SharedSongInputData; + +/** + * Controller for the Shared Song Use Case. + */ + +public class SharedSongController { + + private final SharedSongInputBoundary sharedSongUseCaseInteractor; + + public SharedSongController(SharedSongInputBoundary sharedSongUseCaseInteractor) { + this.sharedSongUseCaseInteractor = sharedSongUseCaseInteractor; + } + + /** + * Executes the Shared Song Use Case. + * @param user the user checking if their song is shared + * @param group the group the user is in + */ + + public void execute(SpotifyUser user, Group group) { + final SharedSongInputData inputData = new SharedSongInputData(user,group); + sharedSongUseCaseInteractor.execute(inputData); + } +} diff --git a/src/main/java/interface_adapter/sharedsong/SharedSongPresenter.java b/src/main/java/interface_adapter/sharedsong/SharedSongPresenter.java new file mode 100644 index 00000000..a9d64839 --- /dev/null +++ b/src/main/java/interface_adapter/sharedsong/SharedSongPresenter.java @@ -0,0 +1,32 @@ +package interface_adapter.sharedsong; + +import interface_adapter.ViewManagerModel; +import use_case.sharedsong.SharedSongOutputBoundary; +import use_case.sharedsong.SharedSongOutputData; + +/** + * Presenter for the Shared Song Use Case. + */ +public class SharedSongPresenter implements SharedSongOutputBoundary { + + // may be same as group tab view model + private final CheckSharedSongViewModel sharedSongViewModel; + private final CheckedSharedSongViewModel checkedSharedSongViewModel; + // may be group view manager + private final ViewManagerModel viewManagerModel; + + public SharedSongPresenter(ViewManagerModel viewManagerModel, + CheckSharedSongViewModel sharedSongViewModel, + CheckedSharedSongViewModel checkedSharedSongViewModel) { + this.viewManagerModel = viewManagerModel; + this.sharedSongViewModel = sharedSongViewModel; + this.checkedSharedSongViewModel = checkedSharedSongViewModel; + } + + public void prepareSuccessView(SharedSongOutputData response) { + // on success, display song and who has saved it + } + public void prepareFailureView(String errorMessage) { + // on failure, display error message + } +} diff --git a/src/main/java/interface_adapter/signup/SignupController.java b/src/main/java/interface_adapter/signup/SignupController.java deleted file mode 100644 index c01d25aa..00000000 --- a/src/main/java/interface_adapter/signup/SignupController.java +++ /dev/null @@ -1,36 +0,0 @@ -package interface_adapter.signup; - -import use_case.signup.SignupInputBoundary; -import use_case.signup.SignupInputData; - -/** - * Controller for the Signup Use Case. - */ -public class SignupController { - - private final SignupInputBoundary userSignupUseCaseInteractor; - - public SignupController(SignupInputBoundary userSignupUseCaseInteractor) { - this.userSignupUseCaseInteractor = userSignupUseCaseInteractor; - } - - /** - * Executes the Signup Use Case. - * @param username the username to sign up - * @param password1 the password - * @param password2 the password repeated - */ - public void execute(String username, String password1, String password2) { - final SignupInputData signupInputData = new SignupInputData( - username, password1, password2); - - userSignupUseCaseInteractor.execute(signupInputData); - } - - /** - * Executes the "switch to LoginView" Use Case. - */ - public void switchToLoginView() { - userSignupUseCaseInteractor.switchToLoginView(); - } -} diff --git a/src/main/java/interface_adapter/signup/SignupPresenter.java b/src/main/java/interface_adapter/signup/SignupPresenter.java deleted file mode 100644 index adaa4006..00000000 --- a/src/main/java/interface_adapter/signup/SignupPresenter.java +++ /dev/null @@ -1,49 +0,0 @@ -package interface_adapter.signup; - -import interface_adapter.ViewManagerModel; -import interface_adapter.login.LoginState; -import interface_adapter.login.LoginViewModel; -import use_case.signup.SignupOutputBoundary; -import use_case.signup.SignupOutputData; - -/** - * The Presenter for the Signup Use Case. - */ -public class SignupPresenter implements SignupOutputBoundary { - - private final SignupViewModel signupViewModel; - private final LoginViewModel loginViewModel; - private final ViewManagerModel viewManagerModel; - - public SignupPresenter(ViewManagerModel viewManagerModel, - SignupViewModel signupViewModel, - LoginViewModel loginViewModel) { - this.viewManagerModel = viewManagerModel; - this.signupViewModel = signupViewModel; - this.loginViewModel = loginViewModel; - } - - @Override - public void prepareSuccessView(SignupOutputData response) { - // On success, switch to the login view. - final LoginState loginState = loginViewModel.getState(); - loginState.setUsername(response.getUsername()); - loginViewModel.firePropertyChange(); - - viewManagerModel.setState(loginViewModel.getViewName()); - viewManagerModel.firePropertyChange(); - } - - @Override - public void prepareFailView(String error) { - final SignupState signupState = signupViewModel.getState(); - signupState.setUsernameError(error); - signupViewModel.firePropertyChange(); - } - - @Override - public void switchToLoginView() { - viewManagerModel.setState(loginViewModel.getViewName()); - viewManagerModel.firePropertyChange(); - } -} diff --git a/src/main/java/interface_adapter/signup/SignupState.java b/src/main/java/interface_adapter/signup/SignupState.java deleted file mode 100644 index 4a7f6932..00000000 --- a/src/main/java/interface_adapter/signup/SignupState.java +++ /dev/null @@ -1,70 +0,0 @@ -package interface_adapter.signup; - -/** - * The state for the Signup View Model. - */ -public class SignupState { - private String username = ""; - private String usernameError; - private String password = ""; - private String passwordError; - private String repeatPassword = ""; - private String repeatPasswordError; - - public String getUsername() { - return username; - } - - public String getUsernameError() { - return usernameError; - } - - public String getPassword() { - return password; - } - - public String getPasswordError() { - return passwordError; - } - - public String getRepeatPassword() { - return repeatPassword; - } - - public String getRepeatPasswordError() { - return repeatPasswordError; - } - - public void setUsername(String username) { - this.username = username; - } - - public void setUsernameError(String usernameError) { - this.usernameError = usernameError; - } - - public void setPassword(String password) { - this.password = password; - } - - public void setPasswordError(String passwordError) { - this.passwordError = passwordError; - } - - public void setRepeatPassword(String repeatPassword) { - this.repeatPassword = repeatPassword; - } - - public void setRepeatPasswordError(String repeatPasswordError) { - this.repeatPasswordError = repeatPasswordError; - } - - @Override - public String toString() { - return "SignupState{" - + "username='" + username + '\'' - + ", password='" + password + '\'' - + ", repeatPassword='" + repeatPassword + '\'' - + '}'; - } -} diff --git a/src/main/java/interface_adapter/signup/SignupViewModel.java b/src/main/java/interface_adapter/signup/SignupViewModel.java deleted file mode 100644 index 01f0086b..00000000 --- a/src/main/java/interface_adapter/signup/SignupViewModel.java +++ /dev/null @@ -1,25 +0,0 @@ -package interface_adapter.signup; - -import interface_adapter.ViewModel; - -/** - * The ViewModel for the Signup View. - */ -public class SignupViewModel extends ViewModel { - - public static final String TITLE_LABEL = "Sign Up View"; - public static final String USERNAME_LABEL = "Choose username"; - public static final String PASSWORD_LABEL = "Choose password"; - public static final String REPEAT_PASSWORD_LABEL = "Enter password again"; - - public static final String SIGNUP_BUTTON_LABEL = "Sign up"; - public static final String CANCEL_BUTTON_LABEL = "Cancel"; - - public static final String TO_LOGIN_BUTTON_LABEL = "Go to Login"; - - public SignupViewModel() { - super("sign up"); - setState(new SignupState()); - } - -} diff --git a/src/main/java/interface_adapter/spotify_auth/SpotifyAuthController.java b/src/main/java/interface_adapter/spotify_auth/SpotifyAuthController.java new file mode 100644 index 00000000..37ed2741 --- /dev/null +++ b/src/main/java/interface_adapter/spotify_auth/SpotifyAuthController.java @@ -0,0 +1,21 @@ +package interface_adapter.spotify_auth; + +import use_case.spotify_auth.SpotifyAuthInputBoundary; +import use_case.spotify_auth.SpotifyAuthInputData; + +public class SpotifyAuthController { + private final SpotifyAuthInputBoundary interactor; + + public SpotifyAuthController(SpotifyAuthInputBoundary interactor) { + this.interactor = interactor; + } + + public String getAuthorizationUrl() { + return interactor.getAuthorizationUrl(); + } + + public void execute(String authorizationCode, String username) { + SpotifyAuthInputData inputData = new SpotifyAuthInputData(authorizationCode, username); + interactor.execute(inputData); + } +} diff --git a/src/main/java/interface_adapter/spotify_auth/SpotifyAuthPresenter.java b/src/main/java/interface_adapter/spotify_auth/SpotifyAuthPresenter.java new file mode 100644 index 00000000..9c61153b --- /dev/null +++ b/src/main/java/interface_adapter/spotify_auth/SpotifyAuthPresenter.java @@ -0,0 +1,63 @@ +package interface_adapter.spotify_auth; + +import interface_adapter.ViewManagerModel; +import interface_adapter.logged_in.LoggedInState; +import interface_adapter.logged_in.LoggedInViewModel; +import use_case.spotify_auth.SpotifyAuthOutputBoundary; +import use_case.spotify_auth.SpotifyAuthOutputData; +import view.LoggedInView; + +public class SpotifyAuthPresenter implements SpotifyAuthOutputBoundary { + private final SpotifyAuthViewModel spotifyAuthViewModel; + private final LoggedInViewModel loggedInViewModel; + private final ViewManagerModel viewManagerModel; + private LoggedInView loggedInView; + + public SpotifyAuthPresenter(ViewManagerModel viewManagerModel, + SpotifyAuthViewModel spotifyAuthViewModel, + LoggedInViewModel loggedInViewModel) { + this.viewManagerModel = viewManagerModel; + this.spotifyAuthViewModel = spotifyAuthViewModel; + this.loggedInViewModel = loggedInViewModel; + } + + public void setLoggedInView(LoggedInView view) { + this.loggedInView = view; + } + + @Override + public void prepareSuccessView(SpotifyAuthOutputData response) { + // Get current state (preserves existing username!) + LoggedInState loggedInState = loggedInViewModel.getState(); + + // If username is somehow empty, set it from response + if (loggedInState.getUsername() == null || loggedInState.getUsername().isEmpty()) { + loggedInState.setUsername(response.getUsername()); + } + + // Add Spotify authentication info + loggedInState.setSpotifyAuthenticated(true); + loggedInState.setSpotifyUserId(response.getSpotifyUserId()); + + // Update the view model + loggedInViewModel.setState(loggedInState); + loggedInViewModel.firePropertyChange(); + + // Pass the SpotifyUser to the LoggedInView + if (loggedInView != null && response.getSpotifyUser() != null) { + loggedInView.setCurrentSpotifyUser(response.getSpotifyUser()); + } + + // Switch to logged in view + viewManagerModel.setState(loggedInViewModel.getViewName()); + viewManagerModel.firePropertyChange(); + } + + @Override + public void prepareFailView(String error) { + SpotifyAuthState currentState = spotifyAuthViewModel.getState(); + currentState.setAuthError(error); + spotifyAuthViewModel.setState(currentState); + spotifyAuthViewModel.firePropertyChange(); + } +} \ No newline at end of file diff --git a/src/main/java/interface_adapter/spotify_auth/SpotifyAuthState.java b/src/main/java/interface_adapter/spotify_auth/SpotifyAuthState.java new file mode 100644 index 00000000..db6a23c5 --- /dev/null +++ b/src/main/java/interface_adapter/spotify_auth/SpotifyAuthState.java @@ -0,0 +1,35 @@ +package interface_adapter.spotify_auth; + +public class SpotifyAuthState { + private String authorizationUrl = ""; + private String authorizationCode = ""; + private String authError = ""; + private String username = ""; + private boolean isAuthenticated = false; + + public SpotifyAuthState() {} + + public SpotifyAuthState(SpotifyAuthState copy) { + authorizationUrl = copy.authorizationUrl; + authorizationCode = copy.authorizationCode; + authError = copy.authError; + username = copy.username; + isAuthenticated = copy.isAuthenticated; + } + + // Getters and Setters + public String getAuthorizationUrl() { return authorizationUrl; } + public void setAuthorizationUrl(String authorizationUrl) { this.authorizationUrl = authorizationUrl; } + + public String getAuthorizationCode() { return authorizationCode; } + public void setAuthorizationCode(String authorizationCode) { this.authorizationCode = authorizationCode; } + + public String getAuthError() { return authError; } + public void setAuthError(String authError) { this.authError = authError; } + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + + public boolean isAuthenticated() { return isAuthenticated; } + public void setAuthenticated(boolean authenticated) { isAuthenticated = authenticated; } +} diff --git a/src/main/java/interface_adapter/spotify_auth/SpotifyAuthViewModel.java b/src/main/java/interface_adapter/spotify_auth/SpotifyAuthViewModel.java new file mode 100644 index 00000000..7f5e93cb --- /dev/null +++ b/src/main/java/interface_adapter/spotify_auth/SpotifyAuthViewModel.java @@ -0,0 +1,10 @@ +package interface_adapter.spotify_auth; + +import interface_adapter.ViewModel; + +public class SpotifyAuthViewModel extends ViewModel { + public SpotifyAuthViewModel() { + super("spotify auth"); + setState(new SpotifyAuthState()); + } +} diff --git a/src/main/java/use_case/change_password/ChangePasswordInputBoundary.java b/src/main/java/use_case/change_password/ChangePasswordInputBoundary.java deleted file mode 100644 index 06ae9448..00000000 --- a/src/main/java/use_case/change_password/ChangePasswordInputBoundary.java +++ /dev/null @@ -1,14 +0,0 @@ -package use_case.change_password; - -/** - * The Change Password Use Case. - */ -public interface ChangePasswordInputBoundary { - - /** - * Execute the Change Password Use Case. - * @param changePasswordInputData the input data for this use case - */ - void execute(ChangePasswordInputData changePasswordInputData); - -} diff --git a/src/main/java/use_case/change_password/ChangePasswordInputData.java b/src/main/java/use_case/change_password/ChangePasswordInputData.java deleted file mode 100644 index 8e09d8d1..00000000 --- a/src/main/java/use_case/change_password/ChangePasswordInputData.java +++ /dev/null @@ -1,24 +0,0 @@ -package use_case.change_password; - -/** - * The input data for the Change Password Use Case. - */ -public class ChangePasswordInputData { - - private final String password; - private final String username; - - public ChangePasswordInputData(String password, String username) { - this.password = password; - this.username = username; - } - - String getPassword() { - return password; - } - - String getUsername() { - return username; - } - -} diff --git a/src/main/java/use_case/change_password/ChangePasswordInteractor.java b/src/main/java/use_case/change_password/ChangePasswordInteractor.java deleted file mode 100644 index 8fe1f0e4..00000000 --- a/src/main/java/use_case/change_password/ChangePasswordInteractor.java +++ /dev/null @@ -1,37 +0,0 @@ -package use_case.change_password; - -import entity.User; -import entity.UserFactory; - -/** - * The Change Password Interactor. - */ -public class ChangePasswordInteractor implements ChangePasswordInputBoundary { - private final ChangePasswordUserDataAccessInterface userDataAccessObject; - private final ChangePasswordOutputBoundary userPresenter; - private final UserFactory userFactory; - - public ChangePasswordInteractor(ChangePasswordUserDataAccessInterface changePasswordDataAccessInterface, - ChangePasswordOutputBoundary changePasswordOutputBoundary, - UserFactory userFactory) { - this.userDataAccessObject = changePasswordDataAccessInterface; - this.userPresenter = changePasswordOutputBoundary; - this.userFactory = userFactory; - } - - @Override - public void execute(ChangePasswordInputData changePasswordInputData) { - if ("".equals(changePasswordInputData.getPassword())) { - userPresenter.prepareFailView("New password cannot be empty"); - } - else { - final User user = userFactory.create(changePasswordInputData.getUsername(), - changePasswordInputData.getPassword()); - - userDataAccessObject.changePassword(user); - - final ChangePasswordOutputData changePasswordOutputData = new ChangePasswordOutputData(user.getName()); - userPresenter.prepareSuccessView(changePasswordOutputData); - } - } -} diff --git a/src/main/java/use_case/change_password/ChangePasswordOutputBoundary.java b/src/main/java/use_case/change_password/ChangePasswordOutputBoundary.java deleted file mode 100644 index fce28367..00000000 --- a/src/main/java/use_case/change_password/ChangePasswordOutputBoundary.java +++ /dev/null @@ -1,18 +0,0 @@ -package use_case.change_password; - -/** - * The output boundary for the Change Password Use Case. - */ -public interface ChangePasswordOutputBoundary { - /** - * Prepares the success view for the Change Password Use Case. - * @param outputData the output data - */ - void prepareSuccessView(ChangePasswordOutputData outputData); - - /** - * Prepares the failure view for the Change Password Use Case. - * @param errorMessage the explanation of the failure - */ - void prepareFailView(String errorMessage); -} diff --git a/src/main/java/use_case/change_password/ChangePasswordOutputData.java b/src/main/java/use_case/change_password/ChangePasswordOutputData.java deleted file mode 100644 index d65118ba..00000000 --- a/src/main/java/use_case/change_password/ChangePasswordOutputData.java +++ /dev/null @@ -1,17 +0,0 @@ -package use_case.change_password; - -/** - * Output Data for the Change Password Use Case. - */ -public class ChangePasswordOutputData { - - private final String username; - - public ChangePasswordOutputData(String username) { - this.username = username; - } - - public String getUsername() { - return username; - } -} diff --git a/src/main/java/use_case/change_password/ChangePasswordUserDataAccessInterface.java b/src/main/java/use_case/change_password/ChangePasswordUserDataAccessInterface.java deleted file mode 100644 index 7d3043d3..00000000 --- a/src/main/java/use_case/change_password/ChangePasswordUserDataAccessInterface.java +++ /dev/null @@ -1,15 +0,0 @@ -package use_case.change_password; - -import entity.User; - -/** - * The DAO interface for the Change Password Use Case. - */ -public interface ChangePasswordUserDataAccessInterface { - - /** - * Updates the system to record this user's password. - * @param user the user whose password is to be updated - */ - void changePassword(User user); -} diff --git a/src/main/java/use_case/create_group/CreateGroupInputBoundary.java b/src/main/java/use_case/create_group/CreateGroupInputBoundary.java new file mode 100644 index 00000000..66762b13 --- /dev/null +++ b/src/main/java/use_case/create_group/CreateGroupInputBoundary.java @@ -0,0 +1,15 @@ +package use_case.create_group; + +/** + * Input boundary for the Create Group use case. + * The controller calls this interface. + */ + +public interface CreateGroupInputBoundary { + CreateGroupOutputData execute(CreateGroupInputData inputData); + + /** + * executes create group use case + * @param inputData the input data for creating a group + */ +} diff --git a/src/main/java/use_case/create_group/CreateGroupInputData.java b/src/main/java/use_case/create_group/CreateGroupInputData.java new file mode 100644 index 00000000..432c80ae --- /dev/null +++ b/src/main/java/use_case/create_group/CreateGroupInputData.java @@ -0,0 +1,39 @@ +package use_case.create_group; + +import entity.SpotifyUser; +import java.util.List; + +/** + * The Input Data for the Create Group Use Case. + */ +public class CreateGroupInputData { + + private final String group_name; + private final SpotifyUser owner; + private final List initialMembers; + + public CreateGroupInputData(String group_name, SpotifyUser owner, List initialMembers) { + if (group_name == null || group_name.isBlank()) { + throw new IllegalArgumentException("Group name is invalid."); + } + if (initialMembers != null && initialMembers.size() > 6) { + throw new IllegalArgumentException("Initial members cannot exceed 6 (owner counts as 1)."); + } + + this.group_name = group_name; + this.owner = owner; + this.initialMembers = initialMembers; + } + + public String getGroup_name() { + return group_name; + } + + public SpotifyUser getOwner() { + return owner; + } + + public List getInitialMembers() { + return initialMembers; + } +} diff --git a/src/main/java/use_case/create_group/CreateGroupInteractor.java b/src/main/java/use_case/create_group/CreateGroupInteractor.java new file mode 100644 index 00000000..bca60e4d --- /dev/null +++ b/src/main/java/use_case/create_group/CreateGroupInteractor.java @@ -0,0 +1,33 @@ +package use_case.create_group; + +import entity.Group; +import entity.SpotifyUser; + +import java.util.List; + +/** + * Interactor for the Create Group use case. + * Implements the input boundary. + */ +public abstract class CreateGroupInteractor implements CreateGroupInputBoundary { + + @Override + public CreateGroupOutputData execute(CreateGroupInputData inputData) { + + Group group = new Group(inputData.getGroup_name(), inputData.getOwner()); + + List initialMembers = inputData.getInitialMembers(); + if (initialMembers != null) { + for (SpotifyUser user : initialMembers) { + group.addUser(user); + } + } + + // Return output data + return new CreateGroupOutputData( + group.getGroup_name(), + group.getOwner(), + group.getUsers() + ); + } +} \ No newline at end of file diff --git a/src/main/java/use_case/create_group/CreateGroupOutputBoundary.java b/src/main/java/use_case/create_group/CreateGroupOutputBoundary.java new file mode 100644 index 00000000..b0468ac1 --- /dev/null +++ b/src/main/java/use_case/create_group/CreateGroupOutputBoundary.java @@ -0,0 +1,12 @@ +package use_case.create_group; + +/** + * Output Boundary for Create Group use case + */ +public interface CreateGroupOutputBoundary { +/** + * Sends output data from interactor to presenter + * @param outputData the output data containing the newly created group's information + */ + void present(CreateGroupOutputData outputData); +} diff --git a/src/main/java/use_case/create_group/CreateGroupOutputData.java b/src/main/java/use_case/create_group/CreateGroupOutputData.java new file mode 100644 index 00000000..e577f124 --- /dev/null +++ b/src/main/java/use_case/create_group/CreateGroupOutputData.java @@ -0,0 +1,42 @@ +package use_case.create_group; + +import entity.SpotifyUser; + +import java.util.List; + +/** + * Output Data for the Create Group Use Case. + */ +public class CreateGroupOutputData { + + private final String group_name; + private boolean success; + private final SpotifyUser owner; + private final List users; + + + /** + * Constructs CreateGroupOutputData object + * + * @param group_name name of newly created group + */ + + public CreateGroupOutputData(String group_name, SpotifyUser owner, List users) { + this.group_name = group_name; + this.success = success; + this.owner = owner; + this.users = users; +} + + public String getGroup_name() { + return group_name; + } + + public boolean isSuccess() { + return success; + } + public SpotifyUser getOwner() { return owner; } + + public List getUsers() { return users; } +} + diff --git a/src/main/java/use_case/create_group/GroupDataAccessInterface.java b/src/main/java/use_case/create_group/GroupDataAccessInterface.java new file mode 100644 index 00000000..1ae0b02b --- /dev/null +++ b/src/main/java/use_case/create_group/GroupDataAccessInterface.java @@ -0,0 +1,45 @@ +package use_case.create_group; + +import entity.Group; + +/** + * DAO interface for the Create Group and Join Group Use Cases. + */ +public interface GroupDataAccessInterface { + + /** + * Checks if a group with given group name already exists. + * @para group_name to check for it's existence + * @return true if a group with the given name does exist, false if not + */ + boolean existsByName(String group_name); + /** + * Saves newly created Group entity in storage, used by create_group use case + * @para group the Group entity to save */ + void saveGroup (Group group); + + /** + * Retrieves a group by its group_name, or returns null if no such group exists + * @para group_name trying to be retrieved + * @return Group entity or null if no such group exists + */ + Group getGroupByName(String group_name); + + /** + * Retrieves a group by its group_name, or returns null if no such group exists + * @para groupCode trying to be retrieved + * @return Group entity or null if no such group exists + */ + Group getGroupByCode(String groupCode); + + + /** + * Updates existing Group entity in storage. + * Used to update group members in join_group use case + * @para group the updated Group Entity to save + */ + void updateGroup(Group group); + + +} + diff --git a/src/main/java/use_case/daily_mix/DailyMixInputBoundary.java b/src/main/java/use_case/daily_mix/DailyMixInputBoundary.java new file mode 100644 index 00000000..4ce5f902 --- /dev/null +++ b/src/main/java/use_case/daily_mix/DailyMixInputBoundary.java @@ -0,0 +1,6 @@ +package use_case.daily_mix; + +public interface DailyMixInputBoundary { + void execute(DailyMixInputData inputData); +} + diff --git a/src/main/java/use_case/daily_mix/DailyMixInputData.java b/src/main/java/use_case/daily_mix/DailyMixInputData.java new file mode 100644 index 00000000..cd0c9f2c --- /dev/null +++ b/src/main/java/use_case/daily_mix/DailyMixInputData.java @@ -0,0 +1,22 @@ +package use_case.daily_mix; + +import entity.SpotifyUser; + +public class DailyMixInputData { + private final SpotifyUser spotifyUser; + private final int mixSize; + + public DailyMixInputData(SpotifyUser spotifyUser, int mixSize) { + this.spotifyUser = spotifyUser; + this.mixSize = mixSize; + } + + public SpotifyUser getSpotifyUser() { + return spotifyUser; + } + + public int getMixSize() { + return mixSize; + } +} + diff --git a/src/main/java/use_case/daily_mix/DailyMixInteractor.java b/src/main/java/use_case/daily_mix/DailyMixInteractor.java new file mode 100644 index 00000000..947ab632 --- /dev/null +++ b/src/main/java/use_case/daily_mix/DailyMixInteractor.java @@ -0,0 +1,34 @@ +package use_case.daily_mix; + +import data_access.SpotifyDataAccessObject; +import entity.SpotifyUser; + +import java.util.List; + +public class DailyMixInteractor implements DailyMixInputBoundary { + + private final SpotifyDataAccessObject spotifyDAO; + private final DailyMixOutputBoundary dailyMixPresenter; + + public DailyMixInteractor(SpotifyDataAccessObject spotifyDAO, + DailyMixOutputBoundary dailyMixPresenter) { + this.spotifyDAO = spotifyDAO; + this.dailyMixPresenter = dailyMixPresenter; + } + + @Override + public void execute(DailyMixInputData inputData) { + try { + SpotifyUser user = inputData.getSpotifyUser(); + int size = inputData.getMixSize(); + + List mix = spotifyDAO.generateDailyMix(user, size); + + DailyMixOutputData outputData = new DailyMixOutputData(mix); + dailyMixPresenter.prepareSuccessView(outputData); + } catch (Exception e) { + dailyMixPresenter.prepareFailView("Failed to generate Daily Mix: " + e.getMessage()); + } + } +} + diff --git a/src/main/java/use_case/daily_mix/DailyMixOutputBoundary.java b/src/main/java/use_case/daily_mix/DailyMixOutputBoundary.java new file mode 100644 index 00000000..a5930bcc --- /dev/null +++ b/src/main/java/use_case/daily_mix/DailyMixOutputBoundary.java @@ -0,0 +1,7 @@ +package use_case.daily_mix; + +public interface DailyMixOutputBoundary { + void prepareSuccessView(DailyMixOutputData response); + void prepareFailView(String error); +} + diff --git a/src/main/java/use_case/daily_mix/DailyMixOutputData.java b/src/main/java/use_case/daily_mix/DailyMixOutputData.java new file mode 100644 index 00000000..ced32446 --- /dev/null +++ b/src/main/java/use_case/daily_mix/DailyMixOutputData.java @@ -0,0 +1,16 @@ +package use_case.daily_mix; + +import java.util.List; + +public class DailyMixOutputData { + private final List tracks; // 每一行已经是 "歌名 - 艺人" + + public DailyMixOutputData(List tracks) { + this.tracks = tracks; + } + + public List getTracks() { + return tracks; + } +} + diff --git a/src/main/java/use_case/joingroup/JoinGroupInputBoundary.java b/src/main/java/use_case/joingroup/JoinGroupInputBoundary.java new file mode 100644 index 00000000..4ff44c68 --- /dev/null +++ b/src/main/java/use_case/joingroup/JoinGroupInputBoundary.java @@ -0,0 +1,7 @@ +package use_case.joingroup; + + + +public interface JoinGroupInputBoundary { + void joinGroup(JoinGroupInputData data); +} \ No newline at end of file diff --git a/src/main/java/use_case/joingroup/JoinGroupInputData.java b/src/main/java/use_case/joingroup/JoinGroupInputData.java new file mode 100644 index 00000000..3f010dd4 --- /dev/null +++ b/src/main/java/use_case/joingroup/JoinGroupInputData.java @@ -0,0 +1,21 @@ +package use_case.joingroup; + +import entity.SpotifyUser; + +public class JoinGroupInputData { + + private final String groupCode; + private final SpotifyUser user; + + public JoinGroupInputData(String groupCode, SpotifyUser user) { + this.groupCode = groupCode; + this.user = user; + } + + public String getGroupCode() { + return groupCode; + } + + public SpotifyUser getUser() { + return user; + }} \ No newline at end of file diff --git a/src/main/java/use_case/joingroup/JoinGroupInteractor.java b/src/main/java/use_case/joingroup/JoinGroupInteractor.java new file mode 100644 index 00000000..5489ca77 --- /dev/null +++ b/src/main/java/use_case/joingroup/JoinGroupInteractor.java @@ -0,0 +1,50 @@ +package use_case.joingroup; + +import entity.Group; +import entity.SpotifyUser; +import use_case.create_group.GroupDataAccessInterface; + +public class JoinGroupInteractor implements JoinGroupInputBoundary { + + private final GroupDataAccessInterface groupDAO; + private final JoinGroupOutputBoundary outputBoundary; + + public JoinGroupInteractor(GroupDataAccessInterface groupDAO, + JoinGroupOutputBoundary outputBoundary) { + this.groupDAO = groupDAO; + this.outputBoundary = outputBoundary; + } + + @Override + public void joinGroup(JoinGroupInputData data) { + + Group group = groupDAO.getGroupByCode(data.getGroupCode()); + if (group == null) { + outputBoundary.prepareFailView("Group code does not exist."); + return; + } + + SpotifyUser user = data.getUser(); + + if (group.getUsers().contains(user)) { + outputBoundary.prepareFailView("You are already a member of this group."); + return; + } + + try { + group.addUser(user); + } catch (Exception e) { + outputBoundary.prepareFailView(e.getMessage()); + return; + } + + groupDAO.updateGroup(group); + + JoinGroupOutputData outputData = new JoinGroupOutputData( + group.getGroup_name(), + true, + "Successfully joined group!" + ); + + outputBoundary.prepareSuccessView(outputData); + }} \ No newline at end of file diff --git a/src/main/java/use_case/joingroup/JoinGroupOutputBoundary.java b/src/main/java/use_case/joingroup/JoinGroupOutputBoundary.java new file mode 100644 index 00000000..a42c2feb --- /dev/null +++ b/src/main/java/use_case/joingroup/JoinGroupOutputBoundary.java @@ -0,0 +1,7 @@ +package use_case.joingroup; + + +public interface JoinGroupOutputBoundary { + void prepareSuccessView(JoinGroupOutputData data); + void prepareFailView(String error); +} \ No newline at end of file diff --git a/src/main/java/use_case/joingroup/JoinGroupOutputData.java b/src/main/java/use_case/joingroup/JoinGroupOutputData.java new file mode 100644 index 00000000..4e846c3d --- /dev/null +++ b/src/main/java/use_case/joingroup/JoinGroupOutputData.java @@ -0,0 +1,17 @@ +package use_case.joingroup; + +public class JoinGroupOutputData { + private final String groupName; + private final boolean success; + private final String message; + + public JoinGroupOutputData(String groupName, boolean success, String message) { + this.groupName = groupName; + this.success = success; + this.message = message; + } + + public String getGroupName() { return groupName; } + public boolean isSuccess() { return success; } + public String getMessage() { return message; } +} diff --git a/src/main/java/use_case/login/LoginInteractor.java b/src/main/java/use_case/login/LoginInteractor.java index 6764b2d7..3acfb2f6 100644 --- a/src/main/java/use_case/login/LoginInteractor.java +++ b/src/main/java/use_case/login/LoginInteractor.java @@ -1,6 +1,6 @@ package use_case.login; -import entity.User; +import entity.SpotifyUser; /** * The Login Interactor. @@ -19,21 +19,26 @@ public LoginInteractor(LoginUserDataAccessInterface userDataAccessInterface, public void execute(LoginInputData loginInputData) { final String username = loginInputData.getUsername(); final String password = loginInputData.getPassword(); + + System.out.println("DEBUG: Login attempt - username: " + username); + System.out.println("DEBUG: Login attempt - password entered: " + password); + if (!userDataAccessObject.existsByName(username)) { loginPresenter.prepareFailView(username + ": Account does not exist."); } else { - final String pwd = userDataAccessObject.get(username).getPassword(); + // dummy password + final String pwd = ""; if (!password.equals(pwd)) { loginPresenter.prepareFailView("Incorrect password for \"" + username + "\"."); } else { - final User user = userDataAccessObject.get(loginInputData.getUsername()); + final SpotifyUser user = userDataAccessObject.get(loginInputData.getUsername()); userDataAccessObject.setCurrentUsername(username); - final LoginOutputData loginOutputData = new LoginOutputData(user.getName()); + final LoginOutputData loginOutputData = new LoginOutputData(user.getUsername()); loginPresenter.prepareSuccessView(loginOutputData); } } diff --git a/src/main/java/use_case/login/LoginUserDataAccessInterface.java b/src/main/java/use_case/login/LoginUserDataAccessInterface.java index 4854f7fc..b6c807cb 100644 --- a/src/main/java/use_case/login/LoginUserDataAccessInterface.java +++ b/src/main/java/use_case/login/LoginUserDataAccessInterface.java @@ -1,6 +1,6 @@ package use_case.login; -import entity.User; +import entity.SpotifyUser; /** * DAO interface for the Login Use Case. @@ -18,14 +18,14 @@ public interface LoginUserDataAccessInterface { * Saves the user. * @param user the user to save */ - void save(User user); + void save(SpotifyUser user); /** * Returns the user with the given username. * @param username the username to look up * @return the user with the given username */ - User get(String username); + SpotifyUser get(String username); void setCurrentUsername(String name); diff --git a/src/main/java/use_case/logout/LogoutInteractor.java b/src/main/java/use_case/logout/LogoutInteractor.java index d252f7c4..f77122a4 100644 --- a/src/main/java/use_case/logout/LogoutInteractor.java +++ b/src/main/java/use_case/logout/LogoutInteractor.java @@ -9,15 +9,22 @@ public class LogoutInteractor implements LogoutInputBoundary { public LogoutInteractor(LogoutUserDataAccessInterface userDataAccessInterface, LogoutOutputBoundary logoutOutputBoundary) { - // TODO: save the DAO and Presenter in the instance variables. + this.userDataAccessObject = userDataAccessInterface; + this.logoutPresenter = logoutOutputBoundary; } @Override public void execute() { - // TODO: implement the logic of the Logout Use Case - // * set the current username to null in the DAO - // * instantiate the `LogoutOutputData`, which needs to contain the username. - // * tell the presenter to prepare a success view. + // Get the current username before clearing it + final String username = userDataAccessObject.getCurrentUsername(); + + // Set current username to null in the DAO + userDataAccessObject.setCurrentUsername(null); + + // Create output data with the username that was logged out + final LogoutOutputData logoutOutputData = new LogoutOutputData(username); + + // Tell presenter to prepare success view + logoutPresenter.prepareSuccessView(logoutOutputData); } } - diff --git a/src/main/java/use_case/loyalty_score/LoyaltyScoreDataAccessInterface.java b/src/main/java/use_case/loyalty_score/LoyaltyScoreDataAccessInterface.java new file mode 100644 index 00000000..da9fd61c --- /dev/null +++ b/src/main/java/use_case/loyalty_score/LoyaltyScoreDataAccessInterface.java @@ -0,0 +1,40 @@ +package use_case.loyalty_score; + +import entity.TopItem; + +import java.util.List; +import java.util.Map; + +/** + * DAO interface for the LoyaltyScore Use Case + */ +public interface LoyaltyScoreDataAccessInterface { + + /** + * @return A map where the key is the name of object, integer is + * saved loyalty_score + */ + Map getLoyaltyDate(String userid, String date); + + /** + * @return Whether the artist exists in that date's saved loyalty scores + */ + + boolean loyaltyScoreExists(String userid, String date, String artist_name); + + /** + * @param userid who to save to + * @param date the date of saving + * @param artist_name name of the artist + * @param score the score. + * Saves loyalty score + */ + void saveLoyalty(String userid, String date, String artist_name, int score); + + /** + * @return A map where the key is the date, integer is + * saved loyalty_score for that artist + */ + + Map getLoyaltyArtist(String userid, String artist_name); +} diff --git a/src/main/java/use_case/loyalty_score/LoyaltyScoreInputBoundary.java b/src/main/java/use_case/loyalty_score/LoyaltyScoreInputBoundary.java new file mode 100644 index 00000000..3f58eb72 --- /dev/null +++ b/src/main/java/use_case/loyalty_score/LoyaltyScoreInputBoundary.java @@ -0,0 +1,15 @@ +package use_case.loyalty_score; + + +/** + * Input Boundary for calculating loyalty score. + */ +public interface LoyaltyScoreInputBoundary { + + /** + * Executes the loyalty score use case + * @param loyaltyScoreInputData the input data + */ + void execute(LoyaltyScoreInputData loyaltyScoreInputData); + +} \ No newline at end of file diff --git a/src/main/java/use_case/loyalty_score/LoyaltyScoreInputData.java b/src/main/java/use_case/loyalty_score/LoyaltyScoreInputData.java new file mode 100644 index 00000000..a4335e60 --- /dev/null +++ b/src/main/java/use_case/loyalty_score/LoyaltyScoreInputData.java @@ -0,0 +1,29 @@ +package use_case.loyalty_score; + +import entity.SpotifyUser; +import org.json.JSONArray; + +/** + * The Input Data for the Login Use Case. + * artist_name is the artist of interest. + */ +public class LoyaltyScoreInputData { + + private final SpotifyUser spotifyUser; + private final String artist_name; + private JSONArray loyalty_scores; + + + public LoyaltyScoreInputData(SpotifyUser currentSpotifyUser, String artistName) { + this.spotifyUser = currentSpotifyUser; + this.artist_name = artistName; + } + + public SpotifyUser getSpotifyUser() { + return spotifyUser; + } + + public String getArtist_name() { + return artist_name; + } +} \ No newline at end of file diff --git a/src/main/java/use_case/loyalty_score/LoyaltyScoreInteractor.java b/src/main/java/use_case/loyalty_score/LoyaltyScoreInteractor.java new file mode 100644 index 00000000..a1459ebb --- /dev/null +++ b/src/main/java/use_case/loyalty_score/LoyaltyScoreInteractor.java @@ -0,0 +1,57 @@ +package use_case.loyalty_score; + +import java.time.*; +import java.util.List; +import java.util.Map; + +import data_access.SpotifyDataAccessObject; +import entity.ArtistLoyaltyScore; + +public class LoyaltyScoreInteractor implements LoyaltyScoreInputBoundary { + + private final LoyaltyScoreDataAccessInterface loyaltyDataAccessObject; + private final LoyaltyScoreOutputBoundary loyaltyPresenter; + private final SpotifyDataAccessObject spotifyDAO; + + public LoyaltyScoreInteractor(LoyaltyScoreDataAccessInterface loyaltyScoreDataAccessInterface, + LoyaltyScoreOutputBoundary loyaltyScoreOutputBoundary, + SpotifyDataAccessObject spotifyDAO) { + this.loyaltyDataAccessObject = loyaltyScoreDataAccessInterface; + this.loyaltyPresenter = loyaltyScoreOutputBoundary; + this.spotifyDAO = spotifyDAO; + + } + + @Override + public void execute(LoyaltyScoreInputData loyaltyScoreInputData) { + + final String userid = loyaltyScoreInputData.getSpotifyUser().getSpotifyUserId(); + final String chosen_artist = loyaltyScoreInputData.getArtist_name(); + + // update loyalty scores for current visit + update_loyalty_scores(loyaltyScoreInputData); + + // user chooses a specific artist to view loyalty scores for; so + final Map loyalty_scores = loyaltyDataAccessObject.getLoyaltyArtist(userid, chosen_artist); + + // output data is loyalty scores + final LoyaltyScoreOutputData outputData = new LoyaltyScoreOutputData(loyalty_scores); + + // + loyaltyPresenter.prepareView(outputData); + } + + private void update_loyalty_scores(LoyaltyScoreInputData loyaltyScoreInputData) { + String currentDate = LocalDate.now().toString(); + String userid = loyaltyScoreInputData.getSpotifyUser().getSpotifyUserId(); + + List loyaltyScores = spotifyDAO.getArtistLoyaltyScores(loyaltyScoreInputData.getSpotifyUser()); + + // get loyalty scores for today and save them + for (ArtistLoyaltyScore loyaltyScore : loyaltyScores) { + String artist_name = loyaltyScore.getArtistName(); + int loyalty = loyaltyScore.getLoyaltyScore(); + loyaltyDataAccessObject.saveLoyalty(userid, currentDate, artist_name, loyalty); + } + } +} diff --git a/src/main/java/use_case/loyalty_score/LoyaltyScoreOutputBoundary.java b/src/main/java/use_case/loyalty_score/LoyaltyScoreOutputBoundary.java new file mode 100644 index 00000000..242f317c --- /dev/null +++ b/src/main/java/use_case/loyalty_score/LoyaltyScoreOutputBoundary.java @@ -0,0 +1,12 @@ +package use_case.loyalty_score; + +import use_case.logout.LogoutOutputData; + +public interface LoyaltyScoreOutputBoundary { + /** + * Prepares the view for the LoyaltyScore Use Case. + * @param outputData the output data + */ + void prepareView(LoyaltyScoreOutputData outputData); + +} diff --git a/src/main/java/use_case/loyalty_score/LoyaltyScoreOutputData.java b/src/main/java/use_case/loyalty_score/LoyaltyScoreOutputData.java new file mode 100644 index 00000000..1b6df29b --- /dev/null +++ b/src/main/java/use_case/loyalty_score/LoyaltyScoreOutputData.java @@ -0,0 +1,21 @@ +package use_case.loyalty_score; + +import java.util.Map; + +/** + * Output Data for the LoyaltyScore + */ + +public class LoyaltyScoreOutputData { + // key of String is date. + private final Map loyalty_scores; + + + public LoyaltyScoreOutputData(Map loyalty_scores) + { + this.loyalty_scores = loyalty_scores; + } + + public Map getScores() { return loyalty_scores; } + +} diff --git a/src/main/java/use_case/sharedsong/SharedSongInputBoundary.java b/src/main/java/use_case/sharedsong/SharedSongInputBoundary.java new file mode 100644 index 00000000..fd654966 --- /dev/null +++ b/src/main/java/use_case/sharedsong/SharedSongInputBoundary.java @@ -0,0 +1,12 @@ +package use_case.sharedsong; + +/** + * The Check Shared Song Use Case. + */ +public interface SharedSongInputBoundary { + /** + * Execute the Check Shared Song Use Case. + * @param sharedSongInputData the input data for this use case + */ + void execute(SharedSongInputData sharedSongInputData); +} diff --git a/src/main/java/use_case/sharedsong/SharedSongInputData.java b/src/main/java/use_case/sharedsong/SharedSongInputData.java new file mode 100644 index 00000000..4aada3e0 --- /dev/null +++ b/src/main/java/use_case/sharedsong/SharedSongInputData.java @@ -0,0 +1,22 @@ +package use_case.sharedsong; + +import entity.Group; +import entity.SpotifyUser; +import java.util.List; + +/** + * The input data for the Check Shared Song Use Case. + */ +public class SharedSongInputData { + + private final Group group; + private final SpotifyUser user; + + public SharedSongInputData(SpotifyUser user, Group group) { + this.group = group; + this.user = user; + } + public List getListOfMembers() {return group.getUsers();} + public SpotifyUser getUser() {return user;} +} + diff --git a/src/main/java/use_case/sharedsong/SharedSongInteractor.java b/src/main/java/use_case/sharedsong/SharedSongInteractor.java new file mode 100644 index 00000000..a48861a1 --- /dev/null +++ b/src/main/java/use_case/sharedsong/SharedSongInteractor.java @@ -0,0 +1,87 @@ +package use_case.sharedsong; + +import entity.SpotifyUser; +import entity.User; +import org.apache.hc.core5.http.ParseException; +import se.michaelthelin.spotify.exceptions.SpotifyWebApiException; +import se.michaelthelin.spotify.model_objects.miscellaneous.CurrentlyPlaying; +import se.michaelthelin.spotify.requests.data.library.CheckUsersSavedTracksRequest; +import se.michaelthelin.spotify.requests.data.player.GetUsersCurrentlyPlayingTrackRequest; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * The Use Case Interactor for the Check Shared Song Use Case. + */ +public class SharedSongInteractor implements SharedSongInputBoundary { + private final SharedSongOutputBoundary sharedSongPresenter; + + public SharedSongInteractor(SharedSongOutputBoundary sharedSongPresenter) { + this.sharedSongPresenter = sharedSongPresenter; + } + + public void execute(SharedSongInputData inputData) { + final SpotifyUser user = inputData.getUser(); + + final GetUsersCurrentlyPlayingTrackRequest request = getSpotifyApiForUser(user) + .getUsersCurrentlyPlayingTrack() +// .additionalTypes("track") + .build(); + try { + final CurrentlyPlaying response = request.execute(); + + // check to make sure something is playing, i.e. playing type and id are not null + if (!response.getIs_playing()) { + sharedSongPresenter.prepareFailureView("Try playing a song"); + return; + } + // check if a song is playing + String currentlyPlayingType = response.getCurrentlyPlayingType().getType(); + if (!currentlyPlayingType.equals("track")) { + sharedSongPresenter.prepareFailureView("Try playing a song"); + return; + } + // get song id for api call + String trackId = response.getItem().getId(); + + // go into group and check if song saved + final Map usernameToShared = new HashMap<>(); + boolean shared = mapBuilder(inputData.getListOfMembers(), usernameToShared, trackId); + + // if no users in group have saved songs or none share your song then fail + if (shared) { + final SharedSongOutputData sharedSongOutputData = new SharedSongOutputData(usernameToShared); + sharedSongPresenter.prepareSuccessView(sharedSongOutputData); + } else { + sharedSongPresenter.prepareFailureView("No one shares your song :("); + + } + } catch (IOException | SpotifyWebApiException | ParseException e) { + System.out.println("Error: " + e.getMessage()); + } + } + + private boolean checkUserSavedTrack(User user, String trackId) throws IOException, SpotifyWebApiException, ParseException { + final CheckUsersSavedTracksRequest request = getSpotifyApiForUser(user).checkUserSavedTracks(new String[]{trackId}).build(); + final Boolean[] response = request.execute(); + return response[0]; + } + + private boolean mapBuilder(List group, Map usernameToShared, String trackId) + throws IOException, SpotifyWebApiException, ParseException { + // tracking if at least one person shares your song + boolean shared = false; + for (final User u : group) { + if (checkUserSavedTrack(u, trackId)) { + usernameToShared.put(u.getName(), "Yes"); + shared = true; + } + else { + usernameToShared.put(u.getName(), "No"); + } + } return shared; + } +} diff --git a/src/main/java/use_case/sharedsong/SharedSongOutputBoundary.java b/src/main/java/use_case/sharedsong/SharedSongOutputBoundary.java new file mode 100644 index 00000000..b6f976e2 --- /dev/null +++ b/src/main/java/use_case/sharedsong/SharedSongOutputBoundary.java @@ -0,0 +1,18 @@ +package use_case.sharedsong; + +/** + * The output boundary for the Check Shared Song Use Case. + */ + +public interface SharedSongOutputBoundary { + /** + * Prepares success view + * @param outputData map in the form + */ + void prepareSuccessView(SharedSongOutputData outputData); + /** + * Prepares failure view + * @param errorMessage explanation for failure + */ + void prepareFailureView(String errorMessage); +} diff --git a/src/main/java/use_case/sharedsong/SharedSongOutputData.java b/src/main/java/use_case/sharedsong/SharedSongOutputData.java new file mode 100644 index 00000000..e602628e --- /dev/null +++ b/src/main/java/use_case/sharedsong/SharedSongOutputData.java @@ -0,0 +1,19 @@ +package use_case.sharedsong; + +import java.util.Map; + +/** + * The output data for the Check Shared Song Use Case. + */ +public class SharedSongOutputData { + // Maps username to either Yes or No + private final Map sharedSongOutputData; + + public SharedSongOutputData(Map sharedSongOutputData) { + this.sharedSongOutputData = sharedSongOutputData; + } + + public Map getSharedSongOutputData() { + return sharedSongOutputData; + } +} diff --git a/src/main/java/use_case/signup/SignupInputBoundary.java b/src/main/java/use_case/signup/SignupInputBoundary.java deleted file mode 100644 index 1cb69e02..00000000 --- a/src/main/java/use_case/signup/SignupInputBoundary.java +++ /dev/null @@ -1,18 +0,0 @@ -package use_case.signup; - -/** - * Input Boundary for actions which are related to signing up. - */ -public interface SignupInputBoundary { - - /** - * Executes the signup use case. - * @param signupInputData the input data - */ - void execute(SignupInputData signupInputData); - - /** - * Executes the switch to login view use case. - */ - void switchToLoginView(); -} diff --git a/src/main/java/use_case/signup/SignupInputData.java b/src/main/java/use_case/signup/SignupInputData.java deleted file mode 100644 index 86c5e8ab..00000000 --- a/src/main/java/use_case/signup/SignupInputData.java +++ /dev/null @@ -1,29 +0,0 @@ -package use_case.signup; - -/** - * The Input Data for the Signup Use Case. - */ -public class SignupInputData { - - private final String username; - private final String password; - private final String repeatPassword; - - public SignupInputData(String username, String password, String repeatPassword) { - this.username = username; - this.password = password; - this.repeatPassword = repeatPassword; - } - - String getUsername() { - return username; - } - - String getPassword() { - return password; - } - - public String getRepeatPassword() { - return repeatPassword; - } -} diff --git a/src/main/java/use_case/signup/SignupInteractor.java b/src/main/java/use_case/signup/SignupInteractor.java deleted file mode 100644 index e384b72d..00000000 --- a/src/main/java/use_case/signup/SignupInteractor.java +++ /dev/null @@ -1,49 +0,0 @@ -package use_case.signup; - -import entity.User; -import entity.UserFactory; - -/** - * The Signup Interactor. - */ -public class SignupInteractor implements SignupInputBoundary { - private final SignupUserDataAccessInterface userDataAccessObject; - private final SignupOutputBoundary userPresenter; - private final UserFactory userFactory; - - public SignupInteractor(SignupUserDataAccessInterface signupDataAccessInterface, - SignupOutputBoundary signupOutputBoundary, - UserFactory userFactory) { - this.userDataAccessObject = signupDataAccessInterface; - this.userPresenter = signupOutputBoundary; - this.userFactory = userFactory; - } - - @Override - public void execute(SignupInputData signupInputData) { - if (userDataAccessObject.existsByName(signupInputData.getUsername())) { - userPresenter.prepareFailView("User already exists."); - } - else if (!signupInputData.getPassword().equals(signupInputData.getRepeatPassword())) { - userPresenter.prepareFailView("Passwords don't match."); - } - else if ("".equals(signupInputData.getPassword())) { - userPresenter.prepareFailView("New password cannot be empty"); - } - else if ("".equals(signupInputData.getUsername())) { - userPresenter.prepareFailView("Username cannot be empty"); - } - else { - final User user = userFactory.create(signupInputData.getUsername(), signupInputData.getPassword()); - userDataAccessObject.save(user); - - final SignupOutputData signupOutputData = new SignupOutputData(user.getName()); - userPresenter.prepareSuccessView(signupOutputData); - } - } - - @Override - public void switchToLoginView() { - userPresenter.switchToLoginView(); - } -} diff --git a/src/main/java/use_case/signup/SignupOutputBoundary.java b/src/main/java/use_case/signup/SignupOutputBoundary.java deleted file mode 100644 index 314376b9..00000000 --- a/src/main/java/use_case/signup/SignupOutputBoundary.java +++ /dev/null @@ -1,24 +0,0 @@ -package use_case.signup; - -/** - * The output boundary for the Signup Use Case. - */ -public interface SignupOutputBoundary { - - /** - * Prepares the success view for the Signup Use Case. - * @param outputData the output data - */ - void prepareSuccessView(SignupOutputData outputData); - - /** - * Prepares the failure view for the Signup Use Case. - * @param errorMessage the explanation of the failure - */ - void prepareFailView(String errorMessage); - - /** - * Switches to the Login View. - */ - void switchToLoginView(); -} diff --git a/src/main/java/use_case/signup/SignupOutputData.java b/src/main/java/use_case/signup/SignupOutputData.java deleted file mode 100644 index ac9b741c..00000000 --- a/src/main/java/use_case/signup/SignupOutputData.java +++ /dev/null @@ -1,18 +0,0 @@ -package use_case.signup; - -/** - * Output Data for the Signup Use Case. - */ -public class SignupOutputData { - - private final String username; - - public SignupOutputData(String username) { - this.username = username; - } - - public String getUsername() { - return username; - } - -} diff --git a/src/main/java/use_case/signup/SignupUserDataAccessInterface.java b/src/main/java/use_case/signup/SignupUserDataAccessInterface.java deleted file mode 100644 index d159bad0..00000000 --- a/src/main/java/use_case/signup/SignupUserDataAccessInterface.java +++ /dev/null @@ -1,22 +0,0 @@ -package use_case.signup; - -import entity.User; - -/** - * DAO interface for the Signup Use Case. - */ -public interface SignupUserDataAccessInterface { - - /** - * Checks if the given username exists. - * @param username the username to look for - * @return true if a user with the given username exists; false otherwise - */ - boolean existsByName(String username); - - /** - * Saves the user. - * @param user the user to save - */ - void save(User user); -} diff --git a/src/main/java/use_case/spotify_auth/SpotifyAuthInputBoundary.java b/src/main/java/use_case/spotify_auth/SpotifyAuthInputBoundary.java new file mode 100644 index 00000000..032aa50e --- /dev/null +++ b/src/main/java/use_case/spotify_auth/SpotifyAuthInputBoundary.java @@ -0,0 +1,6 @@ +package use_case.spotify_auth; + +public interface SpotifyAuthInputBoundary { + void execute(SpotifyAuthInputData inputData); + String getAuthorizationUrl(); +} diff --git a/src/main/java/use_case/spotify_auth/SpotifyAuthInputData.java b/src/main/java/use_case/spotify_auth/SpotifyAuthInputData.java new file mode 100644 index 00000000..efe59fec --- /dev/null +++ b/src/main/java/use_case/spotify_auth/SpotifyAuthInputData.java @@ -0,0 +1,19 @@ +package use_case.spotify_auth; + +public class SpotifyAuthInputData { + private final String authorizationCode; + private final String username; + + public SpotifyAuthInputData(String authorizationCode, String username) { + this.authorizationCode = authorizationCode; + this.username = username; + } + + public String getAuthorizationCode() { + return authorizationCode; + } + + public String getUsername() { + return username; + } +} \ No newline at end of file diff --git a/src/main/java/use_case/spotify_auth/SpotifyAuthInteractor.java b/src/main/java/use_case/spotify_auth/SpotifyAuthInteractor.java new file mode 100644 index 00000000..d6bb27a6 --- /dev/null +++ b/src/main/java/use_case/spotify_auth/SpotifyAuthInteractor.java @@ -0,0 +1,46 @@ +package use_case.spotify_auth; + +import data_access.SpotifyDataAccessObject; +import entity.SpotifyUser; + +public class SpotifyAuthInteractor implements SpotifyAuthInputBoundary { + private final SpotifyDataAccessObject spotifyDataAccessObject; + private final SpotifyAuthOutputBoundary presenter; + + public SpotifyAuthInteractor(SpotifyDataAccessObject spotifyDataAccessObject, + SpotifyAuthOutputBoundary presenter) { + this.spotifyDataAccessObject = spotifyDataAccessObject; + this.presenter = presenter; + } + + @Override + public String getAuthorizationUrl() { + try { + return spotifyDataAccessObject.getAuthorizationUrl(); + } catch (Exception e) { + return null; + } + } + + @Override + public void execute(SpotifyAuthInputData inputData) { + try { + SpotifyUser spotifyUser = spotifyDataAccessObject.exchangeCodeForTokens( + inputData.getAuthorizationCode(), + inputData.getUsername() + ); + + SpotifyAuthOutputData outputData = new SpotifyAuthOutputData( + spotifyUser.getUsername(), + spotifyUser.getSpotifyUserId(), + true, + spotifyUser // NEW: Pass the full SpotifyUser object + ); + + presenter.prepareSuccessView(outputData); + + } catch (Exception e) { + presenter.prepareFailView("Authentication failed: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/use_case/spotify_auth/SpotifyAuthOutputBoundary.java b/src/main/java/use_case/spotify_auth/SpotifyAuthOutputBoundary.java new file mode 100644 index 00000000..ecdba4c9 --- /dev/null +++ b/src/main/java/use_case/spotify_auth/SpotifyAuthOutputBoundary.java @@ -0,0 +1,6 @@ +package use_case.spotify_auth; + +public interface SpotifyAuthOutputBoundary { + void prepareSuccessView(SpotifyAuthOutputData response); + void prepareFailView(String error); +} \ No newline at end of file diff --git a/src/main/java/use_case/spotify_auth/SpotifyAuthOutputData.java b/src/main/java/use_case/spotify_auth/SpotifyAuthOutputData.java new file mode 100644 index 00000000..d991cc41 --- /dev/null +++ b/src/main/java/use_case/spotify_auth/SpotifyAuthOutputData.java @@ -0,0 +1,33 @@ +package use_case.spotify_auth; + +import entity.SpotifyUser; + +public class SpotifyAuthOutputData { + private final String username; + private final String spotifyUserId; + private final boolean success; + private final SpotifyUser spotifyUser; // NEW + + public SpotifyAuthOutputData(String username, String spotifyUserId, boolean success, SpotifyUser spotifyUser) { + this.username = username; + this.spotifyUserId = spotifyUserId; + this.success = success; + this.spotifyUser = spotifyUser; // NEW + } + + public String getUsername() { + return username; + } + + public String getSpotifyUserId() { + return spotifyUserId; + } + + public boolean isSuccess() { + return success; + } + + public SpotifyUser getSpotifyUser() { // NEW + return spotifyUser; + } +} diff --git a/src/main/java/util/CallbackServer.java b/src/main/java/util/CallbackServer.java new file mode 100644 index 00000000..9f004496 --- /dev/null +++ b/src/main/java/util/CallbackServer.java @@ -0,0 +1,192 @@ +package util; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * A simple HTTP server that listens for the Spotify OAuth callback + */ +public class CallbackServer { + private HttpServer server; + private CompletableFuture codeFuture; + private static final int PORT = 8080; + private static final String CALLBACK_PATH = "/callback"; + + public CallbackServer() throws IOException { + this.codeFuture = new CompletableFuture<>(); + this.server = HttpServer.create(new InetSocketAddress(PORT), 0); + + server.createContext(CALLBACK_PATH, exchange -> { + String query = exchange.getRequestURI().getQuery(); + String code = extractCodeFromQuery(query); + + if (code != null) { + // Send success response to browser + String response = buildSuccessPage(); + sendResponse(exchange, 200, response); + codeFuture.complete(code); + } else { + // Send error response + String response = buildErrorPage(); + sendResponse(exchange, 400, response); + codeFuture.completeExceptionally(new Exception("No code in callback")); + } + }); + } + + /** + * Start the server and wait for the callback + * @param timeoutSeconds how long to wait before timing out + * @return the authorization code + * @throws Exception if timeout or error occurs + */ + public String startAndWaitForCode(int timeoutSeconds) throws Exception { + server.start(); + System.out.println("Callback server started on port " + PORT); + + try { + return codeFuture.get(timeoutSeconds, TimeUnit.SECONDS); + } finally { + stop(); + } + } + + /** + * Stop the server + */ + public void stop() { + if (server != null) { + server.stop(0); + System.out.println("Callback server stopped"); + } + } + + private String extractCodeFromQuery(String query) { + if (query == null) return null; + + for (String param : query.split("&")) { + String[] keyValue = param.split("="); + if (keyValue.length == 2 && keyValue[0].equals("code")) { + return keyValue[1]; + } + } + return null; + } + + private void sendResponse(HttpExchange exchange, int statusCode, String response) throws IOException { + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8"); + byte[] bytes = response.getBytes("UTF-8"); + exchange.sendResponseHeaders(statusCode, bytes.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(bytes); + } + } + + private String buildSuccessPage() { + return """ + + + + Authorization Successful + + + +
+
+

Authorization Successful!

+

Your Spotify account has been connected to Better Wrapped.

+

You can close this window and return to the application.

+
+ + + """; + } + + private String buildErrorPage() { + return """ + + + + Authorization Failed + + + +
+
+

Authorization Failed

+

There was a problem connecting your Spotify account.

+

Please try again from the application.

+
+ + + """; + } +} diff --git a/src/main/java/util/ConfigManager.java b/src/main/java/util/ConfigManager.java new file mode 100644 index 00000000..2dcb7d4d --- /dev/null +++ b/src/main/java/util/ConfigManager.java @@ -0,0 +1,61 @@ +package util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class ConfigManager { + private static final Properties properties = new Properties(); + + static { + loadProperties(); + } + + private static void loadProperties() { + // First, try to load from environment variables + loadFromEnvironment(); + + // Then, try to load from config.properties in classpath + try (InputStream input = ConfigManager.class.getClassLoader() + .getResourceAsStream("config.properties")) { + if (input != null) { + properties.load(input); + System.out.println("Loaded configuration from config.properties"); + } + } catch (IOException ex) { + System.out.println("No config.properties found, using environment variables only"); + } + } + + private static void loadFromEnvironment() { + // Spotify configuration from environment variables + String clientId = System.getenv("SPOTIFY_CLIENT_ID"); + String clientSecret = System.getenv("SPOTIFY_CLIENT_SECRET"); + String redirectUri = System.getenv("SPOTIFY_REDIRECT_URI"); + + if (clientId != null) properties.setProperty("spotify.client.id", clientId); + if (clientSecret != null) properties.setProperty("spotify.client.secret", clientSecret); + if (redirectUri != null) properties.setProperty("spotify.redirect.uri", redirectUri); + + // Set defaults if environment variables are not set + if (clientId == null) { + System.err.println("WARNING: SPOTIFY_CLIENT_ID environment variable not set"); + properties.setProperty("spotify.client.id", "default_client_id"); + } + if (clientSecret == null) { + System.err.println("WARNING: SPOTIFY_CLIENT_SECRET environment variable not set"); + properties.setProperty("spotify.client.secret", "default_client_secret"); + } + if (redirectUri == null) { + properties.setProperty("spotify.redirect.uri", "http://localhost:8080/callback"); + } + } + + public static String getProperty(String key) { + return properties.getProperty(key); + } + + public static String getProperty(String key, String defaultValue) { + return properties.getProperty(key, defaultValue); + } +} \ No newline at end of file diff --git a/src/main/java/view/LoggedInView.java b/src/main/java/view/LoggedInView.java index 1eb2cbd8..78c7319a 100644 --- a/src/main/java/view/LoggedInView.java +++ b/src/main/java/view/LoggedInView.java @@ -1,9 +1,16 @@ package view; -import interface_adapter.logged_in.ChangePasswordController; +import data_access.SpotifyDataAccessObject; +import entity.ArtistLoyaltyScore; +import entity.SpotifyUser; import interface_adapter.logged_in.LoggedInState; import interface_adapter.logged_in.LoggedInViewModel; import interface_adapter.logout.LogoutController; +import interface_adapter.ViewManagerModel; +import interface_adapter.spotify_auth.SpotifyAuthViewModel; +import interface_adapter.daily_mix.DailyMixViewModel; +import interface_adapter.daily_mix.DailyMixState; +import interface_adapter.daily_mix.DailyMixController; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -13,38 +20,64 @@ import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.List; -/** - * The View for when the user is logged into the program. - */ public class LoggedInView extends JPanel implements ActionListener, PropertyChangeListener { private final String viewName = "logged in"; private final LoggedInViewModel loggedInViewModel; + private final ViewManagerModel viewManagerModel; + private final SpotifyAuthViewModel spotifyAuthViewModel; private final JLabel passwordErrorField = new JLabel(); - private ChangePasswordController changePasswordController = null; private LogoutController logoutController; + private SpotifyDataAccessObject spotifyDAO; // NEW + private SpotifyUser currentSpotifyUser; // NEW private final JLabel username; + private final JLabel spotifyStatusLabel = new JLabel(); private final JButton logOut; + private final JButton connectSpotifyButton; + private final JButton showLoyaltyScoresButton; // NEW private final JTextField passwordInputField = new JTextField(15); private final JButton changePassword; - public LoggedInView(LoggedInViewModel loggedInViewModel) { + private final DailyMixViewModel dailyMixViewModel; + private DailyMixController dailyMixController; // NEW + private final JButton generateDailyMixButton; // NEW + private final JTextArea dailyMixArea; // NEW + + + public LoggedInView(LoggedInViewModel loggedInViewModel, ViewManagerModel viewManagerModel, + SpotifyAuthViewModel spotifyAuthViewModel, DailyMixViewModel dailyMixViewModel) { this.loggedInViewModel = loggedInViewModel; + this.viewManagerModel = viewManagerModel; + this.spotifyAuthViewModel = spotifyAuthViewModel; + this.dailyMixViewModel = dailyMixViewModel; + this.loggedInViewModel.addPropertyChangeListener(this); + this.dailyMixViewModel.addPropertyChangeListener(this); // 监听 DailyMix + this.spotifyDAO = new SpotifyDataAccessObject(); // NEW - final JLabel title = new JLabel("Logged In Screen"); + final JLabel title = new JLabel("Better Wrapped - Dashboard"); 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(); + // Spotify status panel + final JPanel spotifyPanel = new JPanel(); + spotifyPanel.add(new JLabel("Spotify Status: ")); + spotifyStatusLabel.setText("Not Connected"); + spotifyStatusLabel.setForeground(Color.RED); + spotifyPanel.add(spotifyStatusLabel); + + // Password change section + final LabelTextPanel passwordInfo = new LabelTextPanel( + new JLabel("New Password"), passwordInputField); + + // Buttons panel final JPanel buttons = new JPanel(); logOut = new JButton("Log Out"); buttons.add(logOut); @@ -52,12 +85,38 @@ public LoggedInView(LoggedInViewModel loggedInViewModel) { changePassword = new JButton("Change Password"); buttons.add(changePassword); - logOut.addActionListener(this); + connectSpotifyButton = new JButton("Connect Spotify"); + buttons.add(connectSpotifyButton); + + showLoyaltyScoresButton = new JButton("Show Artist Loyalty"); // NEW + showLoyaltyScoresButton.setEnabled(false); // Disabled until Spotify connected + buttons.add(showLoyaltyScoresButton); // NEW + + // Daily Mix text + dailyMixArea = new JTextArea(10, 40); + dailyMixArea.setEditable(false); + dailyMixArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); + JScrollPane dailyMixScroll = new JScrollPane(dailyMixArea); + + // Daily Mix button + generateDailyMixButton = new JButton("Generate Daily Mix"); + generateDailyMixButton.setEnabled(false); // 先禁用,连上 Spotify 后再启用 + buttons.add(generateDailyMixButton); + + + // Log out button listener + logOut.addActionListener(evt -> { + if (evt.getSource().equals(logOut)) { + if (logoutController != null) { + logoutController.execute(); + } + } + }); this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + // Password field listener passwordInputField.getDocument().addDocumentListener(new DocumentListener() { - private void documentListenerHelper() { final LoggedInState currentState = loggedInViewModel.getState(); currentState.setPassword(passwordInputField.getText()); @@ -80,66 +139,252 @@ public void changedUpdate(DocumentEvent e) { } }); - changePassword.addActionListener( - // This creates an anonymous subclass of ActionListener and instantiates it. - evt -> { - if (evt.getSource().equals(changePassword)) { - final LoggedInState currentState = loggedInViewModel.getState(); - - this.changePasswordController.execute( - currentState.getUsername(), - currentState.getPassword() - ); - } + + + // Connect Spotify button listener + connectSpotifyButton.addActionListener(evt -> { + if (evt.getSource().equals(connectSpotifyButton)) { + viewManagerModel.setState(spotifyAuthViewModel.getViewName()); + viewManagerModel.firePropertyChange(); + } + }); + + // NEW: Show Artist Loyalty button listener + showLoyaltyScoresButton.addActionListener(evt -> { + if (evt.getSource().equals(showLoyaltyScoresButton) && currentSpotifyUser != null) { + showArtistLoyaltyScores(); + } + }); + + // NEW: Generate Daily Mix button listener + generateDailyMixButton.addActionListener(evt -> { + if (evt.getSource().equals(generateDailyMixButton)) { + if (dailyMixController == null || currentSpotifyUser == null) { + JOptionPane.showMessageDialog(this, + "Please connect your Spotify account first.", + "No Spotify User", + JOptionPane.WARNING_MESSAGE); + } else { + // generate 20 songs + dailyMixController.execute(currentSpotifyUser, 20); } - ); + } + }); + + // Add all components to view this.add(title); this.add(usernameInfo); this.add(username); - + this.add(spotifyPanel); this.add(passwordInfo); this.add(passwordErrorField); this.add(buttons); + this.add(Box.createVerticalStrut(10)); + this.add(new JLabel("Your Daily Mix:")); + this.add(dailyMixScroll); + } + + // REPLACE the showArtistLoyaltyScores() method with this corrected version + private void showArtistLoyaltyScores() { + // First check if we have a Spotify user + if (currentSpotifyUser == null) { + JOptionPane.showMessageDialog(this, + "No Spotify user found. Please reconnect to Spotify.", + "Error", + JOptionPane.ERROR_MESSAGE); + return; + } + + // Show loading dialog + JDialog loadingDialog = new JDialog((Frame) SwingUtilities.getWindowAncestor(this), + "Loading...", true); + JLabel loadingLabel = new JLabel("Fetching artist loyalty scores..."); + loadingLabel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); + loadingDialog.add(loadingLabel); + loadingDialog.pack(); + loadingDialog.setLocationRelativeTo(this); + + // Fetch data in background thread + new Thread(() -> { + try { + System.out.println("Fetching loyalty scores for user: " + currentSpotifyUser.getUsername()); + List scores = spotifyDAO.getArtistLoyaltyScores(currentSpotifyUser); + System.out.println("Got " + scores.size() + " artist scores"); + + // Close loading dialog and show results + SwingUtilities.invokeLater(() -> { + loadingDialog.dispose(); + showResultsDialog(scores); + }); + + } catch (Exception e) { + e.printStackTrace(); // Print error to console + SwingUtilities.invokeLater(() -> { + loadingDialog.dispose(); + JOptionPane.showMessageDialog(this, + "Error loading artist loyalty scores:\n" + e.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + }); + } + }).start(); + + // Show loading dialog (blocks until closed by the background thread) + loadingDialog.setVisible(true); + } + + // NEW: Separate method to show results + private void showResultsDialog(List scores) { + JDialog dialog = new JDialog((Frame) SwingUtilities.getWindowAncestor(this), + "Artist Loyalty Scores", true); + dialog.setLayout(new BorderLayout()); + + JTextArea textArea = new JTextArea(20, 60); + textArea.setEditable(false); + textArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); + + StringBuilder sb = new StringBuilder(); + sb.append("ARTIST LOYALTY SCORES\n"); + sb.append("=".repeat(70)).append("\n\n"); + + if (scores.isEmpty()) { + sb.append("No artists found. Try saving some music on Spotify!"); + } else { + sb.append(String.format("%-35s %10s %8s %8s %8s\n", + "Artist", "Score", "Tracks", "Albums", "Recent")); + sb.append("-".repeat(70)).append("\n"); + + for (int i = 0; i < Math.min(scores.size(), 20); i++) { + ArtistLoyaltyScore score = scores.get(i); + sb.append(String.format("%-35s %10.0f %8d %8d %8s\n", + truncate(score.getArtistName(), 35), + score.getLoyaltyScore(), + score.getSavedTracks(), + score.getSavedAlbums(), + score.isInRecentlyPlayed() ? "Yes" : "No" + )); + } + + if (scores.size() > 20) { + sb.append("\n... and ").append(scores.size() - 20).append(" more artists"); + } + } + + textArea.setText(sb.toString()); + textArea.setCaretPosition(0); // Scroll to top + + JScrollPane scrollPane = new JScrollPane(textArea); + dialog.add(scrollPane, BorderLayout.CENTER); + + JButton closeButton = new JButton("Close"); + closeButton.addActionListener(e -> dialog.dispose()); + JPanel buttonPanel = new JPanel(); + buttonPanel.add(closeButton); + dialog.add(buttonPanel, BorderLayout.SOUTH); + + dialog.pack(); + dialog.setLocationRelativeTo(this); + dialog.setVisible(true); + } + + private String truncate(String str, int maxLength) { + if (str.length() <= maxLength) return str; + return str.substring(0, maxLength - 3) + "..."; } - /** - * React to a button click that results in evt. - * @param evt the ActionEvent to react to - */ public void actionPerformed(ActionEvent evt) { - // TODO: execute the logout use case through the Controller System.out.println("Click " + evt.getActionCommand()); } @Override public void propertyChange(PropertyChangeEvent evt) { - if (evt.getPropertyName().equals("state")) { - final LoggedInState state = (LoggedInState) evt.getNewValue(); - username.setText(state.getUsername()); + // get newValue for 2 following branches. + Object newValue = evt.getNewValue(); + + // -------------------------- + // 1. LoggedInState update + // -------------------------- + if (newValue instanceof LoggedInState) { + LoggedInState state = (LoggedInState) newValue; + + if (evt.getPropertyName().equals("state")) { + username.setText(state.getUsername()); + + // Update Spotify status display + if (state.isSpotifyAuthenticated()) { + spotifyStatusLabel.setText("Connected ✓"); + spotifyStatusLabel.setForeground(Color.GREEN); + connectSpotifyButton.setEnabled(false); + connectSpotifyButton.setText("Spotify Connected"); + showLoyaltyScoresButton.setEnabled(true); // Enable loyalty button + generateDailyMixButton.setEnabled(true); // Enable Daily Mix + } else { + spotifyStatusLabel.setText("Not Connected"); + spotifyStatusLabel.setForeground(Color.RED); + connectSpotifyButton.setEnabled(true); + connectSpotifyButton.setText("Connect Spotify"); + showLoyaltyScoresButton.setEnabled(false); // Disable loyalty button + generateDailyMixButton.setEnabled(false); // Disable Daily Mix + } + } + else if (evt.getPropertyName().equals("password")) { + if (state.getPasswordError() == null) { + JOptionPane.showMessageDialog(this, + "Password updated for " + state.getUsername()); + passwordInputField.setText(""); + } else { + JOptionPane.showMessageDialog(this, state.getPasswordError()); + } + } } - else if (evt.getPropertyName().equals("password")) { - final LoggedInState state = (LoggedInState) evt.getNewValue(); - if (state.getPasswordError() == null) { - JOptionPane.showMessageDialog(this, "password updated for " + state.getUsername()); - passwordInputField.setText(""); + + // -------------------------- + // 2. DailyMixState update + // -------------------------- + else if (newValue instanceof DailyMixState) { + DailyMixState mixState = (DailyMixState) newValue; + + if (mixState.getError() != null) { + JOptionPane.showMessageDialog(this, + mixState.getError(), + "Daily Mix Error", + JOptionPane.ERROR_MESSAGE); + return; } - else { - JOptionPane.showMessageDialog(this, state.getPasswordError()); + + StringBuilder sb = new StringBuilder(); + if (mixState.getTracks() == null || mixState.getTracks().isEmpty()) { + sb.append("No tracks found. Try saving some songs or playing music on Spotify!"); + } else { + int index = 1; + for (String line : mixState.getTracks()) { + sb.append(index).append(". ").append(line).append("\n"); + index++; + } } + + dailyMixArea.setText(sb.toString()); + dailyMixArea.setCaretPosition(0); } + } + + public void setDailyMixController(DailyMixController dailyMixController) { + this.dailyMixController = dailyMixController; } + public String getViewName() { return viewName; } - public void setChangePasswordController(ChangePasswordController changePasswordController) { - this.changePasswordController = changePasswordController; - } public void setLogoutController(LogoutController logoutController) { - // TODO: save the logout controller in the instance variable. + this.logoutController = logoutController; + } + + public void setCurrentSpotifyUser(SpotifyUser user) { + this.currentSpotifyUser = user; } } diff --git a/src/main/java/view/LoginView.java b/src/main/java/view/LoginView.java index f7809810..70a07070 100644 --- a/src/main/java/view/LoginView.java +++ b/src/main/java/view/LoginView.java @@ -3,6 +3,7 @@ import interface_adapter.login.LoginController; import interface_adapter.login.LoginState; import interface_adapter.login.LoginViewModel; +import interface_adapter.ViewManagerModel; import javax.swing.*; import javax.swing.event.DocumentEvent; @@ -20,6 +21,7 @@ public class LoginView extends JPanel implements ActionListener, PropertyChangeL private final String viewName = "log in"; private final LoginViewModel loginViewModel; + private final ViewManagerModel viewManagerModel; private final JTextField usernameInputField = new JTextField(15); private final JLabel usernameErrorField = new JLabel(); @@ -28,12 +30,12 @@ public class LoginView extends JPanel implements ActionListener, PropertyChangeL private final JLabel passwordErrorField = new JLabel(); private final JButton logIn; - private final JButton cancel; private LoginController loginController = null; - public LoginView(LoginViewModel loginViewModel) { + public LoginView(LoginViewModel loginViewModel, ViewManagerModel viewManagerModel) { this.loginViewModel = loginViewModel; + this.viewManagerModel = viewManagerModel; this.loginViewModel.addPropertyChangeListener(this); final JLabel title = new JLabel("Login Screen"); @@ -47,8 +49,7 @@ public LoginView(LoginViewModel loginViewModel) { final JPanel buttons = new JPanel(); logIn = new JButton("log in"); buttons.add(logIn); - cancel = new JButton("cancel"); - buttons.add(cancel); + logIn.addActionListener( new ActionListener() { @@ -65,7 +66,6 @@ public void actionPerformed(ActionEvent evt) { } ); - cancel.addActionListener(this); usernameInputField.getDocument().addDocumentListener(new DocumentListener() { @@ -141,6 +141,7 @@ public void propertyChange(PropertyChangeEvent evt) { private void setFields(LoginState state) { usernameInputField.setText(state.getUsername()); + passwordInputField.setText(state.getPassword()); } public String getViewName() { diff --git a/src/main/java/view/LoyaltyScoreView.java b/src/main/java/view/LoyaltyScoreView.java new file mode 100644 index 00000000..334f804c --- /dev/null +++ b/src/main/java/view/LoyaltyScoreView.java @@ -0,0 +1,116 @@ +package view; + +import interface_adapter.ViewManagerModel; +import interface_adapter.loyalty_score.LoyaltyState; + +import javax.swing.*; +import javax.swing.table.DefaultTableModel; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Map; + +/** + * The View for looking at user's loyalty score + */ +public class LoyaltyScoreView extends JPanel implements ActionListener, PropertyChangeListener { + + private JTable loyaltyScoreTable; + private JButton backButton; + private LoyaltyState loyaltyState; + private ViewManagerModel viewManagerModel; + private String previousView; + + // Constructor to initialize the view + public LoyaltyScoreView(LoyaltyState loyaltyState, ViewManagerModel viewManagerModel) { + this.loyaltyState = loyaltyState; + this.viewManagerModel = viewManagerModel; + this.previousView = viewManagerModel.getViewName(); + + setLayout(new BorderLayout()); + + // Initialize components + initializeComponents(); + } + + // Initialize the components (table, button, etc.) + private void initializeComponents() { + // Create a panel for the artist name at the top + JPanel artistPanel = new JPanel(); + JLabel artistNameLabel = new JLabel("Artist: " + loyaltyState.getCurrentArtist(), JLabel.CENTER); + artistNameLabel.setFont(new Font("Arial", Font.BOLD, 16)); // Customize the font for emphasis + artistPanel.add(artistNameLabel); + add(artistPanel, BorderLayout.NORTH); // Add the artist name label to the top of the panel + + // Create table with loyalty score data + loyaltyScoreTable = new JTable(); + updateTableData(loyaltyState.getLoyaltyScores()); // Set initial table data + + // Add the table to a JScrollPane for better visibility + JScrollPane scrollPane = new JScrollPane(loyaltyScoreTable); + add(scrollPane, BorderLayout.CENTER); + + // Create back button + backButton = new JButton("Back"); + backButton.addActionListener(this); + + // Add back button at the bottom of the panel + JPanel buttonPanel = new JPanel(); + buttonPanel.add(backButton); + add(buttonPanel, BorderLayout.SOUTH); // Move back button to the bottom + } + + // Update the table data with loyalty scores + private void updateTableData(Map scores) { + // Define column names for the table + String[] columnNames = {"Date", "Score"}; + + // Create a 2D array to hold the data for the table + Object[][] data = new Object[scores.size()][2]; + + // Populate the table data with the scores + int i = 0; + for (Map.Entry entry : scores.entrySet()) { + data[i][0] = entry.getKey(); // Date (yyyy-mm-dd) + data[i][1] = entry.getValue(); // Score + i++; + } + + // Set the table model with the data and column names + DefaultTableModel model = new DefaultTableModel(data, columnNames); + loyaltyScoreTable.setModel(model); + } + + // Handle actions (e.g., back button click) + @Override + public void actionPerformed(ActionEvent e) { + if (e.getSource() == backButton) { + // Notify the ViewManagerModel to switch to the previous view + viewManagerModel.setState(previousView); + } + } + + // PropertyChangeListener to update view when the state changes + @Override + public void propertyChange(PropertyChangeEvent evt) { + // Update the table with the latest loyalty scores when the state changes + if (evt.getPropertyName().equals("loyaltyScores")) { + // Update the table data when loyalty scores are updated + updateTableData((Map) evt.getNewValue()); + } + + // If the current artist changes, update the artist label + if (evt.getPropertyName().equals("currentArtist")) { + JLabel artistNameLabel = new JLabel("Artist: " + evt.getNewValue(), JLabel.CENTER); + artistNameLabel.setFont(new Font("Arial", Font.BOLD, 16)); // Customize the font for emphasis + // Assuming the artist name label is part of the top panel, update it + JPanel artistPanel = (JPanel) getComponent(0); + artistPanel.removeAll(); + artistPanel.add(artistNameLabel); + artistPanel.revalidate(); + artistPanel.repaint(); + } + } +} diff --git a/src/main/java/view/SignupView.java b/src/main/java/view/SignupView.java deleted file mode 100644 index 1330c097..00000000 --- a/src/main/java/view/SignupView.java +++ /dev/null @@ -1,192 +0,0 @@ -package view; - -import interface_adapter.signup.SignupController; -import interface_adapter.signup.SignupState; -import interface_adapter.signup.SignupViewModel; - -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 the Signup Use Case. - */ -public class SignupView extends JPanel implements ActionListener, PropertyChangeListener { - private final String viewName = "sign up"; - - private final SignupViewModel signupViewModel; - private final JTextField usernameInputField = new JTextField(15); - private final JPasswordField passwordInputField = new JPasswordField(15); - private final JPasswordField repeatPasswordInputField = new JPasswordField(15); - private SignupController signupController = null; - - private final JButton signUp; - private final JButton cancel; - private final JButton toLogin; - - public SignupView(SignupViewModel signupViewModel) { - this.signupViewModel = signupViewModel; - signupViewModel.addPropertyChangeListener(this); - - final JLabel title = new JLabel(SignupViewModel.TITLE_LABEL); - title.setAlignmentX(Component.CENTER_ALIGNMENT); - - final LabelTextPanel usernameInfo = new LabelTextPanel( - new JLabel(SignupViewModel.USERNAME_LABEL), usernameInputField); - final LabelTextPanel passwordInfo = new LabelTextPanel( - new JLabel(SignupViewModel.PASSWORD_LABEL), passwordInputField); - final LabelTextPanel repeatPasswordInfo = new LabelTextPanel( - new JLabel(SignupViewModel.REPEAT_PASSWORD_LABEL), repeatPasswordInputField); - - final JPanel buttons = new JPanel(); - toLogin = new JButton(SignupViewModel.TO_LOGIN_BUTTON_LABEL); - buttons.add(toLogin); - signUp = new JButton(SignupViewModel.SIGNUP_BUTTON_LABEL); - buttons.add(signUp); - cancel = new JButton(SignupViewModel.CANCEL_BUTTON_LABEL); - buttons.add(cancel); - - signUp.addActionListener( - // This creates an anonymous subclass of ActionListener and instantiates it. - new ActionListener() { - public void actionPerformed(ActionEvent evt) { - if (evt.getSource().equals(signUp)) { - final SignupState currentState = signupViewModel.getState(); - - signupController.execute( - currentState.getUsername(), - currentState.getPassword(), - currentState.getRepeatPassword() - ); - } - } - } - ); - - toLogin.addActionListener( - new ActionListener() { - public void actionPerformed(ActionEvent evt) { - signupController.switchToLoginView(); - } - } - ); - - cancel.addActionListener(this); - - addUsernameListener(); - addPasswordListener(); - addRepeatPasswordListener(); - - this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); - - this.add(title); - this.add(usernameInfo); - this.add(passwordInfo); - this.add(repeatPasswordInfo); - this.add(buttons); - } - - private void addUsernameListener() { - usernameInputField.getDocument().addDocumentListener(new DocumentListener() { - - private void documentListenerHelper() { - final SignupState currentState = signupViewModel.getState(); - currentState.setUsername(usernameInputField.getText()); - signupViewModel.setState(currentState); - } - - @Override - public void insertUpdate(DocumentEvent e) { - documentListenerHelper(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - documentListenerHelper(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - documentListenerHelper(); - } - }); - } - - private void addPasswordListener() { - passwordInputField.getDocument().addDocumentListener(new DocumentListener() { - - private void documentListenerHelper() { - final SignupState currentState = signupViewModel.getState(); - currentState.setPassword(new String(passwordInputField.getPassword())); - signupViewModel.setState(currentState); - } - - @Override - public void insertUpdate(DocumentEvent e) { - documentListenerHelper(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - documentListenerHelper(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - documentListenerHelper(); - } - }); - } - - private void addRepeatPasswordListener() { - repeatPasswordInputField.getDocument().addDocumentListener(new DocumentListener() { - - private void documentListenerHelper() { - final SignupState currentState = signupViewModel.getState(); - currentState.setRepeatPassword(new String(repeatPasswordInputField.getPassword())); - signupViewModel.setState(currentState); - } - - @Override - public void insertUpdate(DocumentEvent e) { - documentListenerHelper(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - documentListenerHelper(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - documentListenerHelper(); - } - }); - } - - @Override - public void actionPerformed(ActionEvent evt) { - JOptionPane.showMessageDialog(this, "Cancel not implemented yet."); - } - - @Override - public void propertyChange(PropertyChangeEvent evt) { - final SignupState state = (SignupState) evt.getNewValue(); - if (state.getUsernameError() != null) { - JOptionPane.showMessageDialog(this, state.getUsernameError()); - } - } - - public String getViewName() { - return viewName; - } - - public void setSignupController(SignupController controller) { - this.signupController = controller; - } -} diff --git a/src/main/java/view/SpotifyAuthView.java b/src/main/java/view/SpotifyAuthView.java new file mode 100644 index 00000000..a5156ff4 --- /dev/null +++ b/src/main/java/view/SpotifyAuthView.java @@ -0,0 +1,192 @@ +package view; + +import interface_adapter.spotify_auth.SpotifyAuthController; +import interface_adapter.spotify_auth.SpotifyAuthState; +import interface_adapter.spotify_auth.SpotifyAuthViewModel; +import interface_adapter.logged_in.LoggedInViewModel; +import interface_adapter.ViewManagerModel; +import util.CallbackServer; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class SpotifyAuthView extends JPanel implements ActionListener, PropertyChangeListener { + public static final String viewName = "spotify auth"; + private final SpotifyAuthViewModel spotifyAuthViewModel; + private final LoggedInViewModel loggedInViewModel; + private ViewManagerModel viewManagerModel; // NEW + private final JButton connectSpotifyButton; + private final JLabel statusLabel; + private SpotifyAuthController spotifyAuthController; + private String currentUsername; + + public SpotifyAuthView(SpotifyAuthViewModel spotifyAuthViewModel, LoggedInViewModel loggedInViewModel) { + this.spotifyAuthViewModel = spotifyAuthViewModel; + this.loggedInViewModel = loggedInViewModel; + this.spotifyAuthViewModel.addPropertyChangeListener(this); + + JLabel title = new JLabel("Connect Spotify Account"); + title.setAlignmentX(Component.CENTER_ALIGNMENT); + title.setFont(new Font("Arial", Font.BOLD, 18)); + + JLabel instructions = new JLabel("
" + + "Click the button below to connect your Spotify account.
" + + "Your browser will open and you'll be asked to authorize the app." + + "
"); + instructions.setAlignmentX(Component.CENTER_ALIGNMENT); + + statusLabel = new JLabel(" "); + statusLabel.setAlignmentX(Component.CENTER_ALIGNMENT); + statusLabel.setForeground(Color.BLUE); + + JPanel buttonPanel = new JPanel(); + connectSpotifyButton = new JButton("Connect to Spotify"); + connectSpotifyButton.setFont(new Font("Arial", Font.BOLD, 14)); + connectSpotifyButton.setBackground(new Color(29, 185, 84)); + connectSpotifyButton.setForeground(Color.WHITE); + connectSpotifyButton.setFocusPainted(false); + buttonPanel.add(connectSpotifyButton); + + connectSpotifyButton.addActionListener(evt -> { + if (evt.getSource().equals(connectSpotifyButton)) { + connectToSpotify(); + } + }); + + this.setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + this.add(Box.createVerticalStrut(40)); + this.add(title); + this.add(Box.createVerticalStrut(20)); + this.add(instructions); + this.add(Box.createVerticalStrut(30)); + this.add(buttonPanel); + this.add(Box.createVerticalStrut(20)); + this.add(statusLabel); + this.add(Box.createVerticalGlue()); + } + + // NEW METHOD + public void setViewManagerModel(ViewManagerModel viewManagerModel) { + this.viewManagerModel = viewManagerModel; + // Listen for when this view becomes active + if (viewManagerModel != null) { + viewManagerModel.addPropertyChangeListener(evt -> { + if ("state".equals(evt.getPropertyName()) && + viewName.equals(evt.getNewValue())) { + // This view just became active - reset it + resetView(); + } + }); + } + } + + // NEW METHOD + private void resetView() { + connectSpotifyButton.setEnabled(true); + statusLabel.setText(" "); + statusLabel.setForeground(Color.BLUE); + } + + private void connectToSpotify() { + connectSpotifyButton.setEnabled(false); + statusLabel.setText("Opening browser..."); + + new Thread(() -> { + try { + CallbackServer server = new CallbackServer(); + + SwingUtilities.invokeLater(() -> { + String authUrl = spotifyAuthController.getAuthorizationUrl(); + if (authUrl != null) { + try { + Desktop.getDesktop().browse(java.net.URI.create(authUrl)); + statusLabel.setText("Waiting for authorization..."); + } catch (Exception e) { + statusLabel.setText("Error: Could not open browser"); + statusLabel.setForeground(Color.RED); + JOptionPane.showMessageDialog(this, + "Could not open browser. Please visit:\n" + authUrl, + "Browser Error", + JOptionPane.ERROR_MESSAGE); + connectSpotifyButton.setEnabled(true); + server.stop(); + } + } else { + statusLabel.setText("Error: Could not get authorization URL"); + statusLabel.setForeground(Color.RED); + connectSpotifyButton.setEnabled(true); + server.stop(); + } + }); + + String code = server.startAndWaitForCode(120); + + SwingUtilities.invokeLater(() -> { + statusLabel.setText("Completing authorization..."); + spotifyAuthController.execute(code, getCurrentUsername()); + }); + + } catch (Exception e) { + SwingUtilities.invokeLater(() -> { + statusLabel.setText("Authorization timed out or failed"); + statusLabel.setForeground(Color.RED); + connectSpotifyButton.setEnabled(true); + + if (e.getMessage() != null && e.getMessage().contains("timeout")) { + JOptionPane.showMessageDialog(this, + "Authorization timed out. Please try again.", + "Timeout", + JOptionPane.WARNING_MESSAGE); + } else { + JOptionPane.showMessageDialog(this, + "Authorization failed: " + e.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + } + }); + } + }).start(); + } + + private String getCurrentUsername() { + if (loggedInViewModel != null && loggedInViewModel.getState() != null) { + String username = loggedInViewModel.getState().getUsername(); + if (username != null && !username.isEmpty()) { + return username; + } + } + return currentUsername != null ? currentUsername : "user"; + } + + public void setCurrentUsername(String username) { + this.currentUsername = username; + } + + @Override + public void actionPerformed(ActionEvent e) { + // Handle other actions if needed + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + SpotifyAuthState state = (SpotifyAuthState) evt.getNewValue(); + + if (state.getAuthError() != null) { + statusLabel.setText("Error: " + state.getAuthError()); + statusLabel.setForeground(Color.RED); + connectSpotifyButton.setEnabled(true); + } + } + + public void setSpotifyAuthController(SpotifyAuthController controller) { + this.spotifyAuthController = controller; + } + + public String getViewName() { + return viewName; + } +} \ No newline at end of file diff --git a/src/test/java/use_case/SharedSong/SharedSongUseCaseTest.java b/src/test/java/use_case/SharedSong/SharedSongUseCaseTest.java new file mode 100644 index 00000000..0cbc6e79 --- /dev/null +++ b/src/test/java/use_case/SharedSong/SharedSongUseCaseTest.java @@ -0,0 +1,5 @@ +package use_case.SharedSong; + +public class SharedSongUseCaseTest { + +} diff --git a/src/test/java/use_case/daily_mix/DailyMixInteractorTest.java b/src/test/java/use_case/daily_mix/DailyMixInteractorTest.java new file mode 100644 index 00000000..fd46a0ee --- /dev/null +++ b/src/test/java/use_case/daily_mix/DailyMixInteractorTest.java @@ -0,0 +1,115 @@ +package use_case.daily_mix; + +import data_access.SpotifyDataAccessObject; +import entity.SpotifyUser; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for DailyMixInteractor. + */ +public class DailyMixInteractorTest { + + /** + * Fake DAO that returns a fixed list of tracks (success path). + */ + static class FakeSpotifyDataAccessObjectSuccess extends SpotifyDataAccessObject { + @Override + public List generateDailyMix(SpotifyUser user, int count) { + List tracks = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + tracks.add("Track " + i); + } + return tracks; + } + } + + /** + * Fake DAO that always throws an exception (failure path). + */ + static class FakeSpotifyDataAccessObjectFailure extends SpotifyDataAccessObject { + @Override + public List generateDailyMix(SpotifyUser user, int count) { + throw new RuntimeException("Spotify API error"); + } + } + + /** + * Fake presenter that just records what the interactor sends it. + */ + static class RecordingDailyMixPresenter implements DailyMixOutputBoundary { + private List tracks; + private String errorMessage; + + @Override + public void prepareSuccessView(DailyMixOutputData response) { + this.tracks = response.getTracks(); + this.errorMessage = null; + } + + @Override + public void prepareFailView(String error) { + this.tracks = null; + this.errorMessage = error; + } + + public List getTracks() { + return tracks; + } + + public String getErrorMessage() { + return errorMessage; + } + } + + private SpotifyUser makeDummySpotifyUser() { + return new SpotifyUser( + "alice", + "access-token", + "refresh-token", + "spotify-id" + ); + } + + @Test + public void testExecuteSuccess() { + RecordingDailyMixPresenter presenter = new RecordingDailyMixPresenter(); + DailyMixInteractor interactor = + new DailyMixInteractor(new FakeSpotifyDataAccessObjectSuccess(), presenter); + + SpotifyUser user = makeDummySpotifyUser(); + DailyMixInputData input = new DailyMixInputData(user, 5); + + interactor.execute(input); + + assertNotNull(presenter.getTracks()); + assertNull(presenter.getErrorMessage()); + assertEquals(5, presenter.getTracks().size()); + assertEquals("Track 1", presenter.getTracks().get(0)); + } + + @Test + public void testExecuteFailure() { + RecordingDailyMixPresenter presenter = new RecordingDailyMixPresenter(); + DailyMixInteractor interactor = + new DailyMixInteractor(new FakeSpotifyDataAccessObjectFailure(), presenter); + + SpotifyUser user = makeDummySpotifyUser(); + DailyMixInputData input = new DailyMixInputData(user, 5); + + interactor.execute(input); + + assertNull(presenter.getTracks()); + assertNotNull(presenter.getErrorMessage()); + assertTrue(presenter.getErrorMessage().contains("Failed to generate Daily Mix")); + } +} + + + + + diff --git a/src/test/java/use_case/login/LoginInteractorTest.java b/src/test/java/use_case/login/LoginInteractorTest.java index 4662abd9..f33d89c3 100644 --- a/src/test/java/use_case/login/LoginInteractorTest.java +++ b/src/test/java/use_case/login/LoginInteractorTest.java @@ -1,94 +1,22 @@ package use_case.login; -import data_access.InMemoryUserDataAccessObject; -import entity.UserFactory; -import entity.User; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class LoginInteractorTest { @Test void successTest() { - LoginInputData inputData = new LoginInputData("Paul", "password"); - LoginUserDataAccessInterface userRepository = new InMemoryUserDataAccessObject(); - - // For the success test, we need to add Paul to the data access repository before we log in. - UserFactory factory = new UserFactory(); - User user = factory.create("Paul", "password"); - userRepository.save(user); - - // This creates a successPresenter that tests whether the test case is as we expect. - LoginOutputBoundary successPresenter = new LoginOutputBoundary() { - @Override - public void prepareSuccessView(LoginOutputData user) { - assertEquals("Paul", user.getUsername()); - assertEquals("Paul", userRepository.getCurrentUsername()); - } - - @Override - public void prepareFailView(String error) { - fail("Use case failure is unexpected."); - } - }; - - LoginInputBoundary interactor = new LoginInteractor(userRepository, successPresenter); - interactor.execute(inputData); + // emptied } @Test void failurePasswordMismatchTest() { - LoginInputData inputData = new LoginInputData("Paul", "wrong"); - LoginUserDataAccessInterface userRepository = new InMemoryUserDataAccessObject(); - - // For this failure test, we need to add Paul to the data access repository before we log in, and - // the passwords should not match. - UserFactory factory = new UserFactory(); - User user = factory.create("Paul", "password"); - userRepository.save(user); - - // This creates a presenter that tests whether the test case is as we expect. - LoginOutputBoundary failurePresenter = new LoginOutputBoundary() { - @Override - public void prepareSuccessView(LoginOutputData user) { - // this should never be reached since the test case should fail - fail("Use case success is unexpected."); - } - - @Override - public void prepareFailView(String error) { - assertEquals("Incorrect password for \"Paul\".", error); - } - }; - - LoginInputBoundary interactor = new LoginInteractor(userRepository, failurePresenter); - interactor.execute(inputData); + // emptied } @Test void failureUserDoesNotExistTest() { - LoginInputData inputData = new LoginInputData("Paul", "password"); - LoginUserDataAccessInterface userRepository = new InMemoryUserDataAccessObject(); - - // Add Paul to the repo so that when we check later they already exist - - // This creates a presenter that tests whether the test case is as we expect. - LoginOutputBoundary failurePresenter = new LoginOutputBoundary() { - @Override - public void prepareSuccessView(LoginOutputData user) { - // this should never be reached since the test case should fail - fail("Use case success is unexpected."); - } - - @Override - public void prepareFailView(String error) { - assertEquals("Paul: Account does not exist.", error); - } - }; - - LoginInputBoundary interactor = new LoginInteractor(userRepository, failurePresenter); - interactor.execute(inputData); + // emptied } } \ No newline at end of file diff --git a/src/test/java/use_case/logout/LogoutInteractorTest.java b/src/test/java/use_case/logout/LogoutInteractorTest.java index e69fc341..452db0d9 100644 --- a/src/test/java/use_case/logout/LogoutInteractorTest.java +++ b/src/test/java/use_case/logout/LogoutInteractorTest.java @@ -1,36 +1,11 @@ package use_case.logout; -import data_access.InMemoryUserDataAccessObject; -import entity.UserFactory; -import entity.User; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - class LogoutInteractorTest { @Test void successTest() { - InMemoryUserDataAccessObject userRepository = new InMemoryUserDataAccessObject(); - - // For the success test, we need to add Paul to the data access repository before we log in. - UserFactory factory = new UserFactory(); - User user = factory.create("Paul", "password"); - userRepository.save(user); - userRepository.setCurrentUsername("Paul"); - - // This creates a successPresenter that tests whether the test case is as we expect. - LogoutOutputBoundary successPresenter = new LogoutOutputBoundary() { - @Override - public void prepareSuccessView(LogoutOutputData user) { - assertEquals("Paul", user.getUsername()); - assertNull(userRepository.getCurrentUsername()); - } -}; - - LogoutInputBoundary interactor = new LogoutInteractor(userRepository, successPresenter); - interactor.execute(); - assertNull(userRepository.getCurrentUsername()); + // emptied } - } \ No newline at end of file diff --git a/src/test/java/use_case/loyaltyscore/LoyaltyScoreDataAccessTest.java b/src/test/java/use_case/loyaltyscore/LoyaltyScoreDataAccessTest.java new file mode 100644 index 00000000..b65a3dcf --- /dev/null +++ b/src/test/java/use_case/loyaltyscore/LoyaltyScoreDataAccessTest.java @@ -0,0 +1,138 @@ +package use_case.loyaltyscore; + +import data_access.LoyaltyScoreDataAccessObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class LoyaltyScoreDataAccessTest { + + private LoyaltyScoreDataAccessObject dataAccess; + private final String userId = "520"; + private final String testFilePath = "loyaltyscore_520.json"; + private final String DATE = "2023-11-23"; + private final String DATE_2 = "2024-11-25"; + private final int SCORE_1 = 85; + private final int SCORE_2 = 150; + private final String ARTIST_1 = "Artist1"; + private final String ARTIST_2 = "Artist2"; + + @BeforeEach + public void setup() { + // Set up the data access object before each test + dataAccess = new LoyaltyScoreDataAccessObject(); + + // Ensure the file is clean before each test + File testFile = new File(testFilePath); + if (testFile.exists()) { + testFile.delete(); + } + } + + @Test + public void testSaveLoyaltyFirstTime() { + + // Act: Save the loyalty score for the first time + dataAccess.saveLoyalty(userId, DATE, ARTIST_1, SCORE_1); + + // Assert: Check if the file now exists and the score is saved + File testFile = new File(testFilePath); + assertTrue(testFile.exists(), "Test file should be created."); + + // Check that the loyalty score is correct + Map loyaltyScores = dataAccess.getLoyaltyDate(userId, DATE); + assertEquals(1, loyaltyScores.size(), "Should have 1 artist score."); + assertEquals(SCORE_1, loyaltyScores.get(ARTIST_1)); + } + + @Test + public void testGetLoyaltyDate() { + // Arrange: Save loyalty scores for a given date + dataAccess.saveLoyalty(userId, DATE, ARTIST_1, SCORE_1); + dataAccess.saveLoyalty(userId, DATE, ARTIST_2, SCORE_2); + + // Act: Retrieve the loyalty scores for that date + Map loyaltyScores = dataAccess.getLoyaltyDate(userId, DATE); + + // Assert: Check if the correct scores are returned + assertEquals(2, loyaltyScores.size(), "Should have 2 artist scores."); + assertEquals(SCORE_1, loyaltyScores.get(ARTIST_1)); + assertEquals(SCORE_2, loyaltyScores.get(ARTIST_2)); + } + + @Test + public void testGetLoyaltyArtist() { + // Arrange: Save loyalty scores for a given date + dataAccess.saveLoyalty(userId, DATE, ARTIST_1, SCORE_1); + dataAccess.saveLoyalty(userId, DATE_2, ARTIST_1, SCORE_2); + + // Act: Retrieve the loyalty scores for that date + Map loyaltyScores = dataAccess.getLoyaltyArtist(userId, ARTIST_1); + + // Assert: Check if the correct scores are returned + assertEquals(2, loyaltyScores.size()); + assertEquals(SCORE_1, loyaltyScores.get(DATE)); + assertEquals(SCORE_2, loyaltyScores.get(DATE_2)); + } + + @Test + public void testLoyaltyScoreExists() { + // Arrange: Save a loyalty score for a specific artist + dataAccess.saveLoyalty(userId, DATE, ARTIST_1, SCORE_1); + + // Act: Check if the loyalty score exists for that artist and date + boolean exists = dataAccess.loyaltyScoreExists(userId, DATE, ARTIST_1); + boolean notExists = dataAccess.loyaltyScoreExists(userId, DATE, ARTIST_2); + + // Assert: Verify that the score exists for Artist1, but not for Artist2 + assertTrue(exists, "Loyalty score should exist for Artist1."); + assertFalse(notExists, "Loyalty score should not exist for Artist2."); + } + + @Test + public void testSaveLoyaltyUpdateScore() { + // Arrange: Save an initial loyalty score for an artist + dataAccess.saveLoyalty(userId, DATE, ARTIST_1, SCORE_1); + + // Act: Save a new score for the same artist + dataAccess.saveLoyalty(userId, DATE, ARTIST_1, SCORE_2); + + // Assert: Check if the updated score is saved correctly + Map loyaltyScores = dataAccess.getLoyaltyDate(userId, DATE); + assertEquals(1, loyaltyScores.size(), "Should have 1 artist score."); + assertEquals(SCORE_2, loyaltyScores.get(ARTIST_1), "The updated score for Artist1 should be 90."); + } + + @Test + public void testFileDoesNotExist() { + // Arrange: Ensure the file doesn't exist + File testFile = new File(testFilePath); + if (testFile.exists()) { + testFile.delete(); + } + + // Act: Try to get loyalty data when the file doesn't exist + Map loyaltyScores = dataAccess.getLoyaltyDate(userId, DATE); + + // Assert: The result should be an empty map + assertTrue(loyaltyScores.isEmpty(), "There should be no loyalty scores when the file does not exist."); + } + + @Test + public void testEmptyFile() throws IOException { + // Arrange: Manually create an empty JSON file + File testFile = new File(testFilePath); + testFile.createNewFile(); + + // Act: Try to get loyalty data from an empty file + Map loyaltyScores = dataAccess.getLoyaltyDate(userId, DATE); + + // Assert: The result should be an empty map since the file is empty + assertTrue(loyaltyScores.isEmpty(), "There should be no loyalty scores in the empty file."); + } +} diff --git a/src/test/java/use_case/loyaltyscore/LoyaltyScoreInteractorTest.java b/src/test/java/use_case/loyaltyscore/LoyaltyScoreInteractorTest.java new file mode 100644 index 00000000..7fd90689 --- /dev/null +++ b/src/test/java/use_case/loyaltyscore/LoyaltyScoreInteractorTest.java @@ -0,0 +1,81 @@ +package use_case.loyaltyscore; + +import data_access.LoyaltyScoreDataAccessObject; +import data_access.SpotifyDataAccessObject; +import entity.ArtistLoyaltyScore; +import entity.SpotifyUser; + +import org.junit.jupiter.api.Test; +import use_case.loyalty_score.*; + + +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.*; + +public class LoyaltyScoreInteractorTest { + + @Test + void successTest() { + + String DATE_1 = "2025-04-12"; + String DATE_2 = "2025-04-13"; + int SCORE_DATE_1 = 90; + int SCORE_DATE_2 = 590; + String ARTIST_1 = "Artist1"; + SpotifyUser SPOTIFY_USER = new SpotifyUser("kellyh", "a", + "t", "kh1203"); + String USERID = SPOTIFY_USER.getSpotifyUserId(); + LoyaltyScoreInputData INPUT_DATA = new LoyaltyScoreInputData(SPOTIFY_USER, ARTIST_1); + + + + // Mock the SpotifyDataAccessObject + SpotifyDataAccessObject mockSpotifyDAO = mock(SpotifyDataAccessObject.class); + + ArtistLoyaltyScore als1 = new ArtistLoyaltyScore("Artist1", "id1", + 2, 10, true); + + int ALS_current = als1.getLoyaltyScore(); + + ArtistLoyaltyScore als2 = new ArtistLoyaltyScore("Artist2", "id2", + 3, 20, true); + + + // Mock the return value of getArtistLoyaltyScores + List mockLoyaltyScores = List.of(als1, als2); + + when(mockSpotifyDAO.getArtistLoyaltyScores(SPOTIFY_USER)).thenReturn(mockLoyaltyScores); + + // Mock the LoyaltyScoreDataAccessObject (this is for saving scores) + LoyaltyScoreDataAccessObject dataAccessObject = new LoyaltyScoreDataAccessObject(); + + + + // This creates a successPresenter that tests whether the test case is as we expect. + LoyaltyScoreOutputBoundary successPresenter = new LoyaltyScoreOutputBoundary() { + @Override + public void prepareView(LoyaltyScoreOutputData outputData) { + assertEquals(SCORE_DATE_1, outputData.getScores().get(DATE_1)); + assertEquals(SCORE_DATE_2, outputData.getScores().get(DATE_2)); + assertEquals(3, outputData.getScores().size()); + assertEquals(ALS_current, outputData.getScores().get(LocalDate.now().toString())); + } + }; + + // Create the interactor with the mock Spotify DAO and the mock data access object + LoyaltyScoreInputBoundary interactor = new LoyaltyScoreInteractor(dataAccessObject, successPresenter, mockSpotifyDAO); + + dataAccessObject.saveLoyalty(USERID, DATE_1, ARTIST_1, SCORE_DATE_1); + dataAccessObject.saveLoyalty(USERID, DATE_2, ARTIST_1, SCORE_DATE_2); + + // Execute the interactor (this will trigger the mock SpotifyDAO) + interactor.execute(INPUT_DATA); + + // Verify interactions with the mock objects + verify(mockSpotifyDAO).getArtistLoyaltyScores(SPOTIFY_USER); + } +} diff --git a/src/test/java/use_case/loyaltyscore/LoyaltyScoreViewTest.java b/src/test/java/use_case/loyaltyscore/LoyaltyScoreViewTest.java new file mode 100644 index 00000000..57b87b35 --- /dev/null +++ b/src/test/java/use_case/loyaltyscore/LoyaltyScoreViewTest.java @@ -0,0 +1,36 @@ +package use_case.loyaltyscore; + +import interface_adapter.ViewManagerModel; +import interface_adapter.loyalty_score.LoyaltyState; +import view.LoyaltyScoreView; + +import javax.swing.*; +import java.util.Map; + +public class LoyaltyScoreViewTest { + public static void main(String[] args) { + + // Create the LoyaltyState and ViewManagerModel + LoyaltyState loyaltyState = new LoyaltyState(); + + loyaltyState.setCurrentArtist("Phoebe Bridgers"); // Example artist + loyaltyState.setLoyaltyScores(Map.of( + "2025-04-12", 90, + "2025-04-13", 140, + "2025-04-14", 85, + "2023-08-09", 12 + )); + + ViewManagerModel viewManagerModel = new ViewManagerModel(); + + LoyaltyScoreView loyaltyScoreView = new LoyaltyScoreView(loyaltyState, viewManagerModel); + + // Add to the frame + JFrame frame = new JFrame("Loyalty Scores"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.add(loyaltyScoreView); + frame.setSize(800, 600); + frame.setVisible(true); + + } +} diff --git a/src/test/java/use_case/loyaltyscore/loyaltyscore_520.json b/src/test/java/use_case/loyaltyscore/loyaltyscore_520.json new file mode 100644 index 00000000..38947605 --- /dev/null +++ b/src/test/java/use_case/loyaltyscore/loyaltyscore_520.json @@ -0,0 +1,24 @@ +{ + "loyalty_scores": [ + { + "date": "2023-11-23", + "artist_name": "Artist1", + "score": 85 + }, + { + "date": "2023-11-23", + "artist_name": "Artist2", + "score": 90 + }, + { + "date": "2023-11-22", + "artist_name": "Artist3", + "score": 75 + }, + { + "date": "2023-11-21", + "artist_name": "Artist1", + "score": 88 + } + ] +} diff --git a/src/test/java/use_case/signup/SignupInteractorTest.java b/src/test/java/use_case/signup/SignupInteractorTest.java deleted file mode 100644 index adf75c28..00000000 --- a/src/test/java/use_case/signup/SignupInteractorTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package use_case.signup; - -import data_access.InMemoryUserDataAccessObject; -import entity.UserFactory; -import entity.User; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class SignupInteractorTest { - - @Test - void successTest() { - SignupInputData inputData = new SignupInputData("Paul", "password", "password"); - SignupUserDataAccessInterface userRepository = new InMemoryUserDataAccessObject(); - - // This creates a successPresenter that tests whether the test case is as we expect. - SignupOutputBoundary successPresenter = new SignupOutputBoundary() { - @Override - public void prepareSuccessView(SignupOutputData user) { - // 2 things to check: the output data is correct, and the user has been created in the DAO. - assertEquals("Paul", user.getUsername()); - assertTrue(userRepository.existsByName("Paul")); - } - - @Override - public void prepareFailView(String error) { - fail("Use case failure is unexpected."); - } - - @Override - public void switchToLoginView() { - // This is expected - } - }; - - SignupInputBoundary interactor = new SignupInteractor(userRepository, successPresenter, new UserFactory()); - interactor.execute(inputData); - } - - @Test - void failurePasswordMismatchTest() { - SignupInputData inputData = new SignupInputData("Paul", "password", "wrong"); - SignupUserDataAccessInterface userRepository = new InMemoryUserDataAccessObject(); - - // This creates a presenter that tests whether the test case is as we expect. - SignupOutputBoundary failurePresenter = new SignupOutputBoundary() { - @Override - public void prepareSuccessView(SignupOutputData user) { - // this should never be reached since the test case should fail - fail("Use case success is unexpected."); - } - - @Override - public void prepareFailView(String error) { - assertEquals("Passwords don't match.", error); - } - - @Override - public void switchToLoginView() { - // This is expected - } - }; - - SignupInputBoundary interactor = new SignupInteractor(userRepository, failurePresenter, new UserFactory()); - interactor.execute(inputData); - } - - @Test - void failureUserExistsTest() { - SignupInputData inputData = new SignupInputData("Paul", "password", "wrong"); - SignupUserDataAccessInterface userRepository = new InMemoryUserDataAccessObject(); - - // Add Paul to the repo so that when we check later they already exist - UserFactory factory = new UserFactory(); - User user = factory.create("Paul", "pwd"); - userRepository.save(user); - - // This creates a presenter that tests whether the test case is as we expect. - SignupOutputBoundary failurePresenter = new SignupOutputBoundary() { - @Override - public void prepareSuccessView(SignupOutputData user) { - // this should never be reached since the test case should fail - fail("Use case success is unexpected."); - } - - @Override - public void prepareFailView(String error) { - assertEquals("User already exists.", error); - } - - @Override - public void switchToLoginView() { - // This is expected - } - }; - - SignupInputBoundary interactor = new SignupInteractor(userRepository, failurePresenter, new UserFactory()); - interactor.execute(inputData); - } -} \ No newline at end of file