From 80d278309af80bb24d7af2d1d25dac650bc138d8 Mon Sep 17 00:00:00 2001 From: Piotr Kaliszuk Date: Fri, 25 Oct 2024 17:57:57 +0200 Subject: [PATCH 1/2] solution --- pom.xml | 24 ++++++ .../rickandmorty/config/AppConfig.java | 29 +++++++ .../controller/CharacterController.java | 35 ++++++++ .../academy/rickandmorty/model/Character.java | 25 ++++++ .../rickandmorty/model/RickAndMorty.java | 18 ++++ .../model/RickAndMortyResponse.java | 13 +++ .../repository/CharacterRepository.java | 9 ++ .../service/CharacterService.java | 15 ++++ .../service/impl/CharacterServiceImpl.java | 82 +++++++++++++++++++ src/main/resources/application.properties | 6 ++ src/test/resources/application.properties | 1 + 11 files changed, 257 insertions(+) create mode 100644 src/main/java/mate/academy/rickandmorty/config/AppConfig.java create mode 100644 src/main/java/mate/academy/rickandmorty/controller/CharacterController.java create mode 100644 src/main/java/mate/academy/rickandmorty/model/Character.java create mode 100644 src/main/java/mate/academy/rickandmorty/model/RickAndMorty.java create mode 100644 src/main/java/mate/academy/rickandmorty/model/RickAndMortyResponse.java create mode 100644 src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java create mode 100644 src/main/java/mate/academy/rickandmorty/service/CharacterService.java create mode 100644 src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java diff --git a/pom.xml b/pom.xml index 0c754f19..2d6d7aa8 100644 --- a/pom.xml +++ b/pom.xml @@ -26,12 +26,36 @@ spring-boot-starter + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-web + + org.springframework.boot spring-boot-starter-test test + + org.projectlombok + lombok + 1.18.28 + provided + + org.springframework.boot spring-boot-starter-data-jpa diff --git a/src/main/java/mate/academy/rickandmorty/config/AppConfig.java b/src/main/java/mate/academy/rickandmorty/config/AppConfig.java new file mode 100644 index 00000000..29541476 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/config/AppConfig.java @@ -0,0 +1,29 @@ +package mate.academy.rickandmorty.config; + +import java.util.Random; +import mate.academy.rickandmorty.service.CharacterService; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + public Random random() { + return new Random(); + } + + @Bean + public CommandLineRunner commandLineRunner(CharacterService characterService) { + return args -> { + characterService.fetchCharacters(); + }; + } +} 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..6b92fad6 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -0,0 +1,35 @@ +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 java.util.Optional; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.model.Character; +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 +@RequiredArgsConstructor +@RequestMapping(value = "/api/characters") +@Tag(name = "Character API", description = "Operations related to characters from Rick and Morty") +public class CharacterController { + private final CharacterService characterService; + + @GetMapping("/random") + @Operation(summary = "Get a random character", + description = "Returns a random character from the database.") + public Optional getRandomCharacter() { + return characterService.getRandomCharacter(); + } + + @GetMapping("/search") + @Operation(summary = "Search characters by name", + description = "Returns a list of characters whose name contains the specified word.") + public List searchCharactersByName(@RequestParam String word) { + return characterService.searchCharactersByName(word); + } +} 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..698c6d13 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/Character.java @@ -0,0 +1,25 @@ +package mate.academy.rickandmorty.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "characters") +public class Character { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private int externalId; + @NotNull + private String name; + private String status; + private String gender; +} diff --git a/src/main/java/mate/academy/rickandmorty/model/RickAndMorty.java b/src/main/java/mate/academy/rickandmorty/model/RickAndMorty.java new file mode 100644 index 00000000..6de0e567 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/RickAndMorty.java @@ -0,0 +1,18 @@ +package mate.academy.rickandmorty.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RickAndMorty { + @JsonProperty("id") + private int id; + @JsonProperty("name") + private String name; + @JsonProperty("status") + private String status; + @JsonProperty("gender") + private String gender; +} diff --git a/src/main/java/mate/academy/rickandmorty/model/RickAndMortyResponse.java b/src/main/java/mate/academy/rickandmorty/model/RickAndMortyResponse.java new file mode 100644 index 00000000..52337c3a --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/RickAndMortyResponse.java @@ -0,0 +1,13 @@ +package mate.academy.rickandmorty.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RickAndMortyResponse { + @JsonProperty("results") + private List results; +} 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..fd513c6d --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.repository; + +import mate.academy.rickandmorty.model.Character; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface CharacterRepository extends JpaRepository { +} 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..f534df6a --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java @@ -0,0 +1,15 @@ +package mate.academy.rickandmorty.service; + +import java.util.List; +import java.util.Optional; +import mate.academy.rickandmorty.model.Character; +import org.springframework.stereotype.Service; + +@Service +public interface CharacterService { + void fetchCharacters(); + + Optional getRandomCharacter(); + + List searchCharactersByName(String word); +} 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..eee7e8fd --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java @@ -0,0 +1,82 @@ +package mate.academy.rickandmorty.service.impl; + +import java.util.List; +import java.util.Optional; +import java.util.Random; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.model.RickAndMortyResponse; +import mate.academy.rickandmorty.repository.CharacterRepository; +import mate.academy.rickandmorty.service.CharacterService; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +public class CharacterServiceImpl implements CharacterService { + private final RestTemplate restTemplate; + private final CharacterRepository characterRepository; + private final Random random; + + @Override + public void fetchCharacters() { + String url = "https://rickandmortyapi.com/api/character"; + int page = 1; + + while (true) { + String pageUrl = url + "?page=" + page; + RickAndMortyResponse response; + + try { + response = restTemplate.getForObject(pageUrl, RickAndMortyResponse.class); + } catch (HttpClientErrorException e) { + if (e.getStatusCode() == HttpStatus.NOT_FOUND) { + break; + } + throw e; + } + + if (response == null || response.getResults() == null + || response.getResults().isEmpty()) { + break; + } + + List characters = response.getResults() + .stream() + .map(result -> { + Character character = new Character(); + character.setExternalId(result.getId()); + + if (result.getName() != null) { + character.setName(result.getName()); + } else { + throw new IllegalArgumentException("Field name is missing"); + } + + character.setGender(result.getGender()); + character.setStatus(result.getStatus()); + return character; + }).collect(Collectors.toList()); + + characterRepository.saveAll(characters); + + page++; + } + } + + @Override + public Optional getRandomCharacter() { + int randomId = random.nextInt(1, (int) characterRepository.count()); + return characterRepository.findById(Long.valueOf(randomId)); + } + + @Override + public List searchCharactersByName(String word) { + return characterRepository.findAll().stream() + .filter(ch -> ch.getName().toLowerCase().contains(word.toLowerCase())) + .collect(Collectors.toList()); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..986c9ed9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.h2.console.enabled=true diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bc2fdde8..6239a984 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,3 +3,4 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.h2.console.enabled=true From 4e09d092d88e0c9a7ef1614ed0b1b0048d63eb29 Mon Sep 17 00:00:00 2001 From: Piotr Kaliszuk Date: Sun, 27 Oct 2024 16:51:19 +0100 Subject: [PATCH 2/2] fixes --- .../controller/CharacterController.java | 9 +++--- .../rickandmorty/dto/CharacterListDto.java | 14 ++++++++++ .../dto/CharacterResponseDto.java | 10 +++++++ ...ndMorty.java => CharacterApiResponse.java} | 2 +- .../model/RickAndMortyResponse.java | 13 --------- .../service/CharacterService.java | 7 ++--- .../service/impl/CharacterServiceImpl.java | 28 ++++++++++++++----- 7 files changed, 53 insertions(+), 30 deletions(-) create mode 100644 src/main/java/mate/academy/rickandmorty/dto/CharacterListDto.java create mode 100644 src/main/java/mate/academy/rickandmorty/dto/CharacterResponseDto.java rename src/main/java/mate/academy/rickandmorty/model/{RickAndMorty.java => CharacterApiResponse.java} (91%) delete mode 100644 src/main/java/mate/academy/rickandmorty/model/RickAndMortyResponse.java diff --git a/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java index 6b92fad6..2e74b230 100644 --- a/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -3,9 +3,8 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; -import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.dto.CharacterResponseDto; import mate.academy.rickandmorty.service.CharacterService; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -22,14 +21,14 @@ public class CharacterController { @GetMapping("/random") @Operation(summary = "Get a random character", description = "Returns a random character from the database.") - public Optional getRandomCharacter() { + public CharacterResponseDto getRandomCharacter() { return characterService.getRandomCharacter(); } - @GetMapping("/search") + @GetMapping @Operation(summary = "Search characters by name", description = "Returns a list of characters whose name contains the specified word.") - public List searchCharactersByName(@RequestParam String word) { + public List searchCharactersByName(@RequestParam String word) { return characterService.searchCharactersByName(word); } } diff --git a/src/main/java/mate/academy/rickandmorty/dto/CharacterListDto.java b/src/main/java/mate/academy/rickandmorty/dto/CharacterListDto.java new file mode 100644 index 00000000..13d317b5 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/CharacterListDto.java @@ -0,0 +1,14 @@ +package mate.academy.rickandmorty.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Getter; +import lombok.Setter; +import mate.academy.rickandmorty.model.CharacterApiResponse; + +@Getter +@Setter +public class CharacterListDto { + @JsonProperty("results") + private List results; +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/CharacterResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/CharacterResponseDto.java new file mode 100644 index 00000000..29aab06c --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/CharacterResponseDto.java @@ -0,0 +1,10 @@ +package mate.academy.rickandmorty.dto; + +public record CharacterResponseDto( + Long id, + int externalId, + String name, + String status, + String gender +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/model/RickAndMorty.java b/src/main/java/mate/academy/rickandmorty/model/CharacterApiResponse.java similarity index 91% rename from src/main/java/mate/academy/rickandmorty/model/RickAndMorty.java rename to src/main/java/mate/academy/rickandmorty/model/CharacterApiResponse.java index 6de0e567..a8c0740b 100644 --- a/src/main/java/mate/academy/rickandmorty/model/RickAndMorty.java +++ b/src/main/java/mate/academy/rickandmorty/model/CharacterApiResponse.java @@ -6,7 +6,7 @@ @Getter @Setter -public class RickAndMorty { +public class CharacterApiResponse { @JsonProperty("id") private int id; @JsonProperty("name") diff --git a/src/main/java/mate/academy/rickandmorty/model/RickAndMortyResponse.java b/src/main/java/mate/academy/rickandmorty/model/RickAndMortyResponse.java deleted file mode 100644 index 52337c3a..00000000 --- a/src/main/java/mate/academy/rickandmorty/model/RickAndMortyResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package mate.academy.rickandmorty.model; - -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class RickAndMortyResponse { - @JsonProperty("results") - private List results; -} diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterService.java b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java index f534df6a..c26b9ebc 100644 --- a/src/main/java/mate/academy/rickandmorty/service/CharacterService.java +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java @@ -1,15 +1,14 @@ package mate.academy.rickandmorty.service; import java.util.List; -import java.util.Optional; -import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.dto.CharacterResponseDto; import org.springframework.stereotype.Service; @Service public interface CharacterService { void fetchCharacters(); - Optional getRandomCharacter(); + CharacterResponseDto getRandomCharacter(); - List searchCharactersByName(String word); + List searchCharactersByName(String word); } diff --git a/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java index eee7e8fd..50fb28d0 100644 --- a/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java @@ -1,12 +1,12 @@ package mate.academy.rickandmorty.service.impl; import java.util.List; -import java.util.Optional; import java.util.Random; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.CharacterListDto; +import mate.academy.rickandmorty.dto.CharacterResponseDto; import mate.academy.rickandmorty.model.Character; -import mate.academy.rickandmorty.model.RickAndMortyResponse; import mate.academy.rickandmorty.repository.CharacterRepository; import mate.academy.rickandmorty.service.CharacterService; import org.springframework.http.HttpStatus; @@ -28,10 +28,10 @@ public void fetchCharacters() { while (true) { String pageUrl = url + "?page=" + page; - RickAndMortyResponse response; + CharacterListDto response; try { - response = restTemplate.getForObject(pageUrl, RickAndMortyResponse.class); + response = restTemplate.getForObject(pageUrl, CharacterListDto.class); } catch (HttpClientErrorException e) { if (e.getStatusCode() == HttpStatus.NOT_FOUND) { break; @@ -68,15 +68,29 @@ public void fetchCharacters() { } @Override - public Optional getRandomCharacter() { + public CharacterResponseDto getRandomCharacter() { int randomId = random.nextInt(1, (int) characterRepository.count()); - return characterRepository.findById(Long.valueOf(randomId)); + Character character = characterRepository + .findById(Long.valueOf(randomId)) + .orElseThrow(() -> new RuntimeException("Character not found")); + return toDto(character); } @Override - public List searchCharactersByName(String word) { + public List searchCharactersByName(String word) { return characterRepository.findAll().stream() .filter(ch -> ch.getName().toLowerCase().contains(word.toLowerCase())) + .map(this::toDto) .collect(Collectors.toList()); } + + public CharacterResponseDto toDto(Character character) { + return new CharacterResponseDto( + character.getId(), + character.getExternalId(), + character.getName(), + character.getStatus(), + character.getGender() + ); + } }