diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 1ea972e389..5a7f64c3fa 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -22,11 +22,12 @@ jobs: run: git checkout --progress --force ${{ github.sha }} - name: Validate Gradle Wrapper - uses: gradle/wrapper-validation-action@v1 + uses: gradle/actions/wrapper-validation@v3 - name: Setup JDK 17 - uses: actions/setup-java@v1 + uses: actions/setup-java@v4 with: + distribution: 'zulu' java-version: '17' java-package: jdk+fx @@ -47,4 +48,4 @@ jobs: if: always() && runner.os == 'Windows' working-directory: ${{ github.workspace }}/text-ui-test shell: cmd - run: runtest.bat \ No newline at end of file + run: runtest.bat diff --git a/.gitignore b/.gitignore index 2873e189e1..14b25e5b3c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ /out/ /*.iml +*.class + # Gradle build files /.gradle/ /build/ @@ -15,3 +17,7 @@ bin/ /text-ui-test/ACTUAL.TXT text-ui-test/EXPECTED-UNIX.TXT +bookkeeper.log +bookkeeper.log.lck +/data/bookKeeper_bookList.txt +/data/bookKeeper_loanList.txt diff --git a/README.md b/README.md index f3d0bded12..86f549f545 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,14 @@ -# Duke project template +# BookKeeper project template -This is a project template for a greenfield Java project. It's named after the Java mascot _Duke_. Given below are instructions on how to use it. +BookKeeper is a Command Line Interface (CLI) library manager application for effective tracking of library loans and inventory. It allows users to manage books, loans, and notes efficiently through a set of commands. ## Setting up in Intellij Prerequisites: JDK 17 (use the exact version), update Intellij to the most recent version. 1. **Ensure Intellij JDK 17 is defined as an SDK**, as described [here](https://www.jetbrains.com/help/idea/sdk.html#set-up-jdk) -- this step is not needed if you have used JDK 17 in a previous Intellij project. -1. **Import the project _as a Gradle project_**, as described [here](https://se-education.org/guides/tutorials/intellijImportGradleProject.html). -1. **Verify the setup**: After the importing is complete, locate the `src/main/java/seedu/duke/Duke.java` file, right-click it, and choose `Run Duke.main()`. If the setup is correct, you should see something like the below: - ``` - > Task :compileJava - > Task :processResources NO-SOURCE - > Task :classes - - > Task :Duke.main() - Hello from - ____ _ - | _ \ _ _| | _____ - | | | | | | | |/ / _ \ - | |_| | |_| | < __/ - |____/ \__,_|_|\_\___| - - What is your name? - ``` - Type some word and press enter to let the execution proceed to the end. +2. **Import the project _as a Gradle project_**, as described [here](https://se-education.org/guides/tutorials/intellijImportGradleProject.html). +3. **Verify the setup**: After the importing is complete, locate the `src/main/java/BookKeeper.java` file, right-click it, and choose `Run BookKeeper.main()`. **Warning:** Keep the `src\main\java` folder as the root folder for Java files (i.e., don't rename those folders or move Java files to another folder outside of this folder path), as this is the default location some tools (e.g., Gradle) expect to find Java files. @@ -41,7 +25,7 @@ Prerequisites: JDK 17 (use the exact version), update Intellij to the most recen ### JUnit tests -* A skeleton JUnit test (`src/test/java/seedu/duke/DukeTest.java`) is provided with this project template. +* A skeleton JUnit test (`src/test/java/DukeTest.java`) is provided with this project template. * If you are new to JUnit, refer to the [JUnit Tutorial at se-education.org/guides](https://se-education.org/guides/tutorials/junit.html). ## Checkstyle diff --git a/build.gradle b/build.gradle index ea82051fab..41f7b87879 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,11 @@ test { } application { - mainClass.set("seedu.duke.Duke") + mainClass.set("bookkeeper.BookKeeper") } shadowJar { - archiveBaseName.set("duke") + archiveBaseName.set("BookKeeper") archiveClassifier.set("") } @@ -42,5 +42,6 @@ checkstyle { } run{ + enableAssertions = true standardInput = System.in } diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 0f072953ea..b9262d2050 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -1,9 +1,9 @@ # About us -Display | Name | Github Profile | Portfolio ---------|:----:|:--------------:|:---------: -![](https://via.placeholder.com/100.png?text=Photo) | John Doe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Joe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Ron John | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | John Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) -![](https://via.placeholder.com/100.png?text=Photo) | Don Roe | [Github](https://github.com/) | [Portfolio](docs/team/johndoe.md) +| Display | Name | Github Profile | Portfolio | +|-----------------------------------------------------|:--------------:| :------------------------------------: |:-----------------------------------:| +| ![](images/Portrait_placeholder.png) | Dylan Khoo | [Github](https://github.com/dylankhoo) | [Portfolio](team/dylankhoo.md) | +| ![](images/Portrait_placeholder.png) | Chan Sheng Bin | [Github](https://github.com/ShengBin-101) | [Portfolio](team/shengbin-101.md) | + ![](images/Portrait_placeholder.png) | Phua Lock Hian | [Github](https://github.com/phua-lock-hian) | [Portfolio](team/phua-lock-hian.md) | + ![](images/Portrait_placeholder.png) | Kam Sheng Jie | [Github](https://github.com/ShengJie13245) | [Portfolio](team/shengjie13245.md) + diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 64e1f0ed2b..d9e14ede6c 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -2,37 +2,808 @@ ## Acknowledgements -{list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +BookKeeper uses the following tools for documentation, development and testing: -## Design & implementation +1. [JUnit](https://junit.org/junit5/) - Used for software testing. +2. [Gradle](https://gradle.org) - Used for build automation. +3. [PlantUML](https://plantuml.com/) - Used for diagram creation. -{Describe the design and implementation of the product. Use UML diagrams and short code snippets where applicable.} +## Setting Up the Development Environment +### Prerequisites + +- JDK 17 +- Gradle 7.6.2 or higher +- IntelliJ IDEA (recommended) + +### Getting Started + +1. Clone the repository: + + ``` + git clone https://github.com/AY2425S2-CS2113-T12-2/tp.git + ``` + +2. Import the project as a Gradle project in IntelliJ IDEA: + + - Open IntelliJ IDEA + - Select "Import Project" + - Navigate to the project directory and select the `build.gradle` file + - Follow the prompts to complete the import + +3. Verify the setup: + - Run the tests: `./gradlew test` + - Run the application: `./gradlew run` + +## Design + +### Component Overview + +The architecture of BookKeeper follows a layered design, with each component responsible for a specific aspect of the application. Below is an overview of the key components: + +#### 1. Main Component (`BookKeeper.java`) + +- Serves as the entry point of the application. +- Manages the main execution flow. + +#### 2. UI Component (`Formatter.java`) + +- Displays messages, prompts, and results to the user. +- Formats and prints lists, error messages, and success messages. + +#### 3. Logic Component + +- **InputHandler (`InputHandler.java`)**: Processes user input and executes the corresponding commands. +- **InputParser (`InputParser.java`)**: Parses user input into command arguments and validates them. + +#### 4. Model Component + +- **Book and Loan Classes**: Define the core data structures for books and loans. +- **BookList and LoanList**: Manage collections of books and loans, providing methods for adding, removing, and searching. +- **Category and Condition**: Enumeration classes to classify books + +#### 5. Storage Component (`Storage.java`) + +- Handles persistence of data. +- Manages saving and loading of book and loan data. +- Ensures data integrity during file I/O operations. + +### Class Structure + +The following diagram shows the high-level structure of the application: + +``` +bookkeeper/ +├── BookKeeper.java # Main class +├── logic/ +│ ├── InputHandler.java # Processes user input +│ └── InputParser.java # Parses and validates input +├── exceptions/ +│ ├── BookNotFoundException.java +│ ├── ErrorMessages.java +│ └── IncorrectFormatException.java +├── model/ +│ ├── Book.java # Represents a book +│ ├── Loan.java # Represents a loan +| ├── Condition.java # Enum for condition of books +| └── Category.java # Enum for category of books +├── list/ +│ ├── BookList.java # Manages the collection of books +│ └── LoanList.java # Manages the collection of loans +├── storage/ +│ └── Storage.java # Handles data persistence +└── ui/ + └── Formatter.java # Formats and displays output +``` + +The following is the class diagram of the application: + +![](images/classDiagram.png) + +## Implementation + +The following sections provide detailed sequence diagrams for key features of the application, such as adding books, adding loans, and removing books. + +### Adding Books + +The `add-book` feature allows the user to add a new book to the inventory. Each book includes details of its title, author, category, condition, location, as well as optional notes. The system ensures that a book of the same title does not exist in the inventory and before creating the book. + +`InputHandler` coordinates with `InputParser`, `BookList`, `Book`, `Formatter`, and `Storage` classes to implement the feature. + +The following UML sequence diagram shows how the `add-book BOOK_TITLE a/AUTHOR cat/CATEGORY cond/CONDITION loc/LOCATION [note/NOTE]` command is handled. + +![addBook.png](images/addBook.png) + +1. User issues command: + + The user inputs the command in the CLI with the required arguments, e.g., `add-book The Great Gatsby a/F. Scott Fitzgerald cat/Fiction cond/Good loc/Shelf B1 note/important`. + +2. Command arguments are extracted: + + `InputHandler` first calls `InputParser.extractCommandArgs(...)` to split the user input into command arguments. + + - For example, the input `add-book The Great Gatsby a/F. Scott Fitzgerald cat/Fiction cond/Good loc/Shelf B1 note/important` is split into: + - `commandArgs[0]`: `"add-book"` + - `commandArgs[1]`: `"The Great Gatsby a/F. Scott Fitzgerald cat/Fiction cond/Good loc/Shelf B1 note/important"` + +3. Book arguments are parsed: + + `InputHandler` invokes `InputParser.extractAddBookArgs(...)` to parse the second part of the command (`commandArgs[1]`) into the following components: + + - Book title + - Author + - Category + - Condition + - Location + - Note (Optional) + +4. Book is validated: + + BookKeeper does not support multiple books of the same title. `InputHandler` calls `BookList.searchBook(bookTitle)` to check if the book exists in the inventory. + + - If the book is found, `InputHandler` uses `Formatter` to print a "Book already exists in inventory: {Title}" message and exits early. + - If the book is not found, the flow continues. + +5. Book is created: + + If the book does not exist in the inventory, + + - A new `Book` object is created using the parsed arguments. The `note` field is optional and defaults to an empty string if not provided. + - The `category` string is converted to a `Category` enum using `Category.fromString(...)`. + - The `condition` string is converted to a `Condition` enum using `Condition.fromString(...)`. + - The book is added to the `BookList` using `bookList.addBook(...)`. + +6. Changes are saved to persistent storage: + + `InputHandler` calls `Storage.saveLoans(...)` and `Storage.saveInventory(...)` to save the updated book list and inventory. + +7. Success message is displayed: + + `InputHandler` uses `Formatter` to print a message indicating that the book was successfully added. + +### Adding Loans + +The `add-loan` feature allows the user to add a loan for a book in the inventory. The loan includes details such as the borrower's name, return date, phone number, and email. The system ensures that the book exists in the inventory and is not already on loan before creating the loan. + +`InputHandler` coordinates with `InputParser`, `BookList`, `LoanList`, `Loan`, `Formatter`, and `Storage` classes to implement the feature. + +The following UML sequence diagram shows how the `add-loan BOOK_TITLE n/BORROWER_NAME d/RETURN_DATE p/PHONE_NUMBER e/EMAIL` command is handled. + +- The RETURN_DATE must be in the format **DD-MM-YYYY** when provided as input. +- The RETURN_DATE cannot be in the past. +- The PHONE_NUMBER accepts Singapore numbers only (8 numbers, starting with 9, 8 or 6). + +![addLoan.png](images/addLoan.png) + +1. User issues command: + + The user inputs the command in the CLI with the required arguments, e.g., `add-loan The Great Gatsby n/John Doe d/28-06-2025 p/98765432 e/john.doe@example.com`. + +2. Command arguments are extracted: + + `InputHandler` first calls `InputParser.extractCommandArgs(...)` to split the user input into command arguments. + + - For example, the input `add-loan The Great Gatsby n/John Doe d/28-06-2025 p/98765432 e/john.doe@example.com` is split into: + - `commandArgs[0]`: `"add-loan"` + - `commandArgs[1]`: `"The Great Gatsby n/John Doe d/28-06-2025 p/98765432 e/john.doe@example.com"` + +3. Loan arguments are parsed: + + `InputHandler` invokes `InputParser.extractAddLoanArgs(...)` to parse the second part of the command (`commandArgs[1]`) into the following components: + + - Book title + - Borrower's name + - Return date + - Phone number + - Email + +4. Book is validated: + + `InputHandler` calls `BookList.searchBook(bookTitle)` to check if the book exists in the inventory. + + - If the book is not found, `InputHandler` uses `Formatter` to print a "Book not found in inventory" message and exits early. + - If the book is found, the flow continues. + +5. Loan status is validated: + + `InputHandler` checks if the book is already on loan using `Book.getOnLoan()`. + + - If the book is already on loan, `InputHandler` uses `Formatter` to print a "The book is currently out on loan" message and exits early. + - If the book is not on loan, the flow continues. + +6. Loan is created: + + If the book exists and is not already on loan: + + - A new `Loan` object is created using the parsed arguments, including the borrower's name, return date, phone number, and email. + - The loan is added to the `LoanList` using `LoanList.addLoan(...)`. This method automatically updates the book's `onLoan` status to `true`. + +7. Changes are saved to persistent storage: + + `InputHandler` calls `Storage.saveLoans(...)` and `Storage.saveInventory(...)` to save the updated loan list and inventory. + +8. Success message is displayed: + + `InputHandler` uses `Formatter` to print a message indicating that the loan was successfully added. + +### Removing Books + +The `remove-book` feature allows the user to remove a book from the inventory using the book title as the identifier. +The system will first check if the book exists, remove all associated loans (if any) before finally removing the book +from the inventory. This prevents orphaned loan records from remaining in the system. + +`InputHandler` coordinates with `BookList`, `LoanList`, `InputParser`, `Formatter` and `Storage` classes to implement the feature. + +The following UML sequence diagram shows how the `remove-book BOOK_TITLE` command is handled. + +![removeBook.png](images/removeBook.png) + +1. User issues command: + + The user inputs the command in the CLI with the book title as an argument, e.g. `remove-book The Hobbit` + +2. `InputHandler` first extracts command arguments by invoking `extractCommandArgs(...)`. + + Then, `removeBook(commandArgs)` is invoked to handle the command. + +3. `BookList` is queried for the book: + + `InputHandler` calls `BookList.searchBook(bookTitle)` to search for the book. + + - If the book is not found `(toRemove == null)`, `InputHandler` uses `Formatter` to print a "Book not found" message + and exits early. + - If the book is found, the flow continues. + +4. Associated loans are removed: + + `LoanList.removeLoansByBook(toRemove)` is called to remove all loans associated with the book. This method uses `loanList.removeIf(...)` to filter and remove loans linked to the specified book. + +5. Book is removed from the system: + + `InputHandler` calls `BookList.removeBook(toRemove)` to remove the book. + +6. Changes are saved to persistent storage: + + `InputHandler` calls `Storage.saveInventory(...)` to save the updated inventory. + +7. Success message is displayed: + + `Formatter` is used to print a message indicating successful removal. + +### Delete Loans + +The `delete-loan` feature allows the user to remove a loan from the loan list. The book title is used as the identifier. +The program will check if first the book exists, then it will use the book object and the borrower name to search if the loan exist before proceeding to remove it. + +`InputHandler` coordinates with `InputParser`, `LoanList`, `BookList`, `Book`, `Formatter` and `Storage` classes to implement the feature. + +The following UML sequence diagram shows the behaviour of `delete-loans BOOK_TITLE`: + +![delete_loan.png](images/deleteLoan.png) + +1. User issues command: + + The user inputs the command with book title as argument e.g `delete-loan The Hobbit`. + +2. `InputHandler` extract command arguments with `extractCommandArgs(...)` followed by deleteLoan(commandArgs). + +3. `InputHandler` calls `BookList.searchBook(bookTitle)` to search for the book. + `InputHandler` calls `LoanList.findLoan(book)` to search for the loan. + + - If the book is not found `(loanedBook == null)`, not on loan, or there is no existing loan, `InputHandler` uses `Formatter` to print an error message and stops the command early. + - If the book is found, the flow continues. + +4. Delete corresponding loan: + + `InputHandler` calls `LoanList.deleteLoan(loan)` to delete the loan. + +5. Sets book to not on loan: + + `InputHandler` calls loanedBook.setOnLoan(false). + +6. Success message is displayed: + + `Formatter` is used to print a message indicating successful removal. + +### Viewing Books + +The `view-inventory` feature allows the user to view all existing books in the inventory. All book information will be printed out in a numbered list. The program will first check if the inventory is empty before proceeding to print out the list. + +`InputHandler` coordinates with `InputParser`, `Book`, `BookList` and `Formatter` classes to implement the feature. + +The following UML sequence diagram shows how the `view-inventory` command is handled. + +![viewInventory.png](images/viewInventory.png) + +1. User issues command: + + The user inputs the command in the CLI with the required arguments, e.g., `view-inventory`. + +2. Command arguments are extracted: + + `InputHandler` first calls `InputParser.extractCommandArgs(...)` to extract the command to execute from the user input. + + - For example, the input `view-inventory` is split into: + - `commandArgs[0]`: `"view-inventory"` + +3. Command is executed: + + `InputHandler` invokes `BookList.viewBookList()` to list the existing books. + +4. Check for empty inventory: + + `BookList` calls `BookList.isEmpty()` to check if there are existing books in the inventory. + + - If no book is found, `BookList` uses `Formatter` to print a "Book List Empty!" message and exits early. + - If there are books found, the flow continues. + +5. Inventory is printed: + + `BookList` calls `Formatter.printBookList(bookList)` to format and print the books in the inventory. `Formatter` will then start by printing "Here are the books in your inventory:" to indicate the start of the printing. For each book, `Formatter` will then: + + 1. Increment a `count` to number the books + 2. Invoke `Book.toString()` to convert all the book's information to a string + 3. Print the concatenated `count` and book information. + +### Viewing Loans + +The `view-loans` feature allows the user to view all existing loans in the inventory. All loan information will be printed out in a numbered list. The program will first check if the loan list is empty before proceeding to print out the list. + +`InputHandler` coordinates with `InputParser`, `Loan`, `LoanList` and `Formatter` classes to implement the feature. + +The following UML sequence diagram shows how the `view-loans` command is handled. + +![viewLoans.png](images/viewLoans.png) + +1. User issues command: + + The user inputs the command in the CLI with the required arguments, e.g., `view-loans`. + +2. Command arguments are extracted: + + `InputHandler` first calls `InputParser.extractCommandArgs(...)` to extract the command to execute from the user input. + + - For example, the input `view-loans` is split into: + - `commandArgs[0]`: `"view-loans"` + +3. Command is executed: + + `InputHandler` invokes `LoanList.viewLoanList()` to list the existing loans. + +4. Check for empty loan list: + + `LoanList` calls `LoanList.isEmpty()` to check if there are existing loans in the loan list. + + - If no loans are found, `LoanList` uses `Formatter` to print a "Loan List Empty!" message and exits early. + - If there are any loans found, the flow continues. + +5. Loan list is printed: + + `LoanList` calls `Formatter.printLoanList(loanList)` to format and print the loans. `Formatter` will then start by printing "Here are the active loans:" to indicate the start of the printing. For each loan, `Formatter` will then: + + 1. Increment a `count` to number the loans + 2. Invoke `Loan.toString()` to convert all the loan's information to a string + 3. Print the concatenated `count` and loan information. + +### Updating Books + +The `update-book` feature allows the user to add update existing book details. The system ensures that a book of the same title exists in the inventory and before performing the update. Note that the book title is updated separately in the `update-title` command. + +`InputHandler` coordinates with `InputParser`, `BookList`, `Book`, `Formatter`, and `Storage` classes to implement the feature. + +The following UML sequence diagram shows how the `update-book BOOK_TITLE [a/AUTHOR] [cat/CATEGORY] [cond/CONDITION] [loc/LOCATION] [note/NOTE]` command is handled. + +![updateBook.png](images/updateBook.png) + +1. User issues command: + + The user inputs the command in the CLI with the required arguments, e.g., `update-book The Great Gatsby a/F. Scott Fitzgerald cat/Fiction cond/POOR loc/Shelf B3 note/Replace ASAP`. + +2. Command arguments are extracted: + + `InputHandler` first calls `InputParser.extractCommandArgs(...)` to split the user input into command arguments. + + - For example, the input `update-book The Great Gatsby a/F. Scott Fitzgerald cat/Fiction cond/POOR loc/Shelf B3 note/Replace ASAP` is split into: + - `commandArgs[0]`: `"update-book"` + - `commandArgs[1]`: `"The Great Gatsby a/F. Scott Fitzgerald cat/Fiction cond/POOR loc/Shelf B3 note/Replace ASAP"` + +3. Book arguments are parsed: + + All arguments are optional except Book Title, but at least one field is expected for updating. + `InputHandler` invokes `InputParser.extractUpdateBookArgs(...)` to parse the second part of the command (`commandArgs[1]`) into the following components: + + - Book title + - Author + - Category + - Condition + - Location + - Note + +4. Book is validated: + + `InputHandler` calls `BookList.searchBook(bookTitle)` to check if the book exists in the inventory. + + - If the book is not found, `InputHandler` uses `Formatter` to print a exception message and exits early. + - If the book is found, the flow continues. + +5. Book is updated: + + `InputHandler` updates the book details by invoking the `setBookField` method from `Book` class. + +6. Changes are saved to persistent storage: + + `InputHandler` calls `Storage.saveLoans(...)` and `Storage.saveInventory(...)` to save the updated book details. + +7. Success message is displayed: + + `InputHandler` uses `Formatter` to print a message indicating that the book was successfully updated. + +### Updating Loans + +The `edit-loan` feature allows the user to add update existing loan details. The system ensures that a book of the same title and a corresponding loan exists before performing the update. + +`InputHandler` coordinates with `InputParser`, `BookList`, `LoanList`, `Loan`, `Formatter`, and `Storage` classes to implement the feature. + +The following UML sequence diagram shows how the `edit-loan BOOK_TITLE [n/BORROWER_NAME] [d/RETURN_DATE] [p/PHONE_NUMBER] [e/EMAIL]` command is handled. + +- The RETURN_DATE must be in the format **DD-MM-YYYY** when provided as input. +- The RETURN_DATE cannot be in the past. +- The PHONE_NUMBER accepts Singapore numbers only (8 numbers, starting with 9, 8 or 6). + +![editLoan.png](images/editLoan.png) + +1. User issues command: + + The user inputs the command in the CLI with the required arguments, e.g., + `edit-loan Great Gatsby n/Mary d/29-06-2025 p/91234567 e/123abc@gmail.com`. + +2. Command arguments are extracted: + + `InputHandler` first calls `InputParser.extractCommandArgs(...)` to split the user input into command arguments. + + - For example, the input `edit-loan Great Gatsby n/Mary d/29-06-2025 p/91234567 e/123abc@gmail.com` is split into: + - `commandArgs[0]`: `"edit-loan"` + - `commandArgs[1]`: `"Great Gatsby n/Mary d/29-06-2025 p/91234567 e/123abc@gmail.com"` + +3. Loan arguments are parsed: + + `InputHandler` invokes `InputParser.extractEditLoanArgs(...)` to parse the second part of the command (`commandArgs[1]`) into the following components: + + - Book title + - Borrower Name + - Return Date + - Phone Number + - Email + +4. Loan is validated: + + `InputHandler` calls `LoanList.findLoan(book)`and `BookList.searchBook(bookTitle)` to check if the book and loan exists correspondingly. + + - If the book is not found, `InputHandler` uses `Formatter` to print a exception message and exits early. + - If the loan is not found, `InputHandler` uses `Formatter` to print a message and exits early. + - If the book and loan is found, the flow continues. + +5. Loan is updated: + + `InputHandler` updates the loan details if needed by invoking the `setLoanFields` methods from `Loan` class: + +6. Changes are saved to persistent storage: + + `InputHandler` calls `Storage.saveLoans(...)` and `Storage.saveInventory(...)` to save the updated loan details. + +7. Success message is displayed: + + `InputHandler` uses `Formatter` to print a message indicating that the loan was successfully updated. + +### Updating Titles + +The `update-title` feature allows the user to update existing book titles. The system ensures that a book of the current title exists in the inventory and before performing the update. + +`InputHandler` coordinates with `InputParser`, `BookList`, `Book`, `Formatter`, and `Storage` classes to implement the feature. + +The following UML sequence diagram shows how the `update-book BOOK_TITLE new/NEW_TITLE` command is handled. + +![updateTitle.png](images/updateTitle.png) + +1. User issues command: + + The user inputs the command in the CLI with the required arguments, e.g., `update-title BOOK_TITLE new/NEW_TITLE`. + +2. Command arguments are extracted: + + `InputHandler` first calls `InputParser.extractCommandArgs(...)` to split the user input into command arguments. + + - For example, the input `update-title Great Gatsby new/The Great Gatsby` is split into: + - `commandArgs[0]`: `"update-title"` + - `commandArgs[1]`: `"Great Gatsby new/The Great Gatsby"` + +3. Title arguments are parsed: + + `InputHandler` invokes `InputParser.extractUpdateTitleArgs(...)` to parse the second part of the command (`commandArgs[1]`) into the following components: + + - old Title + - new Title + +4. Title is validated: + + `InputHandler` checks if `oldTitle` is the same as `newTitle`. `InputHandler` also calls `BookList.searchBook(newTitle)` to check if there is an existing book with the same title as `newTitle`. + + - If the a `oldTitle` and `newTitle` are the same, `InputHandler` uses `Formatter` to print a exception message and exits early. + - If the a book with the same title as `newTitle` is found, `InputHandler` uses `Formatter` to print a exception message and exits early. + - If no book is found, the flow continues. + +5. Book is validated: + + `InputHandler` calls `BookList.searchBook(oldTitle)` to check if the book exists in the inventory. + + - If the book is not found, `InputHandler` uses `Formatter` to print a exception message and exits early. + - If the book is found, the flow continues. + +6. Title is updated: + + `InputHandler` updates the book details by invoking the following methods from `Book` class: + + - `Book.setTitle(newTitle)` + +7. Changes are saved to persistent storage: + + `InputHandler` calls `Storage.saveLoans(...)` and `Storage.saveInventory(...)` to save the updated book title. + +8. Success message is displayed: + + `InputHandler` uses `Formatter` to print a message indicating that the book was successfully updated. + +### Delete Note + +The `delete-note` feature allows the user to delete a note that is attached to a book in the inventory. The system ensures that the book has a note before it can be updated. + +`InputHandler` coordinates with `InputParser`, `BookList`, `Book`, `Formatter`, and `Storage` classes to implement the feature. + +The following UML sequence diagram shows how the `delete-note BOOK_TITLE` command is handled. + +![deleteNote.png](images/deleteNote.png) + +1. User issues command: + + The user inputs the command in the CLI with the required arguments, e.g., `delete-note The Great Gatsby`. + +2. Command arguments are extracted: + + `InputHandler` first calls `InputParser.extractCommandArgs(...)` to split the user input into command arguments. + + - For example, the input `delete-note The Great Gatsby` is split into: + - `commandArgs[0]`: `delete-note` + - `commandArgs[1]`: `The Great Gatsby` + +3. Book is validated: + + `InputHandler` calls `BookList.searchBook(bookTitle)` to check if the book exists in the inventory. + + - If the book is not found, `InputHandler` uses `Formatter` to print a exception message and exits early. + - If the book is found, the flow continues. + +4. Note is validated: + + - If the book does not have a note attached, `InputHandler` uses `Formatter` to print a "No note exists for the book: Book_Title" message and exits early. + - If the book is found with a note, the flow continues. + +5. Note deleted: + + - The note is deleted from the book + +6. Changes are saved to persistent storage: + + `InputHandler` calls `Storage.saveLoans(...)` and `Storage.saveInventory(...)` to save the updated book list and inventory. + +7. Success message is displayed: + + `InputHandler` uses `Formatter` to print a message indicating that the note was successfully deleted + +### Search Title + +The `search-title` feature allows the user to search for books in the inventory by providing a keyword. The system returns a list of books whose titles contain the specified keyword. + +`InputHandler` coordinates with `InputParser`, `BookList`, and `Formatter` classes to implement the feature. + +![searchTitle.png](images/searchTitle.png) + +1. User issues command: + The user inputs the command in the CLI with the required keyword, e.g., `search-title Gatsby`. +2. `InputHandler` first calls `InputParser.extractCommandArgs(...)` to split the user input into command arguments. +3. `InputHandler` checks if the keyword is valid (non-empty). If invalid, an error message is displayed. +4. InputHandler calls `BookList.findBooksByKeyword(keyword)` to retrieve a list of books whose titles contain the keyword. +5. `InputHandler` uses `Formatter` to print the list of matching books. + +Design Considerations: +- Case Insensitivity: The search is case-insensitive to improve usability. + + +### Save Inventory + +The save inventory feature automatically saves the inventory each time the user makes a change. +If no existing persistent storage file is detected, it will be created in the default location `./data/bookKeeper_bookList.txt`. The file path can also be customized using the `setInventoryFilePath()` method. + +The method `saveInventory(bookList)` is invoked by `InputHandler` after any method call that makes changes to the current inventory. + +`InputHandler` coordinates with `File`, `FileWriter`, `BookList`, `Book`, `Formatter`, and `Storage` classes to implement the feature. + +The following UML sequence diagram shows the relevant behaviour: + +![saveInventory.png](images/saveInventory.png) + +1. Initiation: + +- `InputHandler` invokes `Storage.saveInventory(bookList)`. + +2. Directory Check: + +- A `File` object is created for the directory. If the directory does not exist, it is created using `mkdirs()`. + +3. FileWriter Creation: + +- A new `FileWriter` is created for the file at the path specified by `inventoryFilePath`. + +4. Retrieving Book List: + +- `getBookList()` is called on the `BookList` instance passed into `saveInventory(bookList)` to obtain the list of `Book` objects. + +5. Writing Each Book: + +- For each `Book` in the list, `toFileString()` is called to get a string representation. This string is then written to the file via `FileWriter`. + +6. Closing: + +- After writing all books, `FileWriter` is closed to complete the writing process. + +Error Handling: If an `IOException` occurs during any file operations, an error message is displayed via `Formatter.printBorderedMessage()`. + +### Load Inventory + +The load inventory feature loads the inventory from the existing persistent data storage file if it exists. If it does not exist, an empty inventory is used. The file path defaults to `./data/bookKeeper_bookList.txt` but can be customized using the `setInventoryFilePath()` method. + +The method `loadInventory()` is called once by `InputHandler` at the start of the program. + +`InputHandler` coordinates with `File`, `Scanner`, `BookList`, `Book`, `Formatter`, and `Storage` classes to implement the feature. + +The following UML sequence diagram shows the relevant behaviour: + +![loadInventory.png](images/loadInventory.png) + +1. Initialization: + +`InputHandler` invokes `Storage.loadInventory()`, which initializes an empty `ArrayList`. + +2. File Existence Check: + + A `File` object is created for the inventory file path. + + - If the file does not exist: + - A message is printed using `Formatter.printBorderedMessage()` indicating no saved inventory was found. + - A new file is created, and an empty `ArrayList` is returned. + +3. File Reading: + + If the file exists, + + - A `Scanner` reads the file line by line. + - Each line is passed to `parseBookFromString(line)` to convert it into a `Book` object. + +4. Book Validation: + + - If the `Book` is `null`, a message is printed indicating the entry was skipped. + - If valid, duplicates are checked using `bookList.stream().anyMatch(...)`. + - If a duplicate is found, a message is printed, and the book is skipped. + - Otherwise, the book is added to the `bookList`. + +5. Completion: + + - After processing all lines in file, the `Scanner` is closed. + - A message is printed indicating the number of books loaded. + - The populated `bookList` is returned. + + +### Save Loans + +The save loans feature ensures that all loans are saved to persistent storage whenever there is a change to the `LoanList`. If no existing persistent storage file is detected, it will be created in the default location `./data/bookKeeper_loanList.txt`. The file path can also be customized using the setLoanFilePath() method. + +The method `saveLoans(loanList)` is invoked by `InputHandler` after any method call that makes changes to the current loan list. + +The following UML sequence diagram shows the relevant behavior: + +![saveLoans.png](images/saveLoans.png) + +1. Initialization: `InputHandler` invokes `Storage.saveLoans(loanList)`. +2. Directory Check: A `File` object is created for the directory. If the directory does not exist, it is created using `mkdirs()`. +3. FileWriter Creation: A new `FileWriter` is created for the file at the path specified by `loanListFilePath`. +4. Retrieving Loan List: `getLoanList()` is called on the `LoanList` instance passed into `saveLoans(loanList)` to obtain the list of Loan objects. +5. Writing Each Loan: For each `Loan` in the list, `toFileString()` is called to get a string representation. This string is then written to the file via `FileWriter`. +6. Closing: After writing all loans, `FileWriter` is closed to complete the writing process. + +Error Handling: If an `IOException` occurs during any file operations, an error message is displayed via `Formatter.printBorderedMessage()`. + +### Load Loans + +The load loans feature loads the loan list from the existing persistent data storage file if it exists. If it does not exist, an empty loan list is used. The file path defaults to `./data/bookKeeper_loanList.txt` but can be customized using the `setLoanFilePath()` method. + +The method `loadLoans(bookList)` is called once by `InputHandler` at the start of the program. + +The following UML sequence diagram shows the relevant behavior: + +![loadLoans.png](images/loadLoans.png) + +1. Initialization: + +`InputHandler` invokes `Storage.loadLoans(bookList)`, which initializes an empty `ArrayList`. + +2. File Existence Check: A `File` object is created for the loan file path. +- If the file does not exist: +A message is printed using `Formatter.printBorderedMessage()` indicating no saved loans were found. A new file is created, and an empty `ArrayList` is returned. + +3. File Reading: If the file exists, a `Scanner` reads the file line by line. Each line is passed to `parseLoanFromString(line, bookList)` to convert it into a Loan object. + +4. Loan Validation: If the `Loan` is null, a message is printed indicating the entry was skipped. +- If valid, duplicates are checked using `loanList.stream().anyMatch(...)`. + - If a duplicate is found, a message is printed, and the loan is skipped. + - Otherwise, the loan is added to the loanList. + +5. Completion: After processing all lines in the file, the `Scanner` is closed. A message is printed indicating the number of loans loaded. The populated `loanList` is returned. + +## Appendix A: Product scope -## Product scope ### Target user profile -{Describe the target user profile} +#### Primary Users: + +Small-scale library managers, community librarians, school library staff, or volunteers who need a lightweight, +no-frills way to track books and loans. + +#### User Background: + +- Has basic computer literacy +- Comfortable using the command line +- Prefers typing and desktop apps ### Value proposition -{Describe the value proposition: what problem does it solve?} +BookKeeper is a lightweight, command-line library manager designed for simplicity and speed. +It empowers small libraries to efficiently track book inventories and manage loans without the need for bulky software or cloud subscriptions. +BookKeeper gives you full control over your collection in a clean, offline-friendly CLI format that’s easy to set up and use. + +## Appendix B: User Stories + +| Version | As a... | I want to... | So that I can | +| ------- | ------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------- | +| `v1.0` | Librarian | View inventory, including book count | See my existing books | +| `v1.0` | Librarian | Add new books to the system easily | Update my inventory when acquiring new books | +| `v1.0` | Librarian | Remove books when lost or permanently borrowed | Maintain an accurate inventory | +| `v1.0` | Librarian | Add book loans, including borrower details and return dates | Ensure books are returned on time and inform others of availability | +| `v1.0` | Librarian | Delete book loans, including borrower details and return dates | Maintain accurate loan records | +| `v1.0` | Librarian | View on-going loans | Keep track of what books are loaned out | +| `v2.0` | Librarian | Categorize my inventory | Make searching for books more convenient | +| `v2.0` | Librarian | Manage/Update book availability, including borrowed and reserved books | Efficiently allocate books | +| `v2.0` | Librarian | Track book conditions (e.g good, fair, poor) | Maintain detailed records | +| `v2.0` | Librarian | Add personal notes about individual books | Maintain detailed records | +| `v2.0` | Librarian | Edit existing book loans' due dates | Better track by updating book loans | +| `v2.0` | Librarian | Add contact details for borrowers | Easily reach out to borrowers when needed | +| `v2.0` | Librarian | Keep track of where available books are in the library | Help visitors find books | +| `v2.0` | New Librarian | View a list of available commands | Learn how to use the application | +| `v2.1` | Librarian | Update existing personal notes about individual books | Maintain accurate records | + +## Appendix C: Non-Functional Requirements -## User Stories +1. Technical Requirements: Any _mainstream OS_ with Java 17 installed. +2. Project Scope Constraints: Data storage is only to be performed locally. -|Version| As a ... | I want to ... | So that I can ...| -|--------|----------|---------------|------------------| -|v1.0|new user|see usage instructions|refer to them when I forget how to use the application| -|v2.0|user|find a to-do item by name|locate a to-do without having to go through the entire list| +## Appendix D: Glossary -## Non-Functional Requirements +- _Mainstream OS_ - Windows, Linux, Unix, MacOS -{Give non-functional requirements} +## Appendix E: Instructions for manual testing -## Glossary +### Manual Testing -* *glossary item* - Definition +View the [User Guide](UserGuide.md) for the full list of UI commands and their related use case and expected outcomes. -## Instructions for manual testing +### JUnit Testing -{Give instructions on how to do a manual product testing e.g., how to load sample data to be used for testing} +JUnit tests are written in the subdirectory `test` and serve to test key methods part of the application. diff --git a/docs/README.md b/docs/README.md index bbcc99c1e7..ef8ac03bc4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# Duke +# BookKeeper -{Give product intro here} +BookKeeper is a Command Line Interface (CLI) library manager application for effective tracking of library loans and inventory. It allows users to manage books, loans, and notes efficiently through a set of commands. Useful links: * [User Guide](UserGuide.md) diff --git a/docs/UserGuide.md b/docs/UserGuide.md index d6cf4c3b3a..33f166de1f 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,42 +1,472 @@ -# User Guide +# User Guide (v2.1) + +## Table of Contents + +- [User Guide (v2.1)](#user-guide-v21) + - [Table of Contents](#table-of-contents) + - [Introduction](#introduction) + - [Quick Start](#quick-start) + - [Features](#features) + - [Adding a book: `add-book`](#adding-a-book-add-book) + - [Adding a Title that already exists in inventory](#adding-a-title-that-already-exists-in-inventory) + - [Removing a book: `remove-book`](#removing-a-book-remove-book) + - [Updating a Book: `update-book`](#updating-a-book-update-book) + - [Updating a Title: `update-title`](#updating-a-title-update-title) + - [Searching for a Book: `search-title`](#searching-for-a-book-search-title) + - [View Book Collection: `view-inventory`](#view-book-collection-view-inventory) + - [List Category: `list-category`](#list-category-list-category) + - [Adding a Loan: `add-loan`](#adding-a-loan-add-loan) + - [Adding a Loan for a Book Already on Loan](#adding-a-loan-for-a-book-already-on-loan) + - [Deleting a Loan: `delete-loan`](#deleting-a-loan-delete-loan) + - [Editing a Loan: `edit-loan`](#editing-a-loan-edit-loan) + - [View Current Loans: `view-loans`](#view-current-loans-view-loans) + - [Deleting a note: `delete-note`](#deleting-a-note-delete-note) + - [Displaying Help: `help`](#displaying-help-help) + - [Exiting the program: `exit`](#exiting-the-program-exit) + - [Persistent State](#persistent-state) + - [Editing data file](#editing-data-file) + - [CAUTION: Edits that make the data invalid can cause BookKeeper to behave in unexpected ways. Edit data files only if you are confident that you can update it correctly.](#caution-edits-that-make-the-data-invalid-can-cause-bookkeeper-to-behave-in-unexpected-ways-edit-data-files-only-if-you-are-confident-that-you-can-update-it-correctly) + - [Data Validation](#data-validation) + - [Notes](#notes) + - [Command Summary](#command-summary) + +
## Introduction -{Give a product intro} +BookKeeper is a Command Line Interface (CLI) library manager application for effective tracking of library loans and inventory. + +--- ## Quick Start -{Give steps to get started quickly} +Welcome to BookKeeper! This guide will help you get started with using the system for managing your library's books and loan records. 1. Ensure that you have Java 17 or above installed. -1. Down the latest version of `Duke` from [here](http://link.to/duke). +2. Download the latest version of `BookKeeper` from [here](https://github.com/AY2425S2-CS2113-T12-2/tp/releases/tag/Release-v2.1). +3. Copy the file to the folder you want to use as the home folder for BookKeeper. +4. Open a command prompt/terminal and navigate to the folder where you placed the jar file. +5. Run the application using: `java -jar BookKeeper.jar` +6. Type a command in the command box and press Enter to execute it. +7. Follow the interactive prompts to complete the command. -## Features +--- -{Give detailed description of each feature} +## Features -### Adding a todo: `todo` -Adds a new item to the list of todo items. +This version of the system focuses on key functionalities for managing inventory and book loans. Below is a breakdown of the available features and commands: -Format: `todo n/TODO_NAME d/DEADLINE` +### Adding a book: `add-book` -* The `DEADLINE` can be in a natural language format. -* The `TODO_NAME` cannot contain punctuation. +Adds a book to the library collection. -Example of usage: +Format: `add-book BOOK_TITLE a/AUTHOR cat/CATEGORY cond/CONDITION loc/LOCATION [note/NOTE]` -`todo n/Write the rest of the User Guide d/next week` +Notes: -`todo n/Refactor the User Guide to remove passive voice d/13/04/2020` +- Books in inventory are unique. +- Valid categories are: romance, adventure, action, horror, mystery, fiction, nonfiction, scifi, education. +- Valid conditions are: poor, fair, good. +- BookKeeper considers books of different casing as separate books. -## FAQ +Example: -**Q**: How do I transfer my data to another computer? +``` +add-book Great Gatsby a/Fitzgerald cat/Fiction cond/Good loc/Shelf B1 +``` -**A**: {your answer here} +or -## Command Summary +``` +add-book Great Gatsby a/Fitzgerald cat/Fiction cond/Good loc/Shelf B1 note/important +``` + +Expected Outcome: + +``` +New book added: Great Gatsby +``` + +#### Adding a Title that already exists in inventory + +BookKeeper does not support multiple copies of books with the same title. If the the user attempts to add a duplicate title, the system will not permit the addition. You will see an error message indicating that the book already exists. + +Example: +`Book already exists in inventory: Great Gatsby` + +### Removing a book: `remove-book` + +Removes a book from the library collection. + +Format: `remove-book BOOK_TITLE` + +Example: + +``` +remove-book Great Gatsby +``` + +Expected Outcome: + +``` +Removed book: Great Gatsby +``` + +Notes: + +- Removing a book with an existing loan will delete the associated loan as well + +### Updating a Book: `update-book` + +Updates the author, category, condition, location and note with the information provided. While the 4 fields are optional, `update-book` expects at least one field to be updated. If a user wants to update a book title, they should use the `update-title` command instead. + +Format: `update-book BOOK_TITLE [a/AUTHOR] [cat/CATEGORY] [cond/CONDITION] [loc/LOCATION] [note/NOTE]` + +Example: + +``` +update-book Great Gatsby a/Fitzgerald cat/Fiction cond/POOR loc/B3 note/Replace ASAP +``` + +Expected Outcome: + +``` +Book Updated: +Title: Great Gatsby + Author: Fitzgerald + Category: Fiction + Condition: POOR + On Loan: false + Location: Shelf B3 + Note: Replace ASAP +``` + +Notes: + +- Valid categories are: romance, adventure, action, horror, mystery, fiction, nonfiction, scifi, education. +- Valid conditions are: poor, fair, good. +- Blank Fields will not be updated + +### Updating a Title: `update-title` + +Updates the title of a book. + +Format: `update-title BOOK_TITLE new/NEW_TITLE` + +Example: + +``` +update-title Great Gatsby new/The Great Gatsby +``` + +Expected Outcome: + +``` +Book Updated: +Title: The Great Gatsby + Author: Fitzgerald + Category: Fiction + Condition: POOR + On Loan: false + Location: Shelf B3 + Note: Replace ASAP +``` + +### Searching for a Book: `search-title` + +Search for a book in the inventory by title based on the keyword. Unlike all other commands, search-title uses _case-insensitive_ searching for a better user experience. + +Format: `search-title KEYWORD` + +Example: + +``` +search-title Great +``` + +Expected Outcome: + +``` +Here are the books in your inventory: +1. Title: Great Gatsby + Author: Fitzgerald + Category: Fiction + Condition: GOOD + On Loan: false + Location: Shelf B1 + Note: No notes available +2. + ... +``` + +### View Book Collection: `view-inventory` + +View all books currently in the collection. + +Format: `view-inventory` + +Example: + +``` +view-inventory +``` + +Expected Outcome: + +``` +Here are the books in your inventory: +1. Title: Great Gatsby + Author: Fitzgerald + Category: Fiction + Condition: GOOD + On Loan: false + Location: Shelf B1 + Note: No notes available + +2. Title: Cheese Chronicles + Author: Jerry + ... +``` + +### List Category: `list-category` + +List all books currently in the inventory that belong to the given category. + +Format: `list-category CATEGORY` + +Example: + +``` +list-category Fiction +``` + +Expected Outcome: + +``` +Here are the books in your inventory: + 1. Title: Great Gatsby + Author: Fitzgerald + Category: Fiction + Condition: GOOD + On Loan: false + Location: Shelf B1 + Note: No notes available +``` + +Notes: + +- Valid categories are: romance, adventure, action, horror, mystery, fiction, nonfiction, scifi, education. + +### Adding a Loan: `add-loan` + +Adds a loan using the book title. + +Format: `add-loan BOOK_TITLE n/BORROWER_NAME d/RETURN_DATE p/PHONE_NUMBER e/EMAIL` + +Notes: + +- The book to be loaned out must exist in the inventory. +- The RETURN_DATE must be in the format DD-MM-YYYY. +- The RETURN_DATE cannot be in the past. +- The PHONE_NUMBER accepts Singapore numbers only (8 numbers, starting with 9, 8 or 6). + +Example: -{Give a 'cheat sheet' of commands here} +``` +add-loan Great Gatsby n/John Doe d/01-12-2025 p/98765432 e/abc123@gmail.com +``` + +Expected Output: + +``` +Loan added successfully for book: Great Gatsby +``` + +#### Adding a Loan for a Book Already on Loan + +If the book is already on loan, the system will not allow adding another loan for the same book. You will see an error message indicating that the book is unavailable. + +Example: +`Error: The book "Great Gatsby" is already on loan.` + +### Deleting a Loan: `delete-loan` + +Deletes a loan using the book title. + +Format: `delete-loan BOOK_TITLE` + +Example: + +``` +delete-loan Great Gatsby +``` + +Expected Outcome: + +``` +Loan deleted successfully for book: Great Gatsby +``` + +### Editing a Loan: `edit-loan` + +Edits loan borrower name, return date of the loan, borrower's phone number and email. While the 4 fields are optional, `edit-loan` expects at least one field to be updated. + +Format: `edit-loan BOOK_TITLE [n/BORROWER_NAME] [d/RETURN_DATE] [p/PHONE_NUMBER] [e/EMAIL]` + +Notes: + +- The RETURN_DATE must be in the format DD-MM-YYYY. +- The RETURN_DATE cannot be in the past. +- The PHONE_NUMBER accepts Singapore numbers only (8 numbers, starting with 9, 8 or 6). +- Blank Fields will not be updated + +Example: + +``` +edit-loan Great Gatsby n/John Doe d/01-11-2025 p/91234567 e/123abc@gmail.com +``` + +Expected Outcome: + +``` +Loan Updated: +Title: Great Gatsby + Borrower: John Doe + Return Date: 01-11-2025 + Contact Number: 91234567 + Email: 123abc@gmail.com +``` + +### View Current Loans: `view-loans` + +View all currently ongoing loans. + +Format: `view-loans` + +Example: + +``` +view-loans +``` + +Expected Outcome: + +``` +Here are the active loans: +1. Title: Great Gatsby + Borrower: John Doe + Return Date: 12-01-2023 + Contact Number: 98765432 + Email: abc123@gmail.com + +2. Title: Cheese Chronicles + Borrower: Gerald + Return Date: 11-04-2025 + ... +``` + +### Deleting a Note: `delete-note` + +Deletes a note currently attached to a book. + +Format: `delete-note BOOK_TITLE` + +Example: + +``` +delete-note Great Gatsby +``` + +Expected Outcome: + +``` +Note deleted for book: Great Gatsby +``` + +### Displaying Help: `help` + +Displays a list of all available commands and their formats. + +Format: `help` + +Example: + +``` +help +``` + +### Exiting the program: `exit` + +Exits the program. + +Format: `exit` + +Example: + +``` +exit +``` + +Expected Outcome: + +``` +Exiting BookKeeper... +``` + +
+ +## Persistent State + +BookKeeper data is saved in the hard disk automatically after any command that changes the data. There is no need to save manually. When you close and start up BookKeeper again, BookKeeper will load all previous data automatically. + +### Editing data file + +BookKeeper data is saved automatically as .txt files. +Inventory data is stored at `[JAR file location]/data/bookKeeper_bookList.txt`. +Loan data is stored at `[JAR file location]/data/bookKeeper_loanList.txt`. +Advanced users are welcome to update data directly by editing these files. + +#### CAUTION: Edits that make the data invalid can cause BookKeeper to behave in unexpected ways. Edit data files only if you are confident that you can update it correctly. + +### Data Validation + +BookKeeper validates data when loading from files. If invalid data is encountered (e.g., missing fields, incorrect formats), the system will skip the invalid entries and log a warning. + +Example: +Invalid Entry in Inventory File: + +`Great Gatsby | Fitzgerald | Fiction` + +Warning Message: + +`Invalid book entry skipped: Great Gatsby | Fitzgerald | Fiction` + +### Notes: + +- **Commands Are Case-Sensitive**: Ensure that commands and inputs (e.g., book titles, borrower names) match the exact case. +- **Books Are Unique**: Each book in the inventory is unique and identified by its title. Duplicate books are not allowed. Books are case sensitive. +- **Input Character Limitations**: We guarantee support for the English keyboard only. For contact numbers, only Singapore numbers are supported (omit +65). Please do not use the character `|` in your inputs. Special sequences such as ANSI escape code are not supported. +- **Data Size/Length Limitations**: Inventory size, loan list length and no. of characters in user input should not exceed `2147483647` (>2 billion). +- **User Responsibility**: User is responsible for text between flags (demarcated by `/`), e.g. `" "` is considered a valid book title if user follows proper command syntax. +- **File Permissions**: Users should not tamper with BookKeeper file permissions to ensure a seamless experience. + +
+ +## Command Summary -* Add todo `todo n/TODO_NAME d/DEADLINE` +| Action | Format | +| -------------- | ---------------------------------------------------------------------------------------------- | +| Add Book | `add-book BOOK_TITLE a/AUTHOR cat/CATEGORY cond/CONDITION loc/LOCATION [note/NOTE]` | +| Remove Book | `remove-book BOOK_TITLE` | +| Update Book | `update-book BOOK_TITLE [a/AUTHOR] [cat/CATEGORY] [cond/CONDITION] [loc/LOCATION] [note/NOTE]` | +| Update Title | `update-title BOOK_TITLE new/NEW_TITLE` | +| Search Book | `search-title KEYWORD` | +| View Inventory | `view-inventory` | +| Delete Note | `delete-note BOOK_TITLE` | +| List Category | `list-category CATEGORY` | +| Add Loan | `add-loan BOOK_TITLE n/BORROWER_NAME d/RETURN_DATE p/PHONE_NUMBER e/EMAIL` | +| Delete Loan | `delete-loan BOOK_TITLE` | +| Edit Loan | `edit-loan BOOK_TITLE [n/BORROWER_NAME] [d/RETURN_DATE] [p/PHONE_NUMBER] [e/EMAIL]` | +| View Loans | `view-loans` | +| Display Help | `help` | +| Exit Program | `exit` | diff --git a/docs/diagrams/addBook.puml b/docs/diagrams/addBook.puml new file mode 100644 index 0000000000..f76ddbdc5b --- /dev/null +++ b/docs/diagrams/addBook.puml @@ -0,0 +1,86 @@ +@startuml +actor User +participant InputHandler as ":InputHandler" +participant InputParser as ":InputParser" +participant BookList as ":BookList" +participant Book as ":Book" +participant Formatter as ":Formatter" +participant Storage as ":Storage" + +User -> InputHandler: userInputLine +activate InputHandler + +InputHandler -> InputParser: extractCommandArgs(userInputLine) +activate InputParser + +InputParser --> InputHandler: commandArgs[] +deactivate InputParser + +alt commandArgs[0] == add-book + InputHandler -> InputHandler : addBook(commandArgs) + activate InputHandler + + opt commandArgs.length < 2 + note right + rest of sequence is cancelled in error cases denoted in opt frames + end note + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> InputParser: extractAddBookArgs(commandArgs[1]) + activate InputParser + + InputParser --> InputHandler: parsedArgs + deactivate InputParser + + InputHandler -> BookList: findBookByTitle(BOOK_TITLE) + activate BookList + + BookList --> InputHandler: Book + deactivate BookList + + opt Book != null + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> Book ** : Book(parsedArgs) + activate Book + + Book --> InputHandler : newBook + deactivate Book + + InputHandler -> BookList: addBook(newBook) + activate BookList + + + BookList --> InputHandler: Book added + deactivate BookList + + InputHandler -> Formatter: printBorderedMessage("New book added: {BOOK_TITLE}") + activate Formatter + + Formatter --> InputHandler + deactivate Formatter + + InputHandler -> Storage: saveInventory(bookList) + activate Storage + + Storage --> InputHandler + deactivate Storage + +InputHandler --> InputHandler +deactivate InputHandler + +else else + note over InputHandler : other commands +end + +InputHandler --> User +deactivate InputHandler +@enduml \ No newline at end of file diff --git a/docs/diagrams/addLoan.puml b/docs/diagrams/addLoan.puml new file mode 100644 index 0000000000..83c822e993 --- /dev/null +++ b/docs/diagrams/addLoan.puml @@ -0,0 +1,111 @@ +@startuml +actor User +participant InputHandler as ":InputHandler" +participant InputParser as ":InputParser" +participant BookList as ":BookList" +participant LoanList as ":LoanList" +participant Formatter as ":Formatter" +participant Storage as ":Storage" +participant Book as ":Book" + +User -> InputHandler: userInputLine +activate InputHandler + +InputHandler -> InputParser: extractCommandArgs(userInputLine) +activate InputParser + +InputParser --> InputHandler: commandArgs[] +deactivate InputParser + +alt commandArgs[0] == add-loan + InputHandler -> InputHandler : addLoan(commandArgs) + activate InputHandler + +opt commandArgs.length < 2 + note right + rest of sequence is cancelled in error cases denoted in opt frames + end note + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter +end + InputHandler -> InputParser: extractAddLoanArgs(commandArgs[1]) + activate InputParser + + InputParser --> InputHandler: parsedArgs + deactivate InputParser + + InputHandler -> BookList: searchBook(BOOK_TITLE) + activate BookList + + BookList --> InputHandler: Book + deactivate BookList + + opt Book == null + InputHandler -> Formatter: Print exception message + activate Formatter + + Formatter --> InputHandler + deactivate Formatter + end + InputHandler -> Book: getOnLoan() + activate Book + + Book --> InputHandler + deactivate Book + + opt loanedBook.getOnLoan() == true + InputHandler -> Formatter: Print exception message + activate Formatter + + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> Loan ** : Loan(parsedArgs) + activate Loan + + Loan --> InputHandler : newLoan + deactivate Loan + + + InputHandler -> LoanList: addLoan(newLoan) + activate LoanList + LoanList --> InputHandler: Loan added + deactivate LoanList + + InputHandler -> Book: setOnLoan(true) + activate Book + + Book --> InputHandler + deactivate Book + + InputHandler -> Formatter: printBorderedMessage("Loan added successfully for book: {BOOK_TITLE}") + activate Formatter + + Formatter --> InputHandler + deactivate Formatter + + InputHandler -> Storage: saveLoans(loanList) + activate Storage + + Storage --> InputHandler + deactivate Storage + + InputHandler -> Storage: saveInventory(bookList) + activate Storage + + Storage --> InputHandler + deactivate Storage + +InputHandler --> InputHandler +deactivate InputHandler + +else else + note over InputHandler : other commands +end + +InputHandler --> User +deactivate InputHandler +@enduml \ No newline at end of file diff --git a/docs/diagrams/classDiagram.puml b/docs/diagrams/classDiagram.puml new file mode 100644 index 0000000000..4dd91d15b3 --- /dev/null +++ b/docs/diagrams/classDiagram.puml @@ -0,0 +1,107 @@ +@startuml +hide circle +skinparam ClassAttributeIconSize 0 +show enum + +class BookList { + - listName : String + + BookList(String listName, \n ArrayList bookList) + + addBook() + + removeBook(Book book) + + findBookByTitle(): String + + findBooksByKeyword(String keyword): \n ArrayList + + findBooksByCategory(String category): \n ArrayList +} + +class LoanList { + - listName : String + + LoanList(String listName, \n ArrayList loanList) + + addLoan(Loan loan) + + deleteLoan(Loan loan) + + removeLoansByBook(Book book) + + findLoan(Book book, String borrower): Loan +} + +class Book { + - title : String + - author : String + - onLoan : boolean + - location : String + - note : String + + Book(String title, String author...) +} + +class Loan { + - borrowerName : String + - returnDate : LocalDate + - phoneNumber : String + - email : String + + Loan(Book book, String borrowerName...) +} + +enum Condition <> { + GOOD + FAIR + POOR +} + +enum Category <> { + ROMANCE + ADVENTURE + ACTION + HORROR + MYSTERY + FICTION + NONFICTION + SCIFI + EDUCATION +} + +class Storage { + - FOLDER_PATH: String + - INVENTORY_FILE_NAME: String + - LOAN_LIST_FILE_NAME: String + - inventoryFilePath: String + - loanListFilePath: String + + saveInventory(BookList bookList) + + saveLoans(LoanList loanList) + + loadInventory(): ArrayList + + loadLoans(ArrayList): ArrayList + + validateStorage(BookList bookList, LoanList loanList) +} + +class InputHandler { + + askInput() + - displayHelp() + - addLoan(String[] commandArgs) + - addBook(String[] commandArgs) +} + +class InputParser { + + extractCommandArgs(String input): String[] + + extractAddBookArgs(String input): String[] +} +note "Other methods omitted but follow similarly for other commands." as N0 + +class BookKeeper { + +main(String[] args) + +displayWelcomeMessage() +} + +note "Utility methods (getters, setters, etc.) omitted" as N1 + +BookList -> "*" Book : contains > +LoanList -> "*" Loan : contains > +InputHandler --> BookList +InputHandler --> LoanList +InputHandler ..> Storage +InputHandler ..> InputParser +BookKeeper ..> InputHandler +Book -> "1" Condition +Book -> "1" Category +Loan --> "1" Book +Storage ..> BookList +Storage ..> LoanList + + +@enduml diff --git a/docs/diagrams/deleteLoan.puml b/docs/diagrams/deleteLoan.puml new file mode 100644 index 0000000000..8e6e42d63a --- /dev/null +++ b/docs/diagrams/deleteLoan.puml @@ -0,0 +1,50 @@ +@startuml +autoactivate on +actor User +participant InputHandler as ":InputHandler" +participant InputParser as ":InputParser" +participant Book as ":Book" +participant BookList as ":BookList" +participant LoanList as ":LoanList" +participant Formatter as ":Formatter" +participant Storage as ":Storage" +User -> InputHandler : userInputLine + InputHandler -> InputParser : extractCommandArgs(userInputLine) + return commandArgs +alt commandArgs[0] == delete-loan + InputHandler -> InputHandler : deleteLoan(commandArgs) + InputHandler -> InputParser : extractDeleteLoanArgs() + return deleteLoanArgs + InputHandler -> BookList : findBookByTitle() + return loanedBook + InputHandler -> LoanList : findLoan(loanedBook) + return loan + + opt No book with provided title OR Book not on loan OR Loan does not exist + note right + rest of sequence is cancelled in + error cases denoted in opt frame + end note + InputHandler -> Formatter : Print exception message + return + end + + InputHandler -> LoanList : deleteLoan(loan) + return + InputHandler -> Book : setOnLoan(true) + return + InputHandler -> Formatter : printBorderedMessage(Success Message) + return + InputHandler -> Storage : saveLoans(loanList) + return + InputHandler -> Storage : saveInventory(bookList) + return + + return + +else else + note over InputHandler : other commands +end +return +@enduml + \ No newline at end of file diff --git a/docs/diagrams/deleteNote.puml b/docs/diagrams/deleteNote.puml new file mode 100644 index 0000000000..af4df281ca --- /dev/null +++ b/docs/diagrams/deleteNote.puml @@ -0,0 +1,52 @@ +@startuml +autoactivate on + +actor User +participant InputHandler as ":InputHandler" +participant BookList as ":BookList" +participant InputParser as ":InputParser" +participant Storage as ":Storage" +participant Formatter as ":Formatter" +participant Book as ":Book" + +User -> InputHandler : delete-note(BOOK_TITLE) +InputHandler -> InputParser : extractCommandArgs(User input) +return commandArgs + +alt commandArgs[0] == delete-note + InputHandler -> InputHandler : deleteNote(commandArgs) + opt commandArgs.length < 2 + InputHandler -> Formatter : printBorderedMessage(Invalid Format) + return + end + + InputHandler -> BookList : findBookByTitle(bookTitle) + return book + + opt book == null + InputHandler -> Formatter : printBorderedMessage(Book Not Found) + return + end + + opt book has no note + InputHandler -> Formatter : printBorderedmessage(Book has no note) + return + end + + InputHandler -> Book : setNote("") + return + + InputHandler -> Formatter : printBorderedMessage(Note deleted) + return + + InputHandler -> Storage : saveInventory(bookList) + return + +else else +note over InputHandler : other commands + +end + +return +return +@enduml \ No newline at end of file diff --git a/docs/diagrams/editLoan.puml b/docs/diagrams/editLoan.puml new file mode 100644 index 0000000000..a374fbdd31 --- /dev/null +++ b/docs/diagrams/editLoan.puml @@ -0,0 +1,93 @@ +@startuml +actor User +participant InputHandler as ":InputHandler" +participant InputParser as ":InputParser" +participant BookList as ":BookList" +participant LoanList as ":LoanList" +participant Loan as ":Loan" +participant Formatter as ":Formatter" +participant Storage as ":Storage" + +User -> InputHandler: userInputLine +activate InputHandler + +InputHandler -> InputParser: extractCommandArgs(userInputLine) +activate InputParser + +InputParser --> InputHandler: commandArgs[] +deactivate InputParser + +alt commandArgs[0] == edit-loan + InputHandler -> InputHandler : editLoan(commandArgs) + activate InputHandler + + opt commandArgs.length < 2 + note right + rest of sequence is cancelled in error cases denoted in opt frames + end note + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> InputParser: extractEditLoanArgs(commandArgs[1]) + activate InputParser + + InputParser --> InputHandler: parsedArgs + deactivate InputParser + + InputHandler -> BookList: findBookByTitle(BOOK_TITLE) + activate BookList + + BookList --> InputHandler: Book or null + deactivate BookList + + InputHandler -> LoanList: findLoan(book) + activate LoanList + + LoanList --> InputHandler: loan + deactivate LoanList + + alt Book == null + InputHandler -> Formatter: printBorderedMessage("Book is not found in inventory: {BOOK_TITLE}") + activate Formatter + Formatter --> InputHandler + deactivate Formatter + + else !book.isOnLoan() + InputHandler -> Formatter: printBorderedMessage("The book {BOOK_TITLE} is not currently out on loan.") + activate Formatter + Formatter --> InputHandler + deactivate Formatter + + else else + InputHandler -> Loan : setLoanFields(...) + activate Loan + Loan --> InputHandler + deactivate Loan + + InputHandler -> Formatter: printBorderedMessage("Loan Updated: {Loan}") + activate Formatter + Formatter --> InputHandler + deactivate Formatter + + InputHandler -> Storage: saveLoans(bookList) + activate Storage + Storage --> InputHandler + deactivate Storage + + end + + +InputHandler --> InputHandler +deactivate InputHandler + +else else + note over InputHandler : other commands +end + + +InputHandler --> User +deactivate InputHandler +@enduml \ No newline at end of file diff --git a/docs/diagrams/loadInventory.puml b/docs/diagrams/loadInventory.puml new file mode 100644 index 0000000000..3aad835f03 --- /dev/null +++ b/docs/diagrams/loadInventory.puml @@ -0,0 +1,108 @@ +@startuml +participant InputHandler as ":InputHandler" +participant Storage as ":Storage" +participant File as ":File" +participant Formatter as ":Formatter" +participant Scanner as ":Scanner" +participant Book as ":Book" + + +InputHandler -> Storage: loadInventory() +activate Storage + +' Create a new empty bookList +create BookList as ":BookList" +Storage -> BookList: new ArrayList() +activate BookList +BookList --> Storage: bookList +deactivate BookList + +' Create File object for INVENTORY_FILE_PATH +Create File +Storage -> File: new File(INVENTORY_FILE_PATH) +activate File +File --> Storage: file +deactivate File + +opt !file.exists() + Storage -> Formatter: printBorderedMessage("No saved inventory found...") + activate Formatter + + Formatter --> Storage + deactivate Formatter + + Storage --> InputHandler: empty bookList + end + ' Open the file using a Scanner + create Scanner + Storage -> Scanner: new Scanner(file) + activate Scanner + Scanner --> Storage: scanner + deactivate Scanner + + loop scanner.hasNextLine() + Storage -> Scanner: nextLine() + activate Scanner + + Scanner --> Storage: line + deactivate Scanner + + Storage -> Storage: parseBookFromString(line) + activate Storage + alt line has invalid format + note over File + returns null if the line has invalid format + end note + else else + create Book + Storage -> Book: create book with arguments provided in line + activate Book + Book --> Storage: book + deactivate Book + end + Storage --> Storage: book + deactivate Storage + + opt book != null + ' Check for duplicate book + Storage -> BookList: Check for duplicate book objects using stream + activate BookList + BookList --> Storage: isDuplicate + deactivate BookList + alt isDuplicate + ' Book already exists, skip adding it + Storage -> Formatter: printBorderedMessage("Duplicate book found...") + activate Formatter + Formatter --> Storage + deactivate Formatter + + else else + ' Add book to bookList + Storage -> BookList: add(book) + activate BookList + BookList --> Storage + deactivate BookList + end + end + end + + + + Storage -> Scanner: close() + activate Scanner + Scanner --> Storage + deactivate Scanner + destroy Scanner + ' Print message with number of books loaded + Storage -> Formatter: printBorderedMessage("Loaded...") + activate Formatter + Formatter --> Storage + deactivate Formatter + + + Storage --> InputHandler: bookList + destroy File + deactivate Storage + +deactivate Storage +@enduml diff --git a/docs/diagrams/loadLoans.puml b/docs/diagrams/loadLoans.puml new file mode 100644 index 0000000000..413e1ffaaf --- /dev/null +++ b/docs/diagrams/loadLoans.puml @@ -0,0 +1,108 @@ +@startuml +participant InputHandler as ":InputHandler" +participant Storage as ":Storage" +participant File as ":File" +participant Formatter as ":Formatter" +participant Scanner as ":Scanner" +participant loan as ":Loan" + + +InputHandler -> Storage: loadInventory() +activate Storage + +' Create a new empty loanList +create loanList as ":loanList" +Storage -> loanList: new ArrayList() +activate loanList +loanList --> Storage: loanList +deactivate loanList + +' Create File object for LOAN_LIST_FILE_PATH +Create File +Storage -> File: new File(LOAN_LIST_FILE_PATH) +activate File +File --> Storage: file +deactivate File + +opt !file.exists() + Storage -> Formatter: printBorderedMessage("No saved loans found...") + activate Formatter + + Formatter --> Storage + deactivate Formatter + + Storage --> InputHandler: empty loanList + end + ' Open the file using a Scanner + create Scanner + Storage -> Scanner: new Scanner(file) + activate Scanner + Scanner --> Storage: scanner + deactivate Scanner + + loop scanner.hasNextLine() + Storage -> Scanner: nextLine() + activate Scanner + + Scanner --> Storage: line + deactivate Scanner + + Storage -> Storage: parseloanFromString(line) + activate Storage + alt line has invalid format + note over File + returns null if the line has invalid format + end note + else else + create loan + Storage -> loan: create loan with arguments provided in line + activate loan + loan --> Storage: loan + deactivate loan + end + Storage --> Storage: loan + deactivate Storage + + opt loan != null + ' Check for duplicate loan + Storage -> loanList: Check for duplicate loan objects using stream + activate loanList + loanList --> Storage: isDuplicate + deactivate loanList + alt isDuplicate + ' loan already exists, skip adding it + Storage -> Formatter: printBorderedMessage("Duplicate loan found...") + activate Formatter + Formatter --> Storage + deactivate Formatter + + else else + ' Add loan to loanList + Storage -> loanList: add(loan) + activate loanList + loanList --> Storage + deactivate loanList + end + end + end + + + + Storage -> Scanner: close() + activate Scanner + Scanner --> Storage + deactivate Scanner + destroy Scanner + ' Print message with number of loans loaded + Storage -> Formatter: printBorderedMessage("Loaded...") + activate Formatter + Formatter --> Storage + deactivate Formatter + + + Storage --> InputHandler: loanList + destroy File + deactivate Storage + +deactivate Storage +@enduml diff --git a/docs/diagrams/removeBook.puml b/docs/diagrams/removeBook.puml new file mode 100644 index 0000000000..db8c7a5597 --- /dev/null +++ b/docs/diagrams/removeBook.puml @@ -0,0 +1,59 @@ +@startuml +actor User +participant InputHandler as ":InputHandler" +participant InputParser as ":InputParser" +participant BookList as ":BookList" +participant LoanList as ":LoanList" +participant Formatter as ":Formatter" +participant Storage as ":Storage" + +User -> InputHandler: "remove-book BOOK_TITLE" +activate InputHandler + + InputHandler -> InputParser: extractCommandArgs(userInputLine) + activate InputParser + InputParser --> InputHandler: commandArgs[] + deactivate InputParser + alt commandArgs[0] == "remove-book" + InputHandler -> InputHandler: removeBook(commandArgs) + activate InputHandler + InputHandler -> BookList: findBookByTitle(bookTitle) + activate BookList + BookList --> InputHandler: toRemove + deactivate BookList + alt toRemove == null + InputHandler -> Formatter: printBorderedMessage("Book not found in inventory: {BOOK_TITLE}") + activate Formatter + Formatter --> InputHandler + deactivate Formatter + else else + InputHandler -> LoanList: removeLoansByBook(toRemove) + activate LoanList + LoanList --> InputHandler + deactivate LoanList + + InputHandler -> BookList: removeBook(toRemove) + activate BookList + BookList --> InputHandler + deactivate BookList + InputHandler -> Storage: saveInventory(bookList) + activate Storage + Storage --> InputHandler + deactivate Storage + InputHandler -> Formatter: printBorderedMessage("Removed book: {BOOK_TITLE}") + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + InputHandler --> InputHandler + deactivate InputHandler + + else else + note over InputHandler + other command cases + end note + end + +InputHandler --> User +deactivate InputHandler +@enduml diff --git a/docs/diagrams/saveInventory.puml b/docs/diagrams/saveInventory.puml new file mode 100644 index 0000000000..7afaa03e76 --- /dev/null +++ b/docs/diagrams/saveInventory.puml @@ -0,0 +1,68 @@ +@startuml + +participant InputHandler as ":InputHandler" +participant Storage as ":Storage" +participant BookList as ":BookList" +participant File as ":File" +participant FileWriter as ":FileWriter" +participant Book as ":Book" +participant Formatter as ":Formatter" + +activate InputHandler + +InputHandler -> Storage: saveInventory(bookList) +activate Storage + +' Check if directory exists +Storage -> File ** : new File(FOLDER_PATH) +activate File +File --> Storage: directory +deactivate File +opt Directory does not exist + Storage -> File: mkdirs() + activate File + File --> Storage: + deactivate File +end + +deactivate File + +' Create FileWriter for INVENTORY_FILE_PATH +Storage -> FileWriter ** : new FileWriter(INVENTORY_FILE_PATH) +activate FileWriter + +FileWriter --> Storage: fileWriter +deactivate FileWriter + +' Retrieve list of books from BookList +Storage -> BookList: getBookList() +activate BookList +BookList --> Storage: List +deactivate BookList + +' Loop through each Book and write its file string +loop for each book in BookList + Storage -> Book: toFileString() + activate Book + Book --> Storage: fileString + deactivate Book + Storage -> FileWriter: write(fileString + lineSeparator) + activate FileWriter + FileWriter --> Storage: + deactivate FileWriter +end + +' Close the FileWriter +Storage -> FileWriter: close() +activate FileWriter + +FileWriter --> Storage +deactivate FileWriter + +destroy FileWriter +Storage --> InputHandler +deactivate Storage + +destroy File + +@enduml \ No newline at end of file diff --git a/docs/diagrams/saveLoans.puml b/docs/diagrams/saveLoans.puml new file mode 100644 index 0000000000..28cac50410 --- /dev/null +++ b/docs/diagrams/saveLoans.puml @@ -0,0 +1,68 @@ +@startuml + +participant InputHandler as ":InputHandler" +participant Storage as ":Storage" +participant LoanList as ":LoanList" +participant File as ":File" +participant FileWriter as ":FileWriter" +participant Loan as ":Loan" +participant Formatter as ":Formatter" + +activate InputHandler + +InputHandler -> Storage: saveLoans(loanList) +activate Storage + +' Check if directory exists +Storage -> File ** : new File(FOLDER_PATH) +activate File +File --> Storage: directory +deactivate File +opt Directory does not exist + Storage -> File: mkdirs() + activate File + File --> Storage: + deactivate File +end + +deactivate File + +' Create FileWriter for LOAN_LIST_FILE_PATH +Storage -> FileWriter ** : new FileWriter(LOAN_LIST_FILE_PATH) +activate FileWriter + +FileWriter --> Storage: fileWriter +deactivate FileWriter + +' Retrieve list of Loans from LoanList +Storage -> LoanList: getLoanList() +activate LoanList +LoanList --> Storage: List +deactivate LoanList + +' Loop through each Loan and write its file string +loop for each Loan in LoanList + Storage -> Loan: toFileString() + activate Loan + Loan --> Storage: fileString + deactivate Loan + Storage -> FileWriter: write(fileString + lineSeparator) + activate FileWriter + FileWriter --> Storage: + deactivate FileWriter +end + +' Close the FileWriter +Storage -> FileWriter: close() +activate FileWriter + +FileWriter --> Storage +deactivate FileWriter + +destroy FileWriter +Storage --> InputHandler +deactivate Storage + +destroy File + +@enduml \ No newline at end of file diff --git a/docs/diagrams/searchTitle.plantuml b/docs/diagrams/searchTitle.plantuml new file mode 100644 index 0000000000..77043bf7f9 --- /dev/null +++ b/docs/diagrams/searchTitle.plantuml @@ -0,0 +1,103 @@ +@startuml +actor User +participant InputHandler as ":InputHandler" +participant InputParser as ":InputParser" +participant BookList as ":BookList" +participant Book as ":Book" +participant Formatter as ":Formatter" +participant Storage as ":Storage" + +User -> InputHandler: userInputLine +activate InputHandler + +InputHandler -> InputParser: extractCommandArgs(userInputLine) +activate InputParser + +InputParser --> InputHandler: commandArgs[] +deactivate InputParser + +alt commandArgs[0] == update-title + InputHandler -> InputHandler : updateTitle(commandArgs) + activate InputHandler + + opt commandArgs.length < 2 + note right + rest of sequence is cancelled in error cases denoted in opt frames + end note + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> InputParser: extractUpdateTitleArgs(commandArgs[1]) + activate InputParser + + InputParser --> InputHandler: parsedArgs + deactivate InputParser + + opt oldTitle.equals(newTitle) + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> BookList: searchBook(newTitle) + activate BookList + + BookList --> InputHandler: Book + deactivate BookList + + opt Book != null + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> BookList: searchBook(oldTitle) + activate BookList + + BookList --> InputHandler: Book + deactivate BookList + + opt Book == null + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + InputHandler -> Book : setTitle(newTitle) + activate Book + Book --> InputHandler + deactivate Book + + InputHandler -> Formatter: printBorderedMessage("Book Updated: {BOOK}") + activate Formatter + + Formatter --> InputHandler + deactivate Formatter + + InputHandler -> Storage: saveInventory(bookList) + activate Storage + + Storage --> InputHandler + deactivate Storage + + InputHandler -> Storage: saveInventory(loanList) + activate Storage + + Storage --> InputHandler + deactivate Storage + +InputHandler --> InputHandler +deactivate InputHandler + +else else + note over InputHandler : other commands +end + +InputHandler --> User +deactivate InputHandler +@enduml \ No newline at end of file diff --git a/docs/diagrams/updateBook.puml b/docs/diagrams/updateBook.puml new file mode 100644 index 0000000000..df36c208f1 --- /dev/null +++ b/docs/diagrams/updateBook.puml @@ -0,0 +1,77 @@ +@startuml +actor User +participant InputHandler as ":InputHandler" +participant InputParser as ":InputParser" +participant BookList as ":BookList" +participant Book as ":Book" +participant Formatter as ":Formatter" +participant Storage as ":Storage" + +User -> InputHandler: userInputLine +activate InputHandler + +InputHandler -> InputParser: extractCommandArgs(userInputLine) +activate InputParser + +InputParser --> InputHandler: commandArgs[] +deactivate InputParser + +alt commandArgs[0] == update-book + InputHandler -> InputHandler : updateBook(commandArgs) + activate InputHandler + + opt commandArgs.length < 2 + note right + rest of sequence is cancelled in error cases denoted in opt frames + end note + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> InputParser: extractUpdateBookArgs(commandArgs[1]) + activate InputParser + + InputParser --> InputHandler: parsedArgs + deactivate InputParser + + InputHandler -> BookList: searchBook(BOOK_TITLE) + activate BookList + + BookList --> InputHandler: Book + deactivate BookList + + opt Book == null + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + InputHandler -> Book : setBookFields(...) + activate Book + Book --> InputHandler + deactivate Book + + InputHandler -> Formatter: printBorderedMessage("Book Updated: {Book}") + activate Formatter + + Formatter --> InputHandler + deactivate Formatter + + InputHandler -> Storage: saveInventory(bookList) + activate Storage + + Storage --> InputHandler + deactivate Storage + +InputHandler --> InputHandler +deactivate InputHandler + +else else + note over InputHandler : other commands +end + +InputHandler --> User +deactivate InputHandler +@enduml \ No newline at end of file diff --git a/docs/diagrams/updateTitle.puml b/docs/diagrams/updateTitle.puml new file mode 100644 index 0000000000..cbaf3c2644 --- /dev/null +++ b/docs/diagrams/updateTitle.puml @@ -0,0 +1,103 @@ +@startuml +actor User +participant InputHandler as ":InputHandler" +participant InputParser as ":InputParser" +participant BookList as ":BookList" +participant Book as ":Book" +participant Formatter as ":Formatter" +participant Storage as ":Storage" + +User -> InputHandler: userInputLine +activate InputHandler + +InputHandler -> InputParser: extractCommandArgs(userInputLine) +activate InputParser + +InputParser --> InputHandler: commandArgs[] +deactivate InputParser + +alt commandArgs[0] == update-title + InputHandler -> InputHandler : updateTitle(commandArgs) + activate InputHandler + + opt commandArgs.length < 2 + note right + rest of sequence is cancelled in error cases denoted in opt frames + end note + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> InputParser: extractUpdateTitleArgs(commandArgs[1]) + activate InputParser + + InputParser --> InputHandler: parsedArgs + deactivate InputParser + + opt oldTitle.equals(newTitle) + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> BookList: findBookByTitle(newTitle) + activate BookList + + BookList --> InputHandler: Book + deactivate BookList + + opt Book != null + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + + InputHandler -> BookList: findBookByTitle(oldTitle) + activate BookList + + BookList --> InputHandler: Book + deactivate BookList + + opt Book == null + InputHandler -> Formatter: Print exception message + activate Formatter + Formatter --> InputHandler + deactivate Formatter + end + InputHandler -> Book : setTitle(newTitle) + activate Book + Book --> InputHandler + deactivate Book + + InputHandler -> Formatter: printBorderedMessage("Book Updated: {BOOK}") + activate Formatter + + Formatter --> InputHandler + deactivate Formatter + + InputHandler -> Storage: saveInventory(bookList) + activate Storage + + Storage --> InputHandler + deactivate Storage + + InputHandler -> Storage: saveInventory(loanList) + activate Storage + + Storage --> InputHandler + deactivate Storage + +InputHandler --> InputHandler +deactivate InputHandler + +else else + note over InputHandler : other commands +end + +InputHandler --> User +deactivate InputHandler +@enduml \ No newline at end of file diff --git a/docs/diagrams/viewInventory.puml b/docs/diagrams/viewInventory.puml new file mode 100644 index 0000000000..fc956cf137 --- /dev/null +++ b/docs/diagrams/viewInventory.puml @@ -0,0 +1,47 @@ +@startuml +actor User +participant InputHandler as ":InputHandler" +participant InputParser as ":InputParser" +participant BookList as ":BookList" +participant Formatter as ":Formatter" +participant Book as ":Book" + + +User -> InputHandler: "view-inventory" +activate InputHandler + InputHandler -> InputParser: extractCommandArgs(userInputLine) + activate InputParser + InputParser --> InputHandler: commandArgs[] + deactivate InputParser + alt commandArgs[0] == "view-inventory" + InputHandler -> BookList: viewBookList() + activate BookList + alt BookList.isEmpty() + BookList -> Formatter : printBorderedMessage("Book List Empty!") + activate Formatter + Formatter --> BookList + deactivate Formatter + BookList --> InputHandler + else else + BookList -> Formatter: printBookList(bookList) + activate Formatter + loop for each Book + Formatter -> Book: toString() + activate Book + Book --> Formatter: Book info + deactivate Book + end + Formatter --> BookList + deactivate Formatter + BookList --> InputHandler + deactivate BookList + end + else else + note over InputHandler + other command cases + end note +end +InputHandler --> User +deactivate InputHandler + +@enduml diff --git a/docs/diagrams/viewLoans.puml b/docs/diagrams/viewLoans.puml new file mode 100644 index 0000000000..926d0b5c0f --- /dev/null +++ b/docs/diagrams/viewLoans.puml @@ -0,0 +1,48 @@ +@startuml +actor User +participant InputHandler as ":InputHandler" +participant InputParser as ":InputParser" +participant LoanList as ":LoanList" +participant Formatter as ":Formatter" +participant Loan as ":Loan" + + +User -> InputHandler: "view-loans" +activate InputHandler + InputHandler -> InputParser: extractCommandArgs(userInputLine) + activate InputParser + InputParser --> InputHandler: commandArgs[] + deactivate InputParser + alt commandArgs[0] == "view-loans" + InputHandler -> LoanList: viewLoanList() + activate LoanList + alt LoanList.isEmpty() + LoanList -> Formatter : printBorderedMessage("Loan List Empty!") + activate Formatter + Formatter --> LoanList + deactivate Formatter + LoanList --> InputHandler + else else + LoanList -> Formatter: printLoanList(LoanList) + activate Formatter + loop for each Loan + Formatter -> Loan: toString() + activate Loan + Loan --> Formatter: Loan info + deactivate Loan + end + Formatter --> LoanList + deactivate Formatter + LoanList --> InputHandler + deactivate LoanList + end + else else + note over InputHandler + other command cases + end note +end +InputHandler --> User +deactivate InputHandler + + +@enduml diff --git a/docs/images/Portrait_placeholder.png b/docs/images/Portrait_placeholder.png new file mode 100644 index 0000000000..410f812cd2 Binary files /dev/null and b/docs/images/Portrait_placeholder.png differ diff --git a/docs/images/addBook.png b/docs/images/addBook.png new file mode 100644 index 0000000000..9b0f0ed8d1 Binary files /dev/null and b/docs/images/addBook.png differ diff --git a/docs/images/addLoan.png b/docs/images/addLoan.png new file mode 100644 index 0000000000..c56ae6ac44 Binary files /dev/null and b/docs/images/addLoan.png differ diff --git a/docs/images/addNote.png b/docs/images/addNote.png new file mode 100644 index 0000000000..dbe3d59ecb Binary files /dev/null and b/docs/images/addNote.png differ diff --git a/docs/images/classDiagram.png b/docs/images/classDiagram.png new file mode 100644 index 0000000000..3726ecee63 Binary files /dev/null and b/docs/images/classDiagram.png differ diff --git a/docs/images/deleteLoan.png b/docs/images/deleteLoan.png new file mode 100644 index 0000000000..c99119c020 Binary files /dev/null and b/docs/images/deleteLoan.png differ diff --git a/docs/images/deleteNote.png b/docs/images/deleteNote.png new file mode 100644 index 0000000000..3308611813 Binary files /dev/null and b/docs/images/deleteNote.png differ diff --git a/docs/images/editLoan.png b/docs/images/editLoan.png new file mode 100644 index 0000000000..dca6c14cb1 Binary files /dev/null and b/docs/images/editLoan.png differ diff --git a/docs/images/loadInventory.png b/docs/images/loadInventory.png new file mode 100644 index 0000000000..b4ce88db95 Binary files /dev/null and b/docs/images/loadInventory.png differ diff --git a/docs/images/loadLoans.png b/docs/images/loadLoans.png new file mode 100644 index 0000000000..5feefc20e3 Binary files /dev/null and b/docs/images/loadLoans.png differ diff --git a/docs/images/removeBook.png b/docs/images/removeBook.png new file mode 100644 index 0000000000..3e13d6e675 Binary files /dev/null and b/docs/images/removeBook.png differ diff --git a/docs/images/saveInventory.png b/docs/images/saveInventory.png new file mode 100644 index 0000000000..74f0523ea8 Binary files /dev/null and b/docs/images/saveInventory.png differ diff --git a/docs/images/saveLoans.png b/docs/images/saveLoans.png new file mode 100644 index 0000000000..9f8b5f8f6f Binary files /dev/null and b/docs/images/saveLoans.png differ diff --git a/docs/images/searchTitle.png b/docs/images/searchTitle.png new file mode 100644 index 0000000000..3512715917 Binary files /dev/null and b/docs/images/searchTitle.png differ diff --git a/docs/images/updateBook.png b/docs/images/updateBook.png new file mode 100644 index 0000000000..0857fe5b6a Binary files /dev/null and b/docs/images/updateBook.png differ diff --git a/docs/images/updateNote.png b/docs/images/updateNote.png new file mode 100644 index 0000000000..26efe3289c Binary files /dev/null and b/docs/images/updateNote.png differ diff --git a/docs/images/updateTitle.png b/docs/images/updateTitle.png new file mode 100644 index 0000000000..aa6a29281d Binary files /dev/null and b/docs/images/updateTitle.png differ diff --git a/docs/images/viewInventory.png b/docs/images/viewInventory.png new file mode 100644 index 0000000000..f6b40998d5 Binary files /dev/null and b/docs/images/viewInventory.png differ diff --git a/docs/images/viewLoans.png b/docs/images/viewLoans.png new file mode 100644 index 0000000000..cbb851ce56 Binary files /dev/null and b/docs/images/viewLoans.png differ diff --git a/docs/team/dylankhoo.md b/docs/team/dylankhoo.md new file mode 100644 index 0000000000..5e1bfaa466 --- /dev/null +++ b/docs/team/dylankhoo.md @@ -0,0 +1,76 @@ +## Dylan Khoo - Project Portfolio Page + +### Overview + +BookKeeper is a Command Line Interface (CLI) library manager application for effective tracking of library loans and inventory. It allows users to manage books, loans, and notes efficiently through a set of commands. + +### Summary of Contributions + +#### Code contributed: [RepoSense link](https://nus-cs2113-ay2425s2.github.io/tp-dashboard/?search=dylankhoo&breakdown=true) + +#### Features Implemented + +1. [**Add books to inventory**](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/f4769ae458b23c69df365578a816c669e3388537) + + - **What it does:** allows the user to add books to the inventory. + - **Justification:** This feature is essential as the app should provide a convenient way to add books to the inventory. + +2. [**View books in inventory**](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/84c0f30f0ad80bd822256abea68e87178ce8fb6a) + + - **What it does:** allows the user to add books to the inventory. + - **Justification:** This feature is essential as the app should provide a convenient way for the user to view their existing books and their details. + +3. [**Update books in inventory**](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/b9de07dd5b6fd1c1391860979b59c0bb767001cc) + + - **What it does:** allows the user to update details about books in their inventory. + - **Justification:** This feature is essential as the app should provide the way for the user to update book details such as their condition and location as it changes. + +4. [**Update titles of book**](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/fec3c1972e92294b4ae9f8fd0869be5862b70179) + - **What it does:** allows the user to update titles of the books in their inventory. + - **Justification:** This feature is essential as the app should provide the way for the user to update book titles if there are typos. + +#### Enhancements Implemented + +- [Wrote Input Handler Logic](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/1c2eedbe1e81b3e1ec89f745a03f650e5ad516f8), the overall handler for user inputs +- [Added Exceptions](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/c4fa0f566655f5f545280a6647e8b9cd109e64f5) +- [JUnit testing](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/fec3c1972e92294b4ae9f8fd0869be5862b70179) + +### Contributions to User Guide + +- [Wrote content page](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/1a93d1ff73d0d7271533485ff7bda658d682bacb) +- Added [update title](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/e795971a7e76e9fd169d47fbab547efd8ccd3b2e) +- Added details for all commands, such as [expected outcomes](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/2c72f163fb61d3d59b2b0db5f0df6e89f849b8c4) +- Maintained page breaks as the Guide updated (some examples: [1](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/10e53b37e10fb92efb9027ecb510d19a25a0bb96), [2](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/23c744f5a5243bc9fc831a43fa54fb5211c84516), [3](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/aaf63dfff82daa575414fcfa1fd349dc226feaaf), [4](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/ae493b8d01ca228df927c638fb6ab63477b9c881)) +- Updated UG as after discussions and mock PE (Some examples: [1](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/ec6e09a9acb0bfad711577dbbf949c47a46cf6b8), [2](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/e795971a7e76e9fd169d47fbab547efd8ccd3b2e)) + +### Contributions to Developer Guide + +- Added implementation details and UML sequence diagrams for:: + + - [`add-book`](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/90df5546efa098346ea29c99bd89f3cfe174c144), [`update-title`](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/35447d708eba28ebf72fe4cb6dd683c9f3e5ca6d), [`update-book`](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/90df5546efa098346ea29c99bd89f3cfe174c144), [`view-inventory`](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/90df5546efa098346ea29c99bd89f3cfe174c144), + - [`edit-loan`](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/841b071fd93a5b2cb422777cc63268926150e4d2),[ `view-loans`](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/6a282acbac6c4fff8cd457d313fc5b3a58e7fa0b) + +- Updated documentation and diagrams after discussion, mock PE + + - Some examples: [1](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/6e17842200c01d7f2450e4405934173e021bf094), [2](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/8606aefca7a26194c341f1c397553d5f32a9403a), [3](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/ecccef04211ee8c086871cf3779d2cc1b0b9213a), [4](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/1f2533cee540b73697cc37e35386e3c20bc20854) + +- [Wrote Acknowledgements](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/90df5546efa098346ea29c99bd89f3cfe174c144) + +### Contributions to Team-Based Tasks + +- Set up the GitHub team org/repo [and initial code skeleton for development](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/9c7513042d63e6f80bc9848cab931181e339c74b) +- Maintained the issue tracker and managed milestones +- Assisted in integrating features contributed by team members, such as through + - bug fixes (some examples: [1](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/1e5dcc25e61e7fccce8dc04965c9c6d083fc955e), [2](<(https://github.com/AY2425S2-CS2113-T12-2/tp/commit/859e97da39cd0f1364779463d6130777d29eb0d2)>), [3](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/13334cbd3be90ecd64ba55f13a5140a3c9b87186)) + - [removing redundant commands](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/f4deb48c189a93b7b82bd370a3cf6ce42430f93a) + - [formatting updates](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/928890b5eec16620df2d64f464a991d4c7756fb9) + - [refactoring code](https://github.com/AY2425S2-CS2113-T12-2/tp/commit/e073f8d7e73f11f661481fa87c4e3f0021392f30) +- Created new tags such as `disputable` to help organise bugs after mock PE +- Coordinated Zoom meetings for the team + +### Community Contributions + +- Helped to resolve forum questions and help requests + ([Issue 18](https://github.com/nus-cs2113-AY2425S2/forum/issues/18), [Issue 1](https://github.com/nus-cs2113-AY2425S2/forum/issues/1)) + +- Gave advice on technologies team could use for better development experience, such as [oh my posh](https://ohmyposh.dev/) for easy branch identification diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index ab75b391b8..0000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,6 +0,0 @@ -# John Doe - Project Portfolio Page - -## Overview - - -### Summary of Contributions diff --git a/docs/team/phua-lock-hian.md b/docs/team/phua-lock-hian.md new file mode 100644 index 0000000000..266c59a108 --- /dev/null +++ b/docs/team/phua-lock-hian.md @@ -0,0 +1,63 @@ +## Lock Hian - Project Portfolio Page + +### Overview + +BookKeeper is a Command Line Interface (CLI) library manager application for effective tracking of library loans and inventory. It allows users to manage books, loans, and notes efficiently through a set of commands. + +### Summary of Contributions + +#### Code contributed: [RepoSense link](https://nus-cs2113-ay2425s2.github.io/tp-dashboard/?search=phua-lock-hian&breakdown=true) + +#### Enhancements Implemented +1. **`remove-book` command** + * **What it does:** Allows the user to remove books from the inventory. + * **Justification:** This feature is essential as the app should provide a convenient way to remove books from the inventory. + * **Highlights:** Provides a straightforward command for users to maintain the accuracy of the inventory. + +2. **`help` command** + * **What it does:** Display a command summary table when the user inputs `help` into the CLI. + * **Justification:** This feature is essential as the app should provide a convenient way for the user to get help on its usage. + * **Highlights:** Enhances user experience by providing an easy way to access command information. + +3. **Save inventory** + * **What it does:** Saves the inventory automatically to persistent storage whenever there is a change. + * **Justification:** This feature is essential as it ensures that the inventory data is always up to date and not lost between sessions. + * **Highlights:** This enhancement affects existing commands and commands to be added in the future. Ensures data integrity across sessions. + +4. **Load inventory** + * **What it does:** Load inventory automatically from persistent storage every time the program begins. + * **Justification:** This feature ensures that the user always starts with the most recent inventory data. + * **Highlights:** This enhancement affects existing commands and commands to be added in the future. Ensures that users do not need to manually load data. + +5. **`update-note` command** + * **What it does:** Allows user to update note for a particular book. + * **Justification:** Requested from user feedback through PE-D. + * **Highlights:** Increases user convenience in updating notes. + +6. **Enhancements to existing features**: + * Consolidated error messages to constants in `ErrorMessages` class. + * Updated existing features to support new attribute `location` of Book. + * Updated `delete-loan` to no longer require borrower name based on user feedback from PE-D. + * Contributed JUnit tests for `BookList`, `LoanList` and `InputParser`. + * Updated tests to reflect expected behaviour when new features are added. + +### Contributions to User Guide +- Wrote the Introduction and Quick Start sections to help new users get started quickly. +- Added documentation for features: + - `remove-book` - Provides detailed usage instructions for removing books. + - `view-loans` - Explains how users can view current loans. + - Data storage - User-facing documentation on how data is saved and loaded. +- Added notes on user input constraints and user responsibility after group discussion and agreement. + +### Contributions to Developer Guide +- Wrote Appendix A of developer guide. +- Added implementation details and UML sequence diagrams for: + - `remove-book` + - `saveInventory` + - `loadInventory` +- Applied changes spanning across all UML diagrams based on feedback and group discussion. + +### Contributions to Team-Based Tasks +- Maintained the issue tracker and managed milestones. +- Assisted in integrating features contributed by team members. +- Set up Developer Guide and User Guide structure. diff --git a/docs/team/shengbin-101.md b/docs/team/shengbin-101.md new file mode 100644 index 0000000000..8375ddffb4 --- /dev/null +++ b/docs/team/shengbin-101.md @@ -0,0 +1,60 @@ +# Sheng Bin - Project Portfolio Page + +## Overview + +BookKeeper is a Command Line Interface (CLI) library manager application for effective tracking of library loans and inventory. It allows users to manage books, loans, and notes efficiently through a set of commands. + +### Summary of Contributions + +#### **Code Contributed** +- [RepoSense Link](https://nus-cs2113-ay2425s2.github.io/tp-dashboard/?search=shengbin-101&breakdown=true) + +#### **Features Implemented** +1. **`add-loan` command**: + - **What it does**: Feature to add loans for books, specifying borrower details such as name, return date, phone number, and email. + - **Justification**: This feature is essential for tracking which books are currently on loan and who borrowed them. It enhances the core functionality of the library management system. + - **Highlights**: Includes validation to prevent adding loans for books that are already on loan. + +2. **`Formatter` class**: + - **What it does**: Provides a centralized utility for formatting output messages, such as error messages, success messages, and lists. + - **Justification**: Improves the user experience by ensuring consistent and readable output across all commands. + - **Highlights**: Supports formatting for book lists, loan lists, and error messages. + +3. **Storage Validation**: + - **What it does**: Validates the consistency of data between the `BookList` and `LoanList` during loading. Removes invalid loans and fixes discrepancies in book statuses (e.g., `onLoan` status). + - **Justification**: Ensures data integrity when loading from files, preventing crashes and maintaining a consistent state. + - **Highlights**: Handles invalid entries, duplicate books, invalid loans, and mismatched `onLoan` statuses. + +4. **Dynamic Ordering of Arguments in `update-book` and `edit-loan`**: + - **What it does**: Allows users to input command arguments in any order, as long as all required arguments are provided. For example, the command `edit-loan` accepts arguments like `BORROWER_NAME`, `RETURN_DATE`, `PHONE_NUMBER`, and `EMAIL` in any order. + - **Justification**: This feature improves the user experience by making commands more flexible and forgiving. Users do not need to memorize a strict order for arguments, reducing the likelihood of input errors. + - **Highlights**: + - Implemented InputParser to dynamically map arguments based on prefixes (e.g., n/ for name, d/ for return date). + - Includes checking of duplicate prefixes/arguments given by user. + +#### **Contributions to the User Guide** +- Added documentation for `add-loan`, `delete-loan`, `edit-loan`, `view-loans` commands. +- Added sections for: + - Data Validation + - Notes on case sensitivity and unique books + +#### **Contributions to the Developer Guide** +- Wrote the implementation details for: + - Section on Adding Loans + - Section on Search Title + - Section on Save/Load Inventory and Loans +- Added Sequence Diagrams for: + - Sequence Diagram for Adding Loans + - Sequence Diagram for Save/Load Inventory + - Sequence Diagram for Save/Load Loans + +#### **Contributions to Team-Based Tasks** +- Maintained the issue tracker and managed milestones. +- Assisted in integrating features contributed by team members. +- Conducted testing for features implemented by other team members. + +#### **Review/Mentoring Contributions** +- Reviewed and provided help on PRs: + - [#67](https://github.com/AY2425S2-CS2113-T12-2/tp/pull/67) + +--- \ No newline at end of file diff --git a/docs/team/shengjie13245.md b/docs/team/shengjie13245.md new file mode 100644 index 0000000000..304282218c --- /dev/null +++ b/docs/team/shengjie13245.md @@ -0,0 +1,58 @@ +# Sheng Jie - Project Portfolio Page + +## Overview + +BookKeeper is a Command Line Interface (CLI) library manager application for effective tracking of library loans and inventory. It allows users to manage books, loans, and notes efficiently through a set of commands. + +## Summary of Contributions + +### Code Contributed +[RepoSense Link](https://nus-cs2113-ay2425s2.github.io/tp-dashboard/?search=shengjie13245&breakdown=true) + +### Enhancement Implemented +1. Loan Deletion + * **What it does**: Allows the user to delete loans that are currently saved. + * **Justification**: This is a key feature in the product, as a librarian will constantly need to delete loans for books that have been returned. + * **Highlights**: This enhancement work hand in hand with all other basic functions, thus requiring coordination to ensure that there is consistency in the implementation especially when parsing the input by the user. + +2. Search-Title + * **What it does**: Allows the user to get the list of all books that contains the provided keyword in its title. + * **Justification**: This serves as a convenient way for the user to get a list of all the books that is in the inventory without having to look through the whole list. This is an important feature if there is a large amount of books. + +3. Listing Books by Category + * **What it does**: Allows the user to get the list of books that belongs to the provided category + * **Justification**: Provides a convenient way for user to search for books when having a large invenotry of books. + +4. Edit Loans + * **What it does**: Allows the user to edit previously saved loans. + * **Justification**: The features improves the products as it provides a convenient way for users to edit their previously saved loans without first deleting and adding a new loan. + * **Highlights**: Ensures that the loan being edited it actually present and the book is actually on loan. Allows for optional inputs of the different fields which allows the user to only input the fields that is needed to be updated + +5. Save/Load Loans + * **What it does**: Automatically saves all loans to a txt whenever there is a change to the LoanList and loads them back to program when restarted + * **Justification**: Removes the need for user to manually save and import the loans in order to add it whenever they start the program. + * **Highlights**: Save and load loans work together to provide convenience to the user. It requires an analysis on how to loans should be saved as the book object cannot be saved in plain text and checks to prevent invalid inputs and format + +### Enhancement to existing features: +- Wrote JUnit tests for inputParser class. + +### Contributions to the UG: +- Added documentation for the following features: + - `delete-loan` + - `search-title` + - `list-category` + - `update/add/delete-note` + +## Contributions to the DG: +- Added documentation and UML diagram for the following sections: + - `delete-loan` + - `update/add/delete-note` +- Added UML diagram for following sections: + - Class Diagram +- Proof Reading and Fixing errors in DG + +## Contribution to team-based tasks: +- Maintained the issue tracker and managed milestones. +- Assisted in integrating features contributed by team members. +- Managed release for v1.0 and v2.0 +- Bug testing of product for refinement diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c4cd..afba109285 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradlew b/gradlew index fcb6fca147..65dcd68d65 100755 --- a/gradlew +++ b/gradlew @@ -85,6 +85,9 @@ done APP_BASE_NAME=${0##*/} APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -130,13 +133,10 @@ location of your Java installation." fi else JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi fi # Increase the maximum file descriptors if we can. @@ -144,7 +144,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +152,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -197,10 +197,6 @@ if "$cygwin" || "$msys" ; then done fi - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - # Collect all arguments for the java command; # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # shell script including quotes and variable substitutions, so put them in diff --git a/src/main/java/bookkeeper/BookKeeper.java b/src/main/java/bookkeeper/BookKeeper.java new file mode 100644 index 0000000000..9af200e12b --- /dev/null +++ b/src/main/java/bookkeeper/BookKeeper.java @@ -0,0 +1,27 @@ +package bookkeeper; + +import bookkeeper.logic.InputHandler; +import bookkeeper.storage.LoggerConfig; +import bookkeeper.ui.Formatter; + +import java.util.logging.Logger; + +public class BookKeeper { + /** + * Main entry-point for the BookKeeper application. + */ + private static final Logger logger = Logger.getLogger(BookKeeper.class.getName()); + + public static void main(String[] args) { + LoggerConfig.configureLogger(logger); // Configure the logger + logger.info("Starting BookKeeper..."); + displayWelcomeMessage(); + InputHandler inputHandler = new InputHandler(); + inputHandler.askInput(); + logger.info("Exiting BookKeeper..."); + } + + public static void displayWelcomeMessage() { + Formatter.printBorderedMessage("Welcome to BookKeeper."); + } +} diff --git a/src/main/java/bookkeeper/exceptions/BookNotFoundException.java b/src/main/java/bookkeeper/exceptions/BookNotFoundException.java new file mode 100644 index 0000000000..d6abaf5e7d --- /dev/null +++ b/src/main/java/bookkeeper/exceptions/BookNotFoundException.java @@ -0,0 +1,7 @@ +package bookkeeper.exceptions; + +public class BookNotFoundException extends Exception { + public BookNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/bookkeeper/exceptions/ErrorMessages.java b/src/main/java/bookkeeper/exceptions/ErrorMessages.java new file mode 100644 index 0000000000..b485215875 --- /dev/null +++ b/src/main/java/bookkeeper/exceptions/ErrorMessages.java @@ -0,0 +1,92 @@ +package bookkeeper.exceptions; + +//class to store error messages as constants for easier editing +public final class ErrorMessages { + public static final String INVALID_FORMAT_ADD_BOOK = "Invalid format for add-book.\n" + + "Expected format: add-book BOOK_TITLE a/AUTHOR cat/CATEGORY cond/CONDITION loc/LOCATION [note/NOTE]"; + + public static final String INVALID_FORMAT_ADD_BOOK_DUPLICATE_PREFIX = "Invalid format for add-book. " + + "Duplicate prefixes! \n" + + "Expected format: add-book BOOK_TITLE a/AUTHOR cat/CATEGORY cond/CONDITION loc/LOCATION [note/NOTE]"; + + public static final String INVALID_FORMAT_UPDATE_BOOK = "Invalid format for update-book.\n" + + "Expected format: update-book BOOK_TITLE [a/AUTHOR] [cat/CATEGORY] [cond/CONDITION] [loc/LOCATION] " + + "[note/NOTE]"; + + public static final String INVALID_FORMAT_UPDATE_BOOK_NO_UPDATES = "Invalid format for update-book.\n" + + "No fields provided to update."; + + public static final String INVALID_FORMAT_UPDATE_BOOK_DUPLICATE_PREFIX = "Invalid format for update-book. " + + "Duplicate prefixes! \n" + + "Expected format: update-book BOOK_TITLE [a/AUTHOR] [cat/CATEGORY] [cond/CONDITION] [loc/LOCATION] " + + "[note/NOTE]"; + + public static final String INVALID_FORMAT_UPDATE_TITLE = "Invalid format for update-title.\n" + + "Expected format: update-title BOOK_TITLE new/NEW_TITLE"; + + public static final String INVALID_FORMAT_UPDATE_TITLE_DUPLICATE_PREFIX = "Invalid format for update-title. " + + "Duplicate prefixes! \n" + + "Expected format: update-title BOOK_TITLE new/NEW_TITLE"; + + public static final String INVALID_FORMAT_SAME_TITLE = "Invalid format for update-title.\n" + + "Titles entered are the same."; + + public static final String INVALID_FORMAT_DUPLICATE_TITLE = "Invalid format for update-title.\n" + + "There already is a book of that title."; + + public static final String INVALID_FORMAT_REMOVE_BOOK = "Invalid format for remove-book.\n" + + "Expected format: remove-book BOOK_TITLE"; + + public static final String INVALID_FORMAT_ADD_LOAN = "Invalid format for add-loan.\n" + + "Expected format: add-loan BOOK_TITLE n/BORROWER_NAME d/RETURN_DATE p/PHONE_NUMBER e/EMAIL"; + + public static final String INVALID_FORMAT_ADD_LOAN_DUPLICATE_PREFIX = "Invalid format for add-loan. " + + "Duplicate prefixes! \n" + + "Expected format: add-loan BOOK_TITLE n/BORROWER_NAME d/RETURN_DATE p/PHONE_NUMBER e/EMAIL"; + + public static final String INVALID_PHONE_NUMBER_ADD_LOAN = "Invalid argument for add-loan.\n" + + "Invalid phone number! \n"+ + "Expected format: Singapore numbers only (8 numbers, starting with 9, 8 or 6)"; + + public static final String INVALID_EMAIL_ADD_LOAN = "Invalid argument for add-loan.\n" + + "Invalid email"; + + public static final String INVALID_FORMAT_EDIT_LOAN = "Invalid format for edit-loan.\n" + + "Expected format: edit-loan BOOK_TITLE [n/BORROWER_NAME] [d/RETURN_DATE] [p/PHONE_NUMBER] [e/EMAIL]"; + + public static final String INVALID_FORMAT_EDIT_LOAN_DUPLICATE_PREFIX = "Invalid format for edit-loan. " + + "Duplicate prefixes! \n" + + "Expected format: edit-loan BOOK_TITLE [n/BORROWER_NAME] [d/RETURN_DATE] [p/PHONE_NUMBER] " + + "[e/EMAIL]"; + + public static final String INVALID_FORMAT_EDIT_LOAN_NO_EDITS = "Invalid format for edit-loan.\n" + + "No fields provided for edits"; + + public static final String INVALID_PHONE_NUMBER_EDIT_LOAN = "Invalid argument for edit-loan.\n" + + "Invalid phone number! \n"+ + "Expected format: Singapore numbers only (8 numbers, starting with 9, 8 or 6)"; + + public static final String INVALID_EMAIL_EDIT_LOAN = "Invalid argument for edit-loan.\n" + + "Invalid email"; + + public static final String INVALID_FORMAT_DELETE_LOAN = "Invalid format for delete-loan.\n" + + "Expected format: delete-loan BOOK_TITLE"; + + public static final String INVALID_FORMAT_ADD_NOTE = "Invalid format for add-note.\n" + + "Expected format: add-note BOOK_TITLE note/NOTE"; + + public static final String INVALID_FORMAT_DELETE_NOTE = "Invalid format for delete-note.\n" + + "Expected format: delete-note BOOK_TITLE"; + + public static final String INVALID_FORMAT_SEARCH_TITLE = "Invalid format for search-title.\n" + + "Expected format: search-title KEYWORD"; + + public static final String INVALID_FORMAT_LIST_CATEGORY = "Invalid format for list-category.\n" + + "Expected format: list-category CATEGORY"; + + public static final String INVALID_FORMAT_UPDATE_NOTE = "Invalid format for update-note. \n" + + "Expected format: update-note BOOK_TITLE note/NOTE"; + + private ErrorMessages() { + } //private constructor to prevent instantiation +} diff --git a/src/main/java/bookkeeper/exceptions/IncorrectFormatException.java b/src/main/java/bookkeeper/exceptions/IncorrectFormatException.java new file mode 100644 index 0000000000..5363c9b471 --- /dev/null +++ b/src/main/java/bookkeeper/exceptions/IncorrectFormatException.java @@ -0,0 +1,7 @@ +package bookkeeper.exceptions; + +public class IncorrectFormatException extends Exception { + public IncorrectFormatException(String message) { + super(message); + } +} diff --git a/src/main/java/bookkeeper/exceptions/InvalidArgumentException.java b/src/main/java/bookkeeper/exceptions/InvalidArgumentException.java new file mode 100644 index 0000000000..678c085d91 --- /dev/null +++ b/src/main/java/bookkeeper/exceptions/InvalidArgumentException.java @@ -0,0 +1,7 @@ +package bookkeeper.exceptions; + +public class InvalidArgumentException extends Exception { + public InvalidArgumentException(String message) { + super(message); + } +} diff --git a/src/main/java/bookkeeper/list/BookList.java b/src/main/java/bookkeeper/list/BookList.java new file mode 100644 index 0000000000..080809ecc0 --- /dev/null +++ b/src/main/java/bookkeeper/list/BookList.java @@ -0,0 +1,82 @@ +package bookkeeper.list; + +import bookkeeper.model.Book; +import bookkeeper.ui.Formatter; +import bookkeeper.model.Category; + +import java.util.ArrayList; + +public class BookList { + private final ArrayList bookList; + private final String listName; + + public BookList(String listName, ArrayList bookList) { + this.listName = listName; + this.bookList = bookList; + } + + public String getListName() { + return listName; + } + + public ArrayList getBookList() { + return bookList; + } + + public void addBook(Book book) { + bookList.add(book); + } + + public Book searchBook(String title) { + for (Book book : bookList) { + if (book.getTitle().equals(title)) { + return book; + } + } + return null; + } + + public ArrayList findBooksByKeyword(String keyword) { + ArrayList filteredBookList = new ArrayList<>(); + for (Book book : bookList) { + String titleLowerCase = book.getTitle().toLowerCase(); + String keywordLowerCase = keyword.toLowerCase(); + if (titleLowerCase.contains(keywordLowerCase)) { + filteredBookList.add(book); + } + } + return filteredBookList; + } + + public ArrayList findBooksByCategory(String category) throws IllegalArgumentException { + ArrayList filteredBookList = new ArrayList<>(); + + // Normalize the input category string to a Category enum + Category targetCategory = Category.fromString(category); + for (Book book : bookList) { + if (book.getCategory() == targetCategory) { // Compare using the enum value + filteredBookList.add(book); + } + } + // Handle invalid category input (optional) + return filteredBookList; + } + + public void removeBook(Book book) { + bookList.remove(book); + } + + /** + * Prints all books in the bookList. + * First prints the book title, followed by the remaining attributes indented. + */ + public void viewBookList() { + + if (bookList.isEmpty()) { + Formatter.printBorderedMessage("Book List Empty!"); + return; + } + + Formatter.printBookList(bookList); + } +} diff --git a/src/main/java/bookkeeper/list/LoanList.java b/src/main/java/bookkeeper/list/LoanList.java new file mode 100644 index 0000000000..92044ae3a1 --- /dev/null +++ b/src/main/java/bookkeeper/list/LoanList.java @@ -0,0 +1,80 @@ +package bookkeeper.list; + +import bookkeeper.model.Book; +import bookkeeper.model.Loan; +import bookkeeper.storage.LoggerConfig; +import bookkeeper.ui.Formatter; + +import java.util.ArrayList; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LoanList { + private static final Logger logger = Logger.getLogger(LoanList.class.getName()); + private final ArrayList loanList; + private final String listName; + + public LoanList(String listName, ArrayList loanList) { + LoggerConfig.configureLogger(logger); + this.listName = listName; + this.loanList = loanList; + logger.log(Level.INFO, "LoanList created with name: {0}", listName); + } + + public ArrayList getLoanList() { + return loanList; + } + + public String getListName() { + return listName; + } + + public void addLoan(Loan loan) { + assert loan != null : "Loan cannot be null"; + loanList.add(loan); + loan.getBook().setOnLoan(true); + logger.log(Level.INFO, "Loan added: {0}", loan); + } + + public void deleteLoan(Loan loan) { + assert loan != null : "Loan cannot be null"; + if (loanList.remove(loan)) { + loan.getBook().setOnLoan(false); + logger.log(Level.INFO, "Loan removed: {0}", loan); + } else { + logger.log(Level.WARNING, "Attempted to remove a loan that does not exist: {0}", loan); + } + } + + public void viewLoanList() { + if (loanList.isEmpty()) { + Formatter.printBorderedMessage("Loan List Empty!"); + return; + } + Formatter.printLoanList(loanList); + } + + public Loan findLoan(Book book) { + for (Loan loan : loanList) { + Book bookIter = loan.getBook(); + if (bookIter.equals(book)) { + return loan; + } + } + return null; + } + + public Loan findLoanByIndex(int index) { + try { + return (loanList.get(index - 1)); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + public void removeLoansByBook(Book book) { + assert book != null : "Book cannot be null"; + loanList.removeIf(loan -> loan.getBook().equals(book)); + logger.log(Level.INFO, "Removed all loans associated with book: {0}", book.getTitle()); + } +} diff --git a/src/main/java/bookkeeper/logic/InputHandler.java b/src/main/java/bookkeeper/logic/InputHandler.java new file mode 100644 index 0000000000..519a0b5e50 --- /dev/null +++ b/src/main/java/bookkeeper/logic/InputHandler.java @@ -0,0 +1,476 @@ +package bookkeeper.logic; + +import java.util.Scanner; +import java.util.logging.Logger; + +import bookkeeper.list.BookList; +import bookkeeper.list.LoanList; +import bookkeeper.storage.LoggerConfig; +import bookkeeper.storage.Storage; +import bookkeeper.exceptions.BookNotFoundException; +import bookkeeper.exceptions.IncorrectFormatException; +import bookkeeper.exceptions.InvalidArgumentException; +import bookkeeper.exceptions.ErrorMessages; +import bookkeeper.model.Book; +import bookkeeper.model.Loan; +import bookkeeper.ui.Formatter; + +public class InputHandler { + private static final Logger logger = Logger.getLogger(InputHandler.class.getName()); + private final BookList bookList; + private final LoanList loanList; + + public InputHandler() { + LoggerConfig.configureLogger(logger); // Configure the logger + this.bookList = new BookList("Inventory", Storage.loadInventory()); + this.loanList = new LoanList("Loan List", Storage.loadLoans(this.bookList)); + logger.info("InputHandler initialized"); + } + + public void askInput() { + boolean isAskingInput = true; + String userInputLine; + Scanner scanner = new Scanner(System.in); + + displayHelp(); + + while (isAskingInput) { + + System.out.println("Enter a command:"); + + if (!scanner.hasNextLine()) { // Prevents NoSuchElementException + break; + } + userInputLine = scanner.nextLine(); + if (userInputLine.isEmpty()) { + Formatter.printBorderedMessage("Please enter a command"); + } else if (userInputLine.contains("|")){ + Formatter.printBorderedMessage("Please do not use \"|\" in your inputs"); + } else { + Storage.validateStorage(bookList, loanList); + try { + String[] commandArgs = InputParser.extractCommandArgs(userInputLine); + assert commandArgs.length > 0 : "commandArgs should have at least one element"; + + switch (commandArgs[0]) { + case "add-book": + addBook(commandArgs); + break; + case "view-inventory": + bookList.viewBookList(); + break; + case "remove-book": + removeBook(commandArgs); + break; + case "add-loan": + addLoan(commandArgs); + break; + case "delete-loan": + deleteLoan(commandArgs); + break; + case "edit-loan": + editLoan(commandArgs); + break; + case "view-loans": + loanList.viewLoanList(); + break; + case "update-book": + updateBook(commandArgs); + break; + case "search-title": + searchTitle(commandArgs); + break; + case "list-category": + listCategory(commandArgs); + break; + case "update-title": + updateTitle(commandArgs); + break; + case "delete-note": + deleteNote(commandArgs); + break; + case "help": + displayHelp(); + break; + case "exit": + Formatter.printBorderedMessage("Exiting BookKeeper..."); + isAskingInput = false; + break; + default: + throw new IncorrectFormatException("Unknown command: " + commandArgs[0]); + } + } catch (IncorrectFormatException | BookNotFoundException | InvalidArgumentException e) { + Formatter.printBorderedMessage(e.getMessage()); + } + } + } + } + + + private void displayHelp() { + Formatter.printSimpleMessage(""" + ------------------------------------------------------------------------------------------------ + | Add Book: | + | add-book BOOK_TITLE a/AUTHOR cat/CATEGORY cond/CONDITION loc/LOCATION [note/NOTE] | + |----------------------------------------------------------------------------------------------| + | Remove Book: | + | remove-book BOOK_TITLE | + |----------------------------------------------------------------------------------------------| + | Update Book: | + | update-book BOOK_TITLE [a/AUTHOR] [cat/CATEGORY] [cond/CONDITION] [loc/LOCATION] [note/NOTE] | + |----------------------------------------------------------------------------------------------| + | Update Title: | + | update-title BOOK_TITLE new/NEW_TITLE | + |----------------------------------------------------------------------------------------------| + | Search Book: | + | search-title KEYWORD | + |----------------------------------------------------------------------------------------------| + | View Inventory: | + | view-inventory | + |----------------------------------------------------------------------------------------------| + | List Category: | + | list-category CATEGORY | + |----------------------------------------------------------------------------------------------| + | Add Loan: | + | add-loan BOOK_TITLE n/BORROWER_NAME d/RETURN_DATE p/PHONE_NUMBER e/EMAIL | + |----------------------------------------------------------------------------------------------| + | Delete Loan: | + | delete-loan BOOK_TITLE | + |----------------------------------------------------------------------------------------------| + | Edit Loan: | + | edit-loan BOOK_TITLE [n/BORROWER_NAME] [d/RETURN_DATE] [p/PHONE_NUMBER] [e/EMAIL] | + |----------------------------------------------------------------------------------------------| + | View Loans: | + | view-loans | + |----------------------------------------------------------------------------------------------| + | Delete Note: | + | delete-note | + |----------------------------------------------------------------------------------------------| + | Display Help: | + | help | + |----------------------------------------------------------------------------------------------| + | Exit Program: | + | exit | + ------------------------------------------------------------------------------------------------ + """); + } + + /** + * Adds loan object to loanList by first extracting arguments needed to create loan object. + * Before adding, book has to exist in bookList and is available for loan. + * + * @param commandArgs The parsed command arguments. + * @throws IncorrectFormatException If the input format is invalid. + * @throws BookNotFoundException If the book is not found in the inventory. + * @throws BookNotFoundException If the book is already on loan. + * @throws InvalidArgumentException If any argument is invalid. + */ + private void addLoan(String[] commandArgs) throws IncorrectFormatException, BookNotFoundException, + InvalidArgumentException { + if (commandArgs.length < 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_LOAN); + } + try { + String[] loanArgs = InputParser.extractAddLoanArgs(commandArgs[1]); + Book loanedBook = bookList.searchBook(loanArgs[0]); + if (loanedBook == null) { + Formatter.printBorderedMessage("Book not found in inventory: " + loanArgs[0]); + } else if (loanedBook.isOnLoan()) { + assert loanedBook.getTitle() != null : "Loaned book must have a title"; + Formatter.printBorderedMessage("The book " + loanArgs[0] + " is currently out on loan."); + } else { + Loan loan = new Loan(loanedBook, loanArgs[1], loanArgs[2], loanArgs[3], loanArgs[4]); + loanList.addLoan(loan); + loanedBook.setOnLoan(true); + Formatter.printBorderedMessage("Loan added successfully for book: " + loanedBook.getTitle()); + Storage.saveLoans(loanList); + Storage.saveInventory(bookList); //to update the onLoan status of the book in inventory + } + } catch (IllegalArgumentException e) { + Formatter.printBorderedMessage(e.getMessage()); + } + } + + /** + * Deletes the note from a specified book. + * + * @param commandArgs The parsed command arguments. + * @throws IncorrectFormatException If the input format is invalid. + * @throws BookNotFoundException If the book is not found in the inventory. + */ + private void deleteNote(String[] commandArgs) throws IncorrectFormatException, BookNotFoundException { + if (commandArgs.length != 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_DELETE_NOTE); + } + + String bookTitle = commandArgs[1].trim(); + + Book book = bookList.searchBook(bookTitle); + if (book == null) { + throw new BookNotFoundException("Book not found in inventory: " + bookTitle); + } + + if (book.getNote().isEmpty()) { + Formatter.printBorderedMessage("No note exists for the book: " + bookTitle); + return; + } + + book.setNote(""); + Formatter.printBorderedMessage("Note deleted for book: " + bookTitle); + Storage.saveInventory(bookList); + } + + /** + * Extract arguments needed to create book object and adds book object to book list. + * + * @param commandArgs The parsed command arguments. + * @throws IncorrectFormatException If the input format is invalid. + */ + private void addBook(String[] commandArgs) throws IncorrectFormatException, IllegalArgumentException { + if (commandArgs.length < 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_BOOK); + } + String[] bookArgs = InputParser.extractAddBookArgs(commandArgs[1]); + assert bookArgs.length >= 5 : "Book arguments should contain at least 5 elements"; + + String bookTitle = bookArgs[0]; //Already trimmed whitespaces in extractAddBookArgs + + // Check if book already exists in the inventory + if (bookList.searchBook(bookTitle) != null) { + Formatter.printBorderedMessage("Book already exists in inventory: " + bookTitle); + return; + } + + // Handle optional note + String note = bookArgs.length == 6 ? bookArgs[5] : ""; // Default to empty string if note is not provided + + try { + // Add the new book to the book list + Book newBook = new Book(bookTitle, bookArgs[1], bookArgs[2], bookArgs[3], bookArgs[4], note); + bookList.addBook(newBook); + Formatter.printBorderedMessage("New book added: " + newBook.getTitle()); + Storage.saveInventory(bookList); + } catch (IllegalArgumentException e) { + Formatter.printBorderedMessage(e.getMessage()); + } + + } + + /** + * Extract arguments needed to remove book object and removes book object from book list. + * + * @param commandArgs The parsed command arguments. + * @throws IncorrectFormatException If the input format is invalid. + * @throws BookNotFoundException If the book is not found in the inventory. + */ + private void removeBook(String[] commandArgs) throws IncorrectFormatException, BookNotFoundException { + if (commandArgs.length != 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_REMOVE_BOOK); + } + String bookTitle = commandArgs[1].trim(); + Book toRemove = bookList.searchBook(bookTitle); + + if (toRemove == null) { + Formatter.printBorderedMessage("Book not found in inventory: " + bookTitle); + } else { + assert toRemove.getTitle() != null : "Book to remove must have a valid title"; + loanList.removeLoansByBook(toRemove); + bookList.removeBook(toRemove); + Formatter.printBorderedMessage("Removed book: " + toRemove.getTitle()); + Storage.saveInventory(bookList); + } + } + + /** + * Extract arguments needed to delete loan and delete loan + * Checks if book and loan exist before deleting + * + * @param commandArgs The parsed command arguments. + * @throws IncorrectFormatException If the input format is invalid. + * @throws BookNotFoundException If the book is not found in the inventory. + */ + private void deleteLoan(String[] commandArgs) throws IncorrectFormatException, BookNotFoundException { + if (commandArgs.length < 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_DELETE_LOAN); + } + try { + String bookTitle = commandArgs[1]; + Book loanedBook = bookList.searchBook(bookTitle); + Loan loan = loanList.findLoan(loanedBook); + if (loanedBook == null) { + Formatter.printBorderedMessage("Book not found in inventory: " + bookTitle); + } else if (!loanedBook.isOnLoan()) { + Formatter.printBorderedMessage("The book " + bookTitle + " is not currently out on loan."); + } else if (loan == null) { + Formatter.printBorderedMessage("No such loan with book title " + bookTitle); + } else { + loanList.deleteLoan(loan); + loanedBook.setOnLoan(false); + Formatter.printBorderedMessage("Loan deleted successfully for book: " + bookTitle); + Storage.saveLoans(loanList); + Storage.saveInventory(bookList); //to update the onLoan status of the book in inventory + } + } catch (IllegalArgumentException e) { + Formatter.printBorderedMessage(e.getMessage()); + } + + } + + /** + * Prints out all books in BookList that contains the keyword. + * + * @param commandArgs The parsed command arguments + * @throws IncorrectFormatException If the input format is invalid + */ + private void searchTitle(String[] commandArgs) throws IncorrectFormatException { + if (commandArgs.length < 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_SEARCH_TITLE); + } + + String keyword = commandArgs[1].trim(); + Formatter.printBookList(bookList.findBooksByKeyword(keyword)); + } + + /** + * Prints out all books in BookList that is of the provided category. + * + * @param commandArgs The parsed command arguments + * @throws IncorrectFormatException If the input format is invalid + */ + private void listCategory(String[] commandArgs) throws IncorrectFormatException { + if (commandArgs.length < 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_LIST_CATEGORY); + } + String category = commandArgs[1].trim(); + try { + Formatter.printBookList(bookList.findBooksByCategory(category)); + } catch (IllegalArgumentException e) { + Formatter.printBorderedMessage("Invalid Category: " + category); + } + } + + /** + * Updates details of an existing book. + * + * @param commandArgs The parsed command arguments. + * @throws IncorrectFormatException If the input format is invalid. + * @throws BookNotFoundException If the book is not found in the inventory. + * @throws IllegalArgumentException If the category or condition is invalid. + */ + private void updateBook(String[] commandArgs) throws IncorrectFormatException, BookNotFoundException, + IllegalArgumentException { + if (commandArgs.length < 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_BOOK); + } + String[] bookArgs = InputParser.extractUpdateBookArgs(commandArgs[1]); + assert bookArgs.length >= 5 : "Book arguments should contain at least 5 elements"; + + String bookTitle = bookArgs[0]; + String author = bookArgs[1]; + String category = bookArgs[2]; + String condition = bookArgs[3]; + String location = bookArgs[4]; + String note = bookArgs[5]; + + // Check if book already exists in the inventory + Book book = bookList.searchBook(bookTitle); + if (book == null) { + throw new BookNotFoundException("Book not found in inventory: " + bookTitle); + } + + if ((author == null || author.isBlank()) && + (category == null || category.isBlank()) && + (condition == null || condition.isBlank()) && + (location == null || location.isBlank()) && + (note == null || note.isBlank())) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_BOOK_NO_UPDATES); + } + + try { + book.setBookFields(author, category, condition, location, note); + Formatter.printBorderedMessage("Book Updated:\n" + book); + Storage.saveInventory(bookList); + } catch + (IllegalArgumentException e) { + Formatter.printBorderedMessage(e.getMessage()); + } + } + + /** + * Updates the title of an existing book. + * + * @param commandArgs The parsed command arguments. + * @throws IncorrectFormatException If the input format is invalid. + * @throws BookNotFoundException If the book is not found in the inventory. + */ + + private void updateTitle(String[] commandArgs) throws IncorrectFormatException, BookNotFoundException, + IllegalArgumentException { + if (commandArgs.length < 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_TITLE); + } + String[] updateTitleArgs = InputParser.extractUpdateTitleArgs(commandArgs[1]); + String oldTitle = updateTitleArgs[0]; + String newTitle = updateTitleArgs[1]; + + if(oldTitle.equals(newTitle)){ + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_SAME_TITLE); + } + + if(bookList.searchBook(newTitle) != null) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_DUPLICATE_TITLE); + } + + // Check if book already exists in the inventory + Book book = bookList.searchBook(oldTitle); + if (book == null) { + throw new BookNotFoundException("Book not found in inventory: " + oldTitle); + } + + book.setTitle(newTitle); + Formatter.printBorderedMessage("Book Updated:\n" + book); + Storage.saveInventory(bookList); + Storage.saveLoans(loanList); + } + + + private void editLoan(String[] commandArgs) throws IncorrectFormatException, BookNotFoundException, + InvalidArgumentException { + if (commandArgs.length < 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_EDIT_LOAN); + } + String[] editLoanArgs = InputParser.extractEditLoanArgs(commandArgs[1]); + assert editLoanArgs.length == 5 : "Book arguments should contain 5 elements"; + + String bookTitle = editLoanArgs[0]; + String borrowerName = editLoanArgs[1]; + String returnDate = editLoanArgs[2]; + String phoneNumber = editLoanArgs[3]; + String email = editLoanArgs[4]; + + // Check if book already exists in the inventory + Book book = bookList.searchBook(bookTitle); + Loan loan = loanList.findLoan(book); + + if (book == null) { + throw new BookNotFoundException("Book not found in inventory: " + bookTitle); + } else if (!book.isOnLoan()) { + Formatter.printBorderedMessage("The book " + bookTitle + " is not currently out on loan."); + } else { + if ((borrowerName == null || borrowerName.isBlank()) && + (returnDate == null || returnDate.isBlank()) && + (phoneNumber == null || phoneNumber.isBlank()) && + (email == null || email.isBlank())) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_EDIT_LOAN_NO_EDITS); + } + + try { + loan.setLoanFields(borrowerName, returnDate, phoneNumber, email); + Formatter.printBorderedMessage("Loan Updated:\n" + loan); + Storage.saveLoans(loanList); + } catch (IllegalArgumentException e) { + Formatter.printBorderedMessage(e.getMessage()); + } + } + } +} diff --git a/src/main/java/bookkeeper/logic/InputParser.java b/src/main/java/bookkeeper/logic/InputParser.java new file mode 100644 index 0000000000..5bdd2359b3 --- /dev/null +++ b/src/main/java/bookkeeper/logic/InputParser.java @@ -0,0 +1,356 @@ +package bookkeeper.logic; + +import bookkeeper.exceptions.IncorrectFormatException; +import bookkeeper.exceptions.InvalidArgumentException; +import bookkeeper.exceptions.ErrorMessages; + +import java.util.HashSet; +import java.util.Set; + +public class InputParser { + + public static String[] extractCommandArgs(String input) throws IncorrectFormatException { + String[] commandArgs = input.trim().split(" ", 2); + if (commandArgs.length < 1) { + throw new IncorrectFormatException("Invalid command format.\nExpected: COMMAND [ARGUMENTS]"); + } + return commandArgs; + } + + + /** + * Extracts the arguments for the add-book command. + *

+ * The expected input format is: + * add-book BOOK_TITLE a/AUTHOR cat/CATEGORY cond/CONDITION [note/NOTE] + * Example: "Cheese Chronicles a/Mouse cat/Adventure cond/Good" + * + * @param input The user input for the add-book command. + * @return An array of strings containing the arguments for the add-book command: + * [0] - Book title + * [1] - Author + * [2] - Category + * [3] - Condition + * [4] - Location + * [5] - Note (Optional) + * @throws IncorrectFormatException if the input format is invalid. + */ + public static String[] extractAddBookArgs(String input) throws IncorrectFormatException { + String bookTitle = null; + String author = null; + String category = null; + String condition = null; + String location = null; + String note = ""; + + Set processedPrefixes = new HashSet<>(); + String[] parts = input.trim().split(" (?=\\b(?:a/\\s*|cat/\\s*|cond/\\s*|loc/\\s*|note/\\s*)\\b)"); + + + if (parts.length == 0 || parts[0].startsWith("a/") || parts[0].startsWith("cat/") || + parts[0].startsWith("cond/") || parts[0].startsWith("loc/") || parts[0].startsWith("note/")) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_BOOK); + } + bookTitle = parts[0].trim(); + + for (int i = 1; i < parts.length; i++) { + String part = parts[i].trim(); + String prefix = part.substring(0, part.indexOf("/") + 1); + + if (processedPrefixes.contains(prefix)) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_BOOK_DUPLICATE_PREFIX); + } + processedPrefixes.add(prefix); + + if (part.startsWith("a/")) { + author = part.substring(2).trim(); + } else if (part.startsWith("cat/")) { + category = part.substring(4).trim(); + } else if (part.startsWith("cond/")) { + condition = part.substring(5).trim(); + } else if (part.startsWith("loc/")) { + location = part.substring(4).trim(); + } else if (part.startsWith("note/")) { + note = part.substring(5).trim(); + } else { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_BOOK); + } + } + + if (bookTitle.isEmpty() || author == null || author.isEmpty() || + category == null || category.isEmpty() || + condition == null || condition.isEmpty() || + location == null || location.isEmpty()) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_BOOK); + } + + return new String[]{bookTitle, author, category, condition, location, note}; + } + + + /** + * Extracts the arguments for the update-title command. + *

+ * The expected input format is: + * update-title BOOK_TITLE new/NEW_TITL + * Example: "Cheese Chronicles new/Cheese Adventures" + * + * @param input The user input for the update-title command. + * @return An array of strings containing the arguments for the update-book command: + * [0] - Old title + * [1] - New title + * @throws IncorrectFormatException if the input format is invalid. + */ + public static String[] extractUpdateTitleArgs(String input) throws IncorrectFormatException { + String[] parts = input.trim().split("\\s+(?=\\w+/|$)"); + Set processedPrefixes = new HashSet<>(); + + if (parts.length == 0 || parts[0].startsWith("new/")) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_TITLE); + } + + String oldTitle = parts[0].trim(); + String newTitle = null; + + for (int i = 1; i < parts.length; i++) { + String part = parts[i].trim(); + String prefix = part.substring(0, part.indexOf("/") + 1); + + if (processedPrefixes.contains(prefix)) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_TITLE_DUPLICATE_PREFIX); + } + processedPrefixes.add(prefix); + + if (part.startsWith("new/")) { + newTitle = part.substring(4).trim(); + } else { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_TITLE); + } + } + + if (oldTitle.isEmpty() || newTitle == null || newTitle.isEmpty()) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_TITLE); + } + return new String[]{oldTitle, newTitle}; + } + + /** + * Extracts the arguments for the update-book command. + *

+ * The expected input format is: + * BOOK_TITLE a/AUTHOR cat/CATEGORY cond/CONDITION [note/NOTE] + * Example: "Cheese Chronicles a/Mouse cat/Adventure cond/Good" + * + * @param input The user input for the update-book command. + * @return An array of strings containing the arguments for the update-book command: + * [0] - Book title + * [1] - Author + * [2] - Category + * [3] - Condition + * [4] - Location + * [5] - Note (Optional) + * @throws IncorrectFormatException if the input format is invalid. + */ + public static String[] extractUpdateBookArgs(String input) throws IncorrectFormatException { + String bookTitle = null; + String author = null; + String category = null; + String condition = null; + String location = null; + String note = null; + + Set processedPrefixes = new HashSet<>(); + String[] parts = input.trim().split(" (?=\\b(?:a/\\s*|cat/\\s*|cond/\\s*|loc/\\s*|note/\\s*)\\b)"); + + if (parts.length == 0 || parts[0].startsWith("a/") || parts[0].startsWith("cat/") || + parts[0].startsWith("cond/") || parts[0].startsWith("loc/") || parts[0].startsWith("note/")) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_BOOK); + } + bookTitle = parts[0].trim(); + + for (int i = 1; i < parts.length; i++) { + String part = parts[i].trim(); + String prefix = part.substring(0, part.indexOf("/") + 1); + + if (processedPrefixes.contains(prefix)) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_BOOK_DUPLICATE_PREFIX); + } + processedPrefixes.add(prefix); + + if (part.startsWith("a/")) { + author = part.substring(2).trim(); + } else if (part.startsWith("cat/")) { + category = part.substring(4).trim(); + } else if (part.startsWith("cond/")) { + condition = part.substring(5).trim(); + } else if (part.startsWith("loc/")) { + location = part.substring(4).trim(); + } else if (part.startsWith("note/")) { + note = part.substring(5).trim(); + } else { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_BOOK); + } + } + + if (bookTitle.isEmpty()) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_BOOK); + } + + return new String[]{bookTitle, author, category, condition, location, note}; + } + + /** + * Extracts the arguments for the add-loan command. + *

+ * The expected input format is: BOOK_TITLE n/BORROWER_NAME d/RETURN_DATE p/PHONE_NUMBER e/EMAIL + * Example: "The Great Gatsby n/John Doe d/01-12-2026 p/1234567890 e/johndoe@example.com" + * + * @param input The user input for the add-loan command. + * @return An array of strings containing the arguments for the add-loan command: + * [0] - Book title + * [1] - Borrower's name + * [2] - Return date + * [3] - Phone number + * [4] - Email + * @throws IncorrectFormatException if the input format is invalid. + * @throws InvalidArgumentException if the input argument is invalid. + */ + public static String[] extractAddLoanArgs(String input) throws IncorrectFormatException, InvalidArgumentException { + // Initialize variables for each argument + String bookTitle = null; + String borrowerName = null; + String returnDate = null; + String phoneNumber = null; + String email = null; + + // Set to track processed prefixes + Set processedPrefixes = new HashSet<>(); + + // Split the input into parts based on spaces + String[] parts = input.trim().split(" (?=\\b(?:n/\\s*|d/\\s*|p/\\s*|e/\\s*)\\b)"); + + // Validate and extract the first argument as the book title + if (parts.length == 0 || parts[0].startsWith("n/") || parts[0].startsWith("d/") || + parts[0].startsWith("p/") || parts[0].startsWith("e/")) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_LOAN); + } + bookTitle = parts[0].trim(); + + // Iterate through the remaining parts and extract arguments based on prefixes + for (int i = 1; i < parts.length; i++) { + String part = parts[i].trim(); + String prefix = part.substring(0, 2); // Extract the prefix (e.g., "n/", "d/") + + // Check for duplicate prefixes + if (processedPrefixes.contains(prefix)) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_LOAN_DUPLICATE_PREFIX); + } + processedPrefixes.add(prefix); // Mark the prefix as processed + + // Extract the argument based on the prefix + if (part.startsWith("n/")) { + borrowerName = part.substring(2).trim(); + } else if (part.startsWith("d/")) { + returnDate = part.substring(2).trim(); + } else if (part.startsWith("p/")) { + phoneNumber = part.substring(2).trim(); + if (!phoneNumber.matches("^[986][0-9]{7}$")) { + throw new InvalidArgumentException(ErrorMessages.INVALID_PHONE_NUMBER_ADD_LOAN); + } + } else if (part.startsWith("e/")) { + email = part.substring(2).trim(); + if (!email.matches("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) { + throw new InvalidArgumentException(ErrorMessages.INVALID_EMAIL_ADD_LOAN); + } + } + } + + // Validate required fields + if (bookTitle.isEmpty() || borrowerName == null || borrowerName.isEmpty() || + returnDate == null || returnDate.isEmpty() || + phoneNumber == null || phoneNumber.isEmpty() || + email == null || email.isEmpty()) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_LOAN); + } + + // Return all fields + return new String[]{bookTitle, borrowerName, returnDate, phoneNumber, email}; + } + + public static String[] extractAddNoteArgs(String input) throws IncorrectFormatException { + String[] splitInput = input.trim().split(" note/", 2); + + if (splitInput.length != 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_NOTE); + } + + String bookTitle = splitInput[0].trim(); + String note = splitInput[1].trim(); + + if (bookTitle.isBlank() || note.isBlank()) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_ADD_NOTE); + } + + return new String[]{bookTitle, note}; + } + + public static String[] extractUpdateNoteArgs(String input) throws IncorrectFormatException { + String[] splitInput = input.trim().split(" note/", 2); + + if (splitInput.length != 2) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_NOTE); + } + + String bookTitle = splitInput[0].trim(); + String note = splitInput[1].trim(); + + if (bookTitle.isBlank() || note.isBlank()) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_UPDATE_NOTE); + } + + return new String[]{bookTitle, note}; + } + + public static String[] extractEditLoanArgs(String input) throws IncorrectFormatException, InvalidArgumentException { + String title = null; + String borrowerName = null; + String returnDate = null; + String phoneNumber = null; + String email = null; + + Set processedPrefixes = new HashSet<>(); + String[] parts = input.trim().split(" (?=\\b(?:n/\\s*|d/\\s*|p/\\s*|e/\\s*)\\b)"); + + title = parts[0].trim(); + + for (int i = 1; i < parts.length; i++) { + String part = parts[i].trim(); + String prefix = part.substring(0, part.indexOf("/") + 1); + + if (processedPrefixes.contains(prefix)) { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_EDIT_LOAN_DUPLICATE_PREFIX); + } + processedPrefixes.add(prefix); + + if (part.startsWith("n/")) { + borrowerName = part.substring(2).trim(); + } else if (part.startsWith("d/")) { + returnDate = part.substring(2).trim(); + } else if (part.startsWith("p/")) { + phoneNumber = part.substring(2).trim(); + if (!phoneNumber.matches("^[986][0-9]{7}$")) { + throw new InvalidArgumentException(ErrorMessages.INVALID_PHONE_NUMBER_EDIT_LOAN); + } + } else if (part.startsWith("e/")) { + email = part.substring(2).trim(); + if (!email.matches("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) { + throw new InvalidArgumentException(ErrorMessages.INVALID_EMAIL_EDIT_LOAN); + } + } else { + throw new IncorrectFormatException(ErrorMessages.INVALID_FORMAT_EDIT_LOAN); + } + } + + return new String[]{title, borrowerName, returnDate, phoneNumber, email}; + } +} diff --git a/src/main/java/bookkeeper/model/Book.java b/src/main/java/bookkeeper/model/Book.java new file mode 100644 index 0000000000..f7cfac4b5f --- /dev/null +++ b/src/main/java/bookkeeper/model/Book.java @@ -0,0 +1,128 @@ +package bookkeeper.model; + +public class Book { + private String title; + private String author; + private Category category; + private Condition condition; + private boolean onLoan; + private String location; + private String note; + + // Constructor with note + public Book(String title, String author, String category, String condition, String location, String note) { + this.title = title; + this.author = author; + this.category = Category.fromString(category); + this.condition = Condition.fromString(condition); + this.note = note; + this.location = location; + this.onLoan = false; + } + + // Constructor with optional note + public Book(String title, String author, String category, String condition, String location) { + this(title, author, category, condition, location, ""); // Default note is an empty string + } + + // Getters + public String getTitle() { + return title; + } + + public String getAuthor() { + return author; + } + + public Category getCategory() { + return category; + } + + public Condition getCondition() { + return condition; + } + + public boolean isOnLoan() { + return onLoan; + } + + public String getLocation() { + return location; + } + + public String getNote() { + return note; + } + + //Setters + + public void setTitle(String title) { + this.title = title; + } + + public void setCondition(String condition){ + this.condition = Condition.fromString(condition); + } + + public void setCategory(String category){ + this.category = Category.fromString(category); + } + + public void setAuthor(String author){ + this.author = author; + } + + public void setNote(String note) { + this.note = note; + } + + public void setOnLoan(boolean onLoan) { + this.onLoan = onLoan; + } + + public void setLocation(String location) { + this.location = location; + } + + public void setBookFields(String author, String category, String condition, String location, String note) { + if(author != null){ + setAuthor(author); + } + if(category != null){ + setCategory(category); + } + if(condition != null){ + setCondition(condition); + } + if(location != null){ + setLocation(location); + } + if(note != null && !note.isBlank()){ + setNote(note); + } + } + + public String toFileString() { + String title = getTitle(); + String author = getAuthor(); + Category category = getCategory(); + Condition condition = getCondition(); + boolean onLoan = isOnLoan(); + String location = getLocation(); + String note = getNote(); + return title + " | " + author + " | " + category + + " | " + condition + " | " + onLoan + + " | " + location + " | " + note; + } + + @Override + public String toString() { + return "Title: " + getTitle() + System.lineSeparator() + + " Author: " + getAuthor() + System.lineSeparator() + + " Category: " + getCategory() + System.lineSeparator() + + " Condition: " + getCondition() + System.lineSeparator() + + " On Loan: " + isOnLoan() + System.lineSeparator() + + " Location: " + (isOnLoan() ? "Out on loan" : getLocation()) + System.lineSeparator() + + " Note: " + (note.isEmpty() ? "No notes available" : getNote()); + } +} diff --git a/src/main/java/bookkeeper/model/Category.java b/src/main/java/bookkeeper/model/Category.java new file mode 100644 index 0000000000..ca49cbffce --- /dev/null +++ b/src/main/java/bookkeeper/model/Category.java @@ -0,0 +1,32 @@ +package bookkeeper.model; + +public enum Category { + ROMANCE, ADVENTURE, ACTION, HORROR, MYSTERY, FICTION, NONFICTION, SCIFI, EDUCATION; + + public static Category fromString(String category) { + switch (category.toLowerCase()) { + case "romance": + return ROMANCE; + case "adventure": + return ADVENTURE; + case "action": + return ACTION; + case "horror": + return HORROR; + case "mystery": + return MYSTERY; + case "fiction": + return FICTION; + case "non-fiction": + case "nonfiction": + return NONFICTION; + case "scifi": + return SCIFI; + case "education": + return EDUCATION; + default: + throw new IllegalArgumentException("Invalid category: " + category + "\nValid categories are: " + + "romance, adventure, action, horror, mystery, fiction, nonfiction, scifi, education"); + } + } +} diff --git a/src/main/java/bookkeeper/model/Condition.java b/src/main/java/bookkeeper/model/Condition.java new file mode 100644 index 0000000000..80f5632f54 --- /dev/null +++ b/src/main/java/bookkeeper/model/Condition.java @@ -0,0 +1,19 @@ +package bookkeeper.model; + +public enum Condition { + GOOD, FAIR, POOR; + + public static Condition fromString(String condition) { + switch (condition.toLowerCase()) { + case "good": + return GOOD; + case "fair": + return FAIR; + case "poor": + return POOR; + default: + throw new IllegalArgumentException("Invalid condition: " + condition + + "\nValid conditions are: good, fair, poor"); + } + } +} diff --git a/src/main/java/bookkeeper/model/Loan.java b/src/main/java/bookkeeper/model/Loan.java new file mode 100644 index 0000000000..898bf34398 --- /dev/null +++ b/src/main/java/bookkeeper/model/Loan.java @@ -0,0 +1,164 @@ +package bookkeeper.model; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +public class Loan { + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + + private Book book; + private String borrowerName; + private LocalDate returnDate; + private String phoneNumber; + private String email; + + public Loan(Book book, String borrowerName, String returnDate, String phoneNumber, String email) { + this.book = book; + this.returnDate = parseAndValidateDate(returnDate); // Parse and validate the date + this.borrowerName = borrowerName; + this.phoneNumber = phoneNumber; + this.email = email; + } + + public Book getBook() { + return book; + } + + public String getTitle() { + return book.getTitle(); + } + + public LocalDate getReturnDate() { + return returnDate; + } + + public String getBorrowerName() { + return borrowerName; + } + + public String getPhoneNumber() { + return this.phoneNumber; + } + + public String getEmail() { + return this.email; + } + + public void setReturnDate(String returnDate) throws IllegalArgumentException { + this.returnDate = parseAndValidateDate(returnDate); // Parse and validate the date + } + + public void setBorrowerName(String borrowerName) { + this.borrowerName = borrowerName; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setLoanFields(String borrowerName, String returnDate, String phoneNumber, String email) { + if (borrowerName != null && !borrowerName.isEmpty()) { + setBorrowerName(borrowerName); + } + if (returnDate != null) { + setReturnDate(returnDate); + } + if (phoneNumber != null) { + setPhoneNumber(phoneNumber); + } + if (email != null) { + setEmail(email); + } + } + + public String toFileString() { + String title = getTitle(); + String borrowerName = getBorrowerName(); + String contactNumber = getPhoneNumber(); + String returnDateString = returnDate.format(DATE_FORMATTER); // Format LocalDate to DD-MM-YYYY + String email = getEmail(); + return title + " | " + borrowerName + " | " + returnDateString + + " | " + contactNumber + " | " + email; + } + + @Override + public String toString() { + return "Title: " + getTitle() + System.lineSeparator() + + " Borrower: " + getBorrowerName() + System.lineSeparator() + + " Return Date: " + returnDate.format(DATE_FORMATTER) + System.lineSeparator() + + " Contact Number: " + getPhoneNumber() + System.lineSeparator() + + " Email: " + getEmail(); + } + + private LocalDate parseAndValidateDate(String date) throws IllegalArgumentException { + try { + // Validate the format of the date string + if (!date.matches("\\d{2}-\\d{2}-\\d{4}")) { + throw new IllegalArgumentException("Invalid date format. Expected format: DD-MM-YYYY"); + } + + // Split the date string into day, month, and year + String[] dateParts = date.split("-"); + int day = Integer.parseInt(dateParts[0]); + int month = Integer.parseInt(dateParts[1]); + int year = Integer.parseInt(dateParts[2]); + + // Validate the day, month, and year + if (month < 1 || month > 12) { + throw new IllegalArgumentException("Invalid month: " + month + ". Month must be between 01 and 12."); + } + if (day < 1 || day > 31) { + throw new IllegalArgumentException("Invalid day: " + day + ". Day must be between 01 and 31."); + } + + // Check if the day is valid for the given month and year + if (!isValidDayForMonth(day, month, year)) { + throw new IllegalArgumentException("Invalid date: " + date + ". Please provide a valid date."); + } + + // Parse the date into a LocalDate + LocalDate parsedDate = LocalDate.parse(date, DATE_FORMATTER); + + // Validate that the date is not in the past + validateNotPastDate(parsedDate); + + return parsedDate; + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Invalid date format. Expected format: DD-MM-YYYY"); + } + } + + private boolean isValidDayForMonth(int day, int month, int year) { + switch (month) { + case 2: // February + if (isLeapYear(year)) { + return day <= 29; // Leap year + } else { + return day <= 28; // Non-leap year + } + case 4: + case 6: + case 9: + case 11: // Months with 30 days + return day <= 30; + default: // Months with 31 days + return day <= 31; + } + } + + private boolean isLeapYear(int year) { + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); + } + + private void validateNotPastDate(LocalDate date) throws IllegalArgumentException { + if (date.isBefore(LocalDate.now())) { + throw new IllegalArgumentException("The return date cannot be in the past. " + + "Please provide a valid future date."); + } + } +} diff --git a/src/main/java/bookkeeper/storage/LoggerConfig.java b/src/main/java/bookkeeper/storage/LoggerConfig.java new file mode 100644 index 0000000000..752c250eae --- /dev/null +++ b/src/main/java/bookkeeper/storage/LoggerConfig.java @@ -0,0 +1,28 @@ +package bookkeeper.storage; + +import java.io.IOException; +import java.util.logging.FileHandler; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +public class LoggerConfig { + private static FileHandler fileHandler; + + static { + try { + // Create a FileHandler that writes to "bookkeeper.log" in append mode + fileHandler = new FileHandler("bookkeeper.log", true); + fileHandler.setFormatter(new SimpleFormatter()); // Use a simple text format + } catch (IOException e) { + Logger.getLogger(LoggerConfig.class.getName()).severe("Failed to initialize FileHandler: " + + e.getMessage()); + } + } + + public static void configureLogger(Logger logger) { + if (fileHandler != null) { + logger.addHandler(fileHandler); // Add the shared FileHandler + logger.setUseParentHandlers(false); // Disable console logging if needed + } + } +} diff --git a/src/main/java/bookkeeper/storage/Storage.java b/src/main/java/bookkeeper/storage/Storage.java new file mode 100644 index 0000000000..6b54bf2f15 --- /dev/null +++ b/src/main/java/bookkeeper/storage/Storage.java @@ -0,0 +1,274 @@ +package bookkeeper.storage; + +import bookkeeper.list.BookList; +import bookkeeper.list.LoanList; +import bookkeeper.model.Book; +import bookkeeper.model.Loan; +import bookkeeper.ui.Formatter; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Scanner; + +public class Storage { + private static final String FOLDER_PATH = "./data"; + + private static final String INVENTORY_FILE_NAME = "bookKeeper_bookList.txt"; + private static final String LOAN_LIST_FILE_NAME = "bookKeeper_loanList.txt"; + + private static String inventoryFilePath = FOLDER_PATH + "/" + INVENTORY_FILE_NAME; + private static String loanListFilePath = FOLDER_PATH + "/" + LOAN_LIST_FILE_NAME; + + + // Setter for inventoryFilePath + public static void setInventoryFilePath(String filePath) { + inventoryFilePath = filePath; + } + + // Setter for loanListFilePath + public static void setLoanFilePath(String filePath) { + loanListFilePath = filePath; + } + + /** + * Saves the given loan list to the file. + * Each loan is saved as a line in the file. + * + * @param loanList LoanList containing the list of loans to save. + */ + public static void saveLoans(LoanList loanList) { + try { + // Ensure the directory exists + File directory = new File(FOLDER_PATH); + if (!directory.exists()) { + directory.mkdirs(); // Create the directory if it doesn't exist + } + + // Create a FileWriter to write to the file + FileWriter fileWriter = new FileWriter(loanListFilePath); + + // Write each loan to the file + ArrayList loans = loanList.getLoanList(); + for (Loan loan : loans) { + fileWriter.write(loan.toFileString() + System.lineSeparator()); + } + + fileWriter.close(); // Close the FileWriter to complete the writing process + } catch (IOException e) { + Formatter.printBorderedMessage("Something went wrong while saving loans: " + e.getMessage()); + } + } + + /** + * Saves the given book list to the file. + * Each book is saved as a line in the file. + * + * @param bookList BookList containing the list of loans to save. + */ + public static void saveInventory(BookList bookList) { + try { + // Ensure the directory exists + File directory = new File(FOLDER_PATH); + if (!directory.exists()) { + directory.mkdirs(); // Create the directory if it doesn't exist + } + + // Create a FileWriter to write to the file + FileWriter fileWriter = new FileWriter(inventoryFilePath); + + // Write each loan to the file + ArrayList books = bookList.getBookList(); + for (Book book : books) { + fileWriter.write(book.toFileString() + System.lineSeparator()); + } + + fileWriter.close(); // Close the FileWriter to complete the writing process + } catch (IOException e) { + Formatter.printBorderedMessage("Something went wrong while saving inventory: " + e.getMessage()); + } + } + + public static ArrayList loadInventory() { + ArrayList bookList = new ArrayList<>(); + File file = new File(inventoryFilePath); + + try { + // Check if the file exists + if (!file.exists()) { + Formatter.printBorderedMessage("No saved inventory found. Starting with an empty inventory.\n" + + "Creating a new text file at " + inventoryFilePath + "."); + return bookList; + } + + // Create a Scanner to read from the file + Scanner scanner = new Scanner(file); + + // Read each line and parse it into a book object + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + Book book = parseBookFromString(line); + + // Skip invalid book entries + if (book == null) { + continue; + } + + // Skip duplicate books + boolean isDuplicate = bookList.stream() + .anyMatch(existingBook -> existingBook.getTitle().equals(book.getTitle())); + if (isDuplicate) { + Formatter.printBorderedMessage("Duplicate book found and skipped: " + book.getTitle()); + continue; + } + + // Add valid book to the list + bookList.add(book); + } + + scanner.close(); // Close the Scanner + } catch (IOException e) { + Formatter.printBorderedMessage("Something went wrong while loading inventory: " + e.getMessage()); + } + + Formatter.printBorderedMessage("Loaded " + bookList.size() + " books from " + inventoryFilePath + "."); + return bookList; + } + + public static ArrayList loadLoans(BookList bookList) { + ArrayList loanList = new ArrayList<>(); + File file = new File(loanListFilePath); + + try { + // Check if the file exists + if (!file.exists()) { + Formatter.printBorderedMessage("No saved loans found. Starting with an empty loan list.\n" + + "Creating a new text file at " + loanListFilePath + "."); + return loanList; + } + + // Create a Scanner to read from the file + Scanner scanner = new Scanner(file); + + // Read each line and parse it into a loan object + while (scanner.hasNextLine()) { + String line = scanner.nextLine(); + Loan loan = parseLoanFromString(line, bookList); + + // Skip invalid loans + if (loan == null) { + continue; + } + + // Skip duplicate loans + boolean isDuplicate = loanList.stream() + .anyMatch(existingLoan -> existingLoan.getBook().equals(loan.getBook())); + if (isDuplicate) { + Formatter.printBorderedMessage("Duplicate loan found and skipped: " + + loan.getBook().getTitle() + " borrowed by " + loan.getBorrowerName()); + continue; + } + + // Add valid loan to the list and mark the book as on loan + loanList.add(loan); + loan.getBook().setOnLoan(true); + } + + scanner.close(); // Close the Scanner + } catch (IOException e) { + Formatter.printBorderedMessage("Something went wrong while loading loans: " + e.getMessage()); + } + + Formatter.printBorderedMessage("Loaded " + loanList.size() + " loans from " + loanListFilePath + "."); + return loanList; + } + + private static Book parseBookFromString(String line) { + String[] parts = line.split(" \\| "); + if (parts.length < 6) { + return null; // Invalid format + } + + String title = parts[0].trim(); + String author = parts[1].trim(); + String category = parts[2].trim(); + String condition = parts[3].trim(); + boolean onLoan = Boolean.parseBoolean(parts[4].trim()); + String location = parts[5].trim(); + String note = (parts.length == 7) ? parts[6].trim() : ""; + + Book book; + // Normalize case for title, author, and category + try { + book = new Book(title, author, category, condition, location, note); + } catch (IllegalArgumentException e) { + // Handle invalid book creation + Formatter.printBorderedMessage("Invalid book entry skipped: " + line + "\nReason: " + e.getMessage()); + return null; // Skip this book + } + return book; + } + + private static Loan parseLoanFromString(String line, BookList bookList) { + String[] parts = line.split(" \\| "); + + if (parts.length < 5) { + Formatter.printBorderedMessage("Invalid loan format: " + line); + return null; + } + + String title = parts[0].trim(); + String borrowerName = parts[1].trim(); + String returnDate = parts[2].trim(); + String phoneNumber = parts[3].trim(); + String email = parts[4].trim(); + + // Find the book in the inventory + Book loanedBook = bookList.searchBook(title); + if (loanedBook == null) { + Formatter.printBorderedMessage("Invalid loan: Book not found in inventory - " + title); + return null; // Skip this loan + } + + if (!phoneNumber.matches("^[986][0-9]{7}$")) { + Formatter.printBorderedMessage("Invalid loan entry skipped: " + line + "\nReason: " + + "Illegal phone number"); + return null; + } + + if (!email.matches("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")) { + Formatter.printBorderedMessage("Invalid loan entry skipped: " + line + "\nReason: " + + "Illegal email"); + return null; + } + + try { + // Attempt to create a Loan object + return new Loan(loanedBook, borrowerName, returnDate, phoneNumber, email); + } catch (IllegalArgumentException e) { + // Handle invalid date or other issues in Loan creation + Formatter.printBorderedMessage("Invalid loan entry skipped: " + line + "\nReason: " + e.getMessage()); + return null; // Skip this loan + } + } + + public static void validateStorage(BookList bookList, LoanList loanList) { + ArrayList books = bookList.getBookList(); + ArrayList loans = loanList.getLoanList(); + + // Reset all books to not on loan + for (Book book : books) { + book.setOnLoan(false); + } + + // Mark books as on loan based on the LoanList + for (Loan loan : loans) { + loan.getBook().setOnLoan(true); + } + + saveLoans(loanList); + saveInventory(bookList); + } + +} diff --git a/src/main/java/bookkeeper/ui/Formatter.java b/src/main/java/bookkeeper/ui/Formatter.java new file mode 100644 index 0000000000..8f26119e0e --- /dev/null +++ b/src/main/java/bookkeeper/ui/Formatter.java @@ -0,0 +1,73 @@ +package bookkeeper.ui; + +import bookkeeper.model.Book; +import bookkeeper.model.Loan; + +import java.util.ArrayList; + +public class Formatter { + + private static final int NORMAL_INDENT = 5; + private static final int MINOR_INDENT = 4; + + /** + * Prints a horizontal line with minor indentation. + */ + public static void printLine() { + System.out.println("____________________________________________________________________".indent(MINOR_INDENT)); + } + + /** + * Prints a bordered message with normal indentation. + * + * @param message The message to print. + */ + public static void printBorderedMessage(String message) { + printLine(); + System.out.print(message.indent(NORMAL_INDENT)); + printLine(); + } + + /** + * Prints a simple message with normal indentation. + * + * @param message The message to print. + */ + public static void printSimpleMessage(String message) { + System.out.print(message.indent(NORMAL_INDENT)); + } + + /** + * Prints a list of books with normal indentation. + * + * @param books The list of books to print. + */ + public static void printBookList(ArrayList books) { + printLine(); + printSimpleMessage("Here are the books in your inventory:"); + int count = 0; + for (Book book : books) { + count += 1; + printSimpleMessage(count + ". " + book.toString()); + System.out.println(); + } + printLine(); + } + + /** + * Prints a list of loans with normal indentation. + * + * @param loans The list of loans to print. + */ + public static void printLoanList(ArrayList loans) { + printLine(); + printSimpleMessage("Here are the active loans:"); + int count = 0; + for (Loan loan : loans) { + count += 1; + printSimpleMessage(count + ". " + loan.toString()); + System.out.println(); + } + printLine(); + } +} diff --git a/src/main/java/seedu/duke/Duke.java b/src/main/java/seedu/duke/Duke.java deleted file mode 100644 index 5c74e68d59..0000000000 --- a/src/main/java/seedu/duke/Duke.java +++ /dev/null @@ -1,21 +0,0 @@ -package seedu.duke; - -import java.util.Scanner; - -public class Duke { - /** - * Main entry-point for the java.duke.Duke application. - */ - public static void main(String[] args) { - String logo = " ____ _ \n" - + "| _ \\ _ _| | _____ \n" - + "| | | | | | | |/ / _ \\\n" - + "| |_| | |_| | < __/\n" - + "|____/ \\__,_|_|\\_\\___|\n"; - System.out.println("Hello from\n" + logo); - System.out.println("What is your name?"); - - Scanner in = new Scanner(System.in); - System.out.println("Hello " + in.nextLine()); - } -} diff --git a/src/test/java/bookkeeper/BookListTest.java b/src/test/java/bookkeeper/BookListTest.java new file mode 100644 index 0000000000..c65ff492c3 --- /dev/null +++ b/src/test/java/bookkeeper/BookListTest.java @@ -0,0 +1,122 @@ +package bookkeeper; + +import bookkeeper.exceptions.IncorrectFormatException; +import bookkeeper.list.BookList; +import bookkeeper.logic.InputParser; +import bookkeeper.model.Book; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.ArrayList; + +class BookListTest { + private BookList bookList; + private Book book1; + private Book book2; + private Book book3; + + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + + @BeforeEach + void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + bookList = new BookList("My Book List", new ArrayList()); + + // Creating mock book objects + book1 = new Book("Book One", "Author One", "Fiction", "poor", "Shelf 1"); + book2 = new Book("Book Two", "Author Two", "Non-Fiction", "Good", "Shelf 2"); + book3 = new Book("Harry Potter", "Author Two", "Non-Fiction", "Good", "Shelf 2"); + } + + @Test + void testGetListName() { + assertEquals("My Book List", bookList.getListName()); + } + + @Test + void testAddBook() { + bookList.addBook(book1); + assertEquals(book1, bookList.searchBook("Book One")); + } + + @Test + void testFindBookByTitle() { + bookList.addBook(book1); + bookList.addBook(book2); + + assertEquals(book1, bookList.searchBook("Book One")); + assertEquals(book2, bookList.searchBook("Book Two")); + assertNull(bookList.searchBook("Nonexistent Book")); + } + + @Test + void testRemoveBook() { + bookList.addBook(book1); + bookList.addBook(book2); + + bookList.removeBook(book1); + assertNull(bookList.searchBook("Book One")); + assertNotNull(bookList.searchBook("Book Two")); + } + + @Test + void testFindBooksByKeyword() { + bookList.addBook(book1); + bookList.addBook(book2); + bookList.addBook(book3); + + ArrayList foundBooks = bookList.findBooksByKeyword("e"); + assertEquals(foundBooks.get(0), book1); + assertNotEquals(foundBooks.get(0), book2); + assertNotEquals(foundBooks.get(1), book2); + assertEquals(foundBooks.get(1), book3); + } + + @Test + void testFindBooksByCategory() { + Book book4 = new Book("Book Four", "Author Four", "nonfiction", "Good", "Shelf 4"); + bookList.addBook(book1); + bookList.addBook(book2); + bookList.addBook(book3); + bookList.addBook(book4); + + ArrayList foundBooks = bookList.findBooksByCategory("Non-Fiction"); + assertEquals(foundBooks.get(0), book2); + assertEquals(foundBooks.get(1), book3); + assertNotEquals(foundBooks.get(0), book1); + assertNotEquals(foundBooks.get(1), book1); + assertTrue(foundBooks.contains(book4)); + } + + @Test + void viewBookList_emptyBookList() { + bookList.viewBookList(); + String output = outputStreamCaptor.toString().trim(); + assertTrue(output.contains("Book List Empty!")); + } + + @Test + void viewBookList_oneBookBookList() { + bookList.addBook(book1); + bookList.viewBookList(); + String output = outputStreamCaptor.toString().trim(); + assertTrue(output.contains("Book One")); + } + + @Test + void extractAddBookArgs_authorWithSlash_success() throws IncorrectFormatException { + String[] arguments = InputParser.extractAddBookArgs("The Great Gatsby a/F. Scott s/o Fitzgerald " + + "cat/Fiction cond/Good loc/Shelf 1"); + String[] output = new String[]{"The Great Gatsby", "F. Scott s/o Fitzgerald", "Fiction", "Good", "Shelf 1", ""}; + assertArrayEquals(arguments, output); + } +} diff --git a/src/test/java/bookkeeper/InputParserTest.java b/src/test/java/bookkeeper/InputParserTest.java new file mode 100644 index 0000000000..1486cd021c --- /dev/null +++ b/src/test/java/bookkeeper/InputParserTest.java @@ -0,0 +1,378 @@ +package bookkeeper; + +import bookkeeper.logic.InputParser; +import org.junit.jupiter.api.Test; + +import bookkeeper.exceptions.IncorrectFormatException; +import bookkeeper.exceptions.InvalidArgumentException; +import bookkeeper.exceptions.ErrorMessages; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class InputParserTest { + + // extractAddBookArgs gives us 5 arguments: name, author, category, condition, note("" if not provided) + @Test + void extractAddBookArgs_validInput_sixArgumentStringArray() throws IncorrectFormatException { + String[] arguments = InputParser.extractAddBookArgs("The Great Gatsby " + + "a/F. Scott Fitzgerald cat/Fiction cond/Good loc/Shelf 1 note/Amazing Book"); + String[] output = new String[]{"The Great Gatsby", "F. Scott Fitzgerald", "Fiction", "Good", "Shelf 1", + "Amazing Book"}; + assertArrayEquals(arguments, output); + } + + @Test + void extractAddBookArgs_inputWithExtraSpace_fiveArgumentStringArray() throws IncorrectFormatException { + String[] arguments = InputParser.extractAddBookArgs("The Great Gatsby " + + "a/F. Scott Fitzgerald cat/Fiction cond/Good loc/Shelf 2 "); + String[] output = new String[]{"The Great Gatsby", "F. Scott Fitzgerald", "Fiction", "Good", "Shelf 2", ""}; + assertArrayEquals(arguments, output); + } + + @Test + void extractAddBookArgs_missingAuthor_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () + -> InputParser.extractAddBookArgs("The Great Gatsby cat/Fiction cond/Good")); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_BOOK, + exception.getMessage()); + } + + @Test + void extractAddBookArgs_missingBookName_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () + -> InputParser.extractAddBookArgs("a/F. Scott Fitzgerald cat/Fiction cond/Good")); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_BOOK, + exception.getMessage()); + } + + // extractUpdateBookArgs gives us 5 arguments: name, author, category, condition, location, note("" if not provided) + @Test + void extractUpdateBookArgs_validInput_fiveArgumentStringArray() throws IncorrectFormatException { + String[] arguments = InputParser.extractUpdateBookArgs("The Great Gatsby " + + "a/F. Scott Fitzgerald cat/Fiction cond/Good loc/Shelf 2"); + String[] output = new String[]{"The Great Gatsby", "F. Scott Fitzgerald", "Fiction", "Good", "Shelf 2", null}; + assertArrayEquals(arguments, output); + } + + @Test + void extractUpdateBookArgs_inputWithExtraSpace_fiveArgumentStringArray() throws IncorrectFormatException { + String[] arguments = InputParser.extractUpdateBookArgs("The Great Gatsby " + + "a/F. Scott Fitzgerald cat/Fiction cond/Good loc/Shelf 2 "); + String[] output = new String[]{"The Great Gatsby", "F. Scott Fitzgerald", "Fiction", "Good", "Shelf 2", null}; + assertArrayEquals(arguments, output); + } + + @Test + void extractUpdateBookArgs_missingBookName_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () + -> InputParser.extractUpdateBookArgs("a/F. Scott Fitzgerald cat/Fiction cond/Good")); + assertEquals(ErrorMessages.INVALID_FORMAT_UPDATE_BOOK, + exception.getMessage()); + } + + @Test + void extractAddLoanArgs_validInput_threeArgumentStringArray() throws IncorrectFormatException, + InvalidArgumentException { + String[] arguments = InputParser.extractAddLoanArgs("The Great Gatsby n/Mary d/13-Mar-2025" + + " p/62345678 e/abc123@gmail.com"); + String[] output = new String[]{"The Great Gatsby", "Mary", "13-Mar-2025", "62345678", "abc123@gmail.com"}; + assertArrayEquals(arguments, output); + } + + @Test + void extractAddLoanArgs_inputWithExtraSpace_threeArgumentStringArray() throws IncorrectFormatException, + InvalidArgumentException { + String[] arguments = InputParser.extractAddLoanArgs("The Great Gatsby n/Mary d/13-Mar-2025 " + + "p/92345678 e/abc123@gmail.com"); + String[] output = new String[]{"The Great Gatsby", "Mary", "13-Mar-2025", "92345678", "abc123@gmail.com"}; + assertArrayEquals(arguments, output); + } + + @Test + void extractAddLoanArgs_missingDate_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () + -> InputParser.extractAddLoanArgs("The Great Gatsby n/Mary")); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_LOAN, exception.getMessage()); + } + + @Test + void extractAddLoanArgs_emptyInput_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractAddLoanArgs(" ")); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_LOAN, exception.getMessage()); + } + + @Test + void extractCommandArgs_validInput_twoArgumentStringArray() throws IncorrectFormatException { + String[] arguments = InputParser.extractCommandArgs("delete-loan The Great Gatsby n/Mary"); + String[] output = new String[]{"delete-loan", "The Great Gatsby n/Mary"}; + assertArrayEquals(arguments, output); + } + + @Test + void extractEditLoanArgs_validInput_twoArgumentArray() throws IncorrectFormatException, InvalidArgumentException { + String[] arguments = InputParser.extractEditLoanArgs("Great Gatsby n/Mary d/13-03-2025 " + + "p/62345678 e/abc123@gmail.com"); + String[] output = new String[]{"Great Gatsby", "Mary", "13-03-2025", "62345678", "abc123@gmail.com"}; + assertArrayEquals(arguments, output); + } + + @Test + void extractEditLoanArgs_inputWithExtraSpace_twoArgumentArray() throws IncorrectFormatException, + InvalidArgumentException { + String[] arguments = InputParser.extractEditLoanArgs("Great Gatsby n/Mary d/13-03-2025 " + + "p/92345678 e/abc123@gmail.com"); + String[] output = new String[]{"Great Gatsby", "Mary", "13-03-2025", "92345678", "abc123@gmail.com"}; + assertArrayEquals(arguments, output); + } + + @Test + void extractAddBookArgs_validInput_success() throws IncorrectFormatException { + String input = "The Great Gatsby a/F. Scott Fitzgerald cat/Fiction cond/Good loc/Shelf 1 note/Classic novel"; + String[] result = InputParser.extractAddBookArgs(input); + String[] expected = {"The Great Gatsby", "F. Scott Fitzgerald", "Fiction", "Good", "Shelf 1", "Classic novel"}; + assertArrayEquals(expected, result); + } + + @Test + void extractAddBookArgs_validInputWithExtraSpaces_success() throws IncorrectFormatException { + String input = "The Great Gatsby a/ F. Scott Fitzgerald cat/ Fiction cond/ Good loc/ Shelf 1 note/" + + " Classic novel"; + String[] result = InputParser.extractAddBookArgs(input); + String[] expected = {"The Great Gatsby", "F. Scott Fitzgerald", "Fiction", "Good", "Shelf 1", "Classic novel"}; + assertArrayEquals(expected, result); + } + + @Test + void extractAddBookArgs_missingFields_exceptionThrown() { + String input = "The Great Gatsby a/F. Scott Fitzgerald loc/Shelf 1"; + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractAddBookArgs(input)); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_BOOK, exception.getMessage()); + } + + @Test + void extractAddBookArgs_duplicatePrefix_exceptionThrown() { + String input = "The Great Gatsby a/F. Scott Fitzgerald a/Another Author cat/Fiction cond/Good loc/Shelf 1"; + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractAddBookArgs(input)); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_BOOK_DUPLICATE_PREFIX, exception.getMessage()); + } + + @Test + void extractUpdateBookArgs_validInput_success() throws IncorrectFormatException { + String input = "The Great Gatsby a/F. Scott Fitzgerald cat/Fiction cond/Good loc/Shelf 1 note/Classic novel"; + String[] result = InputParser.extractUpdateBookArgs(input); + String[] expected = {"The Great Gatsby", "F. Scott Fitzgerald", "Fiction", "Good", "Shelf 1", "Classic novel"}; + assertArrayEquals(expected, result); + } + + @Test + void extractAddLoanArgs_validInput_success() throws IncorrectFormatException, InvalidArgumentException { + String input = "The Great Gatsby n/John Doe d/2023-12-01 p/82345678 e/johndoe@example.com"; + String[] result = InputParser.extractAddLoanArgs(input); + String[] expected = {"The Great Gatsby", "John Doe", "2023-12-01", "82345678", "johndoe@example.com"}; + assertArrayEquals(expected, result); + } + + @Test + void extractAddLoanArgs_missingFields_exceptionThrown() { + String input = "The Great Gatsby n/John Doe d/2023-12-01 p/92345678"; + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractAddLoanArgs(input)); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_LOAN, exception.getMessage()); + } + + @Test + void extractAddLoanArgs_invalidPhoneNumber_exceptionThrown() { + String input = "The Great Gatsby n/John Doe d/2023-12-01 p/9@3!#49 e/johndoe@example.com"; + InvalidArgumentException exception = assertThrows(InvalidArgumentException.class, + () -> InputParser.extractAddLoanArgs(input)); + assertEquals(ErrorMessages.INVALID_PHONE_NUMBER_ADD_LOAN, exception.getMessage()); + } + + @Test + void extractAddLoanArgs_invalidEmail_exceptionThrown() { + String input = "The Great Gatsby n/John Doe d/2023-12-01 p/91222999 e/johndoeexample.com"; + InvalidArgumentException exception = assertThrows(InvalidArgumentException.class, + () -> InputParser.extractAddLoanArgs(input)); + assertEquals(ErrorMessages.INVALID_EMAIL_ADD_LOAN, exception.getMessage()); + } + + @Test + void extractAddLoanArgs_duplicatePrefix_exceptionThrown() { + String input = "The Great Gatsby n/John Doe n/Another Borrower d/2023-12-01 p/1234567890 e/johndoe@example.com"; + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractAddLoanArgs(input)); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_LOAN_DUPLICATE_PREFIX, exception.getMessage()); + } + + @Test + void extractEditLoanArgs_validInput_success() throws IncorrectFormatException, InvalidArgumentException { + String input = "1 n/John Doe d/2023-12-01 p/82345678 e/johndoe@example.com"; + String[] result = InputParser.extractEditLoanArgs(input); + String[] expected = {"1", "John Doe", "2023-12-01", "82345678", "johndoe@example.com"}; + assertArrayEquals(expected, result); + } + + @Test + void extractEditLoanArgs_missingFields_sucessWithoutMissingFields() throws IncorrectFormatException, + InvalidArgumentException { + String input = "1 n/John Doe d/2023-12-01 e/johndoe@example.com"; + String[] result = InputParser.extractEditLoanArgs(input); + String[] expected = {"1", "John Doe", "2023-12-01", null, "johndoe@example.com"}; + assertArrayEquals(expected, result); + } + + @Test + void extractEditLoanArgs_invalidPhoneNumber_exceptionThrown() { + String input = "1 n/John Doe d/2023-12-01 p/9@3!#49 e/johndoe@example.com"; + InvalidArgumentException exception = assertThrows(InvalidArgumentException.class, + () -> InputParser.extractEditLoanArgs(input)); + assertEquals(ErrorMessages.INVALID_PHONE_NUMBER_EDIT_LOAN, exception.getMessage()); + } + + @Test + void extractEditLoanArgs_invalidEmail_exceptionThrown() { + String input = "1 n/John Doe d/2023-12-01 p/91222999 e/johndoeexample.com"; + InvalidArgumentException exception = assertThrows(InvalidArgumentException.class, + () -> InputParser.extractEditLoanArgs(input)); + assertEquals(ErrorMessages.INVALID_EMAIL_EDIT_LOAN, exception.getMessage()); + } + + @Test + void extractEditLoanArgs_duplicatePrefix_exceptionThrown() { + String input = "1 n/John Doe n/Another Borrower d/2023-12-01 p/1234567890 e/johndoe@example.com"; + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractEditLoanArgs(input)); + assertEquals(ErrorMessages.INVALID_FORMAT_EDIT_LOAN_DUPLICATE_PREFIX, exception.getMessage()); + } + + @Test + void extractUpdateTitleArgs_validInput_success() throws IncorrectFormatException { + String input = "Cheese Chronicles new/Cheese Adventures"; + String[] expected = {"Cheese Chronicles", "Cheese Adventures"}; + String[] result = InputParser.extractUpdateTitleArgs(input); + assertArrayEquals(expected, result); + } + + @Test + void extractUpdateTitleArgs_validInputExtraSpaces_success() throws IncorrectFormatException { + String input = " Cheese Chronicles new/ Cheese Adventures "; + String[] expected = {"Cheese Chronicles", "Cheese Adventures"}; + String[] result = InputParser.extractUpdateTitleArgs(input); + assertArrayEquals(expected, result); + } + + @Test + void extractUpdateTitleArgs_missingNewTitle_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractUpdateTitleArgs("Cheese Chronicles")); + assertEquals(ErrorMessages.INVALID_FORMAT_UPDATE_TITLE, exception.getMessage()); + } + + @Test + void extractUpdateTitleArgs_missingOldTitle_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractUpdateTitleArgs("new/Cheese Adventures")); + assertEquals(ErrorMessages.INVALID_FORMAT_UPDATE_TITLE, exception.getMessage()); + } + + @Test + void extractUpdateTitleArgs_emptyNewTitle_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractUpdateTitleArgs("Cheese Chronicles new/")); + assertEquals(ErrorMessages.INVALID_FORMAT_UPDATE_TITLE, exception.getMessage()); + } + + @Test + void extractUpdateTitleArgs_emptyOldTitle_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractUpdateTitleArgs(" new/Cheese Adventures")); + assertEquals(ErrorMessages.INVALID_FORMAT_UPDATE_TITLE, exception.getMessage()); + } + + @Test + void extractUpdateTitleArgs_duplicatePrefix_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractUpdateTitleArgs("Cheese Chronicles new/Cheese Adventures new/Extra")); + assertEquals(ErrorMessages.INVALID_FORMAT_UPDATE_TITLE_DUPLICATE_PREFIX, exception.getMessage()); + } + + + @Test + void extractAddNoteArgs_validInput_success() throws IncorrectFormatException { + String input = "The Great Gatsby note/A classic novel"; + String[] expected = {"The Great Gatsby", "A classic novel"}; + String[] result = InputParser.extractAddNoteArgs(input); + assertArrayEquals(expected, result); + } + + @Test + void extractAddNoteArgs_validInputExtraSpaces_success() throws IncorrectFormatException { + String input = " The Great Gatsby note/ A classic novel "; + String[] expected = {"The Great Gatsby", "A classic novel"}; + String[] result = InputParser.extractAddNoteArgs(input); + assertArrayEquals(expected, result); + } + + @Test + void extractAddNoteArgs_missingNote_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractAddNoteArgs("The Great Gatsby")); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_NOTE, exception.getMessage()); + } + + @Test + void extractAddNoteArgs_blankNote_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractAddNoteArgs("The Great Gatsby note/ ")); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_NOTE, exception.getMessage()); + } + + @Test + void extractAddNoteArgs_blankTitle_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractAddNoteArgs(" note/A classic novel")); + assertEquals(ErrorMessages.INVALID_FORMAT_ADD_NOTE, exception.getMessage()); + } + + @Test + void extractUpdateNoteArgs_validInput_success() throws IncorrectFormatException { + String input = "The Great Gatsby note/Updated note"; + String[] expected = {"The Great Gatsby", "Updated note"}; + String[] result = InputParser.extractUpdateNoteArgs(input); + assertArrayEquals(expected, result); + } + + @Test + void extractUpdateNoteArgs_validInputExtraSpaces_success() throws IncorrectFormatException { + String input = " The Great Gatsby note/ Updated note "; + String[] expected = {"The Great Gatsby", "Updated note"}; + String[] result = InputParser.extractUpdateNoteArgs(input); + assertArrayEquals(expected, result); + } + + @Test + void extractUpdateNoteArgs_missingNote_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractUpdateNoteArgs("The Great Gatsby")); + assertEquals(ErrorMessages.INVALID_FORMAT_UPDATE_NOTE, exception.getMessage()); + } + + @Test + void extractUpdateNoteArgs_blankNote_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractUpdateNoteArgs("The Great Gatsby note/ ")); + assertEquals(ErrorMessages.INVALID_FORMAT_UPDATE_NOTE, exception.getMessage()); + } + + @Test + void extractUpdateNoteArgs_blankTitle_exceptionThrown() { + IncorrectFormatException exception = assertThrows(IncorrectFormatException.class, () -> + InputParser.extractUpdateNoteArgs(" note/Updated note")); + assertEquals(ErrorMessages.INVALID_FORMAT_UPDATE_NOTE, exception.getMessage()); + } +} diff --git a/src/test/java/bookkeeper/LoanListTest.java b/src/test/java/bookkeeper/LoanListTest.java new file mode 100644 index 0000000000..f4b5506399 --- /dev/null +++ b/src/test/java/bookkeeper/LoanListTest.java @@ -0,0 +1,210 @@ +package bookkeeper; + +import bookkeeper.exceptions.IncorrectFormatException; +import bookkeeper.exceptions.InvalidArgumentException; +import bookkeeper.list.BookList; +import bookkeeper.list.LoanList; +import bookkeeper.logic.InputParser; +import bookkeeper.model.Book; +import bookkeeper.model.Loan; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class LoanListTest { + private LoanList loanList; + private Book book1; + private Book book2; + private Loan loan1; + + private final ByteArrayOutputStream outputStreamCaptor = new ByteArrayOutputStream(); + + @BeforeEach + public void setUp() { + System.setOut(new PrintStream(outputStreamCaptor)); + + loanList = new LoanList("Test Loan List", new ArrayList()); + BookList bookList = new BookList("Test Book List", new ArrayList()); + + book1 = new Book("The Great Gatsby", "F. Scott Fitzgerald", "Fiction", + "Good", "Shelf 1"); + book2 = new Book("To Kill a Mockingbird", "Harper Lee", "Fiction", + "Fair", "Shelf 2"); + + bookList.addBook(book1); + bookList.addBook(book2); + + // Set loan1 to 21 days from the current date (valid) + String futureDate = LocalDate.now().plusDays(21).format(DateTimeFormatter.ofPattern("dd-MM-yyyy")); + loan1 = new Loan(book1, "John Doe", futureDate, "98765432", "abc123@gmail.com"); + } + + @Test + void addLoan_successfullyAddsLoan() { + loanList.addLoan(loan1); + Loan foundLoan = loanList.findLoan(book1); + assertNotNull(foundLoan, "Loan should be added to the list"); + assertEquals(loan1, foundLoan, "The added loan should match the expected loan"); + } + + @Test + void addLoan_addNullLoan_expectAssertionError() { + // Expect an AssertionError when adding a null loan + AssertionError error = assertThrows(AssertionError.class, () -> + loanList.addLoan(null), "Adding a null loan should throw an AssertionError"); + + assertEquals("Loan cannot be null", error.getMessage(), + "The error message should indicate that the loan cannot be null"); + } + + @Test + void deleteLoan_deleteNullLoan_expectAssertionError() { + AssertionError error = assertThrows(AssertionError.class, () -> { + loanList.addLoan(loan1); + loanList.deleteLoan(null); + }, "Deleting a null loan should throw an AssertionError"); + + assertEquals("Loan cannot be null", error.getMessage(), + "The error message should indicate that the loan cannot be null"); + } + + @Test + void addLoan_duplicateLoans() { + loanList.addLoan(loan1); + loanList.addLoan(loan1); // Add the same loan again + assertEquals(2, loanList.getLoanList().size(), + "Duplicate loans should be allowed in the list"); + } + + @Test + void deleteLoan_successfullyRemovesLoan() { + loanList.addLoan(loan1); + loanList.deleteLoan(loan1); + Loan foundLoan = loanList.findLoan(book1); + assertNull(foundLoan, "Loan should be removed from the list"); + } + + @Test + void deleteLoan_nonExistingLoan() { + loanList.addLoan(loan1); + Loan nonExistingLoan = new Loan(book2, "Nonexistent Borrower", "10-01-2027", + "87654321", "def321@gmail.com"); + loanList.deleteLoan(nonExistingLoan); // Attempt to delete a non-existing loan + assertEquals(1, loanList.getLoanList().size(), + "Deleting a non-existing loan should not affect the list"); + } + + @Test + void findLoan_existingLoan() { + loanList.addLoan(loan1); + Loan foundLoan = loanList.findLoan(book1); + assertNotNull(foundLoan, "Loan should be found in the list"); + assertEquals(loan1, foundLoan, "The found loan should match the expected loan"); + } + + @Test + void findLoan_nonExistingLoan() { + Loan foundLoan = loanList.findLoan(book1); + assertNull(foundLoan, "Loan should not be found in the list for a nonexistent borrower"); + } + + @Test + void getListName_test() { + assertEquals("Test Loan List", loanList.getListName(), + "getListName() should return the list name provided in the constructor"); + } + + @Test + void findLoanByIndex_validIndex() { + loanList.addLoan(loan1); + Loan found = loanList.findLoanByIndex(1); + assertEquals(loan1, found, "findLoanByIndex(1) should return the first loan"); + } + + @Test + void findLoanByIndex_invalidIndex() { + loanList.addLoan(loan1); + // Index 0 or an index greater than list size should return null. + assertNull(loanList.findLoanByIndex(0), + "findLoanByIndex(0) should return null (invalid index)"); + assertNull(loanList.findLoanByIndex(2), + "findLoanByIndex(2) should return null when no loan exists at that position"); + } + + @Test + void removeLoansByBook_valid() { + // Add two loans for book1 and one for book2. + loanList.addLoan(loan1); + Loan loan2 = new Loan(book1, "Jane Smith", + LocalDate.now().plusDays(30).format(DateTimeFormatter.ofPattern("dd-MM-yyyy")), + "12345678", "jane@example.com"); + Loan loan3 = new Loan(book2, "Bob Brown", + LocalDate.now().plusDays(25).format(DateTimeFormatter.ofPattern("dd-MM-yyyy")), + "87654321", "bob@example.com"); + loanList.addLoan(loan2); + loanList.addLoan(loan3); + + // Verify that three loans are in the list. + assertEquals(3, loanList.getLoanList().size(), + "There should be three loans before removal"); + + // Remove all loans associated with book1. + loanList.removeLoansByBook(book1); + + // Only the loan associated with book2 should remain. + assertEquals(1, loanList.getLoanList().size(), + "After removal, only loans not associated with book1 should remain"); + assertEquals(loan3, loanList.getLoanList().get(0), + "The remaining loan should be the one associated with book2"); + + // Additionally, findLoan() for book1 should now return null. + assertNull(loanList.findLoan(book1), + "No loan for book1 should be found after removal"); + } + + @Test + void removeLoansByBook_nullBook() { + // Passing null should trigger an assertion. + AssertionError error = assertThrows(AssertionError.class, + () -> loanList.removeLoansByBook(null), + "Passing null to removeLoansByBook() should throw an AssertionError"); + assertEquals("Book cannot be null", error.getMessage(), + "Error message should indicate that the book cannot be null"); + } + + @Test + void viewLoanList_emptyLoanList() { + loanList.viewLoanList(); + String output = outputStreamCaptor.toString().trim(); + assertTrue(output.contains("Loan List Empty!")); + } + + @Test + void viewLoanList_oneLoanLoanList() { + loanList.addLoan(loan1); + loanList.viewLoanList(); + String output = outputStreamCaptor.toString().trim(); + assertTrue(output.contains("The Great Gatsby")); + } + + @Test + void extractAddLoanArgs_borrowerWithSlash_success() throws IncorrectFormatException, InvalidArgumentException { + String[] arguments = InputParser.extractAddLoanArgs("The Great Gatsby n/John s/o Doe " + + "d/01-12-2025 p/98765432 e/john.doe@example.com"); + String[] output = new String[]{"The Great Gatsby", "John s/o Doe", "01-12-" + + "2025", "98765432", "john.doe@example.com"}; + assertArrayEquals(arguments, output); + } +} diff --git a/src/test/java/bookkeeper/StorageTest.java b/src/test/java/bookkeeper/StorageTest.java new file mode 100644 index 0000000000..4ae496683d --- /dev/null +++ b/src/test/java/bookkeeper/StorageTest.java @@ -0,0 +1,202 @@ +package bookkeeper; + +import bookkeeper.list.BookList; +import bookkeeper.list.LoanList; +import bookkeeper.model.Book; +import bookkeeper.model.Loan; +import bookkeeper.storage.Storage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.io.File; +import java.io.FileWriter; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class StorageTest { + + private static final String TEST_FOLDER_PATH = "./test_data"; + private static final String TEST_BOOK_LIST_FILE_PATH = TEST_FOLDER_PATH + "/test_bookKeeper_bookList.txt"; + private static final String TEST_LOAN_LIST_FILE_PATH = TEST_FOLDER_PATH + "/test_bookKeeper_loanList.txt"; + + private BookList bookList; + private LoanList loanList; + + @BeforeEach + void setUp() { + // Create test folder + File testFolder = new File(TEST_FOLDER_PATH); + if (!testFolder.exists()) { + testFolder.mkdirs(); + } + + // Initialize empty BookList and LoanList + bookList = new BookList("Test Inventory", new ArrayList<>()); + loanList = new LoanList("Test Loan List", new ArrayList<>()); + + // Update file paths for testing + Storage.setInventoryFilePath(TEST_BOOK_LIST_FILE_PATH); + Storage.setLoanFilePath(TEST_LOAN_LIST_FILE_PATH); + } + + @AfterEach + void tearDown() { + // Delete test files after each test + File bookFile = new File(TEST_BOOK_LIST_FILE_PATH); + File loanFile = new File(TEST_LOAN_LIST_FILE_PATH); + + if (bookFile.exists()) { + bookFile.delete(); + } + if (loanFile.exists()) { + loanFile.delete(); + } + } + + @Test + void saveInventory_validBookList_fileCreatedWithCorrectData() { + // Add books to the BookList + bookList.addBook(new Book("The Great Gatsby", "F. Scott Fitzgerald", "Fiction", + "Good", "Shelf 1", "Classic novel")); + bookList.addBook(new Book("To Kill a Mockingbird", "Harper Lee", "Fiction", + "Fair", "Shelf 2", "Pulitzer Prize winner")); + + // Save the inventory + Storage.saveInventory(bookList); + + // Verify the file exists + File bookFile = new File(TEST_BOOK_LIST_FILE_PATH); + assertTrue(bookFile.exists()); + } + + @Test + void loadInventory_validFile_correctBookListLoaded() { + // Create a test file with valid book data + try (FileWriter writer = new FileWriter(TEST_BOOK_LIST_FILE_PATH)) { + writer.write("The Great Gatsby | F. Scott Fitzgerald | Fiction | Good | false | Shelf 1 | " + + "Classic novel\n"); + writer.write("To Kill a Mockingbird | Harper Lee | Fiction | Fair | true | Shelf 2 | " + + "Pulitzer Prize winner\n"); + } catch (Exception e) { + fail("Failed to create test file: " + e.getMessage()); + } + + // Load the inventory + ArrayList loadedBooks = Storage.loadInventory(); + + // Verify the loaded books + assertEquals(2, loadedBooks.size()); + assertEquals("The Great Gatsby", loadedBooks.get(0).getTitle()); + assertEquals("To Kill a Mockingbird", loadedBooks.get(1).getTitle()); + } + + @Test + void loadInventory_invalidLines_invalidLinesSkipped() { + // Create a test file with invalid lines + try (FileWriter writer = new FileWriter(TEST_BOOK_LIST_FILE_PATH)) { + writer.write("The Great Gatsby | F. Scott Fitzgerald | Fiction | Good | false | Shelf 1 | " + + "Classic novel\n"); + writer.write("Invalid Book Entry\n"); + writer.write("To Kill a Mockingbird | Harper Lee | Fiction | Fair | true | Shelf 2 | " + + "Pulitzer Prize winner\n"); + } catch (Exception e) { + fail("Failed to create test file: " + e.getMessage()); + } + + // Load the inventory + ArrayList loadedBooks = Storage.loadInventory(); + + // Verify the loaded books + assertEquals(2, loadedBooks.size()); + assertEquals("The Great Gatsby", loadedBooks.get(0).getTitle()); + assertEquals("To Kill a Mockingbird", loadedBooks.get(1).getTitle()); + } + + @Test + void saveLoans_validLoanList_fileCreatedWithCorrectData() { + // Add books to the BookList + Book book1 = new Book("The Great Gatsby", "F. Scott Fitzgerald", "Fiction", + "Good", "Shelf 1", "Classic novel"); + Book book2 = new Book("To Kill a Mockingbird", "Harper Lee", "Fiction", + "Fair", "Shelf 2", "Pulitzer Prize winner"); + bookList.addBook(book1); + bookList.addBook(book2); + + // Add loans to the LoanList + String futureDate1 = LocalDate.now().plusDays(21).format(DateTimeFormatter.ofPattern("dd-MM-yyyy")); + String futureDate2 = LocalDate.now().plusDays(30).format(DateTimeFormatter.ofPattern("dd-MM-yyyy")); + loanList.addLoan(new Loan(book1, "John Doe", futureDate1, "1234567890", + "johndoe@example.com")); + loanList.addLoan(new Loan(book2, "Jane Doe", futureDate2, "0987654321", + "janedoe@example.com")); + + // Save the loans + Storage.saveLoans(loanList); + + // Verify the file exists + File loanFile = new File(TEST_LOAN_LIST_FILE_PATH); + assertTrue(loanFile.exists()); + } + + @Test + void loadLoans_validFile_correctLoanListLoaded() { + // Add books to the BookList + Book book1 = new Book("The Great Gatsby", "F. Scott Fitzgerald", "Fiction", + "Good", "Shelf 1", "Classic novel"); + Book book2 = new Book("To Kill a Mockingbird", "Harper Lee", "Fiction", + "Fair", "Shelf 2", "Pulitzer Prize winner"); + bookList.addBook(book1); + bookList.addBook(book2); + + // Create a test file with valid loan data + try (FileWriter writer = new FileWriter(TEST_LOAN_LIST_FILE_PATH)) { + writer.write("The Great Gatsby | John Doe | 21-12-2026 | 81234567 | johndoe@example.com\n"); + writer.write("To Kill a Mockingbird | Jane Doe | 30-12-2026 | 98765432 | janedoe@example.com\n"); + } catch (Exception e) { + fail("Failed to create test file: " + e.getMessage()); + } + + // Load the loans + ArrayList loadedLoans = Storage.loadLoans(bookList); + + // Verify the loaded loans + assertEquals(2, loadedLoans.size()); + assertEquals("The Great Gatsby", loadedLoans.get(0).getBook().getTitle()); + assertEquals("John Doe", loadedLoans.get(0).getBorrowerName()); + assertEquals("To Kill a Mockingbird", loadedLoans.get(1).getBook().getTitle()); + assertEquals("Jane Doe", loadedLoans.get(1).getBorrowerName()); + } + + @Test + void loadLoans_invalidLines_invalidLinesSkipped() { + // Add books to the BookList + Book book1 = new Book("The Great Gatsby", "F. Scott Fitzgerald", "Fiction", + "Good", "Shelf 1", "Classic novel"); + Book book2 = new Book("To Kill a Mockingbird", "Harper Lee", "Fiction", + "Fair", "Shelf 2", "Pulitzer Prize winner"); + bookList.addBook(book1); + bookList.addBook(book2); + + // Create a test file with invalid lines + try (FileWriter writer = new FileWriter(TEST_LOAN_LIST_FILE_PATH)) { + writer.write("The Great Gatsby | John Doe | 21-12-2026 | 1234567 | johndoe@example.com\n"); + writer.write("Invalid Loan Entry\n"); + writer.write("To Kill a Mockingbird | Jane Doe | 30-12-2026 | 98765432 | janedoe@example.com\n"); + } catch (Exception e) { + fail("Failed to create test file: " + e.getMessage()); + } + + // Load the loans + ArrayList loadedLoans = Storage.loadLoans(bookList); + + // Verify the loaded loans + assertEquals(1, loadedLoans.size()); + assertEquals("To Kill a Mockingbird", loadedLoans.get(0).getBook().getTitle()); + } +} diff --git a/src/test/java/seedu/duke/DukeTest.java b/src/test/java/seedu/duke/DukeTest.java deleted file mode 100644 index 2dda5fd651..0000000000 --- a/src/test/java/seedu/duke/DukeTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package seedu.duke; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class DukeTest { - @Test - public void sampleTest() { - assertTrue(true); - } -} diff --git a/text-ui-test/EXPECTED.TXT b/text-ui-test/EXPECTED.TXT index 892cb6cae7..9c5d0725f4 100644 --- a/text-ui-test/EXPECTED.TXT +++ b/text-ui-test/EXPECTED.TXT @@ -1,9 +1,61 @@ -Hello from - ____ _ -| _ \ _ _| | _____ -| | | | | | | |/ / _ \ -| |_| | |_| | < __/ -|____/ \__,_|_|\_\___| - -What is your name? -Hello James Gosling + ____________________________________________________________________ + + Welcome to BookKeeper. + ____________________________________________________________________ + + ____________________________________________________________________ + + No saved inventory found. Starting with an empty inventory. + Creating a new text file at ./data/bookKeeper_bookList.txt. + ____________________________________________________________________ + + ____________________________________________________________________ + + No saved loans found. Starting with an empty loan list. + Creating a new text file at ./data/bookKeeper_loanList.txt. + ____________________________________________________________________ + + ------------------------------------------------------------------------------------------------ + | Add Book: | + | add-book BOOK_TITLE a/AUTHOR cat/CATEGORY cond/CONDITION loc/LOCATION [note/NOTE] | + |----------------------------------------------------------------------------------------------| + | Remove Book: | + | remove-book BOOK_TITLE | + |----------------------------------------------------------------------------------------------| + | Update Book: | + | update-book BOOK_TITLE [a/AUTHOR] [cat/CATEGORY] [cond/CONDITION] [loc/LOCATION] [note/NOTE] | + |----------------------------------------------------------------------------------------------| + | Update Title: | + | update-title BOOK_TITLE new/NEW_TITLE | + |----------------------------------------------------------------------------------------------| + | Search Book: | + | search-title KEYWORD | + |----------------------------------------------------------------------------------------------| + | View Inventory: | + | view-inventory | + |----------------------------------------------------------------------------------------------| + | List Category: | + | list-category CATEGORY | + |----------------------------------------------------------------------------------------------| + | Add Loan: | + | add-loan BOOK_TITLE n/BORROWER_NAME d/RETURN_DATE p/PHONE_NUMBER e/EMAIL | + |----------------------------------------------------------------------------------------------| + | Delete Loan: | + | delete-loan BOOK_TITLE | + |----------------------------------------------------------------------------------------------| + | Edit Loan: | + | edit-loan BOOK_TITLE [n/BORROWER_NAME] [d/RETURN_DATE] [p/PHONE_NUMBER] [e/EMAIL] | + |----------------------------------------------------------------------------------------------| + | View Loans: | + | view-loans | + |----------------------------------------------------------------------------------------------| + | Delete Note: | + | delete-note | + |----------------------------------------------------------------------------------------------| + | Display Help: | + | help | + |----------------------------------------------------------------------------------------------| + | Exit Program: | + | exit | + ------------------------------------------------------------------------------------------------ +Enter a command: diff --git a/text-ui-test/input.txt b/text-ui-test/input.txt index f6ec2e9f95..e69de29bb2 100644 --- a/text-ui-test/input.txt +++ b/text-ui-test/input.txt @@ -1 +0,0 @@ -James Gosling \ No newline at end of file