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..2e74b230 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -0,0 +1,34 @@ +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.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 +@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 CharacterResponseDto getRandomCharacter() { + return characterService.getRandomCharacter(); + } + + @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) { + 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/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/CharacterApiResponse.java b/src/main/java/mate/academy/rickandmorty/model/CharacterApiResponse.java new file mode 100644 index 00000000..a8c0740b --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/CharacterApiResponse.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 CharacterApiResponse { + @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/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..c26b9ebc --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java @@ -0,0 +1,14 @@ +package mate.academy.rickandmorty.service; + +import java.util.List; +import mate.academy.rickandmorty.dto.CharacterResponseDto; +import org.springframework.stereotype.Service; + +@Service +public interface CharacterService { + void fetchCharacters(); + + CharacterResponseDto 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..50fb28d0 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java @@ -0,0 +1,96 @@ +package mate.academy.rickandmorty.service.impl; + +import java.util.List; +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.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; + CharacterListDto response; + + try { + response = restTemplate.getForObject(pageUrl, CharacterListDto.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 CharacterResponseDto getRandomCharacter() { + int randomId = random.nextInt(1, (int) characterRepository.count()); + Character character = characterRepository + .findById(Long.valueOf(randomId)) + .orElseThrow(() -> new RuntimeException("Character not found")); + return toDto(character); + } + + @Override + 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() + ); + } +} 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