diff --git a/pom.xml b/pom.xml
index 0c754f19..d495ffc6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,6 +14,8 @@
jv-rick-and-morty
jv-rick-and-morty
+ 1.6.3
+ 0.2.0
17
3.1.1
@@ -32,6 +34,23 @@
test
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ ${lombok-mapstruct-binding.version}
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
+
org.springframework.boot
spring-boot-starter-data-jpa
@@ -41,14 +60,48 @@
com.h2database
h2
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ mysql
+ mysql-connector-java
+ 8.0.33
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.2.0
+
-
-
- org.springframework.boot
- spring-boot-maven-plugin
-
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${java.version}
+ ${java.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ ${lombok-mapstruct-binding.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+
+
org.apache.maven.plugins
maven-checkstyle-plugin
@@ -66,8 +119,21 @@
true
true
false
+ src/main
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
diff --git a/src/main/java/mate/academy/rickandmorty/Application.java b/src/main/java/mate/academy/rickandmorty/Application.java
index cdea84fc..ca5761f8 100644
--- a/src/main/java/mate/academy/rickandmorty/Application.java
+++ b/src/main/java/mate/academy/rickandmorty/Application.java
@@ -1,12 +1,23 @@
package mate.academy.rickandmorty;
+import lombok.RequiredArgsConstructor;
+import mate.academy.rickandmorty.service.CharacterExternalApiService;
+import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
-public class Application {
+@RequiredArgsConstructor
+public class Application implements CommandLineRunner {
+ private final CharacterExternalApiService externalApiService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
+
+ }
+
+ @Override
+ public void run(String... args) throws Exception {
+ externalApiService.fetchCharacters();
}
}
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..d757f88a
--- /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 = "mate.academy.rickandmorty.mapper.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..a1bf72c1
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java
@@ -0,0 +1,40 @@
+package mate.academy.rickandmorty.controller;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import jakarta.validation.constraints.NotBlank;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import mate.academy.rickandmorty.dto.internal.CharacterDto;
+import mate.academy.rickandmorty.service.CharacterService;
+import org.springframework.validation.annotation.Validated;
+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
+@RequiredArgsConstructor
+@RequestMapping("/characters")
+@Validated
+public class CharacterController {
+ private final CharacterService characterService;
+
+ @Operation(description = "The request randomly generates a wiki "
+ + "about one character in the universe the animated series Rick & Morty")
+ @GetMapping("/random")
+ public CharacterDto getRandomCharacter() {
+ return characterService.getRandomCharacter();
+ }
+
+ @Operation(description = "The request takes a string as an argument,"
+ + " and returns a list of all characters whose name contains the search string.")
+ @GetMapping
+ public List getAllCharactersByName(
+ @Parameter(example = "?name=Rick+Sanchez")
+ @RequestParam("name")
+ @NotBlank
+ String name) {
+ return characterService.findByName(name);
+ }
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterInfoDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterInfoDto.java
new file mode 100644
index 00000000..f1491582
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterInfoDto.java
@@ -0,0 +1,8 @@
+package mate.academy.rickandmorty.dto.external;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record CharacterInfoDto(
+ String next) {
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDto.java
new file mode 100644
index 00000000..6fc334db
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDto.java
@@ -0,0 +1,10 @@
+package mate.academy.rickandmorty.dto.external;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record CharacterResponseDto(
+ CharacterInfoDto info,
+ List results) {
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResultsDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResultsDto.java
new file mode 100644
index 00000000..92992be3
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResultsDto.java
@@ -0,0 +1,11 @@
+package mate.academy.rickandmorty.dto.external;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record CharacterResultsDto(
+ String id,
+ String name,
+ String status,
+ String gender) {
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDto.java b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDto.java
new file mode 100644
index 00000000..5ea4214f
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDto.java
@@ -0,0 +1,12 @@
+package mate.academy.rickandmorty.dto.internal;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record CharacterDto(
+ Long id,
+ String externalId,
+ String name,
+ String status,
+ String gender) {
+}
diff --git a/src/main/java/mate/academy/rickandmorty/exception/ExternalApiException.java b/src/main/java/mate/academy/rickandmorty/exception/ExternalApiException.java
new file mode 100644
index 00000000..a0bdda77
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/exception/ExternalApiException.java
@@ -0,0 +1,7 @@
+package mate.academy.rickandmorty.exception;
+
+public class ExternalApiException extends RuntimeException {
+ public ExternalApiException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
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..77b70507
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java
@@ -0,0 +1,33 @@
+package mate.academy.rickandmorty.mapper;
+
+import java.util.List;
+import mate.academy.rickandmorty.config.MapperConfig;
+import mate.academy.rickandmorty.dto.external.CharacterResultsDto;
+import mate.academy.rickandmorty.dto.internal.CharacterDto;
+import mate.academy.rickandmorty.model.Character;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.Mappings;
+
+@Mapper(config = MapperConfig.class)
+public interface CharacterMapper {
+ CharacterDto toDto(Character character);
+
+ @Mapping(target = "id", ignore = true)
+ Character internalToModel(CharacterDto characterDto);
+
+ @Mappings({
+ @Mapping(target = "externalId", source = "id"),
+ @Mapping(target = "id", ignore = true)
+ })
+ CharacterDto externalToDto(CharacterResultsDto characterResultsDto);
+
+ @Mappings({
+ @Mapping(target = "externalId", source = "id"),
+ @Mapping(target = "id", ignore = true)
+ })
+ List externalToDtoList(List externalDtos);
+
+ void updateEntityFromDto(CharacterDto characterDto, @MappingTarget 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..0b68b47d
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/model/Character.java
@@ -0,0 +1,36 @@
+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 jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+@Getter
+@Setter
+@ToString
+@Table(name = "characters")
+@Entity
+public class Character {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+ @Column(nullable = false, unique = true)
+ @NotNull
+ private String externalId;
+ @Column(nullable = false)
+ @NotBlank
+ private String name;
+ @Column(nullable = false)
+ @NotBlank
+ private String status;
+ @Column(nullable = false)
+ @NotBlank
+ 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..4db25bc0
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java
@@ -0,0 +1,16 @@
+package mate.academy.rickandmorty.repository;
+
+import java.util.List;
+import java.util.Optional;
+import mate.academy.rickandmorty.model.Character;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+
+public interface CharacterRepository extends JpaRepository {
+ List findByNameContainingIgnoreCase(String name);
+
+ Optional findByExternalId(String externalId);
+
+ @Query(value = "SELECT * FROM characters ORDER BY RAND() LIMIT 1", nativeQuery = true)
+ Character findRandomCharacter();
+}
diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterExternalApiService.java b/src/main/java/mate/academy/rickandmorty/service/CharacterExternalApiService.java
new file mode 100644
index 00000000..e96eecd7
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/service/CharacterExternalApiService.java
@@ -0,0 +1,56 @@
+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.external.CharacterResponseDto;
+import mate.academy.rickandmorty.dto.internal.CharacterDto;
+import mate.academy.rickandmorty.exception.ExternalApiException;
+import mate.academy.rickandmorty.mapper.CharacterMapper;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class CharacterExternalApiService {
+ private static final String BASE_URL = "https://rickandmortyapi.com/api/character";
+ private final ObjectMapper objectMapper;
+ private final CharacterMapper characterMapper;
+ private final CharacterService characterService;
+
+ public void fetchCharacters() {
+ List characters = new ArrayList<>();
+ String nextUrl = BASE_URL;
+
+ HttpClient client = HttpClient.newHttpClient();
+ while (nextUrl != null) {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(URI.create(nextUrl))
+ .build();
+ try {
+ HttpResponse response = client.send(
+ request, HttpResponse.BodyHandlers.ofString());
+ if (response.statusCode() != 200) {
+ throw new IOException("API returned non-successful status code: "
+ + response.statusCode());
+ }
+ CharacterResponseDto characterResponseDto = objectMapper.readValue(
+ response.body(), CharacterResponseDto.class);
+
+ characters.addAll(characterMapper.externalToDtoList(
+ characterResponseDto.results()));
+ nextUrl = characterResponseDto.info().next();
+ } catch (IOException | InterruptedException e) {
+ throw new ExternalApiException(
+ "An error occurred while trying to get a response from the server.", e);
+ }
+ }
+ characterService.saveAll(characters);
+ }
+
+}
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..f6bb9e12
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java
@@ -0,0 +1,14 @@
+package mate.academy.rickandmorty.service;
+
+import java.util.List;
+import mate.academy.rickandmorty.dto.internal.CharacterDto;
+
+public interface CharacterService {
+ void saveAll(List dtos);
+
+ CharacterDto getById(Long id);
+
+ List findByName(String name);
+
+ CharacterDto getRandomCharacter();
+}
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..69e1370a
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java
@@ -0,0 +1,55 @@
+package mate.academy.rickandmorty.service;
+
+import jakarta.persistence.EntityNotFoundException;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import mate.academy.rickandmorty.dto.internal.CharacterDto;
+import mate.academy.rickandmorty.mapper.CharacterMapper;
+import mate.academy.rickandmorty.model.Character;
+import mate.academy.rickandmorty.repository.CharacterRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class CharacterServiceImpl implements CharacterService {
+ private final CharacterMapper mapper;
+ private final CharacterRepository repository;
+
+ @Override
+ @Transactional
+ public void saveAll(List dtos) {
+ List characters = dtos.stream()
+ .map(characterDto -> {
+ Character newCharacter = repository
+ .findByExternalId(characterDto.externalId())
+ .orElse(mapper.internalToModel(characterDto));
+ mapper.updateEntityFromDto(characterDto, newCharacter);
+ return newCharacter;
+ })
+ .toList();
+ repository.saveAll(characters);
+ }
+
+ @Override
+ public CharacterDto getById(Long id) {
+ return repository.findById(id)
+ .map(mapper::toDto)
+ .orElseThrow(
+ () -> new EntityNotFoundException(
+ "Could not find Character with id: " + id)
+ );
+ }
+
+ @Override
+ public List findByName(String name) {
+ return repository.findByNameContainingIgnoreCase(name).stream()
+ .map(mapper::toDto)
+ .toList();
+ }
+
+ @Override
+ public CharacterDto getRandomCharacter() {
+ return mapper.toDto(repository.findRandomCharacter());
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 8b137891..2d1822bf 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,6 @@
-
+spring.datasource.url=jdbc:mysql://localhost:3306/rick_and_morty_db
+spring.datasource.username=root
+spring.datasource.password=12345678
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+spring.jooq.sql-dialect=MYSQL
+spring.jpa.hibernate.ddl-auto=create