diff --git a/pom.xml b/pom.xml index 0c754f19..7f8fadfc 100644 --- a/pom.xml +++ b/pom.xml @@ -37,10 +37,49 @@ spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-web + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + + com.h2database h2 + + + org.projectlombok + lombok + + + + org.mapstruct + mapstruct + 1.5.5.Final + + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + + + + mysql + mysql-connector-java + 8.0.33 + + + + com.fasterxml.jackson.core + jackson-databind + @@ -55,7 +94,7 @@ 3.3.0 - compile + verify check diff --git a/src/main/java/mate/academy/rickandmorty/config/ApiConfig.java b/src/main/java/mate/academy/rickandmorty/config/ApiConfig.java new file mode 100644 index 00000000..19ec1826 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/config/ApiConfig.java @@ -0,0 +1,26 @@ +package mate.academy.rickandmorty.config; + +import java.net.http.HttpClient; +import java.util.Random; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ApiConfig { + @Bean + Random buildRandom() { + return new Random(); + } + + @Bean + ObjectMapper buildObjectMapper() { + + return new ObjectMapper(); + } + + @Bean + HttpClient buildHttpClient() { + return HttpClient.newHttpClient(); + } +} 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..b049b596 --- /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 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; + +@Tag(name = "Rick and Morty universe", + description = "Endpoints for managing Rick and Morty universe characters") +@RequiredArgsConstructor +@RestController +@RequestMapping("/characters") +public class CharacterController { + private final CharacterService characterService; + + @GetMapping("/random") + @Operation(summary = "Get the random character", + description = "Get the random character") + public CharacterResponseDto getRandomCharacter() { + return characterService.getRandomCharacter(); + } + + @GetMapping() + @Operation(summary = "Find the character by name", + description = "Find the character by name") + public List searchCharactersByName(@RequestParam String name) { + return characterService.findAllByName(name); + } +} \ No newline at end of file diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDataDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDataDto.java new file mode 100644 index 00000000..1619c515 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDataDto.java @@ -0,0 +1,7 @@ +package mate.academy.rickandmorty.dto.external; + +import java.util.List; + +public record CharacterResponseDataDto(CharacterResponseMetaDataDto info, + List results) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseMetaDataDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseMetaDataDto.java new file mode 100644 index 00000000..1ec569d6 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseMetaDataDto.java @@ -0,0 +1,8 @@ +package mate.academy.rickandmorty.dto.external; + +public record CharacterResponseMetaDataDto(Integer count, + Integer pages, + String next, + String prev +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/ThirdApiCharacterResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/ThirdApiCharacterResponseDto.java new file mode 100644 index 00000000..92ad7145 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/ThirdApiCharacterResponseDto.java @@ -0,0 +1,18 @@ +package mate.academy.rickandmorty.dto.external; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class ThirdApiCharacterResponseDto { + @JsonProperty("id") + private String externalId; + private String name; + private String status; + private String species; + private String type; + private String gender; + +} 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..83ca93d1 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterResponseDto.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto.internal; + +public record CharacterResponseDto(Long id, + String externalId, + String name, + String status, + String gender +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/exception/DataNotFoundException.java b/src/main/java/mate/academy/rickandmorty/exception/DataNotFoundException.java new file mode 100644 index 00000000..fab22e68 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/exception/DataNotFoundException.java @@ -0,0 +1,7 @@ +package mate.academy.rickandmorty.exception; + +public class DataNotFoundException extends RuntimeException { + public DataNotFoundException(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..73284e15 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java @@ -0,0 +1,14 @@ +package mate.academy.rickandmorty.mapper; + +import mate.academy.rickandmorty.config.MapperConfig; +import mate.academy.rickandmorty.dto.external.ThirdApiCharacterResponseDto; +import mate.academy.rickandmorty.dto.internal.CharacterResponseDto; +import mate.academy.rickandmorty.model.Character; +import org.mapstruct.Mapper; + +@Mapper(config = MapperConfig.class) +public interface CharacterMapper { + CharacterResponseDto toDto(Character character); + + Character toCharacter(ThirdApiCharacterResponseDto thirdApiCharacterResponseDto); +} 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..e64c02d9 --- /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 lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity(name = "characters") +public class Character { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(name = "external_id") + 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..dc5399f8 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.repository; + +import java.util.List; +import mate.academy.rickandmorty.model.Character; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CharacterRepository extends JpaRepository { + List findAllByNameContainingIgnoreCase(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..26d4a04f --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java @@ -0,0 +1,10 @@ +package mate.academy.rickandmorty.service; + +import java.util.List; +import mate.academy.rickandmorty.dto.internal.CharacterResponseDto; + +public interface CharacterService { + CharacterResponseDto getRandomCharacter(); + + List findAllByName(String name); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java b/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java new file mode 100644 index 00000000..99f9482d --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java @@ -0,0 +1,49 @@ +package mate.academy.rickandmorty.service; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.external.CharacterResponseDataDto; +import mate.academy.rickandmorty.mapper.CharacterMapper; +import mate.academy.rickandmorty.repository.CharacterRepository; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RickAndMortyClient { + private static final String URI_BASE = "https://rickandmortyapi.com/api/character"; + + private final CharacterRepository characterRepository; + private final CharacterMapper characterMapper; + private final ObjectMapper objectMapper; + private final HttpClient httpClient; + + public void loadDataToDb() { + String page = URI_BASE; + while (page != null) { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(page)) + .build(); + + try { + HttpResponse response = httpClient.send( + request, + HttpResponse.BodyHandlers.ofString()); + CharacterResponseDataDto characterResponseDataDto = objectMapper.readValue( + response.body(), + CharacterResponseDataDto.class); + characterResponseDataDto.results().stream() + .map(characterMapper::toCharacter) + .forEach(characterRepository::save); + page = characterResponseDataDto.info().next(); + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Can't get data from API", e); + } + } + } +} 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..f0151f98 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java @@ -0,0 +1,45 @@ +package mate.academy.rickandmorty.service.impl; + +import java.util.List; +import java.util.Random; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.internal.CharacterResponseDto; +import mate.academy.rickandmorty.exception.DataNotFoundException; +import mate.academy.rickandmorty.mapper.CharacterMapper; +import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.repository.CharacterRepository; +import mate.academy.rickandmorty.service.CharacterService; +import mate.academy.rickandmorty.service.RickAndMortyClient; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CharacterServiceImpl implements CharacterService { + private final CharacterRepository characterRepository; + private final CharacterMapper characterMapper; + private final Random random; + private final RickAndMortyClient rickAndMortyClient; + + @PostConstruct + public void loadDataToDb() { + rickAndMortyClient.loadDataToDb(); + } + + @Override + public CharacterResponseDto getRandomCharacter() { + long count = characterRepository.count(); + Character character = characterRepository.findById(random.nextLong(count)) + .orElseThrow( + () -> new DataNotFoundException("Couldn't find data") + ); + return characterMapper.toDto(character); + } + + @Override + public List findAllByName(String name) { + return characterRepository.findAllByNameContainingIgnoreCase(name).stream() + .map(characterMapper::toDto) + .toList(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..9777b53e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,9 @@ +spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimeZone=UTC +spring.datasource.username=root +spring.datasource.password=12345678 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true + +server.servlet.context-path=/api