From 4104cd25ef3a947ee27ef5a53dc92b1fce6d5a4a Mon Sep 17 00:00:00 2001 From: Jakub Date: Fri, 25 Oct 2024 19:28:38 +0100 Subject: [PATCH 1/2] First Attempt --- pom.xml | 72 +++++++++++++++---- .../academy/rickandmorty/Application.java | 1 + .../rickandmorty/config/MapperConfig.java | 13 ++++ .../controller/CharacterController.java | 27 +++++++ .../academy/rickandmorty/dto/ApiPageInfo.java | 9 +++ .../academy/rickandmorty/dto/ApiResponse.java | 9 +++ .../rickandmorty/dto/CharacterDto.java | 8 +++ .../rickandmorty/mapper/CharacterMapper.java | 18 +++++ .../academy/rickandmorty/model/Character.java | 26 +++++++ .../repository/CharacterRepository.java | 11 +++ .../service/CharacterInitializer.java | 61 ++++++++++++++++ .../service/CharacterService.java | 9 +++ .../service/CharacterServiceImpl.java | 33 +++++++++ src/main/resources/application.properties | 9 +++ src/test/resources/application.properties | 2 + 15 files changed, 293 insertions(+), 15 deletions(-) create mode 100644 src/main/java/mate/academy/rickandmorty/config/MapperConfig.java create mode 100644 src/main/java/mate/academy/rickandmorty/controller/CharacterController.java create mode 100644 src/main/java/mate/academy/rickandmorty/dto/ApiPageInfo.java create mode 100644 src/main/java/mate/academy/rickandmorty/dto/ApiResponse.java create mode 100644 src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java create mode 100644 src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java create mode 100644 src/main/java/mate/academy/rickandmorty/model/Character.java create mode 100644 src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java create mode 100644 src/main/java/mate/academy/rickandmorty/service/CharacterInitializer.java create mode 100644 src/main/java/mate/academy/rickandmorty/service/CharacterService.java create mode 100644 src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java diff --git a/pom.xml b/pom.xml index 0c754f19..58bf662d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.4 + 3.3.4 mate.academy @@ -19,6 +19,9 @@ https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml + 1.18.34 + 0.2.0 + 1.5.5.Final @@ -37,10 +40,35 @@ spring-boot-starter-data-jpa + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + + com.h2database h2 + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + + + com.mysql + mysql-connector-j + runtime + @@ -48,24 +76,38 @@ org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + org.apache.maven.plugins - maven-checkstyle-plugin - 3.3.0 - - - compile - - check - - - + maven-compiler-plugin - ${maven.checkstyle.plugin.configLocation} - true - true - false + ${java.version} + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok.mapstruct.binding.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + diff --git a/src/main/java/mate/academy/rickandmorty/Application.java b/src/main/java/mate/academy/rickandmorty/Application.java index cdea84fc..21801ec9 100644 --- a/src/main/java/mate/academy/rickandmorty/Application.java +++ b/src/main/java/mate/academy/rickandmorty/Application.java @@ -8,5 +8,6 @@ public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); + } } diff --git a/src/main/java/mate/academy/rickandmorty/config/MapperConfig.java b/src/main/java/mate/academy/rickandmorty/config/MapperConfig.java new file mode 100644 index 00000000..450e58db --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/config/MapperConfig.java @@ -0,0 +1,13 @@ +package mate.academy.rickandmorty.config; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.NullValueCheckStrategy; + +@org.mapstruct.MapperConfig( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + implementationPackage = ".impl" +) +public class MapperConfig { +} diff --git a/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java new file mode 100644 index 00000000..952d94ed --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -0,0 +1,27 @@ +package mate.academy.rickandmorty.controller; + +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.service.CharacterService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping(value = "/character") +public class CharacterController { + private final CharacterService characterService; + + @GetMapping("/random") + public Character getRandomCharacter() { + return characterService.getRandomCharacter(); + } + + @GetMapping("/search") + public List getCharactersByName(@RequestParam String name) { + return characterService.findCharacterByName(name); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/ApiPageInfo.java b/src/main/java/mate/academy/rickandmorty/dto/ApiPageInfo.java new file mode 100644 index 00000000..1ef22a41 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/ApiPageInfo.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto; + +public record ApiPageInfo( + int pages, + int count, + String next, + String prev +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/ApiResponse.java b/src/main/java/mate/academy/rickandmorty/dto/ApiResponse.java new file mode 100644 index 00000000..5d23428f --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/ApiResponse.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto; + +import java.util.List; + +public record ApiResponse( + ApiPageInfo info, + List results +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java b/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java new file mode 100644 index 00000000..db943632 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java @@ -0,0 +1,8 @@ +package mate.academy.rickandmorty.dto; + +public record CharacterDto( + Long id, + String name, + String status, + String gender + ) {} diff --git a/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java new file mode 100644 index 00000000..9f202f1c --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java @@ -0,0 +1,18 @@ +package mate.academy.rickandmorty.mapper; + +import mate.academy.rickandmorty.config.MapperConfig; +import mate.academy.rickandmorty.dto.CharacterDto; +import mate.academy.rickandmorty.model.Character; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import java.util.List; + +@Mapper(config = MapperConfig.class) +public interface CharacterMapper { + + @Mapping(source = "id", target = "externalId") + Character toModel(CharacterDto characterDto); + + @Mapping(source = "id", target = "externalId") + List toModelList(List characterDtoList); +} diff --git a/src/main/java/mate/academy/rickandmorty/model/Character.java b/src/main/java/mate/academy/rickandmorty/model/Character.java new file mode 100644 index 00000000..1342dacd --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/Character.java @@ -0,0 +1,26 @@ +package mate.academy.rickandmorty.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@Table(name = "results") + +public class Character { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private Long externalId; + private String name; + private String status; + private String gender; +} diff --git a/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java new file mode 100644 index 00000000..dab6d4e8 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java @@ -0,0 +1,11 @@ +package mate.academy.rickandmorty.repository; + +import mate.academy.rickandmorty.model.Character; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import java.util.List; + +public interface CharacterRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByNameContainingIgnoreCase(String name); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterInitializer.java b/src/main/java/mate/academy/rickandmorty/service/CharacterInitializer.java new file mode 100644 index 00000000..c25d65b1 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterInitializer.java @@ -0,0 +1,61 @@ +package mate.academy.rickandmorty.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.ApiResponse; +import mate.academy.rickandmorty.dto.CharacterDto; +import mate.academy.rickandmorty.mapper.CharacterMapper; +import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.repository.CharacterRepository; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CharacterInitializer { + @Value("${api.url}") + private String apiUrl; + private final CharacterRepository characterRepository; + private final ObjectMapper objectMapper; + private final CharacterMapper characterMapper; + + @PostConstruct + public void init() { + if(characterRepository.count() == 0) { + initCharactersFromExternalApi(apiUrl); + } + } + private void initCharactersFromExternalApi(String apiUrl) { + HttpClient client = HttpClient.newHttpClient(); + List characterDtoList = new ArrayList<>(); + try { + while (apiUrl != null) { + HttpRequest getRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(apiUrl)) + .build(); + HttpResponse response = client.send(getRequest, + HttpResponse.BodyHandlers.ofString()); + ApiResponse apiResponse = objectMapper.readValue( + response.body(), + ApiResponse.class + ); + characterDtoList.addAll(apiResponse.results()); + apiUrl = apiResponse.info().next(); + } + List characters = characterMapper.toModelList(characterDtoList); + characterRepository.saveAll(characters); + } catch (IOException | InterruptedException e) { + throw new EntityNotFoundException("Can't access results from external API", e); + } + } +} diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterService.java b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java new file mode 100644 index 00000000..ace51f11 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.service; + +import mate.academy.rickandmorty.model.Character; +import java.util.List; + +public interface CharacterService { + Character getRandomCharacter(); + List findCharacterByName(String name); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java b/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java new file mode 100644 index 00000000..1bfab424 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java @@ -0,0 +1,33 @@ +package mate.academy.rickandmorty.service; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.repository.CharacterRepository; +import org.springframework.stereotype.Service; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +@Service +@RequiredArgsConstructor +public class CharacterServiceImpl implements CharacterService { + + private final CharacterRepository characterRepository; + + @Override + public Character getRandomCharacter() { + long count = characterRepository.count(); + if(count == 0) { + throw new EntityNotFoundException("No character found"); + } + + long randomNumber = ThreadLocalRandom.current().nextLong(count); + return characterRepository.findById(randomNumber) + .orElseThrow(() -> new EntityNotFoundException("No character id: " + + randomNumber + " found")); + } + @Override + public List findCharacterByName(String name) { + return characterRepository.findByNameContainingIgnoreCase(name); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..cc5dbc13 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,10 @@ +spring.datasource.url=jdbc:mysql://localhost:3306/rick-and-morty?createDatabaseIfNotExist=true +spring.datasource.username=root +spring.datasource.password=Styczen10! +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.jpa.open-in-view=false +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true + +api.url=https://rickandmortyapi.com/api/character \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bc2fdde8..ca131643 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,3 +3,5 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +api.url=https://rickandmortyapi.com/api/character From 3d5f87318e39f31061d6665cec79fd5909b2908d Mon Sep 17 00:00:00 2001 From: Jakub Date: Sun, 27 Oct 2024 16:17:04 +0000 Subject: [PATCH 2/2] Controllers now returning CharacterDto Empty line removed --- .../mate/academy/rickandmorty/Application.java | 1 - .../controller/CharacterController.java | 12 ++++++------ .../rickandmorty/mapper/CharacterMapper.java | 4 ++++ .../rickandmorty/service/CharacterService.java | 6 +++--- .../service/CharacterServiceImpl.java | 16 ++++++++++------ 5 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/main/java/mate/academy/rickandmorty/Application.java b/src/main/java/mate/academy/rickandmorty/Application.java index 21801ec9..cdea84fc 100644 --- a/src/main/java/mate/academy/rickandmorty/Application.java +++ b/src/main/java/mate/academy/rickandmorty/Application.java @@ -8,6 +8,5 @@ public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); - } } diff --git a/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java index 952d94ed..b5872dbe 100644 --- a/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -1,11 +1,11 @@ package mate.academy.rickandmorty.controller; import lombok.RequiredArgsConstructor; -import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.dto.CharacterDto; import mate.academy.rickandmorty.service.CharacterService; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @@ -15,13 +15,13 @@ public class CharacterController { private final CharacterService characterService; - @GetMapping("/random") - public Character getRandomCharacter() { + @GetMapping + public CharacterDto getRandomCharacter() { return characterService.getRandomCharacter(); } - @GetMapping("/search") - public List getCharactersByName(@RequestParam String name) { + @GetMapping("/{name}") + public List getCharactersByName(@PathVariable String name) { return characterService.findCharacterByName(name); } } diff --git a/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java index 9f202f1c..be43977b 100644 --- a/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java +++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java @@ -15,4 +15,8 @@ public interface CharacterMapper { @Mapping(source = "id", target = "externalId") List toModelList(List characterDtoList); + + CharacterDto toDto(Character character); + + List toDtoList(List characterList); } diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterService.java b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java index ace51f11..a4b0342f 100644 --- a/src/main/java/mate/academy/rickandmorty/service/CharacterService.java +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java @@ -1,9 +1,9 @@ package mate.academy.rickandmorty.service; -import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.dto.CharacterDto; import java.util.List; public interface CharacterService { - Character getRandomCharacter(); - List findCharacterByName(String name); + CharacterDto getRandomCharacter(); + List findCharacterByName(String name); } diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java b/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java index 1bfab424..bb31fecd 100644 --- a/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java @@ -2,6 +2,8 @@ import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.CharacterDto; +import mate.academy.rickandmorty.mapper.CharacterMapper; import mate.academy.rickandmorty.model.Character; import mate.academy.rickandmorty.repository.CharacterRepository; import org.springframework.stereotype.Service; @@ -13,21 +15,23 @@ public class CharacterServiceImpl implements CharacterService { private final CharacterRepository characterRepository; + private final CharacterMapper characterMapper; @Override - public Character getRandomCharacter() { + public CharacterDto getRandomCharacter() { long count = characterRepository.count(); if(count == 0) { throw new EntityNotFoundException("No character found"); } - long randomNumber = ThreadLocalRandom.current().nextLong(count); return characterRepository.findById(randomNumber) - .orElseThrow(() -> new EntityNotFoundException("No character id: " - + randomNumber + " found")); + .map(characterMapper::toDto) + .orElseThrow(() -> new EntityNotFoundException("Character not found for ID: " + randomNumber)); } + @Override - public List findCharacterByName(String name) { - return characterRepository.findByNameContainingIgnoreCase(name); + public List findCharacterByName(String name) { + List character = characterRepository.findByNameContainingIgnoreCase(name); + return characterMapper.toDtoList(character); } }