diff --git a/pom.xml b/pom.xml
index 0c754f19..7cb935bf 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
@@ -19,28 +19,56 @@
https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml
+ 0.2.0
org.springframework.boot
spring-boot-starter
-
org.springframework.boot
spring-boot-starter-test
test
-
org.springframework.boot
spring-boot-starter-data-jpa
-
+
+ org.springframework.boot
+ spring-boot-starter-web
+
com.h2database
h2
+
+ mysql
+ mysql-connector-java
+ 8.0.33
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.mapstruct
+ mapstruct
+ 1.5.5.Final
+
+
+ org.mapstruct
+ mapstruct-processor
+ 1.5.2.Final
+ provided
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.1.0
+
@@ -52,7 +80,7 @@
org.apache.maven.plugins
maven-checkstyle-plugin
- 3.3.0
+ 3.3.1
compile
@@ -63,6 +91,7 @@
${maven.checkstyle.plugin.configLocation}
+ true
true
true
false
@@ -70,5 +99,4 @@
-
-
+
\ No newline at end of file
diff --git a/src/main/java/mate/academy/rickandmorty/ExternalDataFetcher.java b/src/main/java/mate/academy/rickandmorty/ExternalDataFetcher.java
new file mode 100644
index 00000000..aaa2d91e
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/ExternalDataFetcher.java
@@ -0,0 +1,63 @@
+package mate.academy.rickandmorty;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+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.external.CharacterDto;
+import mate.academy.rickandmorty.dto.external.CharacterListDto;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class ExternalDataFetcher {
+ @Value("${url}")
+ private String url;
+ private final ObjectMapper objectMapper;
+ private final HttpClient httpClient = HttpClient.newHttpClient();
+
+ public List fetchAllCharacters() {
+ List allCharacters = new ArrayList<>();
+ int page = 1;
+ boolean hasMorePages = true;
+
+ while (hasMorePages) {
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .GET()
+ .uri(new URI(url + "?page=" + page))
+ .build();
+ HttpResponse response = httpClient.send(
+ request,
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ String responseBody = response.body();
+ CharacterListDto characterListDto = parseResponse(responseBody);
+ allCharacters.addAll(characterListDto.characters());
+ hasMorePages = characterListDto.pagesInfo().next() != null;
+ page++;
+ } catch (IOException | InterruptedException | URISyntaxException e) {
+ throw new RuntimeException("Can't fetch data from API.", e);
+ }
+ }
+ return allCharacters;
+ }
+
+ public CharacterListDto parseResponse(String responseBody) {
+ try {
+ return objectMapper.readValue(responseBody, new TypeReference<>() {});
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Can't parse response body.", e);
+ }
+ }
+}
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..f5f11411
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java
@@ -0,0 +1,41 @@
+package mate.academy.rickandmorty.controller;
+
+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.internal.CharacterResponseDto;
+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 = "Characters",
+ description = "API to get info about characters"
+)
+public class CharacterController {
+ private final CharacterService characterService;
+
+ @GetMapping("/random")
+ @Operation(
+ summary = "Get a random character",
+ description = "Use this method to get random character from \"Rick and Morty\""
+ )
+ public CharacterResponseDto getRandom() {
+ return characterService.getRandomCharacter();
+ }
+
+ @GetMapping
+ @Operation(
+ summary = "Get characters by string",
+ description = "Use this method to find characters, whose name contains entered string"
+ )
+ public List search(@RequestParam String name) {
+ return characterService.search(name);
+ }
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterDto.java
new file mode 100644
index 00000000..288a93cc
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterDto.java
@@ -0,0 +1,8 @@
+package mate.academy.rickandmorty.dto.external;
+
+public record CharacterDto(
+ Long id,
+ String status,
+ String gender
+) {
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterListDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterListDto.java
new file mode 100644
index 00000000..da37983c
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterListDto.java
@@ -0,0 +1,10 @@
+package mate.academy.rickandmorty.dto.external;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+
+public record CharacterListDto(
+ @JsonProperty("results") List characters,
+ @JsonProperty("info") PaginationDto pagesInfo
+) {
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/PaginationDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/PaginationDto.java
new file mode 100644
index 00000000..0f41f03b
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/external/PaginationDto.java
@@ -0,0 +1,6 @@
+package mate.academy.rickandmorty.dto.external;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public record PaginationDto(@JsonProperty("next") String next) {
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterResponseDto.java
new file mode 100644
index 00000000..f3253002
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterResponseDto.java
@@ -0,0 +1,10 @@
+package mate.academy.rickandmorty.dto.internal;
+
+public record CharacterResponseDto(
+ Long id,
+ Long externalId,
+ 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..51c0d64e
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java
@@ -0,0 +1,17 @@
+package mate.academy.rickandmorty.mapper;
+
+import mate.academy.rickandmorty.config.MapperConfig;
+import mate.academy.rickandmorty.dto.external.CharacterDto;
+import mate.academy.rickandmorty.dto.internal.CharacterResponseDto;
+import mate.academy.rickandmorty.model.Character;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(config = MapperConfig.class)
+public interface CharacterMapper {
+ @Mapping(source = "id", target = "externalId")
+ @Mapping(target = "id", ignore = true)
+ Character toModel(CharacterDto characterDto);
+
+ CharacterResponseDto toDto(Character character);
+}
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..102e2c69
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/model/Character.java
@@ -0,0 +1,25 @@
+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 lombok.Getter;
+import lombok.Setter;
+
+@Entity(name = "characters")
+@Getter
+@Setter
+public class Character {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true)
+ private String 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..3e9cc12e
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java
@@ -0,0 +1,11 @@
+package mate.academy.rickandmorty.repository;
+
+import java.util.List;
+import mate.academy.rickandmorty.model.Character;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface CharacterRepository extends JpaRepository {
+ List findCharacterByNameLikeIgnoreCase(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..05e818da
--- /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.internal.CharacterResponseDto;
+import org.springframework.stereotype.Service;
+
+@Service
+public interface CharacterService {
+ CharacterResponseDto getRandomCharacter();
+
+ List search(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..01d6d242
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java
@@ -0,0 +1,47 @@
+package mate.academy.rickandmorty.service;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.persistence.EntityNotFoundException;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import mate.academy.rickandmorty.ExternalDataFetcher;
+import mate.academy.rickandmorty.dto.external.CharacterDto;
+import mate.academy.rickandmorty.dto.internal.CharacterResponseDto;
+import mate.academy.rickandmorty.mapper.CharacterMapper;
+import mate.academy.rickandmorty.model.Character;
+import mate.academy.rickandmorty.repository.CharacterRepository;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CharacterServiceImpl implements CharacterService {
+ private final CharacterRepository characterRepository;
+ private final CharacterMapper characterMapper;
+ private final ExternalDataFetcher dataFetcher;
+
+ @Override
+ public CharacterResponseDto getRandomCharacter() {
+ long count = characterRepository.count();
+ long randomId = (long) (Math.random() * count);
+ Character character = characterRepository.findById(randomId)
+ .orElseThrow(() ->
+ new EntityNotFoundException("Can't find character by id: " + randomId));
+ return characterMapper.toDto(character);
+ }
+
+ @Override
+ public List search(String name) {
+ return characterRepository.findCharacterByNameLikeIgnoreCase("%" + name + "%").stream()
+ .map(characterMapper::toDto)
+ .toList();
+ }
+
+ @PostConstruct
+ public void init() {
+ List characterDtos = dataFetcher.fetchAllCharacters();
+ List characters = characterDtos.stream()
+ .map(characterMapper::toModel)
+ .toList();
+ characterRepository.saveAll(characters);
+ }
+}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index bc2fdde8..89aa9893 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
+
+url=https://rickandmortyapi.com/api/character