diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4bbab6cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +.idea/ +*.iml +*.ipr +*.iws + +.classpath +.project +.settings/ + +data/ +*.log + +.DS_Store +Thumbs.db diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..c61c0f5e --- /dev/null +++ b/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.ironhack + ironlibrary + 1.0.0 + IronLibrary + Library Management System - IronHack Unit 3 Homework + + + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/src/main/java/com/ironhack/Application.java b/src/main/java/com/ironhack/Application.java new file mode 100644 index 00000000..2d78ae8d --- /dev/null +++ b/src/main/java/com/ironhack/Application.java @@ -0,0 +1,12 @@ +package com.ironhack; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/main/java/com/ironhack/controller/LibraryMenuController.java b/src/main/java/com/ironhack/controller/LibraryMenuController.java new file mode 100644 index 00000000..81905305 --- /dev/null +++ b/src/main/java/com/ironhack/controller/LibraryMenuController.java @@ -0,0 +1,243 @@ +package com.ironhack.controller; + +import com.ironhack.dto.request.AddBookRequest; +import com.ironhack.dto.request.IssueBookRequest; +import com.ironhack.dto.response.BookResponse; +import com.ironhack.dto.response.IssueResponse; +import com.ironhack.model.MenuOption; +import com.ironhack.service.LibraryService; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Scanner; + +@Component +@org.springframework.boot.autoconfigure.condition.ConditionalOnProperty( + name = "ironlibrary.menu.enabled", + havingValue = "true", + matchIfMissing = true +) +public class LibraryMenuController implements CommandLineRunner { + + private final LibraryService libraryService; + private final Scanner scanner; + + public LibraryMenuController(LibraryService libraryService) { + this.libraryService = libraryService; + this.scanner = new Scanner(System.in); + } + + @Override + public void run(String... args) { + boolean running = true; + + while (running) { + displayMenu(); + try { + int choice = readMenuChoice(); + MenuOption option = MenuOption.fromValue(choice); + + if (option == null) { + System.out.println("Invalid choice. Please select a number from the menu."); + continue; + } + + running = handleMenuOption(option); + } catch (Exception exception) { + System.out.println("Error: " + exception.getMessage()); + } + } + + System.out.println("Thank you for using IronLibrary. Goodbye!"); + } + + public void displayMenu() { + System.out.println(); + System.out.println("===== IronLibrary Menu ====="); + System.out.println("1. Add a book"); + System.out.println("2. Search book by title"); + System.out.println("3. Search book by category"); + System.out.println("4. Search book by Author"); + System.out.println("5. List all books along with author"); + System.out.println("6. Issue book to student"); + System.out.println("7. List books by usn"); + System.out.println("8. List books to be returned today"); + System.out.println("9. Exit"); + System.out.print("Enter your choice: "); + } + + public int readMenuChoice() { + while (!scanner.hasNextInt()) { + scanner.next(); + System.out.print("Invalid input. Please enter a number: "); + } + int choice = scanner.nextInt(); + scanner.nextLine(); + return choice; + } + + public boolean handleMenuOption(MenuOption option) { + return switch (option) { + case ADD_BOOK -> { + handleAddBook(); + yield true; + } + case SEARCH_BY_TITLE -> { + handleSearchByTitle(); + yield true; + } + case SEARCH_BY_CATEGORY -> { + handleSearchByCategory(); + yield true; + } + case SEARCH_BY_AUTHOR -> { + handleSearchByAuthor(); + yield true; + } + case LIST_ALL_BOOKS -> { + handleListAllBooks(); + yield true; + } + case ISSUE_BOOK -> { + handleIssueBook(); + yield true; + } + case LIST_BOOKS_BY_USN -> { + handleListBooksByUsn(); + yield true; + } + case LIST_BOOKS_DUE_TODAY -> { + handleListBooksDueToday(); + yield true; + } + case EXIT -> false; + }; + } + + private void handleAddBook() { + System.out.print("Enter isbn : "); + String isbn = scanner.nextLine(); + System.out.print("Enter title : "); + String title = scanner.nextLine(); + System.out.print("Enter category : "); + String category = scanner.nextLine(); + System.out.print("Enter Author name : "); + String authorName = scanner.nextLine(); + System.out.print("Enter Author mail : "); + String authorEmail = scanner.nextLine(); + System.out.print("Enter number of books : "); + int quantity = readPositiveInteger(); + + libraryService.addBook(new AddBookRequest(isbn, title, category, authorName, authorEmail, quantity)); + System.out.println("Book added successfully."); + } + + private void handleSearchByTitle() { + System.out.print("Enter title : "); + String title = scanner.nextLine(); + printBookTable(libraryService.searchBooksByTitle(title), false); + } + + private void handleSearchByCategory() { + System.out.print("Enter category : "); + String category = scanner.nextLine(); + printBookTable(libraryService.searchBooksByCategory(category), false); + } + + private void handleSearchByAuthor() { + System.out.print("Enter name : "); + String authorName = scanner.nextLine(); + printBookTable(libraryService.searchBooksByAuthor(authorName), false); + } + + private void handleListAllBooks() { + printBookTable(libraryService.listAllBooksWithAuthors(), true); + } + + private void handleIssueBook() { + System.out.print("Enter usn : "); + String usn = scanner.nextLine(); + System.out.print("Enter name : "); + String name = scanner.nextLine(); + System.out.print("Enter book ISBN : "); + String isbn = scanner.nextLine(); + + IssueResponse response = libraryService.issueBookToStudent(new IssueBookRequest(usn, name, isbn)); + System.out.println(); + System.out.println(response.getIssueReturnMessage()); + } + + private void handleListBooksByUsn() { + System.out.print("Enter usn : "); + String usn = scanner.nextLine(); + printIssueTable(libraryService.listBooksByUsn(usn)); + } + + private void handleListBooksDueToday() { + printIssueTable(libraryService.listBooksDueToday()); + } + + public void printBookTable(List books, boolean includeAuthor) { + System.out.println(); + if (includeAuthor) { + System.out.printf("%-24s%-20s%-12s%-16s%-20s%s%n", + "Book ISBN", "Book Title", "Category", "No of Books", "Author name", "Author mail"); + } else { + System.out.printf("%-24s%-20s%-12s%s%n", + "Book ISBN", "Book Title", "Category", "No of Books"); + } + + for (BookResponse book : books) { + if (includeAuthor) { + System.out.printf("%-24s%-20s%-12s%-16d%-20s%s%n", + book.getIsbn(), + book.getTitle(), + book.getCategory(), + book.getQuantity(), + book.getAuthorName(), + book.getAuthorEmail()); + } else { + System.out.printf("%-24s%-20s%-12s%d%n", + book.getIsbn(), + book.getTitle(), + book.getCategory(), + book.getQuantity()); + } + } + } + + public void printIssueTable(List issues) { + System.out.println(); + System.out.printf("%-20s%-16s%s%n", "Book Title", "Student Name", "Return date"); + + for (IssueResponse issue : issues) { + System.out.printf("%-20s%-16s%s%n", + issue.getBookTitle(), + issue.getStudentName(), + issue.getReturnDate()); + } + } + + private int readPositiveInteger() { + while (true) { + try { + if (!scanner.hasNextInt()) { + scanner.next(); + System.out.print("Invalid number. Enter number of books : "); + continue; + } + int value = scanner.nextInt(); + scanner.nextLine(); + if (value <= 0) { + System.out.print("Number must be greater than zero. Enter number of books : "); + continue; + } + return value; + } catch (Exception exception) { + scanner.nextLine(); + System.out.print("Invalid number. Enter number of books : "); + } + } + } +} diff --git a/src/main/java/com/ironhack/dto/request/AddBookRequest.java b/src/main/java/com/ironhack/dto/request/AddBookRequest.java new file mode 100644 index 00000000..3ba78e03 --- /dev/null +++ b/src/main/java/com/ironhack/dto/request/AddBookRequest.java @@ -0,0 +1,72 @@ +package com.ironhack.dto.request; + +public class AddBookRequest { + + private String isbn; + private String title; + private String category; + private String authorName; + private String authorEmail; + private Integer quantity; + + public AddBookRequest() { + } + + public AddBookRequest(String isbn, String title, String category, String authorName, + String authorEmail, Integer quantity) { + this.isbn = isbn; + this.title = title; + this.category = category; + this.authorName = authorName; + this.authorEmail = authorEmail; + this.quantity = quantity; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getAuthorName() { + return authorName; + } + + public void setAuthorName(String authorName) { + this.authorName = authorName; + } + + public String getAuthorEmail() { + return authorEmail; + } + + public void setAuthorEmail(String authorEmail) { + this.authorEmail = authorEmail; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } +} diff --git a/src/main/java/com/ironhack/dto/request/IssueBookRequest.java b/src/main/java/com/ironhack/dto/request/IssueBookRequest.java new file mode 100644 index 00000000..0f003633 --- /dev/null +++ b/src/main/java/com/ironhack/dto/request/IssueBookRequest.java @@ -0,0 +1,41 @@ +package com.ironhack.dto.request; + +public class IssueBookRequest { + + private String usn; + private String studentName; + private String bookIsbn; + + public IssueBookRequest() { + } + + public IssueBookRequest(String usn, String studentName, String bookIsbn) { + this.usn = usn; + this.studentName = studentName; + this.bookIsbn = bookIsbn; + } + + public String getUsn() { + return usn; + } + + public void setUsn(String usn) { + this.usn = usn; + } + + public String getStudentName() { + return studentName; + } + + public void setStudentName(String studentName) { + this.studentName = studentName; + } + + public String getBookIsbn() { + return bookIsbn; + } + + public void setBookIsbn(String bookIsbn) { + this.bookIsbn = bookIsbn; + } +} diff --git a/src/main/java/com/ironhack/dto/response/BookResponse.java b/src/main/java/com/ironhack/dto/response/BookResponse.java new file mode 100644 index 00000000..395e62e2 --- /dev/null +++ b/src/main/java/com/ironhack/dto/response/BookResponse.java @@ -0,0 +1,79 @@ +package com.ironhack.dto.response; + +public class BookResponse { + + private String isbn; + private String title; + private String category; + private Integer quantity; + private String authorName; + private String authorEmail; + + public BookResponse() { + } + + public BookResponse(String isbn, String title, String category, Integer quantity) { + this.isbn = isbn; + this.title = title; + this.category = category; + this.quantity = quantity; + } + + public BookResponse(String isbn, String title, String category, Integer quantity, + String authorName, String authorEmail) { + this.isbn = isbn; + this.title = title; + this.category = category; + this.quantity = quantity; + this.authorName = authorName; + this.authorEmail = authorEmail; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public String getAuthorName() { + return authorName; + } + + public void setAuthorName(String authorName) { + this.authorName = authorName; + } + + public String getAuthorEmail() { + return authorEmail; + } + + public void setAuthorEmail(String authorEmail) { + this.authorEmail = authorEmail; + } +} diff --git a/src/main/java/com/ironhack/dto/response/IssueResponse.java b/src/main/java/com/ironhack/dto/response/IssueResponse.java new file mode 100644 index 00000000..6aaadd24 --- /dev/null +++ b/src/main/java/com/ironhack/dto/response/IssueResponse.java @@ -0,0 +1,54 @@ +package com.ironhack.dto.response; + +public class IssueResponse { + + private String bookTitle; + private String studentName; + private String returnDate; + private String issueReturnMessage; + + public IssueResponse() { + } + + public IssueResponse(String bookTitle, String studentName, String returnDate) { + this.bookTitle = bookTitle; + this.studentName = studentName; + this.returnDate = returnDate; + } + + public IssueResponse(String issueReturnMessage) { + this.issueReturnMessage = issueReturnMessage; + } + + public String getBookTitle() { + return bookTitle; + } + + public void setBookTitle(String bookTitle) { + this.bookTitle = bookTitle; + } + + public String getStudentName() { + return studentName; + } + + public void setStudentName(String studentName) { + this.studentName = studentName; + } + + public String getReturnDate() { + return returnDate; + } + + public void setReturnDate(String returnDate) { + this.returnDate = returnDate; + } + + public String getIssueReturnMessage() { + return issueReturnMessage; + } + + public void setIssueReturnMessage(String issueReturnMessage) { + this.issueReturnMessage = issueReturnMessage; + } +} diff --git a/src/main/java/com/ironhack/entity/AuthorEntity.java b/src/main/java/com/ironhack/entity/AuthorEntity.java new file mode 100644 index 00000000..0719e4df --- /dev/null +++ b/src/main/java/com/ironhack/entity/AuthorEntity.java @@ -0,0 +1,71 @@ +package com.ironhack.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "author") +public class AuthorEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "author_id") + private Integer authorId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String email; + + @OneToOne + @JoinColumn(name = "book_isbn", nullable = false, unique = true) + private BookEntity authorBook; + + public AuthorEntity() { + } + + public AuthorEntity(String name, String email, BookEntity authorBook) { + this.name = name; + this.email = email; + this.authorBook = authorBook; + } + + public Integer getAuthorId() { + return authorId; + } + + public void setAuthorId(Integer authorId) { + this.authorId = authorId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public BookEntity getAuthorBook() { + return authorBook; + } + + public void setAuthorBook(BookEntity authorBook) { + this.authorBook = authorBook; + } +} diff --git a/src/main/java/com/ironhack/entity/BookEntity.java b/src/main/java/com/ironhack/entity/BookEntity.java new file mode 100644 index 00000000..09a09d29 --- /dev/null +++ b/src/main/java/com/ironhack/entity/BookEntity.java @@ -0,0 +1,66 @@ +package com.ironhack.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "book") +public class BookEntity { + + @Id + @Column(name = "isbn", nullable = false, unique = true) + private String isbn; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + private String category; + + @Column(nullable = false) + private Integer quantity; + + public BookEntity() { + } + + public BookEntity(String isbn, String title, String category, Integer quantity) { + this.isbn = isbn; + this.title = title; + this.category = category; + this.quantity = quantity; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } +} diff --git a/src/main/java/com/ironhack/entity/IssueEntity.java b/src/main/java/com/ironhack/entity/IssueEntity.java new file mode 100644 index 00000000..806343d8 --- /dev/null +++ b/src/main/java/com/ironhack/entity/IssueEntity.java @@ -0,0 +1,84 @@ +package com.ironhack.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "issue") +public class IssueEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "issue_id") + private Integer issueId; + + @Column(name = "issue_date", nullable = false) + private String issueDate; + + @Column(name = "return_date", nullable = false) + private String returnDate; + + @ManyToOne + @JoinColumn(name = "student_usn", nullable = false) + private StudentEntity issueStudent; + + @ManyToOne + @JoinColumn(name = "book_isbn", nullable = false) + private BookEntity issueBook; + + public IssueEntity() { + } + + public IssueEntity(String issueDate, String returnDate, StudentEntity issueStudent, BookEntity issueBook) { + this.issueDate = issueDate; + this.returnDate = returnDate; + this.issueStudent = issueStudent; + this.issueBook = issueBook; + } + + public Integer getIssueId() { + return issueId; + } + + public void setIssueId(Integer issueId) { + this.issueId = issueId; + } + + public String getIssueDate() { + return issueDate; + } + + public void setIssueDate(String issueDate) { + this.issueDate = issueDate; + } + + public String getReturnDate() { + return returnDate; + } + + public void setReturnDate(String returnDate) { + this.returnDate = returnDate; + } + + public StudentEntity getIssueStudent() { + return issueStudent; + } + + public void setIssueStudent(StudentEntity issueStudent) { + this.issueStudent = issueStudent; + } + + public BookEntity getIssueBook() { + return issueBook; + } + + public void setIssueBook(BookEntity issueBook) { + this.issueBook = issueBook; + } +} diff --git a/src/main/java/com/ironhack/entity/StudentEntity.java b/src/main/java/com/ironhack/entity/StudentEntity.java new file mode 100644 index 00000000..38e7d294 --- /dev/null +++ b/src/main/java/com/ironhack/entity/StudentEntity.java @@ -0,0 +1,42 @@ +package com.ironhack.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "student") +public class StudentEntity { + + @Id + @Column(name = "usn", nullable = false, unique = true) + private String usn; + + @Column(nullable = false) + private String name; + + public StudentEntity() { + } + + public StudentEntity(String usn, String name) { + this.usn = usn; + this.name = name; + } + + public String getUsn() { + return usn; + } + + public void setUsn(String usn) { + this.usn = usn; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/ironhack/exception/DuplicateResourceException.java b/src/main/java/com/ironhack/exception/DuplicateResourceException.java new file mode 100644 index 00000000..12e1fc13 --- /dev/null +++ b/src/main/java/com/ironhack/exception/DuplicateResourceException.java @@ -0,0 +1,8 @@ +package com.ironhack.exception; + +public class DuplicateResourceException extends RuntimeException { + + public DuplicateResourceException(String message) { + super(message); + } +} diff --git a/src/main/java/com/ironhack/exception/InsufficientQuantityException.java b/src/main/java/com/ironhack/exception/InsufficientQuantityException.java new file mode 100644 index 00000000..ce3c0a92 --- /dev/null +++ b/src/main/java/com/ironhack/exception/InsufficientQuantityException.java @@ -0,0 +1,8 @@ +package com.ironhack.exception; + +public class InsufficientQuantityException extends RuntimeException { + + public InsufficientQuantityException(String message) { + super(message); + } +} diff --git a/src/main/java/com/ironhack/exception/InvalidInputException.java b/src/main/java/com/ironhack/exception/InvalidInputException.java new file mode 100644 index 00000000..4f4c3405 --- /dev/null +++ b/src/main/java/com/ironhack/exception/InvalidInputException.java @@ -0,0 +1,8 @@ +package com.ironhack.exception; + +public class InvalidInputException extends RuntimeException { + + public InvalidInputException(String message) { + super(message); + } +} diff --git a/src/main/java/com/ironhack/exception/ResourceNotFoundException.java b/src/main/java/com/ironhack/exception/ResourceNotFoundException.java new file mode 100644 index 00000000..734d3333 --- /dev/null +++ b/src/main/java/com/ironhack/exception/ResourceNotFoundException.java @@ -0,0 +1,8 @@ +package com.ironhack.exception; + +public class ResourceNotFoundException extends RuntimeException { + + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/ironhack/mapper/LibraryMapper.java b/src/main/java/com/ironhack/mapper/LibraryMapper.java new file mode 100644 index 00000000..f3caf671 --- /dev/null +++ b/src/main/java/com/ironhack/mapper/LibraryMapper.java @@ -0,0 +1,76 @@ +package com.ironhack.mapper; + +import com.ironhack.dto.response.BookResponse; +import com.ironhack.dto.response.IssueResponse; +import com.ironhack.entity.AuthorEntity; +import com.ironhack.entity.BookEntity; +import com.ironhack.entity.IssueEntity; +import com.ironhack.model.Author; +import com.ironhack.model.Book; +import com.ironhack.model.Issue; +import com.ironhack.model.Student; +import org.springframework.stereotype.Component; + +@Component +public class LibraryMapper { + + public Book toModel(BookEntity entity) { + if (entity == null) { + return null; + } + return new Book(entity.getIsbn(), entity.getTitle(), entity.getCategory(), entity.getQuantity()); + } + + public BookEntity toEntity(Book model) { + if (model == null) { + return null; + } + return new BookEntity(model.getIsbn(), model.getTitle(), model.getCategory(), model.getQuantity()); + } + + public Author toModel(AuthorEntity entity) { + if (entity == null) { + return null; + } + Author author = new Author(entity.getName(), entity.getEmail(), toModel(entity.getAuthorBook())); + author.setAuthorId(entity.getAuthorId()); + return author; + } + + public Issue toModel(IssueEntity entity) { + if (entity == null) { + return null; + } + Student student = new Student(entity.getIssueStudent().getUsn(), entity.getIssueStudent().getName()); + Book book = toModel(entity.getIssueBook()); + Issue issue = new Issue(entity.getIssueDate(), entity.getReturnDate(), student, book); + issue.setIssueId(entity.getIssueId()); + return issue; + } + + public BookResponse toBookResponse(BookEntity book) { + return new BookResponse(book.getIsbn(), book.getTitle(), book.getCategory(), book.getQuantity()); + } + + public BookResponse toBookResponse(BookEntity book, AuthorEntity author) { + if (author == null) { + return toBookResponse(book); + } + return new BookResponse( + book.getIsbn(), + book.getTitle(), + book.getCategory(), + book.getQuantity(), + author.getName(), + author.getEmail() + ); + } + + public IssueResponse toIssueResponse(IssueEntity issue) { + return new IssueResponse( + issue.getIssueBook().getTitle(), + issue.getIssueStudent().getName(), + issue.getReturnDate() + ); + } +} diff --git a/src/main/java/com/ironhack/model/Author.java b/src/main/java/com/ironhack/model/Author.java new file mode 100644 index 00000000..0091f984 --- /dev/null +++ b/src/main/java/com/ironhack/model/Author.java @@ -0,0 +1,50 @@ +package com.ironhack.model; + +public class Author { + + private Integer authorId; + private String name; + private String email; + private Book authorBook; + + public Author() { + } + + public Author(String name, String email, Book authorBook) { + this.name = name; + this.email = email; + this.authorBook = authorBook; + } + + public Integer getAuthorId() { + return authorId; + } + + public void setAuthorId(Integer authorId) { + this.authorId = authorId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Book getAuthorBook() { + return authorBook; + } + + public void setAuthorBook(Book authorBook) { + this.authorBook = authorBook; + } +} diff --git a/src/main/java/com/ironhack/model/Book.java b/src/main/java/com/ironhack/model/Book.java new file mode 100644 index 00000000..0ee3d049 --- /dev/null +++ b/src/main/java/com/ironhack/model/Book.java @@ -0,0 +1,51 @@ +package com.ironhack.model; + +public class Book { + + private String isbn; + private String title; + private String category; + private Integer quantity; + + public Book() { + } + + public Book(String isbn, String title, String category, Integer quantity) { + this.isbn = isbn; + this.title = title; + this.category = category; + this.quantity = quantity; + } + + public String getIsbn() { + return isbn; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } +} diff --git a/src/main/java/com/ironhack/model/Issue.java b/src/main/java/com/ironhack/model/Issue.java new file mode 100644 index 00000000..78f7f332 --- /dev/null +++ b/src/main/java/com/ironhack/model/Issue.java @@ -0,0 +1,60 @@ +package com.ironhack.model; + +public class Issue { + + private Integer issueId; + private String issueDate; + private String returnDate; + private Student issueStudent; + private Book issueBook; + + public Issue() { + } + + public Issue(String issueDate, String returnDate, Student issueStudent, Book issueBook) { + this.issueDate = issueDate; + this.returnDate = returnDate; + this.issueStudent = issueStudent; + this.issueBook = issueBook; + } + + public Integer getIssueId() { + return issueId; + } + + public void setIssueId(Integer issueId) { + this.issueId = issueId; + } + + public String getIssueDate() { + return issueDate; + } + + public void setIssueDate(String issueDate) { + this.issueDate = issueDate; + } + + public String getReturnDate() { + return returnDate; + } + + public void setReturnDate(String returnDate) { + this.returnDate = returnDate; + } + + public Student getIssueStudent() { + return issueStudent; + } + + public void setIssueStudent(Student issueStudent) { + this.issueStudent = issueStudent; + } + + public Book getIssueBook() { + return issueBook; + } + + public void setIssueBook(Book issueBook) { + this.issueBook = issueBook; + } +} diff --git a/src/main/java/com/ironhack/model/MenuOption.java b/src/main/java/com/ironhack/model/MenuOption.java new file mode 100644 index 00000000..d785a4d8 --- /dev/null +++ b/src/main/java/com/ironhack/model/MenuOption.java @@ -0,0 +1,32 @@ +package com.ironhack.model; + +public enum MenuOption { + ADD_BOOK(1), + SEARCH_BY_TITLE(2), + SEARCH_BY_CATEGORY(3), + SEARCH_BY_AUTHOR(4), + LIST_ALL_BOOKS(5), + ISSUE_BOOK(6), + LIST_BOOKS_BY_USN(7), + LIST_BOOKS_DUE_TODAY(8), + EXIT(9); + + private final int value; + + MenuOption(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static MenuOption fromValue(int value) { + for (MenuOption option : values()) { + if (option.value == value) { + return option; + } + } + return null; + } +} diff --git a/src/main/java/com/ironhack/model/Student.java b/src/main/java/com/ironhack/model/Student.java new file mode 100644 index 00000000..207583ed --- /dev/null +++ b/src/main/java/com/ironhack/model/Student.java @@ -0,0 +1,31 @@ +package com.ironhack.model; + +public class Student { + + private String usn; + private String name; + + public Student() { + } + + public Student(String usn, String name) { + this.usn = usn; + this.name = name; + } + + public String getUsn() { + return usn; + } + + public void setUsn(String usn) { + this.usn = usn; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/ironhack/repository/AuthorRepository.java b/src/main/java/com/ironhack/repository/AuthorRepository.java new file mode 100644 index 00000000..0546a885 --- /dev/null +++ b/src/main/java/com/ironhack/repository/AuthorRepository.java @@ -0,0 +1,11 @@ +package com.ironhack.repository; + +import com.ironhack.entity.AuthorEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface AuthorRepository extends JpaRepository { + + List findByNameIgnoreCase(String name); +} diff --git a/src/main/java/com/ironhack/repository/BookRepository.java b/src/main/java/com/ironhack/repository/BookRepository.java new file mode 100644 index 00000000..ce6404d5 --- /dev/null +++ b/src/main/java/com/ironhack/repository/BookRepository.java @@ -0,0 +1,13 @@ +package com.ironhack.repository; + +import com.ironhack.entity.BookEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface BookRepository extends JpaRepository { + + List findByTitleIgnoreCase(String title); + + List findByCategoryIgnoreCase(String category); +} diff --git a/src/main/java/com/ironhack/repository/IssueRepository.java b/src/main/java/com/ironhack/repository/IssueRepository.java new file mode 100644 index 00000000..9a023a25 --- /dev/null +++ b/src/main/java/com/ironhack/repository/IssueRepository.java @@ -0,0 +1,16 @@ +package com.ironhack.repository; + +import com.ironhack.entity.IssueEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface IssueRepository extends JpaRepository { + + List findByIssueStudentUsn(String usn); + + @Query("SELECT i FROM IssueEntity i WHERE i.returnDate LIKE :datePrefix%") + List findByReturnDateStartingWith(@Param("datePrefix") String datePrefix); +} diff --git a/src/main/java/com/ironhack/repository/StudentRepository.java b/src/main/java/com/ironhack/repository/StudentRepository.java new file mode 100644 index 00000000..69c1f672 --- /dev/null +++ b/src/main/java/com/ironhack/repository/StudentRepository.java @@ -0,0 +1,7 @@ +package com.ironhack.repository; + +import com.ironhack.entity.StudentEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface StudentRepository extends JpaRepository { +} diff --git a/src/main/java/com/ironhack/service/LibraryService.java b/src/main/java/com/ironhack/service/LibraryService.java new file mode 100644 index 00000000..1e66be9e --- /dev/null +++ b/src/main/java/com/ironhack/service/LibraryService.java @@ -0,0 +1,231 @@ +package com.ironhack.service; + +import com.ironhack.dto.request.AddBookRequest; +import com.ironhack.dto.request.IssueBookRequest; +import com.ironhack.dto.response.BookResponse; +import com.ironhack.dto.response.IssueResponse; +import com.ironhack.entity.AuthorEntity; +import com.ironhack.entity.BookEntity; +import com.ironhack.entity.IssueEntity; +import com.ironhack.entity.StudentEntity; +import com.ironhack.exception.DuplicateResourceException; +import com.ironhack.exception.InsufficientQuantityException; +import com.ironhack.exception.InvalidInputException; +import com.ironhack.exception.ResourceNotFoundException; +import com.ironhack.mapper.LibraryMapper; +import com.ironhack.repository.AuthorRepository; +import com.ironhack.repository.BookRepository; +import com.ironhack.repository.IssueRepository; +import com.ironhack.repository.StudentRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +@Service +public class LibraryService { + + private static final int LOAN_PERIOD_DAYS = 7; + + private final BookRepository bookRepository; + private final AuthorRepository authorRepository; + private final StudentRepository studentRepository; + private final IssueRepository issueRepository; + private final LibraryMapper libraryMapper; + + public LibraryService(BookRepository bookRepository, + AuthorRepository authorRepository, + StudentRepository studentRepository, + IssueRepository issueRepository, + LibraryMapper libraryMapper) { + this.bookRepository = bookRepository; + this.authorRepository = authorRepository; + this.studentRepository = studentRepository; + this.issueRepository = issueRepository; + this.libraryMapper = libraryMapper; + } + + @Transactional + public void addBook(AddBookRequest request) { + validateAddBookRequest(request); + + if (bookRepository.existsById(request.getIsbn())) { + throw new DuplicateResourceException("A book with ISBN " + request.getIsbn() + " already exists."); + } + + BookEntity book = new BookEntity( + request.getIsbn().trim(), + request.getTitle().trim(), + request.getCategory().trim(), + request.getQuantity() + ); + bookRepository.save(book); + + AuthorEntity author = new AuthorEntity( + request.getAuthorName().trim(), + request.getAuthorEmail().trim(), + book + ); + authorRepository.save(author); + } + + public List searchBooksByTitle(String title) { + validateSearchInput(title, "title"); + List books = bookRepository.findByTitleIgnoreCase(title.trim()); + return mapBooksToResponses(books); + } + + public List searchBooksByCategory(String category) { + validateSearchInput(category, "category"); + List books = bookRepository.findByCategoryIgnoreCase(category.trim()); + return mapBooksToResponses(books); + } + + public List searchBooksByAuthor(String authorName) { + validateSearchInput(authorName, "author name"); + List authors = authorRepository.findByNameIgnoreCase(authorName.trim()); + if (authors.isEmpty()) { + throw new ResourceNotFoundException("No books found for author: " + authorName); + } + + List responses = new ArrayList<>(); + for (AuthorEntity author : authors) { + responses.add(libraryMapper.toBookResponse(author.getAuthorBook())); + } + return responses; + } + + public List listAllBooksWithAuthors() { + List authors = authorRepository.findAll(); + if (authors.isEmpty()) { + throw new ResourceNotFoundException("No books available in the library."); + } + + List responses = new ArrayList<>(); + for (AuthorEntity author : authors) { + responses.add(libraryMapper.toBookResponse(author.getAuthorBook(), author)); + } + return responses; + } + + @Transactional + public IssueResponse issueBookToStudent(IssueBookRequest request) { + validateIssueBookRequest(request); + + BookEntity book = bookRepository.findById(request.getBookIsbn().trim()) + .orElseThrow(() -> new ResourceNotFoundException("Book not found with ISBN: " + request.getBookIsbn())); + + if (book.getQuantity() <= 0) { + throw new InsufficientQuantityException("No copies available for book: " + book.getTitle()); + } + + StudentEntity student = studentRepository.findById(request.getUsn().trim()) + .orElseGet(() -> studentRepository.save( + new StudentEntity(request.getUsn().trim(), request.getStudentName().trim()) + )); + + if (!student.getName().equalsIgnoreCase(request.getStudentName().trim())) { + student.setName(request.getStudentName().trim()); + studentRepository.save(student); + } + + Date issueDate = new Date(); + Date returnDate = calculateReturnDate(issueDate); + + IssueEntity issue = new IssueEntity( + issueDate.toString(), + formatReturnDateForStorage(returnDate), + student, + book + ); + issueRepository.save(issue); + + book.setQuantity(book.getQuantity() - 1); + bookRepository.save(book); + + return new IssueResponse("Book issued. Return date : " + returnDate); + } + + public List listBooksByUsn(String usn) { + validateSearchInput(usn, "USN"); + + if (!studentRepository.existsById(usn.trim())) { + throw new ResourceNotFoundException("No student found with USN: " + usn); + } + + List issues = issueRepository.findByIssueStudentUsn(usn.trim()); + if (issues.isEmpty()) { + throw new ResourceNotFoundException("No books issued to student with USN: " + usn); + } + + return issues.stream().map(libraryMapper::toIssueResponse).toList(); + } + + public List listBooksDueToday() { + String todayPrefix = LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE); + List issues = issueRepository.findByReturnDateStartingWith(todayPrefix); + + if (issues.isEmpty()) { + throw new ResourceNotFoundException("No books are due for return today."); + } + + return issues.stream().map(libraryMapper::toIssueResponse).toList(); + } + + public Date calculateReturnDate(Date issueDate) { + LocalDateTime issueDateTime = issueDate.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + LocalDateTime returnDateTime = issueDateTime.plusDays(LOAN_PERIOD_DAYS); + return Date.from(returnDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } + + public String formatReturnDateForStorage(Date returnDate) { + LocalDateTime returnDateTime = returnDate.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDateTime(); + return returnDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")); + } + + private List mapBooksToResponses(List books) { + if (books.isEmpty()) { + throw new ResourceNotFoundException("No books found matching your search."); + } + return books.stream().map(libraryMapper::toBookResponse).toList(); + } + + private void validateAddBookRequest(AddBookRequest request) { + if (request == null) { + throw new InvalidInputException("Book details cannot be empty."); + } + validateSearchInput(request.getIsbn(), "ISBN"); + validateSearchInput(request.getTitle(), "title"); + validateSearchInput(request.getCategory(), "category"); + validateSearchInput(request.getAuthorName(), "author name"); + validateSearchInput(request.getAuthorEmail(), "author email"); + if (request.getQuantity() == null || request.getQuantity() <= 0) { + throw new InvalidInputException("Number of books must be greater than zero."); + } + } + + private void validateIssueBookRequest(IssueBookRequest request) { + if (request == null) { + throw new InvalidInputException("Issue details cannot be empty."); + } + validateSearchInput(request.getUsn(), "USN"); + validateSearchInput(request.getStudentName(), "student name"); + validateSearchInput(request.getBookIsbn(), "book ISBN"); + } + + private void validateSearchInput(String value, String fieldName) { + if (value == null || value.trim().isEmpty()) { + throw new InvalidInputException("Invalid " + fieldName + ". Please enter a valid value."); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 00000000..74141c29 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,16 @@ +spring.application.name=IronLibrary + +spring.main.web-application-type=none +ironlibrary.menu.enabled=true + +spring.datasource.url=jdbc:h2:file:./data/ironlibrary +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=false + +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console diff --git a/src/test/java/com/ironhack/controller/LibraryMenuControllerTest.java b/src/test/java/com/ironhack/controller/LibraryMenuControllerTest.java new file mode 100644 index 00000000..33691575 --- /dev/null +++ b/src/test/java/com/ironhack/controller/LibraryMenuControllerTest.java @@ -0,0 +1,101 @@ +package com.ironhack.controller; + +import com.ironhack.dto.response.BookResponse; +import com.ironhack.dto.response.IssueResponse; +import com.ironhack.model.MenuOption; +import com.ironhack.service.LibraryService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LibraryMenuControllerTest { + + @Mock + private LibraryService libraryService; + + private LibraryMenuController controller; + private ByteArrayOutputStream outputStream; + + @BeforeEach + void setUp() { + controller = new LibraryMenuController(libraryService); + outputStream = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outputStream)); + } + + @Test + void displayMenu_shouldShowAllOptions() { + controller.displayMenu(); + + String output = outputStream.toString(); + assertTrue(output.contains("1. Add a book")); + assertTrue(output.contains("7. List books by usn")); + assertTrue(output.contains("8. List books to be returned today")); + assertTrue(output.contains("9. Exit")); + } + + @Test + void handleMenuOption_shouldReturnFalseForExit() { + assertFalse(controller.handleMenuOption(MenuOption.EXIT)); + } + + @Test + void printBookTable_shouldFormatBooksWithoutAuthor() { + List books = List.of( + new BookResponse("978-3-16-148410-0", "The Notebook", "Romance", 4) + ); + + controller.printBookTable(books, false); + + String output = outputStream.toString(); + assertTrue(output.contains("Book ISBN")); + assertTrue(output.contains("The Notebook")); + assertTrue(output.contains("Romance")); + } + + @Test + void printBookTable_shouldFormatBooksWithAuthor() { + List books = List.of( + new BookResponse( + "978-3-16-148410-0", + "The Notebook", + "Romance", + 4, + "Nicholas Sparks", + "nicholassparks@gmail.com" + ) + ); + + controller.printBookTable(books, true); + + String output = outputStream.toString(); + assertTrue(output.contains("Author name")); + assertTrue(output.contains("Nicholas Sparks")); + assertTrue(output.contains("nicholassparks@gmail.com")); + } + + @Test + void printIssueTable_shouldFormatIssuedBooks() { + List issues = List.of( + new IssueResponse("Da Vinci Code", "John Doe", "2022-08-01 16:45:40.636000") + ); + + controller.printIssueTable(issues); + + String output = outputStream.toString(); + assertTrue(output.contains("Book Title")); + assertTrue(output.contains("Da Vinci Code")); + assertTrue(output.contains("John Doe")); + } +} diff --git a/src/test/java/com/ironhack/mapper/LibraryMapperTest.java b/src/test/java/com/ironhack/mapper/LibraryMapperTest.java new file mode 100644 index 00000000..5bafc601 --- /dev/null +++ b/src/test/java/com/ironhack/mapper/LibraryMapperTest.java @@ -0,0 +1,113 @@ +package com.ironhack.mapper; + +import com.ironhack.dto.response.BookResponse; +import com.ironhack.dto.response.IssueResponse; +import com.ironhack.entity.AuthorEntity; +import com.ironhack.entity.BookEntity; +import com.ironhack.entity.IssueEntity; +import com.ironhack.entity.StudentEntity; +import com.ironhack.model.Author; +import com.ironhack.model.Book; +import com.ironhack.model.Issue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class LibraryMapperTest { + + private LibraryMapper libraryMapper; + private BookEntity bookEntity; + private AuthorEntity authorEntity; + private StudentEntity studentEntity; + + @BeforeEach + void setUp() { + libraryMapper = new LibraryMapper(); + bookEntity = new BookEntity("978-3-16-148410-0", "The Notebook", "Romance", 4); + authorEntity = new AuthorEntity("Nicholas Sparks", "nicholassparks@gmail.com", bookEntity); + authorEntity.setAuthorId(1); + studentEntity = new StudentEntity("09003688800", "John Doe"); + } + + @Test + void toModel_shouldMapBookEntityToBook() { + Book book = libraryMapper.toModel(bookEntity); + + assertEquals("978-3-16-148410-0", book.getIsbn()); + assertEquals("The Notebook", book.getTitle()); + assertEquals("Romance", book.getCategory()); + assertEquals(4, book.getQuantity()); + } + + @Test + void toEntity_shouldMapBookToBookEntity() { + Book book = new Book("978-3-17-148410-0", "Da Vinci Code", "Mystery", 5); + + BookEntity entity = libraryMapper.toEntity(book); + + assertEquals("978-3-17-148410-0", entity.getIsbn()); + assertEquals("Da Vinci Code", entity.getTitle()); + } + + @Test + void toModel_shouldMapAuthorEntityToAuthor() { + Author author = libraryMapper.toModel(authorEntity); + + assertEquals(1, author.getAuthorId()); + assertEquals("Nicholas Sparks", author.getName()); + assertEquals("The Notebook", author.getAuthorBook().getTitle()); + } + + @Test + void toBookResponse_shouldMapBookWithoutAuthor() { + BookResponse response = libraryMapper.toBookResponse(bookEntity); + + assertEquals("978-3-16-148410-0", response.getIsbn()); + assertNull(response.getAuthorName()); + } + + @Test + void toBookResponse_shouldMapBookWithAuthor() { + BookResponse response = libraryMapper.toBookResponse(bookEntity, authorEntity); + + assertEquals("Nicholas Sparks", response.getAuthorName()); + assertEquals("nicholassparks@gmail.com", response.getAuthorEmail()); + } + + @Test + void toIssueResponse_shouldMapIssueEntity() { + IssueEntity issueEntity = new IssueEntity( + "Mon Aug 01 19:45:40 EEST 2022", + "2022-08-01 16:45:40.636000", + studentEntity, + bookEntity + ); + + IssueResponse response = libraryMapper.toIssueResponse(issueEntity); + + assertEquals("The Notebook", response.getBookTitle()); + assertEquals("John Doe", response.getStudentName()); + assertEquals("2022-08-01 16:45:40.636000", response.getReturnDate()); + } + + @Test + void toModel_shouldMapIssueEntityToIssue() { + IssueEntity issueEntity = new IssueEntity( + "Mon Aug 01 19:45:40 EEST 2022", + "2022-08-01 16:45:40.636000", + studentEntity, + bookEntity + ); + issueEntity.setIssueId(10); + + Issue issue = libraryMapper.toModel(issueEntity); + + assertNotNull(issue); + assertEquals(10, issue.getIssueId()); + assertEquals("John Doe", issue.getIssueStudent().getName()); + assertEquals("The Notebook", issue.getIssueBook().getTitle()); + } +} diff --git a/src/test/java/com/ironhack/model/MenuOptionTest.java b/src/test/java/com/ironhack/model/MenuOptionTest.java new file mode 100644 index 00000000..04e3d95a --- /dev/null +++ b/src/test/java/com/ironhack/model/MenuOptionTest.java @@ -0,0 +1,20 @@ +package com.ironhack.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class MenuOptionTest { + + @Test + void fromValue_shouldReturnMatchingOption() { + assertEquals(MenuOption.ADD_BOOK, MenuOption.fromValue(1)); + assertEquals(MenuOption.EXIT, MenuOption.fromValue(9)); + } + + @Test + void fromValue_shouldReturnNullForInvalidValue() { + assertNull(MenuOption.fromValue(99)); + } +} diff --git a/src/test/java/com/ironhack/service/LibraryServiceTest.java b/src/test/java/com/ironhack/service/LibraryServiceTest.java new file mode 100644 index 00000000..04a35e3c --- /dev/null +++ b/src/test/java/com/ironhack/service/LibraryServiceTest.java @@ -0,0 +1,263 @@ +package com.ironhack.service; + +import com.ironhack.dto.request.AddBookRequest; +import com.ironhack.dto.request.IssueBookRequest; +import com.ironhack.dto.response.BookResponse; +import com.ironhack.dto.response.IssueResponse; +import com.ironhack.entity.AuthorEntity; +import com.ironhack.entity.BookEntity; +import com.ironhack.entity.IssueEntity; +import com.ironhack.entity.StudentEntity; +import com.ironhack.exception.DuplicateResourceException; +import com.ironhack.exception.InsufficientQuantityException; +import com.ironhack.exception.InvalidInputException; +import com.ironhack.exception.ResourceNotFoundException; +import com.ironhack.mapper.LibraryMapper; +import com.ironhack.repository.AuthorRepository; +import com.ironhack.repository.BookRepository; +import com.ironhack.repository.IssueRepository; +import com.ironhack.repository.StudentRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class LibraryServiceTest { + + @Mock + private BookRepository bookRepository; + + @Mock + private AuthorRepository authorRepository; + + @Mock + private StudentRepository studentRepository; + + @Mock + private IssueRepository issueRepository; + + @Mock + private LibraryMapper libraryMapper; + + @InjectMocks + private LibraryService libraryService; + + private BookEntity sampleBook; + private AuthorEntity sampleAuthor; + private StudentEntity sampleStudent; + + @BeforeEach + void setUp() { + sampleBook = new BookEntity("978-3-16-148410-0", "The Notebook", "Romance", 4); + sampleAuthor = new AuthorEntity("Nicholas Sparks", "nicholassparks@gmail.com", sampleBook); + sampleStudent = new StudentEntity("09003688800", "John Doe"); + } + + @Test + void addBook_shouldPersistBookAndAuthor() { + AddBookRequest request = new AddBookRequest( + "978-3-16-148410-0", + "The Notebook", + "Romance", + "Nicholas Sparks", + "nicholassparks@gmail.com", + 4 + ); + + when(bookRepository.existsById("978-3-16-148410-0")).thenReturn(false); + + libraryService.addBook(request); + + verify(bookRepository).save(any(BookEntity.class)); + verify(authorRepository).save(any(AuthorEntity.class)); + } + + @Test + void addBook_shouldThrowWhenIsbnAlreadyExists() { + AddBookRequest request = new AddBookRequest( + "978-3-16-148410-0", + "The Notebook", + "Romance", + "Nicholas Sparks", + "nicholassparks@gmail.com", + 4 + ); + + when(bookRepository.existsById("978-3-16-148410-0")).thenReturn(true); + + assertThrows(DuplicateResourceException.class, () -> libraryService.addBook(request)); + verify(bookRepository, never()).save(any()); + } + + @Test + void addBook_shouldThrowWhenQuantityIsInvalid() { + AddBookRequest request = new AddBookRequest( + "978-3-16-148410-0", + "The Notebook", + "Romance", + "Nicholas Sparks", + "nicholassparks@gmail.com", + 0 + ); + + assertThrows(InvalidInputException.class, () -> libraryService.addBook(request)); + } + + @Test + void searchBooksByTitle_shouldReturnMatchingBooks() { + when(bookRepository.findByTitleIgnoreCase("The Notebook")).thenReturn(List.of(sampleBook)); + when(libraryMapper.toBookResponse(sampleBook)) + .thenReturn(new BookResponse("978-3-16-148410-0", "The Notebook", "Romance", 4)); + + List results = libraryService.searchBooksByTitle("The Notebook"); + + assertEquals(1, results.size()); + assertEquals("The Notebook", results.get(0).getTitle()); + } + + @Test + void searchBooksByTitle_shouldThrowWhenNoBooksFound() { + when(bookRepository.findByTitleIgnoreCase("Unknown")).thenReturn(List.of()); + + assertThrows(ResourceNotFoundException.class, () -> libraryService.searchBooksByTitle("Unknown")); + } + + @Test + void searchBooksByCategory_shouldReturnMatchingBooks() { + when(bookRepository.findByCategoryIgnoreCase("Romance")).thenReturn(List.of(sampleBook)); + when(libraryMapper.toBookResponse(sampleBook)) + .thenReturn(new BookResponse("978-3-16-148410-0", "The Notebook", "Romance", 4)); + + List results = libraryService.searchBooksByCategory("Romance"); + + assertEquals(1, results.size()); + assertEquals("Romance", results.get(0).getCategory()); + } + + @Test + void searchBooksByAuthor_shouldReturnBooksForAuthor() { + when(authorRepository.findByNameIgnoreCase("Nicholas Sparks")).thenReturn(List.of(sampleAuthor)); + when(libraryMapper.toBookResponse(sampleBook)) + .thenReturn(new BookResponse("978-3-16-148410-0", "The Notebook", "Romance", 4)); + + List results = libraryService.searchBooksByAuthor("Nicholas Sparks"); + + assertEquals(1, results.size()); + } + + @Test + void listAllBooksWithAuthors_shouldReturnBooksWithAuthorDetails() { + when(authorRepository.findAll()).thenReturn(List.of(sampleAuthor)); + when(libraryMapper.toBookResponse(sampleBook, sampleAuthor)) + .thenReturn(new BookResponse( + "978-3-16-148410-0", + "The Notebook", + "Romance", + 4, + "Nicholas Sparks", + "nicholassparks@gmail.com" + )); + + List results = libraryService.listAllBooksWithAuthors(); + + assertEquals(1, results.size()); + assertEquals("Nicholas Sparks", results.get(0).getAuthorName()); + } + + @Test + void issueBookToStudent_shouldCreateIssueAndDecreaseQuantity() { + IssueBookRequest request = new IssueBookRequest("09003688800", "John Doe", "978-3-16-148410-0"); + + when(bookRepository.findById("978-3-16-148410-0")).thenReturn(Optional.of(sampleBook)); + when(studentRepository.findById("09003688800")).thenReturn(Optional.empty()); + when(studentRepository.save(any(StudentEntity.class))).thenReturn(sampleStudent); + + IssueResponse response = libraryService.issueBookToStudent(request); + + assertNotNull(response.getIssueReturnMessage()); + assertTrue(response.getIssueReturnMessage().startsWith("Book issued. Return date :")); + verify(issueRepository).save(any(IssueEntity.class)); + verify(bookRepository).save(sampleBook); + assertEquals(3, sampleBook.getQuantity()); + } + + @Test + void issueBookToStudent_shouldThrowWhenBookNotFound() { + IssueBookRequest request = new IssueBookRequest("09003688800", "John Doe", "missing-isbn"); + + when(bookRepository.findById("missing-isbn")).thenReturn(Optional.empty()); + + assertThrows(ResourceNotFoundException.class, () -> libraryService.issueBookToStudent(request)); + } + + @Test + void issueBookToStudent_shouldThrowWhenNoCopiesAvailable() { + sampleBook.setQuantity(0); + IssueBookRequest request = new IssueBookRequest("09003688800", "John Doe", "978-3-16-148410-0"); + + when(bookRepository.findById("978-3-16-148410-0")).thenReturn(Optional.of(sampleBook)); + + assertThrows(InsufficientQuantityException.class, () -> libraryService.issueBookToStudent(request)); + } + + @Test + void listBooksByUsn_shouldReturnIssuedBooks() { + IssueEntity issue = new IssueEntity( + "Mon Aug 01 19:45:40 EEST 2022", + "2022-08-01 16:45:40.636000", + sampleStudent, + sampleBook + ); + + when(studentRepository.existsById("09003688800")).thenReturn(true); + when(issueRepository.findByIssueStudentUsn("09003688800")).thenReturn(List.of(issue)); + when(libraryMapper.toIssueResponse(issue)) + .thenReturn(new IssueResponse("Da Vinci Code", "John Doe", "2022-08-01 16:45:40.636000")); + + List results = libraryService.listBooksByUsn("09003688800"); + + assertEquals(1, results.size()); + assertEquals("John Doe", results.get(0).getStudentName()); + } + + @Test + void listBooksByUsn_shouldThrowWhenStudentNotFound() { + when(studentRepository.existsById("000")).thenReturn(false); + + assertThrows(ResourceNotFoundException.class, () -> libraryService.listBooksByUsn("000")); + } + + @Test + void calculateReturnDate_shouldBeSevenDaysAfterIssueDate() { + Date issueDate = new Date(122, 7, 1, 19, 45, 40); + + Date returnDate = libraryService.calculateReturnDate(issueDate); + + assertTrue(returnDate.after(issueDate)); + } + + @Test + void formatReturnDateForStorage_shouldUseExpectedPattern() { + Date returnDate = new Date(122, 7, 8, 16, 45, 40); + + String formatted = libraryService.formatReturnDateForStorage(returnDate); + + assertTrue(formatted.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}")); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 00000000..f4760965 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,13 @@ +spring.application.name=IronLibrary-test + +spring.datasource.url=jdbc:h2:mem:ironlibrary_test;DB_CLOSE_DELAY=-1 +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=false + +spring.main.web-application-type=none +ironlibrary.menu.enabled=false