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