diff --git a/build.gradle b/build.gradle index d2cacde..909ec31 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'javax.validation:validation-api:2.0.1.Final' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation('org.springframework.boot:spring-boot-starter-test') { diff --git a/src/main/java/com/javatodev/api/controller/LibraryController.java b/src/main/java/com/javatodev/api/controller/LibraryController.java index 882c13e..76357f2 100644 --- a/src/main/java/com/javatodev/api/controller/LibraryController.java +++ b/src/main/java/com/javatodev/api/controller/LibraryController.java @@ -8,21 +8,23 @@ import com.javatodev.api.model.request.BookLendRequest; import com.javatodev.api.model.request.MemberCreationRequest; import com.javatodev.api.service.LibraryService; - +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import javax.validation.Valid; import java.util.List; -import lombok.RequiredArgsConstructor; - +@Validated @RestController @RequestMapping(value = "/api/library") @RequiredArgsConstructor public class LibraryController { private final LibraryService libraryService; - @GetMapping("/book") + @GetMapping("/books") public ResponseEntity readBooks(@RequestParam(required = false) String isbn) { if (isbn == null) { return ResponseEntity.ok(libraryService.readBooks()); @@ -31,48 +33,51 @@ public ResponseEntity readBooks(@RequestParam(required = false) String isbn) { } @GetMapping("/book/{bookId}") - public ResponseEntity readBook (@PathVariable String bookId) { + public ResponseEntity readBook(@PathVariable String bookId) { return ResponseEntity.ok(libraryService.readBookById(bookId)); } @PostMapping("/book") - public ResponseEntity createBook (@RequestBody BookCreationRequest request) { + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createBook(@RequestBody @Valid BookCreationRequest request) { return ResponseEntity.ok(libraryService.createBook(request)); } @PatchMapping("/book/{bookId}") - public ResponseEntity updateBook (@PathVariable("bookId") String bookId, @RequestBody BookCreationRequest request) { + public ResponseEntity updateBook(@PathVariable("bookId") String bookId, @RequestBody BookCreationRequest request) { return ResponseEntity.ok(libraryService.updateBook(bookId, request)); } @PostMapping("/author") - public ResponseEntity createAuthor (@RequestBody AuthorCreationRequest request) { + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createAuthor(@RequestBody @Valid AuthorCreationRequest request) { return ResponseEntity.ok(libraryService.createAuthor(request)); } @DeleteMapping("/book/{bookId}") - public ResponseEntity deleteBook (@PathVariable String bookId) { + public ResponseEntity deleteBook(@PathVariable String bookId) { libraryService.deleteBook(bookId); return ResponseEntity.ok().build(); } @PostMapping("/member") - public ResponseEntity createMember (@RequestBody MemberCreationRequest request) { + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createMember(@RequestBody @Valid MemberCreationRequest request) { return ResponseEntity.ok(libraryService.createMember(request)); } - @GetMapping("/member") - public ResponseEntity> readMembers () { + @GetMapping("/members") + public ResponseEntity> readMembers() { return ResponseEntity.ok(libraryService.readMembers()); } @PatchMapping("/member/{memberId}") - public ResponseEntity updateMember (@RequestBody MemberCreationRequest request, @PathVariable String memberId) { + public ResponseEntity updateMember(@RequestBody @Valid MemberCreationRequest request, @PathVariable String memberId) { return ResponseEntity.ok(libraryService.updateMember(memberId, request)); } @PostMapping("/book/lend") - public ResponseEntity> lendABook(@RequestBody BookLendRequest bookLendRequests) { + public ResponseEntity> lendABook(@RequestBody @Valid BookLendRequest bookLendRequests) { return ResponseEntity.ok(libraryService.lendABook(bookLendRequests)); } } diff --git a/src/main/java/com/javatodev/api/exception/EntityNotFoundException.java b/src/main/java/com/javatodev/api/exception/EntityNotFoundException.java index 52b0f7c..538cf66 100644 --- a/src/main/java/com/javatodev/api/exception/EntityNotFoundException.java +++ b/src/main/java/com/javatodev/api/exception/EntityNotFoundException.java @@ -1,7 +1,7 @@ package com.javatodev.api.exception; public class EntityNotFoundException extends RuntimeException { - public EntityNotFoundException(String message) { - super(message); + public EntityNotFoundException(String document) { + super("Related " + document + " not found."); } } diff --git a/src/main/java/com/javatodev/api/exception/GenericExceptionHandler.java b/src/main/java/com/javatodev/api/exception/GenericExceptionHandler.java new file mode 100644 index 0000000..552c1c7 --- /dev/null +++ b/src/main/java/com/javatodev/api/exception/GenericExceptionHandler.java @@ -0,0 +1,35 @@ +package com.javatodev.api.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice +public class GenericExceptionHandler { + + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity handleNotfoundException(EntityNotFoundException exception) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(prepareResponse(exception.getMessage())); + } + + @ExceptionHandler(InvalidMemberStatusException.class) + public ResponseEntity handleInvalidMemberStatusException(InvalidMemberStatusException exception) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(prepareResponse(exception.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception exception) { + return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(prepareResponse(exception.getMessage())); + } + + // Response message can be detailed here + private Map prepareResponse(String message) { + Map response = new HashMap<>(); + response.put("message", message); + return response; + } +} diff --git a/src/main/java/com/javatodev/api/exception/InvalidMemberStatusException.java b/src/main/java/com/javatodev/api/exception/InvalidMemberStatusException.java new file mode 100644 index 0000000..2973dfa --- /dev/null +++ b/src/main/java/com/javatodev/api/exception/InvalidMemberStatusException.java @@ -0,0 +1,7 @@ +package com.javatodev.api.exception; + +public class InvalidMemberStatusException extends RuntimeException { + public InvalidMemberStatusException(String message) { + super(message); + } +} diff --git a/src/main/java/com/javatodev/api/model/Author.java b/src/main/java/com/javatodev/api/model/Author.java index daae8b8..681755a 100644 --- a/src/main/java/com/javatodev/api/model/Author.java +++ b/src/main/java/com/javatodev/api/model/Author.java @@ -1,17 +1,15 @@ package com.javatodev.api.model; +import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter +@Data @Document public class Author { @Id private String id; private String firstName; private String lastName; + } diff --git a/src/main/java/com/javatodev/api/model/Book.java b/src/main/java/com/javatodev/api/model/Book.java index e428e20..eac5953 100644 --- a/src/main/java/com/javatodev/api/model/Book.java +++ b/src/main/java/com/javatodev/api/model/Book.java @@ -1,14 +1,11 @@ package com.javatodev.api.model; +import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter +@Data @Document public class Book { diff --git a/src/main/java/com/javatodev/api/model/Lend.java b/src/main/java/com/javatodev/api/model/Lend.java index cf71d54..5f6389e 100644 --- a/src/main/java/com/javatodev/api/model/Lend.java +++ b/src/main/java/com/javatodev/api/model/Lend.java @@ -1,18 +1,13 @@ package com.javatodev.api.model; +import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.Document; import java.time.Instant; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -@Getter -@Setter +@Data @Document public class Lend { diff --git a/src/main/java/com/javatodev/api/model/LendStatus.java b/src/main/java/com/javatodev/api/model/LendStatus.java index 15f68e6..8a304a1 100644 --- a/src/main/java/com/javatodev/api/model/LendStatus.java +++ b/src/main/java/com/javatodev/api/model/LendStatus.java @@ -1,5 +1,5 @@ package com.javatodev.api.model; public enum LendStatus { - AVAILABLE, BURROWED + AVAILABLE, BORROWED } diff --git a/src/main/java/com/javatodev/api/model/Member.java b/src/main/java/com/javatodev/api/model/Member.java index 7a144b9..62f4280 100644 --- a/src/main/java/com/javatodev/api/model/Member.java +++ b/src/main/java/com/javatodev/api/model/Member.java @@ -1,14 +1,10 @@ package com.javatodev.api.model; +import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter +@Data @Document public class Member { diff --git a/src/main/java/com/javatodev/api/model/request/AuthorCreationRequest.java b/src/main/java/com/javatodev/api/model/request/AuthorCreationRequest.java index 7379c89..cc09918 100644 --- a/src/main/java/com/javatodev/api/model/request/AuthorCreationRequest.java +++ b/src/main/java/com/javatodev/api/model/request/AuthorCreationRequest.java @@ -2,8 +2,12 @@ import lombok.Data; +import javax.validation.constraints.NotBlank; + @Data public class AuthorCreationRequest { + @NotBlank private String firstName; + @NotBlank private String lastName; } diff --git a/src/main/java/com/javatodev/api/model/request/BookCreationRequest.java b/src/main/java/com/javatodev/api/model/request/BookCreationRequest.java index 1a50f50..5f67ce4 100644 --- a/src/main/java/com/javatodev/api/model/request/BookCreationRequest.java +++ b/src/main/java/com/javatodev/api/model/request/BookCreationRequest.java @@ -2,9 +2,14 @@ import lombok.Data; +import javax.validation.constraints.NotBlank; + @Data public class BookCreationRequest { + @NotBlank private String name; + @NotBlank private String isbn; + @NotBlank private String authorId; -} +} \ No newline at end of file diff --git a/src/main/java/com/javatodev/api/model/request/BookLendRequest.java b/src/main/java/com/javatodev/api/model/request/BookLendRequest.java index 587c4bf..733b57f 100644 --- a/src/main/java/com/javatodev/api/model/request/BookLendRequest.java +++ b/src/main/java/com/javatodev/api/model/request/BookLendRequest.java @@ -1,11 +1,15 @@ package com.javatodev.api.model.request; -import java.util.List; - import lombok.Data; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import java.util.List; + @Data public class BookLendRequest { + @NotEmpty private List bookIds; + @NotBlank private String memberId; } diff --git a/src/main/java/com/javatodev/api/model/request/MemberCreationRequest.java b/src/main/java/com/javatodev/api/model/request/MemberCreationRequest.java index 6a728d0..879693c 100644 --- a/src/main/java/com/javatodev/api/model/request/MemberCreationRequest.java +++ b/src/main/java/com/javatodev/api/model/request/MemberCreationRequest.java @@ -2,8 +2,12 @@ import lombok.Data; +import javax.validation.constraints.NotBlank; + @Data public class MemberCreationRequest { + @NotBlank private String firstName; + @NotBlank private String lastName; -} +} \ No newline at end of file diff --git a/src/main/java/com/javatodev/api/service/LibraryService.java b/src/main/java/com/javatodev/api/service/LibraryService.java index 6d92ac0..f9373c4 100644 --- a/src/main/java/com/javatodev/api/service/LibraryService.java +++ b/src/main/java/com/javatodev/api/service/LibraryService.java @@ -1,6 +1,7 @@ package com.javatodev.api.service; import com.javatodev.api.exception.EntityNotFoundException; +import com.javatodev.api.exception.InvalidMemberStatusException; import com.javatodev.api.model.*; import com.javatodev.api.model.request.AuthorCreationRequest; import com.javatodev.api.model.request.BookCreationRequest; @@ -10,7 +11,7 @@ import com.javatodev.api.repository.BookRepository; import com.javatodev.api.repository.LendRepository; import com.javatodev.api.repository.MemberRepository; - +import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; @@ -20,8 +21,6 @@ import java.util.List; import java.util.Optional; -import lombok.RequiredArgsConstructor; - @Service @RequiredArgsConstructor public class LibraryService { @@ -33,10 +32,7 @@ public class LibraryService { public Book readBookById(String id) { Optional book = bookRepository.findById(id); - if (book.isPresent()) { - return book.get(); - } - throw new EntityNotFoundException("Cant find any book under given ID"); + return book.orElseThrow(() -> new EntityNotFoundException("Book")); } public List readBooks() { @@ -45,20 +41,16 @@ public List readBooks() { public Book readBook(String isbn) { Optional book = bookRepository.findByIsbn(isbn); - if (book.isPresent()) { - return book.get(); - } - throw new EntityNotFoundException("Cant find any book under given ISBN"); + return book.orElseThrow(() -> new EntityNotFoundException("Book")); } public Book createBook(BookCreationRequest book) { - Optional author = authorRepository.findById(book.getAuthorId()); - if (!author.isPresent()) { - throw new EntityNotFoundException("Author Not Found"); - } + Optional authorOptional = authorRepository.findById(book.getAuthorId()); + Author author = authorOptional.orElseThrow(() -> new EntityNotFoundException("Author")); + Book bookToCreate = new Book(); BeanUtils.copyProperties(book, bookToCreate); - bookToCreate.setAuthor(author.get()); + bookToCreate.setAuthor(author); return bookRepository.save(bookToCreate); } @@ -73,73 +65,56 @@ public Member createMember(MemberCreationRequest request) { return memberRepository.save(member); } - public Member updateMember (String id, MemberCreationRequest request) { + public Member updateMember(String id, MemberCreationRequest request) { Optional optionalMember = memberRepository.findById(id); - if (!optionalMember.isPresent()) { - throw new EntityNotFoundException("Member not present in the database"); - } - Member member = optionalMember.get(); + Member member = optionalMember.orElseThrow(() -> new EntityNotFoundException("Member")); member.setLastName(request.getLastName()); member.setFirstName(request.getFirstName()); return memberRepository.save(member); } - public Author createAuthor (AuthorCreationRequest request) { + public Author createAuthor(AuthorCreationRequest request) { Author author = new Author(); BeanUtils.copyProperties(request, author); return authorRepository.save(author); } - public List lendABook (BookLendRequest request) { + public List lendABook(BookLendRequest request) { Optional memberForId = memberRepository.findById(request.getMemberId()); - if (!memberForId.isPresent()) { - throw new EntityNotFoundException("Member not present in the database"); - } - - Member member = memberForId.get(); + Member member = memberForId.orElseThrow(() -> new EntityNotFoundException("Member")); if (member.getStatus() != MemberStatus.ACTIVE) { - throw new RuntimeException("User is not active to proceed a lending."); + throw new InvalidMemberStatusException("User is not active to proceed a lending."); } - List booksApprovedToBurrow = new ArrayList<>(); - + List booksApprovedToBorrow = new ArrayList<>(); request.getBookIds().forEach(bookId -> { - Optional bookForId = bookRepository.findById(bookId); - if (!bookForId.isPresent()) { - throw new EntityNotFoundException("Cant find any book under given ID"); - } - - Optional burrowedBook = lendRepository.findByBookAndStatus(bookForId.get(), LendStatus.BURROWED); - if (!burrowedBook.isPresent()) { - booksApprovedToBurrow.add(bookForId.get().getName()); + Book book = bookForId.orElseThrow(() -> new EntityNotFoundException("Book")); + Optional borrowedBook = lendRepository.findByBookAndStatus(book, LendStatus.BORROWED); + if (!borrowedBook.isPresent()) { + booksApprovedToBorrow.add(bookForId.get().getName()); Lend lend = new Lend(); lend.setMember(memberForId.get()); lend.setBook(bookForId.get()); - lend.setStatus(LendStatus.BURROWED); + lend.setStatus(LendStatus.BORROWED); lend.setStartOn(Instant.now()); lend.setDueOn(Instant.now().plus(30, ChronoUnit.DAYS)); lendRepository.save(lend); } }); - return booksApprovedToBurrow; + return booksApprovedToBorrow; } public Book updateBook(String bookId, BookCreationRequest request) { - Optional author = authorRepository.findById(request.getAuthorId()); - if (!author.isPresent()) { - throw new EntityNotFoundException("Author Not Found"); - } + Optional optionalAuthor = authorRepository.findById(request.getAuthorId()); Optional optionalBook = bookRepository.findById(bookId); - if (!optionalBook.isPresent()) { - throw new EntityNotFoundException("Book Not Found"); - } - Book book = optionalBook.get(); + Author author = optionalAuthor.orElseThrow(() -> new EntityNotFoundException("Author")); + Book book = optionalBook.orElseThrow(() -> new EntityNotFoundException("Book")); book.setIsbn(request.getIsbn()); book.setName(request.getName()); - book.setAuthor(author.get()); + book.setAuthor(author); return bookRepository.save(book); }