Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opened /api/catalog/books/{book_id} endpoint for deleting book #97

Merged
merged 18 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@
<version>2.6.3</version>
<scope>test</scope>
</dependency>
<dependency>
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>4.0.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/com/productdock/adapter/in/web/DeleteBookApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.productdock.adapter.in.web;

import com.productdock.application.port.in.DeleteBookUseCase;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/api/catalog/books")
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved
public record DeleteBookApi(DeleteBookUseCase deleteBookUseCase) {
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved

@DeleteMapping("/{bookId}")
public void deleteBook(@PathVariable("bookId") Long bookId){
log.debug("DELETE request recieved with book id: {}", bookId);
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved
deleteBookUseCase.deleteBook(bookId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.productdock.adapter.in.web.dto;

import com.productdock.domain.RentalStatus;

import java.util.Date;

public record BookRentalStateDto (UserProfileDto user, RentalStatus status, Date date){}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we have this record inside in package when it is used in out ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And does it need to be part of adapter layer or part of domain?

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.productdock.adapter.in.web.dto;


public record UserProfileDto (String fullName, String image, String email) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.productdock.adapter.out.kafka;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.productdock.adapter.out.kafka.publisher.KafkaPublisher;
import com.productdock.application.port.out.messaging.DeleteBookMessagingOutPort;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.concurrent.ExecutionException;

@Component
@RequiredArgsConstructor
public class DeletedBookMessagePublisher implements DeleteBookMessagingOutPort {

@Value("${spring.kafka.topic.delete-book}")
private String kafkaTopic;
private final KafkaPublisher publisher;

@Override
public void sendMessage(Long bookId) throws ExecutionException, InterruptedException, JsonProcessingException {
NenadJeckovic marked this conversation as resolved.
Show resolved Hide resolved
publisher.sendMessage(bookId, kafkaTopic);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ public Book save(Book book) {
return bookMapper.toDomain(bookRepository.save(bookJpaEntity));
}

@Override
public void deleteById(Long bookId) {
bookRepository.deleteById(bookId);
}

private Set<TopicJpaEntity> populateBookTopics(List<Book.Topic> topics) {
var topicEntities = topicRepository.findByIds(topics.stream().map(Book.Topic::getId).toList());
if (topics.size() != topicEntities.size())
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/com/productdock/adapter/out/web/RentalsApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.productdock.adapter.out.web;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.productdock.adapter.in.web.dto.BookRentalStateDto;
import com.productdock.application.port.out.web.RentalsClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import org.springframework.web.util.DefaultUriBuilderFactory;

import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Collection;
import java.util.List;

@Slf4j
@Component
public class RentalsApi implements RentalsClient {
NenadJeckovic marked this conversation as resolved.
Show resolved Hide resolved

private String rentalsServiceUrl;
private HttpClient client = HttpClient.newHttpClient();

private ObjectMapper objectMapper = new ObjectMapper();
@Autowired
public RentalsApi(@Value("http://localhost:8083/api/rental/book/") String rentalsServiceUrl) {
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved
this.rentalsServiceUrl = rentalsServiceUrl;
}

@Override
public Collection<BookRentalStateDto> getRentals(Long bookId) throws IOException, InterruptedException {
var jwt = ((Jwt) SecurityContextHolder.getContext().getAuthentication().getCredentials()).getTokenValue();
var uri = new DefaultUriBuilderFactory(rentalsServiceUrl)
.builder()
.path(bookId.toString())
.path("/rentals")
.build();
HttpRequest request = HttpRequest.newBuilder()
NenadJeckovic marked this conversation as resolved.
Show resolved Hide resolved
.uri(uri)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + jwt)
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

log.debug("{}", response.body());
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved
return objectMapper.readValue(response.body(), new TypeReference<Collection<BookRentalStateDto>>() {
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.productdock.application.port.in;

public interface DeleteBookUseCase {

void deleteBook(Long bookId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.productdock.application.port.out.messaging;

import com.fasterxml.jackson.core.JsonProcessingException;

import java.util.concurrent.ExecutionException;

public interface DeleteBookMessagingOutPort {
void sendMessage(Long bookId) throws ExecutionException, InterruptedException, JsonProcessingException;
NenadJeckovic marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ public interface BookPersistenceOutPort {
Optional<Book> findByTitleAndAuthor(String title, String author);

Book save(Book book);

void deleteById(Long bookId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.productdock.application.port.out.web;

import com.productdock.adapter.in.web.dto.BookRentalStateDto;

import java.io.IOException;
import java.util.Collection;

public interface RentalsClient {

Collection<BookRentalStateDto> getRentals(Long bookId) throws IOException, InterruptedException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.productdock.application.service;

import com.productdock.adapter.in.web.dto.BookRentalStateDto;
import com.productdock.application.port.in.DeleteBookUseCase;
import com.productdock.application.port.out.messaging.DeleteBookMessagingOutPort;
import com.productdock.application.port.out.persistence.BookPersistenceOutPort;
import com.productdock.application.port.out.web.RentalsClient;
import com.productdock.domain.exception.BookNotFoundException;
import com.productdock.domain.exception.DeleteBookException;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class DeleteBookService implements DeleteBookUseCase {
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved

private final BookPersistenceOutPort bookRepository;

private final DeleteBookMessagingOutPort deleteBookMessagingOutPort;

private final RentalsClient rentalsClient;

@Override
@SneakyThrows
public void deleteBook(Long bookId) {

if (bookRepository.findById(bookId).isEmpty()) {
NenadJeckovic marked this conversation as resolved.
Show resolved Hide resolved
throw new BookNotFoundException("Book not found.");
}
var bookRentals = rentalsClient.getRentals(bookId);
if (!bookRentals.isEmpty()) {
throw new DeleteBookException(createRentalMessage(bookRentals.stream().findFirst().get()));
}
bookRepository.deleteById(bookId);
deleteBookMessagingOutPort.sendMessage(bookId);
log.debug("deleted book with id: {}", bookId);
}

private String createRentalMessage(BookRentalStateDto bookRentals){
if(bookRentals.status() == null || bookRentals.user() == null){
return "Cannot read rental status.";
}
String status = bookRentals.status().toString().toLowerCase();
String userName = bookRentals.user().fullName();
return "Book is " + status + " by " + userName + ".";
NenadJeckovic marked this conversation as resolved.
Show resolved Hide resolved
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/productdock/domain/RentalStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.productdock.domain;

public enum RentalStatus {
RENTED,
RESERVED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.productdock.domain.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(code = HttpStatus.NOT_FOUND)
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved
public class BookNotFoundException extends RuntimeException{

public BookNotFoundException(String message){ super(message);}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.productdock.domain.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(code = HttpStatus.BAD_REQUEST)
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved
public class DeleteBookException extends RuntimeException {
NenadJeckovic marked this conversation as resolved.
Show resolved Hide resolved

public DeleteBookException(String message){ super(message);}
}
1 change: 1 addition & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ spring:
topic:
book-rating: book-rating
insert-book: insert-book
delete-book: delete-book

jpa:
hibernate:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.productdock.adapter.in.web;

import com.productdock.application.port.in.DeleteBookUseCase;
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 static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
public class DeleteBookApiShould {

@InjectMocks
private DeleteBookApi deleteBookApi;
@Mock
private DeleteBookUseCase deleteBookUseCase;

public static final long DEFAULT_BOOK_ID = 1;
djordjemijailovicpd marked this conversation as resolved.
Show resolved Hide resolved

@Test
void deleteBook(){

deleteBookApi.deleteBook(DEFAULT_BOOK_ID);

verify(deleteBookUseCase).deleteBook(DEFAULT_BOOK_ID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.productdock.adapter.out.kafka;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.productdock.adapter.out.kafka.publisher.KafkaPublisher;
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.concurrent.ExecutionException;

import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
public class DeleteBookMessagePublisherShould {
public static final long BOOK_ID = 1;

@InjectMocks
private DeletedBookMessagePublisher deletedBookMessagePublisher;
@Mock
private KafkaPublisher publisher;

@Test
void sendMessage() throws ExecutionException, InterruptedException, JsonProcessingException {

deletedBookMessagePublisher.sendMessage(BOOK_ID);

verify(publisher).sendMessage(BOOK_ID, null);
}
}
Loading
Loading