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} + ${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