From 489bb932057780dc68e1586feb4cbba92469905e Mon Sep 17 00:00:00 2001 From: Volikkk Date: Fri, 5 Sep 2025 21:04:59 +0300 Subject: [PATCH 1/3] add new file --- pom.xml | 68 +++++++++++++++++-- .../rickandmorty/config/MapperConfig.java | 13 ++++ .../controler/CharacterController.java | 32 +++++++++ .../rickandmorty/dto/CharacterDataDto.java | 10 +++ .../rickandmorty/dto/CharacterDto.java | 12 ++++ .../rickandmorty/dto/CharacterInfoDto.java | 8 +++ .../rickandmorty/mapper/CharacterMapper.java | 14 ++++ .../model/RickAndMortyCharacter.java | 21 ++++++ .../repository/CharacterRepository.java | 9 +++ .../service/CharacterService.java | 12 ++++ .../service/RickAndMortyClient.java | 43 ++++++++++++ .../service/impl/CharacterServiceImpl.java | 50 ++++++++++++++ src/main/resources/application.properties | 10 ++- 13 files changed, 296 insertions(+), 6 deletions(-) create mode 100644 src/main/java/mate/academy/rickandmorty/config/MapperConfig.java create mode 100644 src/main/java/mate/academy/rickandmorty/controler/CharacterController.java create mode 100644 src/main/java/mate/academy/rickandmorty/dto/CharacterDataDto.java create mode 100644 src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java create mode 100644 src/main/java/mate/academy/rickandmorty/dto/CharacterInfoDto.java create mode 100644 src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java create mode 100644 src/main/java/mate/academy/rickandmorty/model/RickAndMortyCharacter.java create mode 100644 src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java create mode 100644 src/main/java/mate/academy/rickandmorty/service/CharacterService.java create mode 100644 src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java create mode 100644 src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java diff --git a/pom.xml b/pom.xml index 0c754f19..417ecde5 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot @@ -15,6 +15,9 @@ jv-rick-and-morty 17 + 1.18.32 + 0.2.0 + 1.6.3 3.1.1 https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml @@ -25,22 +28,56 @@ org.springframework.boot spring-boot-starter - org.springframework.boot spring-boot-starter-test test + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + provided + org.springframework.boot spring-boot-starter-data-jpa - com.h2database h2 + + mysql + mysql-connector-java + 8.0.33 + + + org.projectlombok + lombok + ${lombok.version} + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + com.fasterxml.jackson.core + jackson-databind + 2.19.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + + org.springframework.boot + spring-boot-starter-web + @@ -55,7 +92,7 @@ 3.3.0 - compile + validate check @@ -68,7 +105,28 @@ false + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + - + \ No newline at end of file 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..64e8fd19 --- /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 + +) +public class MapperConfig { +} diff --git a/src/main/java/mate/academy/rickandmorty/controler/CharacterController.java b/src/main/java/mate/academy/rickandmorty/controler/CharacterController.java new file mode 100644 index 00000000..2b8ae927 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controler/CharacterController.java @@ -0,0 +1,32 @@ +package mate.academy.rickandmorty.controler; + +import io.swagger.v3.oas.annotations.Operation; +import java.util.List; +import lombok.RequiredArgsConstructor; +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.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/characters") +@RequiredArgsConstructor +public class CharacterController { + + private final CharacterService characterService; + + @Operation(summary = "Get random character") + @GetMapping("/random") + public CharacterDto getRandomCharacter() { + return characterService.getRandomCharacter(); + } + + @Operation(summary = "Find character by name") + @GetMapping() + public List findCharacterByName(@RequestParam String name) { + return characterService.findCharactersByName(name); + } + +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/CharacterDataDto.java b/src/main/java/mate/academy/rickandmorty/dto/CharacterDataDto.java new file mode 100644 index 00000000..9c54d5a3 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/CharacterDataDto.java @@ -0,0 +1,10 @@ +package mate.academy.rickandmorty.dto; + +import java.util.List; +import lombok.Data; + +@Data +public class CharacterDataDto { + private List results; + private CharacterInfoDto info; +} 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..86466bef --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java @@ -0,0 +1,12 @@ +package mate.academy.rickandmorty.dto; + +import lombok.Data; + +@Data +public class CharacterDto { + private Long id; + private Long externalId; + private String name; + private String status; + private String gender; +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/CharacterInfoDto.java b/src/main/java/mate/academy/rickandmorty/dto/CharacterInfoDto.java new file mode 100644 index 00000000..b08a92b3 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/CharacterInfoDto.java @@ -0,0 +1,8 @@ +package mate.academy.rickandmorty.dto; + +import lombok.Data; + +@Data +public class CharacterInfoDto { + private String next; +} 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..cd1b2e31 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java @@ -0,0 +1,14 @@ +package mate.academy.rickandmorty.mapper; + +import mate.academy.rickandmorty.config.MapperConfig; +import mate.academy.rickandmorty.dto.CharacterDto; +import mate.academy.rickandmorty.model.RickAndMortyCharacter; +import org.mapstruct.Mapper; + +@Mapper(config = MapperConfig.class) +public interface CharacterMapper { + + RickAndMortyCharacter toModel(CharacterDto characterDto); + + CharacterDto toDto(RickAndMortyCharacter rickAndMortyCharacter); +} diff --git a/src/main/java/mate/academy/rickandmorty/model/RickAndMortyCharacter.java b/src/main/java/mate/academy/rickandmorty/model/RickAndMortyCharacter.java new file mode 100644 index 00000000..7a7f3129 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/RickAndMortyCharacter.java @@ -0,0 +1,21 @@ +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.Data; + +@Entity +@Table(name = "characters") +@Data +public class RickAndMortyCharacter { + @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..b1820db6 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.repository; + +import java.util.List; +import mate.academy.rickandmorty.model.RickAndMortyCharacter; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CharacterRepository extends JpaRepository { + List findByNameContainingIgnoreCase(String name); +} 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..cb608818 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java @@ -0,0 +1,12 @@ +package mate.academy.rickandmorty.service; + +import java.util.List; +import mate.academy.rickandmorty.dto.CharacterDto; + +public interface CharacterService { + void saveAllCharacters(); + + CharacterDto getRandomCharacter(); + + List findCharactersByName(String name); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java b/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java new file mode 100644 index 00000000..e5daea59 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java @@ -0,0 +1,43 @@ +package mate.academy.rickandmorty.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +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; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.CharacterDataDto; +import mate.academy.rickandmorty.dto.CharacterDto; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class RickAndMortyClient { + public static final String BASE_URL = "https://rickandmortyapi.com/api/character"; + private final ObjectMapper objectMapper; + + public List getCharacters() { + List allCharacters = new ArrayList<>(); + HttpClient httpClient = HttpClient.newHttpClient(); + String url = BASE_URL; + try { + while (url != null) { + HttpRequest httpRequest = HttpRequest.newBuilder().GET() + .uri(URI.create(url)).build(); + HttpResponse response = httpClient.send(httpRequest, + HttpResponse.BodyHandlers.ofString()); + CharacterDataDto characterResponse = objectMapper + .readValue(response.body(), CharacterDataDto.class); + allCharacters.addAll(characterResponse.getResults()); + url = characterResponse.getInfo().getNext(); + } + return allCharacters; + + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Couldn't get characters from API", e); + } + } +} diff --git a/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java new file mode 100644 index 00000000..dfa0b922 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java @@ -0,0 +1,50 @@ +package mate.academy.rickandmorty.service.impl; + +import jakarta.annotation.PostConstruct; +import java.util.List; +import java.util.Random; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.CharacterDto; +import mate.academy.rickandmorty.mapper.CharacterMapper; +import mate.academy.rickandmorty.model.RickAndMortyCharacter; +import mate.academy.rickandmorty.repository.CharacterRepository; +import mate.academy.rickandmorty.service.CharacterService; +import mate.academy.rickandmorty.service.RickAndMortyClient; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CharacterServiceImpl implements CharacterService { + + private final RickAndMortyClient characterClient; + private final CharacterMapper characterMapper; + private final CharacterRepository characterRepository; + private final Random random = new Random(); + + @PostConstruct + public void saveAllCharacters() { + List characterDtos = characterClient.getCharacters(); + List rickAndMortyCharacters = characterDtos.stream() + .map(characterMapper::toModel).toList(); + characterRepository.saveAll(rickAndMortyCharacters); + } + + @Override + public CharacterDto getRandomCharacter() { + List rickAndMortyCharacters = characterRepository.findAll(); + if (rickAndMortyCharacters.isEmpty()) { + throw new RuntimeException("No characters found in database"); + } + int index = random.nextInt(rickAndMortyCharacters.size()); + RickAndMortyCharacter rickAndMortyCharacter = rickAndMortyCharacters.get(index); + return characterMapper.toDto(rickAndMortyCharacter); + } + + @Override + public List findCharactersByName(String name) { + List charactersByName = characterRepository + .findByNameContainingIgnoreCase(name); + return charactersByName.stream() + .map(characterMapper::toDto).toList(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..4e5b7f99 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,9 @@ - +spring.datasource.url=jdbc:mysql://localhost:3306/testdb +spring.datasource.username=root +spring.datasource.password=Bs5QG4Rd2W_ +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.open-in-view=false +server.error.include-message=always +server.error.include-binding-errors=always From ea23001260352ffbe7ad0107134ca5d66229e5eb Mon Sep 17 00:00:00 2001 From: Volikkk Date: Fri, 5 Sep 2025 22:12:01 +0300 Subject: [PATCH 2/3] fix --- .../client/RickAndMortyClient.java | 53 ++++++++++++++++ .../rickandmorty/client/RickAndMortyPage.java | 44 +++++++++++++ .../rickandmorty/config/AppConfig.java | 13 ++++ .../rickandmorty/config/OpenApiConfig.java | 15 +++++ .../controler/CharacterController.java | 24 ++++--- .../rickandmorty/dto/CharacterDto.java | 10 ++- .../rickandmorty/init/DataInitializer.java | 18 ++++++ .../rickandmorty/mapper/CharacterMapper.java | 25 +++++--- .../model/RickAndMortyCharacter.java | 13 +++- .../repository/CharacterRepository.java | 5 +- .../service/CharacterService.java | 4 +- .../service/RickAndMortyClient.java | 43 ------------- .../service/impl/CharacterServiceImpl.java | 63 ++++++++++++------- .../db/changelog/db.changelog-master.yaml | 28 +++++++++ 14 files changed, 268 insertions(+), 90 deletions(-) create mode 100644 src/main/java/mate/academy/rickandmorty/client/RickAndMortyClient.java create mode 100644 src/main/java/mate/academy/rickandmorty/client/RickAndMortyPage.java create mode 100644 src/main/java/mate/academy/rickandmorty/config/AppConfig.java create mode 100644 src/main/java/mate/academy/rickandmorty/config/OpenApiConfig.java create mode 100644 src/main/java/mate/academy/rickandmorty/init/DataInitializer.java delete mode 100644 src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java create mode 100644 src/main/resources/db/changelog/db.changelog-master.yaml diff --git a/src/main/java/mate/academy/rickandmorty/client/RickAndMortyClient.java b/src/main/java/mate/academy/rickandmorty/client/RickAndMortyClient.java new file mode 100644 index 00000000..d6409522 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/client/RickAndMortyClient.java @@ -0,0 +1,53 @@ +package mate.academy.rickandmorty.client; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class RickAndMortyClient { + private static final String BASE_URL = "https://rickandmortyapi.com/api/character"; + private final RestTemplate restTemplate; + + public RickAndMortyClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public List fetchAllCharacters() { + List results = new ArrayList<>(); + String url = BASE_URL; + while (url != null) { + PageResponse page = restTemplate.getForObject(url, PageResponse.class); + if (page == null || page.getResults() == null) { + break; + } + results.addAll(page.getResults()); + url = page.getInfo() != null ? page.getInfo().getNext() : null; + } + return results; + } + + @Data + public static class PageResponse { + private Info info; + private List results; + } + + @Data + public static class Info { + private Integer count; + private Integer pages; + private String next; + private String prev; + } + + @Data + public static class CharacterResponse { + private String id; + private String name; + private String status; + private String gender; + } +} diff --git a/src/main/java/mate/academy/rickandmorty/client/RickAndMortyPage.java b/src/main/java/mate/academy/rickandmorty/client/RickAndMortyPage.java new file mode 100644 index 00000000..07312191 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/client/RickAndMortyPage.java @@ -0,0 +1,44 @@ +package mate.academy.rickandmorty.client; + +import java.util.List; +import lombok.Data; + +public class RickAndMortyPage { + @Data + public static class Info { + private int count; + private int pages; + private String next; + private String prev; + } + + @Data + public static class Origin { + private String name; + } + + @Data + public static class Location { + private String name; + } + + @Data + public static class Character { + private String id; + private String name; + private String status; + private String species; + private String type; + private String gender; + private Origin origin; + private Location location; + private String image; + } + + @Data + public static class Page { + private Info info; + private List results; + } +} + diff --git a/src/main/java/mate/academy/rickandmorty/config/AppConfig.java b/src/main/java/mate/academy/rickandmorty/config/AppConfig.java new file mode 100644 index 00000000..4368ad60 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/config/AppConfig.java @@ -0,0 +1,13 @@ +package mate.academy.rickandmorty.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/config/OpenApiConfig.java b/src/main/java/mate/academy/rickandmorty/config/OpenApiConfig.java new file mode 100644 index 00000000..f5cb0387 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/config/OpenApiConfig.java @@ -0,0 +1,15 @@ +package mate.academy.rickandmorty.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + @Bean + public OpenAPI openApi() { + return new OpenAPI().info(new Info().title("Rick & Morty API").version("1.0")); + } +} + diff --git a/src/main/java/mate/academy/rickandmorty/controler/CharacterController.java b/src/main/java/mate/academy/rickandmorty/controler/CharacterController.java index 2b8ae927..18510334 100644 --- a/src/main/java/mate/academy/rickandmorty/controler/CharacterController.java +++ b/src/main/java/mate/academy/rickandmorty/controler/CharacterController.java @@ -1,32 +1,30 @@ package mate.academy.rickandmorty.controler; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; 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.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/characters") @RequiredArgsConstructor +@Tag(name = "Rick & Morty") public class CharacterController { + private final CharacterService service; - private final CharacterService characterService; - - @Operation(summary = "Get random character") - @GetMapping("/random") - public CharacterDto getRandomCharacter() { - return characterService.getRandomCharacter(); + @Operation(summary = "Random character (from DB)") + @GetMapping("/api/characters/random") + public CharacterDto random() { + return service.getRandomCharacter(); } - @Operation(summary = "Find character by name") - @GetMapping() - public List findCharacterByName(@RequestParam String name) { - return characterService.findCharactersByName(name); + @Operation(summary = "Search characters by name") + @GetMapping("/api/characters/search") + public List search(@RequestParam("q") String q) { + return service.findCharactersByName(q); } - } diff --git a/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java b/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java index 86466bef..b6ce454a 100644 --- a/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java +++ b/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java @@ -1,11 +1,19 @@ package mate.academy.rickandmorty.dto; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; import lombok.Data; @Data +@AllArgsConstructor +@Schema(description = "Character DTO") public class CharacterDto { + @Schema(description = "Internal DB id") private Long id; - private Long externalId; + + @Schema(description = "Original external id (from Rick & Morty API) as string") + private String externalId; + private String name; private String status; private String gender; diff --git a/src/main/java/mate/academy/rickandmorty/init/DataInitializer.java b/src/main/java/mate/academy/rickandmorty/init/DataInitializer.java new file mode 100644 index 00000000..11617a3c --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/init/DataInitializer.java @@ -0,0 +1,18 @@ +package mate.academy.rickandmorty.init; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.service.CharacterService; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class DataInitializer { + private final CharacterService characterService; + + @PostConstruct + public void init() { + characterService.loadAllCharactersOnce(); + } +} + diff --git a/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java index cd1b2e31..82856c4b 100644 --- a/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java +++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java @@ -1,14 +1,25 @@ package mate.academy.rickandmorty.mapper; -import mate.academy.rickandmorty.config.MapperConfig; +import mate.academy.rickandmorty.client.RickAndMortyClient.CharacterResponse; import mate.academy.rickandmorty.dto.CharacterDto; import mate.academy.rickandmorty.model.RickAndMortyCharacter; -import org.mapstruct.Mapper; +import org.springframework.stereotype.Component; -@Mapper(config = MapperConfig.class) -public interface CharacterMapper { +@Component +public class CharacterMapper { + public RickAndMortyCharacter toModel(CharacterResponse r) { + RickAndMortyCharacter e = new RickAndMortyCharacter(); + e.setExternalId(r.getId()); + e.setName(r.getName()); + e.setStatus(r.getStatus()); + e.setGender(r.getGender()); + return e; + } - RickAndMortyCharacter toModel(CharacterDto characterDto); - - CharacterDto toDto(RickAndMortyCharacter rickAndMortyCharacter); + public CharacterDto toDto(RickAndMortyCharacter e) { + return new CharacterDto(e.getId(), e.getName(), + e.getExternalId(), e.getStatus(), + e.getGender()); + } } + diff --git a/src/main/java/mate/academy/rickandmorty/model/RickAndMortyCharacter.java b/src/main/java/mate/academy/rickandmorty/model/RickAndMortyCharacter.java index 7a7f3129..2ec92781 100644 --- a/src/main/java/mate/academy/rickandmorty/model/RickAndMortyCharacter.java +++ b/src/main/java/mate/academy/rickandmorty/model/RickAndMortyCharacter.java @@ -1,21 +1,32 @@ package mate.academy.rickandmorty.model; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Data; +import lombok.NoArgsConstructor; @Entity @Table(name = "characters") @Data +@NoArgsConstructor public class RickAndMortyCharacter { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private Long externalId; + + @Column(name = "external_id", nullable = false, unique = true, length = 50) + private String externalId; + + @Column(length = 255, nullable = false) private String name; + + @Column(length = 50) private String status; + + @Column(length = 50) 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 index b1820db6..9ec95d6c 100644 --- a/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java +++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java @@ -1,9 +1,12 @@ package mate.academy.rickandmorty.repository; import java.util.List; +import java.util.Optional; import mate.academy.rickandmorty.model.RickAndMortyCharacter; import org.springframework.data.jpa.repository.JpaRepository; public interface CharacterRepository extends JpaRepository { - List findByNameContainingIgnoreCase(String name); + Optional findByExternalId(String externalId); + + List findByNameContainingIgnoreCase(String namePart); } diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterService.java b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java index cb608818..1673a93a 100644 --- a/src/main/java/mate/academy/rickandmorty/service/CharacterService.java +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java @@ -4,9 +4,9 @@ import mate.academy.rickandmorty.dto.CharacterDto; public interface CharacterService { - void saveAllCharacters(); + List findCharactersByName(String namePart); CharacterDto getRandomCharacter(); - List findCharactersByName(String name); + void loadAllCharactersOnce(); } diff --git a/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java b/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java deleted file mode 100644 index e5daea59..00000000 --- a/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java +++ /dev/null @@ -1,43 +0,0 @@ -package mate.academy.rickandmorty.service; - -import com.fasterxml.jackson.databind.ObjectMapper; -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; -import lombok.RequiredArgsConstructor; -import mate.academy.rickandmorty.dto.CharacterDataDto; -import mate.academy.rickandmorty.dto.CharacterDto; -import org.springframework.stereotype.Component; - -@RequiredArgsConstructor -@Component -public class RickAndMortyClient { - public static final String BASE_URL = "https://rickandmortyapi.com/api/character"; - private final ObjectMapper objectMapper; - - public List getCharacters() { - List allCharacters = new ArrayList<>(); - HttpClient httpClient = HttpClient.newHttpClient(); - String url = BASE_URL; - try { - while (url != null) { - HttpRequest httpRequest = HttpRequest.newBuilder().GET() - .uri(URI.create(url)).build(); - HttpResponse response = httpClient.send(httpRequest, - HttpResponse.BodyHandlers.ofString()); - CharacterDataDto characterResponse = objectMapper - .readValue(response.body(), CharacterDataDto.class); - allCharacters.addAll(characterResponse.getResults()); - url = characterResponse.getInfo().getNext(); - } - return allCharacters; - - } catch (IOException | InterruptedException e) { - throw new RuntimeException("Couldn't get characters from API", e); - } - } -} diff --git a/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java index dfa0b922..74b51120 100644 --- a/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java @@ -1,50 +1,69 @@ package mate.academy.rickandmorty.service.impl; -import jakarta.annotation.PostConstruct; import java.util.List; import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.client.RickAndMortyClient; +import mate.academy.rickandmorty.client.RickAndMortyClient.CharacterResponse; import mate.academy.rickandmorty.dto.CharacterDto; import mate.academy.rickandmorty.mapper.CharacterMapper; import mate.academy.rickandmorty.model.RickAndMortyCharacter; import mate.academy.rickandmorty.repository.CharacterRepository; import mate.academy.rickandmorty.service.CharacterService; -import mate.academy.rickandmorty.service.RickAndMortyClient; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class CharacterServiceImpl implements CharacterService { - private final RickAndMortyClient characterClient; - private final CharacterMapper characterMapper; - private final CharacterRepository characterRepository; + private final RickAndMortyClient client; + private final CharacterMapper mapper; + private final CharacterRepository repository; private final Random random = new Random(); - @PostConstruct - public void saveAllCharacters() { - List characterDtos = characterClient.getCharacters(); - List rickAndMortyCharacters = characterDtos.stream() - .map(characterMapper::toModel).toList(); - characterRepository.saveAll(rickAndMortyCharacters); + @Override + @Transactional(readOnly = true) + public List findCharactersByName(String namePart) { + return repository.findByNameContainingIgnoreCase(namePart).stream() + .map(mapper::toDto) + .collect(Collectors.toList()); } @Override + @Transactional(readOnly = true) public CharacterDto getRandomCharacter() { - List rickAndMortyCharacters = characterRepository.findAll(); - if (rickAndMortyCharacters.isEmpty()) { - throw new RuntimeException("No characters found in database"); + long count = repository.count(); + if (count == 0) { + throw new IllegalStateException("No characters in DB"); } - int index = random.nextInt(rickAndMortyCharacters.size()); - RickAndMortyCharacter rickAndMortyCharacter = rickAndMortyCharacters.get(index); - return characterMapper.toDto(rickAndMortyCharacter); + int idx = random.nextInt((int) count); + var page = repository.findAll(org.springframework.data.domain.PageRequest.of(idx, 1)); + var entity = page.getContent().stream().findFirst() + .orElseGet(() -> repository.findAll().get(0)); + return mapper.toDto(entity); } @Override - public List findCharactersByName(String name) { - List charactersByName = characterRepository - .findByNameContainingIgnoreCase(name); - return charactersByName.stream() - .map(characterMapper::toDto).toList(); + @Transactional + public void loadAllCharactersOnce() { + List fetched = client.fetchAllCharacters(); + if (fetched.isEmpty()) { + return; + } + Set existing = repository.findAll().stream() + .map(RickAndMortyCharacter::getExternalId) + .collect(Collectors.toSet()); + + List toSave = fetched.stream() + .filter(r -> r.getId() != null && !existing.contains(r.getId())) + .map(mapper::toModel) + .collect(Collectors.toList()); + + if (!toSave.isEmpty()) { + repository.saveAll(toSave); + } } } diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 00000000..2faecf21 --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,28 @@ +databaseChangeLog: + - changeSet: + id: 1 + author: you + changes: + - createTable: + tableName: characters + columns: + - column: + name: id + type: BIGINT AUTO_INCREMENT + constraints: + primaryKey: true + - column: + name: external_id + type: VARCHAR(64) + constraints: + nullable: false + unique: true + - column: + name: name + type: VARCHAR(255) + - column: + name: status + type: VARCHAR(50) + - column: + name: gender + type: VARCHAR(50) From e20fa1d56b9fa7382c7e89e9acdd26c8aa314dcf Mon Sep 17 00:00:00 2001 From: Volikkk Date: Fri, 5 Sep 2025 22:20:14 +0300 Subject: [PATCH 3/3] fix2.0 --- .../academy/rickandmorty/mapper/CharacterMapper.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java index 82856c4b..7ec21576 100644 --- a/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java +++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java @@ -17,9 +17,12 @@ public RickAndMortyCharacter toModel(CharacterResponse r) { } public CharacterDto toDto(RickAndMortyCharacter e) { - return new CharacterDto(e.getId(), e.getName(), - e.getExternalId(), e.getStatus(), - e.getGender()); + return new CharacterDto( + e.getId(), + e.getExternalId(), + e.getName(), + e.getStatus(), + e.getGender() + ); } } -