diff --git a/jv-rick-and-morty b/jv-rick-and-morty
new file mode 160000
index 00000000..c3bbe60d
--- /dev/null
+++ b/jv-rick-and-morty
@@ -0,0 +1 @@
+Subproject commit c3bbe60d0efb622b5edb1b2a6bdf3d02e6af3fc2
diff --git a/pom.xml b/pom.xml
index 0c754f19..e3465227 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,74 +1,148 @@
- 4.0.0
-
- org.springframework.boot
- spring-boot-starter-parent
- 3.1.4
-
-
- mate.academy
- jv-rick-and-morty
- 0.0.1-SNAPSHOT
- jv-rick-and-morty
- jv-rick-and-morty
-
- 17
- 3.1.1
-
- https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml
-
-
-
-
- org.springframework.boot
- spring-boot-starter
-
+ 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
+ spring-boot-starter-parent
+ 3.1.4
+
+
+ mate.academy
+ jv-rick-and-morty
+ 0.0.1-SNAPSHOT
+ jv-rick-and-morty
+ jv-rick-and-morty
+
+ 17
+ 3.1.1
+
+ https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml
+
+ 1.5.5.Final
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
+
+ org.springframework.boot
+ spring-boot-starter-web
+
-
- org.springframework.boot
- spring-boot-starter-data-jpa
-
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
-
- com.h2database
- h2
-
-
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
-
-
-
- org.springframework.boot
- spring-boot-maven-plugin
-
-
- org.apache.maven.plugins
- maven-checkstyle-plugin
- 3.3.0
-
-
- compile
-
- check
-
-
-
-
- ${maven.checkstyle.plugin.configLocation}
- true
- true
- false
-
-
-
-
+
+ com.h2database
+ h2
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+ provided
+
+
+
+ com.mysql
+ mysql-connector-j
+ 8.0.33
+
+
+
+ org.liquibase
+ liquibase-core
+ ${liquibase.version}
+
+
+
+ org.liquibase
+ liquibase-maven-plugin
+ ${liquibase.version}
+
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ 3.1.4
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.2.0
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.3.0
+
+
+ compile
+
+ check
+
+
+
+
+ ${maven.checkstyle.plugin.configLocation}
+ true
+ true
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ ${java.version}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+
+
+
+
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..cf0258a0
--- /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",
+ nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
+ injectionStrategy = InjectionStrategy.CONSTRUCTOR,
+ 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..9f6ab847
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java
@@ -0,0 +1,36 @@
+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.external.CharacterSearchParametersDto;
+import mate.academy.rickandmorty.dto.external.ExternalCharacter;
+import mate.academy.rickandmorty.service.impl.CharacterServiceImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@Tag(name = "Character manager", description = "Endpoints for managing characters")
+@RestController
+@RequestMapping("/characters")
+@RequiredArgsConstructor
+public class CharacterController {
+ private final CharacterServiceImpl characterService;
+
+ @GetMapping("/random")
+ @Operation(summary = "Get character", description = "Get a random character from everyone")
+ public ExternalCharacter getRandomCharacter() {
+ return characterService.getRandomCharacter();
+ }
+
+ @GetMapping("/search")
+ @Operation(summary = "Get search character",
+ description = "Get all characters according to the search data")
+ public List searchCharacter(
+ CharacterSearchParametersDto searchParametersDto, Pageable pageable
+ ) {
+ return characterService.searchCharacters(searchParametersDto, pageable);
+ }
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterSearchParametersDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterSearchParametersDto.java
new file mode 100644
index 00000000..1c010800
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterSearchParametersDto.java
@@ -0,0 +1,8 @@
+package mate.academy.rickandmorty.dto.external;
+
+public record CharacterSearchParametersDto(
+ String[] name,
+ String[] status,
+ String[] gender
+) {
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/ExternalCharacter.java b/src/main/java/mate/academy/rickandmorty/dto/external/ExternalCharacter.java
new file mode 100644
index 00000000..74c51be6
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/external/ExternalCharacter.java
@@ -0,0 +1,10 @@
+package mate.academy.rickandmorty.dto.external;
+
+public record ExternalCharacter(
+ Long id,
+ Long externalId,
+ String name,
+ String status,
+ String gender
+) {
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDataDto.java b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDataDto.java
new file mode 100644
index 00000000..87fc1590
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDataDto.java
@@ -0,0 +1,15 @@
+package mate.academy.rickandmorty.dto.internal;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class CharacterDataDto {
+ private Long id;
+ private String name;
+ private String status;
+ private String gender;
+
+
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/internal/InfoResourcesDto.java b/src/main/java/mate/academy/rickandmorty/dto/internal/InfoResourcesDto.java
new file mode 100644
index 00000000..7222c5ce
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/internal/InfoResourcesDto.java
@@ -0,0 +1,11 @@
+package mate.academy.rickandmorty.dto.internal;
+
+import lombok.Data;
+
+@Data
+public class InfoResourcesDto {
+ private int count;
+ private int pages;
+ private String next;
+ private String prev;
+}
diff --git a/src/main/java/mate/academy/rickandmorty/dto/internal/PageWIthCharacters.java b/src/main/java/mate/academy/rickandmorty/dto/internal/PageWIthCharacters.java
new file mode 100644
index 00000000..b8783294
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/dto/internal/PageWIthCharacters.java
@@ -0,0 +1,10 @@
+package mate.academy.rickandmorty.dto.internal;
+
+import java.util.List;
+import lombok.Data;
+
+@Data
+public class PageWIthCharacters {
+ private InfoResourcesDto info;
+ private List results;
+}
diff --git a/src/main/java/mate/academy/rickandmorty/exception/SpecificationNotFoundException.java b/src/main/java/mate/academy/rickandmorty/exception/SpecificationNotFoundException.java
new file mode 100644
index 00000000..13e71354
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/exception/SpecificationNotFoundException.java
@@ -0,0 +1,7 @@
+package mate.academy.rickandmorty.exception;
+
+public class SpecificationNotFoundException extends RuntimeException {
+ public SpecificationNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/mate/academy/rickandmorty/exception/UrlNotFoundException.java b/src/main/java/mate/academy/rickandmorty/exception/UrlNotFoundException.java
new file mode 100644
index 00000000..8f9aa018
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/exception/UrlNotFoundException.java
@@ -0,0 +1,7 @@
+package mate.academy.rickandmorty.exception;
+
+public class UrlNotFoundException extends RuntimeException {
+ public UrlNotFoundException(String message) {
+ super(message);
+ }
+}
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..fc34cdf9
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java
@@ -0,0 +1,16 @@
+package mate.academy.rickandmorty.mapper;
+
+import mate.academy.rickandmorty.config.MapperConfig;
+import mate.academy.rickandmorty.dto.external.ExternalCharacter;
+import mate.academy.rickandmorty.dto.internal.CharacterDataDto;
+import mate.academy.rickandmorty.model.Character;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(config = MapperConfig.class)
+public interface CharacterMapper {
+ @Mapping(target = "externalId", source = "characterDto.id")
+ Character toEntity(CharacterDataDto characterDto);
+
+ ExternalCharacter 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..df6980f6
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/model/Character.java
@@ -0,0 +1,23 @@
+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;
+
+@Entity
+@Data
+@Table(name = "characters")
+public class Character {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+ @Column(name = "external_id", nullable = false)
+ private Long externalId;
+ private String name;
+ private String status;
+ private String gender;
+}
diff --git a/src/main/java/mate/academy/rickandmorty/repository/SpecificationBuilder.java b/src/main/java/mate/academy/rickandmorty/repository/SpecificationBuilder.java
new file mode 100644
index 00000000..12e95214
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/SpecificationBuilder.java
@@ -0,0 +1,7 @@
+package mate.academy.rickandmorty.repository;
+
+import org.springframework.data.jpa.domain.Specification;
+
+public interface SpecificationBuilder {
+ Specification build(P searchParameters);
+}
diff --git a/src/main/java/mate/academy/rickandmorty/repository/SpecificationProvider.java b/src/main/java/mate/academy/rickandmorty/repository/SpecificationProvider.java
new file mode 100644
index 00000000..899e7ce7
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/SpecificationProvider.java
@@ -0,0 +1,11 @@
+package mate.academy.rickandmorty.repository;
+
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Component
+public interface SpecificationProvider {
+ Specification getSpecification(String[] params);
+
+ String getKey();
+}
diff --git a/src/main/java/mate/academy/rickandmorty/repository/SpecificationProviderManager.java b/src/main/java/mate/academy/rickandmorty/repository/SpecificationProviderManager.java
new file mode 100644
index 00000000..d4b55e44
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/SpecificationProviderManager.java
@@ -0,0 +1,5 @@
+package mate.academy.rickandmorty.repository;
+
+public interface SpecificationProviderManager {
+ SpecificationProvider getSpecificationProvider(String key);
+}
diff --git a/src/main/java/mate/academy/rickandmorty/repository/character/CharacterRepository.java b/src/main/java/mate/academy/rickandmorty/repository/character/CharacterRepository.java
new file mode 100644
index 00000000..20bee7ba
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/character/CharacterRepository.java
@@ -0,0 +1,14 @@
+package mate.academy.rickandmorty.repository.character;
+
+import java.util.List;
+import java.util.Optional;
+import mate.academy.rickandmorty.model.Character;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface CharacterRepository extends JpaRepository {
+ Optional findByExternalId(Long externalId);
+
+ List findAll(Specification specification, Pageable pageable);
+}
diff --git a/src/main/java/mate/academy/rickandmorty/repository/character/CharacterSpecificationBuilder.java b/src/main/java/mate/academy/rickandmorty/repository/character/CharacterSpecificationBuilder.java
new file mode 100644
index 00000000..759fb2c2
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/character/CharacterSpecificationBuilder.java
@@ -0,0 +1,38 @@
+package mate.academy.rickandmorty.repository.character;
+
+import lombok.RequiredArgsConstructor;
+import mate.academy.rickandmorty.dto.external.CharacterSearchParametersDto;
+import mate.academy.rickandmorty.model.Character;
+import mate.academy.rickandmorty.repository.SpecificationBuilder;
+import mate.academy.rickandmorty.repository.SpecificationProviderManager;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class CharacterSpecificationBuilder implements
+ SpecificationBuilder {
+ private final SpecificationProviderManager providerManager;
+
+ @Override
+ public Specification build(CharacterSearchParametersDto searchParameters) {
+ Specification specification = Specification.where(null);
+
+ if (searchParameters.name() != null && searchParameters.name().length > 0) {
+ specification = specification.and(providerManager.getSpecificationProvider("name")
+ .getSpecification(searchParameters.name()));
+ }
+
+ if (searchParameters.status() != null && searchParameters.status().length > 0) {
+ specification = specification.and(providerManager.getSpecificationProvider("status")
+ .getSpecification(searchParameters.status()));
+ }
+
+ if (searchParameters.gender() != null && searchParameters.gender().length > 0) {
+ specification = specification.and(providerManager.getSpecificationProvider("gender")
+ .getSpecification(searchParameters.gender()));
+ }
+
+ return specification;
+ }
+}
diff --git a/src/main/java/mate/academy/rickandmorty/repository/character/CharacterSpecificationProviderManager.java b/src/main/java/mate/academy/rickandmorty/repository/character/CharacterSpecificationProviderManager.java
new file mode 100644
index 00000000..e2b220bb
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/character/CharacterSpecificationProviderManager.java
@@ -0,0 +1,26 @@
+package mate.academy.rickandmorty.repository.character;
+
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import mate.academy.rickandmorty.exception.SpecificationNotFoundException;
+import mate.academy.rickandmorty.model.Character;
+import mate.academy.rickandmorty.repository.SpecificationProvider;
+import mate.academy.rickandmorty.repository.SpecificationProviderManager;
+import org.springframework.stereotype.Component;
+
+@Component
+@RequiredArgsConstructor
+public class CharacterSpecificationProviderManager implements
+ SpecificationProviderManager {
+ private final List> specificationProviders;
+
+ @Override
+ public SpecificationProvider getSpecificationProvider(String key) {
+ return specificationProviders.stream()
+ .filter(param -> param.getKey().equals(key))
+ .findFirst()
+ .orElseThrow(() -> new SpecificationNotFoundException(
+ "Can't find correct specification provider for key: " + key
+ ));
+ }
+}
diff --git a/src/main/java/mate/academy/rickandmorty/repository/character/spec/GenderSpecificationProvider.java b/src/main/java/mate/academy/rickandmorty/repository/character/spec/GenderSpecificationProvider.java
new file mode 100644
index 00000000..4c586e96
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/character/spec/GenderSpecificationProvider.java
@@ -0,0 +1,28 @@
+package mate.academy.rickandmorty.repository.character.spec;
+
+import jakarta.persistence.criteria.Predicate;
+import java.util.Arrays;
+import mate.academy.rickandmorty.model.Character;
+import mate.academy.rickandmorty.repository.SpecificationProvider;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Component
+public class GenderSpecificationProvider implements SpecificationProvider {
+ @Override
+ public Specification getSpecification(String[] params) {
+ return ((root, query, criteriaBuilder) -> {
+ Predicate[] predicates = Arrays.stream(params)
+ .map(param -> criteriaBuilder.like(
+ criteriaBuilder.lower(root.get("gender")),
+ "%" + param + "%"))
+ .toArray(Predicate[]::new);
+ return criteriaBuilder.or(predicates);
+ });
+ }
+
+ @Override
+ public String getKey() {
+ return "gender";
+ }
+}
diff --git a/src/main/java/mate/academy/rickandmorty/repository/character/spec/NameSpecificationProvider.java b/src/main/java/mate/academy/rickandmorty/repository/character/spec/NameSpecificationProvider.java
new file mode 100644
index 00000000..a1ed9aaf
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/character/spec/NameSpecificationProvider.java
@@ -0,0 +1,28 @@
+package mate.academy.rickandmorty.repository.character.spec;
+
+import jakarta.persistence.criteria.Predicate;
+import java.util.Arrays;
+import mate.academy.rickandmorty.model.Character;
+import mate.academy.rickandmorty.repository.SpecificationProvider;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Component
+public class NameSpecificationProvider implements SpecificationProvider {
+ @Override
+ public Specification getSpecification(String[] params) {
+ return ((root, query, criteriaBuilder) -> {
+ Predicate[] predicates = Arrays.stream(params)
+ .map(param -> criteriaBuilder.like(
+ criteriaBuilder.lower(root.get("name")),
+ "%" + param + "%"))
+ .toArray(Predicate[]::new);
+ return criteriaBuilder.or(predicates);
+ });
+ }
+
+ @Override
+ public String getKey() {
+ return "name";
+ }
+}
diff --git a/src/main/java/mate/academy/rickandmorty/repository/character/spec/StatusSpecificationProvider.java b/src/main/java/mate/academy/rickandmorty/repository/character/spec/StatusSpecificationProvider.java
new file mode 100644
index 00000000..daf8f2a5
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/repository/character/spec/StatusSpecificationProvider.java
@@ -0,0 +1,28 @@
+package mate.academy.rickandmorty.repository.character.spec;
+
+import jakarta.persistence.criteria.Predicate;
+import java.util.Arrays;
+import mate.academy.rickandmorty.model.Character;
+import mate.academy.rickandmorty.repository.SpecificationProvider;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Component;
+
+@Component
+public class StatusSpecificationProvider implements SpecificationProvider {
+ @Override
+ public Specification getSpecification(String[] params) {
+ return ((root, query, criteriaBuilder) -> {
+ Predicate[] predicates = Arrays.stream(params)
+ .map(param -> criteriaBuilder.like(
+ criteriaBuilder.lower(root.get("status")),
+ "%" + param + "%"))
+ .toArray(Predicate[]::new);
+ return criteriaBuilder.or(predicates);
+ });
+ }
+
+ @Override
+ public String getKey() {
+ return "status";
+ }
+}
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..0b074fb3
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java
@@ -0,0 +1,13 @@
+package mate.academy.rickandmorty.service;
+
+import java.util.List;
+import mate.academy.rickandmorty.dto.external.CharacterSearchParametersDto;
+import mate.academy.rickandmorty.dto.external.ExternalCharacter;
+import org.springframework.data.domain.Pageable;
+
+public interface CharacterService {
+ ExternalCharacter getRandomCharacter();
+
+ List searchCharacters(CharacterSearchParametersDto searchParametersDto,
+ Pageable pageable);
+}
diff --git a/src/main/java/mate/academy/rickandmorty/service/ResourceService.java b/src/main/java/mate/academy/rickandmorty/service/ResourceService.java
new file mode 100644
index 00000000..333c4e34
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/service/ResourceService.java
@@ -0,0 +1,11 @@
+package mate.academy.rickandmorty.service;
+
+public interface ResourceService {
+ R getPageFromPageNumber(String resourceName, Class className, int pageNumber);
+
+ C getDataFromId(String resourceName, Class className, int id);
+
+ R getDataFromSearchParam(S searchParam, Class className, String resourceName);
+
+ R getPageFromUrl(String url, Class className);
+}
diff --git a/src/main/java/mate/academy/rickandmorty/service/UrlService.java b/src/main/java/mate/academy/rickandmorty/service/UrlService.java
new file mode 100644
index 00000000..46847f29
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/service/UrlService.java
@@ -0,0 +1,5 @@
+package mate.academy.rickandmorty.service;
+
+public interface UrlService {
+ String getResourcesUrl(String resourcesName);
+}
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..7b409884
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java
@@ -0,0 +1,107 @@
+package mate.academy.rickandmorty.service.impl;
+
+import java.util.List;
+import java.util.Random;
+import lombok.RequiredArgsConstructor;
+import mate.academy.rickandmorty.dto.external.CharacterSearchParametersDto;
+import mate.academy.rickandmorty.dto.external.ExternalCharacter;
+import mate.academy.rickandmorty.dto.internal.CharacterDataDto;
+import mate.academy.rickandmorty.dto.internal.PageWIthCharacters;
+import mate.academy.rickandmorty.mapper.CharacterMapper;
+import mate.academy.rickandmorty.model.Character;
+import mate.academy.rickandmorty.repository.character.CharacterRepository;
+import mate.academy.rickandmorty.repository.character.CharacterSpecificationBuilder;
+import mate.academy.rickandmorty.service.CharacterService;
+import mate.academy.rickandmorty.service.ResourceService;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class CharacterServiceImpl implements CharacterService {
+ private static final String CHARACTER_NAME = "characters";
+ private static final int DEFAULT_PAGE_NUMBER = 1;
+
+ private final ResourceService<
+ PageWIthCharacters,
+ CharacterDataDto,
+ CharacterSearchParametersDto> resourceService;
+ private final CharacterRepository characterRepository;
+ private final CharacterMapper characterMapper;
+ private final CharacterSpecificationBuilder characterSpecificationBuilder;
+
+ @Override
+ public ExternalCharacter getRandomCharacter() {
+ int randomNumberCharacterId = new Random().nextInt(getNumbersOfCharacters());
+ CharacterDataDto characterDto = resourceService.getDataFromId(
+ CHARACTER_NAME,
+ CharacterDataDto.class,
+ randomNumberCharacterId);
+
+ if (characterRepository.findByExternalId(characterDto.getId()).isPresent()) {
+ return characterMapper.toDto(characterRepository.findByExternalId(
+ characterDto.getId()).get());
+ }
+
+ Character saveCharacter = characterRepository.save(characterMapper.toEntity(characterDto));
+ return characterMapper.toDto(saveCharacter);
+ }
+
+ @Override
+ public List searchCharacters(
+ CharacterSearchParametersDto searchParametersDto, Pageable pageable
+ ) {
+ Specification characterSpecification =
+ characterSpecificationBuilder.build(searchParametersDto);
+ saveCharactersFromAllPagesToLocalDb(searchParametersDto);
+
+ return characterRepository.findAll(characterSpecification, pageable).stream()
+ .map(characterMapper::toDto)
+ .toList();
+ }
+
+ private void saveCharactersFromAllPagesToLocalDb(
+ CharacterSearchParametersDto searchParametersDto
+ ) {
+ PageWIthCharacters currentPageData = getPageWithData(searchParametersDto);
+ saveCharactersToLocalDb(currentPageData);
+
+ while (currentPageData.getInfo().getNext() != null) {
+ currentPageData = resourceService.getPageFromUrl(
+ currentPageData.getInfo().getNext(),
+ PageWIthCharacters.class
+ );
+ saveCharactersToLocalDb(currentPageData);
+ }
+ }
+
+ private void saveCharactersToLocalDb(PageWIthCharacters dataFromSearchParam) {
+ dataFromSearchParam.getResults().stream()
+ .map(characterMapper::toEntity)
+ .forEach(character -> {
+ if (!characterRepository.findByExternalId(
+ character.getExternalId()).isPresent()
+ ) {
+ characterRepository.save(character);
+ }
+ });
+ }
+
+ private PageWIthCharacters getPageWithData(CharacterSearchParametersDto searchParametersDto) {
+ return resourceService.getDataFromSearchParam(
+ searchParametersDto,
+ PageWIthCharacters.class,
+ CHARACTER_NAME);
+ }
+
+ private int getNumbersOfCharacters() {
+ PageWIthCharacters pageWithCharacters = resourceService.getPageFromPageNumber(
+ CHARACTER_NAME,
+ PageWIthCharacters.class,
+ DEFAULT_PAGE_NUMBER
+ );
+ return pageWithCharacters.getInfo().getCount();
+ }
+
+}
diff --git a/src/main/java/mate/academy/rickandmorty/service/impl/ResourceServiceImpl.java b/src/main/java/mate/academy/rickandmorty/service/impl/ResourceServiceImpl.java
new file mode 100644
index 00000000..94025595
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/service/impl/ResourceServiceImpl.java
@@ -0,0 +1,135 @@
+package mate.academy.rickandmorty.service.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import lombok.RequiredArgsConstructor;
+import mate.academy.rickandmorty.service.ResourceService;
+import mate.academy.rickandmorty.service.UrlService;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class ResourceServiceImpl implements ResourceService {
+ private static final String PAGE_PATTERN = "?page=%s";
+ private static final String ID_PATTERN = "/%s";
+ private static final String QUESTION_MARK_SEPARATOR = "?";
+ private static final String EQUAL_SIGN_SEPARATOR = "=";
+ private static final String COMMA_SEPARATOR = ",";
+ private static final String AMPERSAND_SEPARATOR = "&";
+ private final UrlService urlServices;
+ private final ObjectMapper objectMapper;
+
+ @Override
+ public R getPageFromPageNumber(String resourceName, Class className, int pageNumber) {
+ String resourcesUrl = urlServices.getResourcesUrl(resourceName);
+ String urlWithPage = resourcesUrl.concat(PAGE_PATTERN).formatted(pageNumber);
+ HttpRequest request = getHttpRequest(
+ pageNumber >= 0 && pageNumber <= 1 ? resourcesUrl : urlWithPage
+ );
+
+ try {
+ HttpClient client = HttpClient.newHttpClient();
+ HttpResponse response = client.send(request,
+ HttpResponse.BodyHandlers.ofString()
+ );
+ return objectMapper.readValue(response.body(), className);
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException("An error occurred while fetching data from URL", e);
+ }
+ }
+
+ @Override
+ public C getDataFromId(String resourceName, Class className, int id) {
+ String resourcesUrl = urlServices.getResourcesUrl(resourceName);
+ String urlWithCharacterId = resourcesUrl.concat(ID_PATTERN).formatted(id);
+ HttpRequest request = getHttpRequest(urlWithCharacterId);
+
+ try {
+ HttpClient client = HttpClient.newHttpClient();
+ HttpResponse response = client.send(request,
+ HttpResponse.BodyHandlers.ofString()
+ );
+ return objectMapper.readValue(response.body(), className);
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException("An error occurred while fetching data from URL", e);
+ }
+ }
+
+ @Override
+ public R getDataFromSearchParam(S searchParam, Class className, String resourceName) {
+ StringBuilder url = new StringBuilder(urlServices.getResourcesUrl(resourceName));
+ String parsedUrl = parseSearchParamsAndGetUrl(searchParam, url);
+ HttpRequest request = getHttpRequest(parsedUrl);
+
+ try {
+ HttpClient client = HttpClient.newHttpClient();
+ HttpResponse response = client.send(request,
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ return objectMapper.readValue(response.body(), className);
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException("An error occurred while fetching data from URL", e);
+ }
+ }
+
+ @Override
+ public R getPageFromUrl(String url, Class className) {
+ HttpRequest request = getHttpRequest(url);
+
+ try {
+ HttpClient client = HttpClient.newHttpClient();
+ HttpResponse response = client.send(request,
+ HttpResponse.BodyHandlers.ofString()
+ );
+
+ return objectMapper.readValue(response.body(), className);
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException("An error occurred while fetching data from URL", e);
+ }
+ }
+
+ private HttpRequest getHttpRequest(String url) {
+ return HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(url))
+ .build();
+ }
+
+ private String parseSearchParamsAndGetUrl(S searchParam, StringBuilder url) {
+ try {
+ int count = 0;
+
+ for (Field field : searchParam.getClass().getDeclaredFields()) {
+ field.setAccessible(true);
+ String fieldName = field.getName();
+
+ if (field.getType().isArray() && field.get(searchParam) != null) {
+ if (count < 1) {
+ url.append(QUESTION_MARK_SEPARATOR)
+ .append(fieldName)
+ .append(EQUAL_SIGN_SEPARATOR);
+ count++;
+ } else {
+ url.append(AMPERSAND_SEPARATOR)
+ .append(fieldName)
+ .append(EQUAL_SIGN_SEPARATOR);
+ }
+ url.append(String.join(COMMA_SEPARATOR, ((String[]) field.get(searchParam))));
+ }
+ }
+ } catch (IllegalAccessException e) {
+ throw new IllegalArgumentException(
+ "Error parsing search parameters and generating URL", e
+ );
+ }
+
+ return url.toString();
+ }
+
+}
diff --git a/src/main/java/mate/academy/rickandmorty/service/impl/UrlServiceImpl.java b/src/main/java/mate/academy/rickandmorty/service/impl/UrlServiceImpl.java
new file mode 100644
index 00000000..73195601
--- /dev/null
+++ b/src/main/java/mate/academy/rickandmorty/service/impl/UrlServiceImpl.java
@@ -0,0 +1,62 @@
+package mate.academy.rickandmorty.service.impl;
+
+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.Arrays;
+import java.util.regex.Pattern;
+import mate.academy.rickandmorty.exception.UrlNotFoundException;
+import mate.academy.rickandmorty.service.UrlService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UrlServiceImpl implements UrlService {
+ private static final String BASE_URL = "https://rickandmortyapi.com/api";
+ private static final String URL_PATTERN = "https://[a-zA-Z0-9.-]+/[^/]+/[^/]+";
+ private static final String COLON_SEPARATOR = ":";
+ private static final String COMMA_SEPARATOR = ",";
+
+ @Override
+ public String getResourcesUrl(String resourcesName) {
+ HttpClient client = HttpClient.newHttpClient();
+
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(BASE_URL))
+ .build();
+
+ try {
+ HttpResponse response = client.send(
+ httpRequest, HttpResponse.BodyHandlers.ofString()
+ );
+ return parseResponseAndGetUrl(response, resourcesName);
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String parseResponseAndGetUrl(HttpResponse response, String resourcesName) {
+ return Arrays.stream(response.body().split(COMMA_SEPARATOR))
+ .filter(value -> value.contains(resourcesName))
+ .map(str -> {
+ int colonIndex = str.indexOf(COLON_SEPARATOR);
+
+ if (colonIndex != -1) {
+ String url = str.substring(colonIndex + 2, str.length() - 1);
+
+ if (Pattern.compile(URL_PATTERN).matcher(url).matches()) {
+ return url;
+ } else {
+ throw new UrlNotFoundException(
+ "URL format doesn't match the expected pattern"
+ );
+ }
+ }
+ throw new RuntimeException("Couldn't find a colon separator");
+ })
+ .findFirst()
+ .orElseThrow(() -> new UrlNotFoundException("Couldn't find the correct URL"));
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 8b137891..71885de6 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,8 @@
-
+spring.datasource.url=jdbc:mysql://localhost:3306/rick_and_morty?serverTimeZone=UTC
+spring.datasource.username=root
+spring.datasource.password=3215987
+spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
+spring.jpa.show-sql=true
+spring.jpa.hibernate.ddl-auto=validate
+spring.jpa.open-in-view=false
+spring.jackson.deserialization.fail-on-unknown-properties=true
diff --git a/src/main/resources/db/changelog/changes/01-create-characters-table.yaml b/src/main/resources/db/changelog/changes/01-create-characters-table.yaml
new file mode 100644
index 00000000..7c456d48
--- /dev/null
+++ b/src/main/resources/db/changelog/changes/01-create-characters-table.yaml
@@ -0,0 +1,36 @@
+databaseChangeLog:
+ - changeSet:
+ id: create-characters-table
+ author: zagar
+ changes:
+ - createTable:
+ tableName: characters
+ columns:
+ - column:
+ name: id
+ type: bigint
+ autoIncrement: true
+ constraints:
+ primaryKey: true
+ nullable: false
+ - column:
+ name: external_id
+ type: bigint
+ constraints:
+ unique: true
+ nullable: false
+ - column:
+ name: name
+ type: varchar(255)
+ constraints:
+ nullable: false
+ - column:
+ name: status
+ type: varchar(255)
+ constraints:
+ nullable: false
+ - column:
+ name: gender
+ type: varchar(255)
+ constraints:
+ nullable: false
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..1ce63bbb
--- /dev/null
+++ b/src/main/resources/db/changelog/db.changelog-master.yaml
@@ -0,0 +1,3 @@
+databaseChangeLog:
+ - include:
+ file: /db/changelog/changes/01-create-characters-table.yaml
\ No newline at end of file
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index bc2fdde8..a0dec620 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -3,3 +3,7 @@ spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+spring.jpa.show-sql=true
+spring.jpa.hibernate.ddl-auto=validate
+spring.jpa.open-in-view=false
+spring.jackson.deserialization.fail-on-unknown-properties=true