diff --git a/pom.xml b/pom.xml index 0c754f19..1eba7fff 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,57 @@ com.h2database h2 + + + mysql + mysql-connector-java + 8.0.33 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + + org.mapstruct + mapstruct + 1.5.5.Final + + + + mysql + mysql-connector-java + 8.0.33 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + + + + org.mapstruct + mapstruct + 1.5.5.Final + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + + @@ -66,6 +117,32 @@ true true false + src + + + + 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 + 1.5.5.Final + + 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..7d9fdd4f --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -0,0 +1,33 @@ +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.CharacterInternalResponseDto; +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 character management", + description = "Endpoints for managing Rick and Morty characters") +@RestController +@RequiredArgsConstructor +@RequestMapping("/characters") +public class CharacterController { + private final CharacterService characterService; + + @GetMapping("/by-name") + @Operation(summary = "Get a character by name", description = "Get a character by name") + public List getCharactersByName(@RequestParam String name) { + return characterService.getCharactersByName(name); + } + + @GetMapping("/random") + @Operation(summary = "Get a random character", description = "Get a random character") + public CharacterInternalResponseDto getRandomCharacter() { + return characterService.getRandomCharacter(); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterExternalResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterExternalResponseDto.java new file mode 100644 index 00000000..6ba4a71c --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterExternalResponseDto.java @@ -0,0 +1,19 @@ +package mate.academy.rickandmorty.dto.external; + +import java.time.LocalDateTime; +import java.util.ArrayList; + +public record CharacterExternalResponseDto( + Long id, + String name, + String status, + String species, + String type, + String gender, + OriginResponseDto origin, + OriginResponseDto location, + String image, + ArrayList episode, + String url, + LocalDateTime created) { +} 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..c7901706 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDataDto.java @@ -0,0 +1,8 @@ +package mate.academy.rickandmorty.dto.external; + +import java.util.List; + +public record CharacterResponseDataDto( + List results, + CharactersInfoDto info) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharactersInfoDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharactersInfoDto.java new file mode 100644 index 00000000..9ef6db91 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharactersInfoDto.java @@ -0,0 +1,4 @@ +package mate.academy.rickandmorty.dto.external; + +public record CharactersInfoDto(String next) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/OriginResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/OriginResponseDto.java new file mode 100644 index 00000000..d2a39fa7 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/OriginResponseDto.java @@ -0,0 +1,6 @@ +package mate.academy.rickandmorty.dto.external; + +public record OriginResponseDto( + String name, + String url) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterInternalResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterInternalResponseDto.java new file mode 100644 index 00000000..dc296809 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterInternalResponseDto.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto.internal; + +public record CharacterInternalResponseDto( + 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..327c64a1 --- /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.CharacterExternalResponseDto; +import mate.academy.rickandmorty.dto.internal.CharacterInternalResponseDto; +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 = "id") + Character toModel(CharacterExternalResponseDto characterExternalResponseDto); + + CharacterInternalResponseDto 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..71be2a3c --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/Character.java @@ -0,0 +1,21 @@ +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 lombok.Data; + +@Entity +@Data +@Table(name = "characters") +public class Character { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private Long 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..6a435cff --- /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 findAllByNameContainingIgnoreCase(String name); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterInit.java b/src/main/java/mate/academy/rickandmorty/service/CharacterInit.java new file mode 100644 index 00000000..ad056445 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterInit.java @@ -0,0 +1,20 @@ +package mate.academy.rickandmorty.service; + +import jakarta.annotation.PostConstruct; +import java.util.List; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.external.CharacterExternalResponseDto; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CharacterInit { + private final RickAndMortyClient client; + private final CharacterService characterService; + + @PostConstruct + public void initCharacters() { + List characters = client.getCharacters(); + characterService.saveAll(characters); + } +} 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..1fcfe0f7 --- /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.CharacterExternalResponseDto; +import mate.academy.rickandmorty.dto.internal.CharacterInternalResponseDto; + +public interface CharacterService { + CharacterInternalResponseDto getRandomCharacter(); + + List getCharactersByName(String name); + + void saveAll(List characterExternalResponseDtoList); +} 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..351b065d --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java @@ -0,0 +1,46 @@ +package mate.academy.rickandmorty.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +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.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.external.CharacterExternalResponseDto; +import mate.academy.rickandmorty.dto.external.CharacterResponseDataDto; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RickAndMortyClient { + public static final String BASE_URL = "https://rickandmortyapi.com/api/character"; + private final ObjectMapper objectMapper; + + public List getCharacters() { + List allCharacters = new ArrayList<>(); + HttpClient httpClient = HttpClient.newHttpClient(); + String url = BASE_URL; + while (url != null) { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .build(); + try { + HttpResponse response = httpClient.send( + request, HttpResponse.BodyHandlers.ofString()); + + CharacterResponseDataDto characterResponseDataDto = objectMapper.readValue( + response.body(), CharacterResponseDataDto.class); + + allCharacters.addAll(characterResponseDataDto.results()); + url = characterResponseDataDto.info().next(); + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Can't load characters from given URL"); + } + } + return allCharacters; + } +} 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..b3c4936e --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java @@ -0,0 +1,46 @@ +package mate.academy.rickandmorty.service.impl; + +import jakarta.persistence.EntityNotFoundException; +import java.util.List; +import java.util.Random; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.external.CharacterExternalResponseDto; +import mate.academy.rickandmorty.dto.internal.CharacterInternalResponseDto; +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 org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CharacterServiceImpl implements CharacterService { + private final CharacterMapper characterMapper; + private final CharacterRepository characterRepository; + private final Random random = new Random(); + + @Override + public CharacterInternalResponseDto getRandomCharacter() { + long randomId = random.nextLong(characterRepository.count()); + Character character = characterRepository.findById(randomId) + .orElseThrow(() -> + new EntityNotFoundException("Cannot find a random character by id: " + + randomId)); + return characterMapper.toDto(character); + } + + @Override + public List getCharactersByName(String name) { + return characterRepository.findAllByNameContainingIgnoreCase(name).stream() + .map(characterMapper::toDto) + .toList(); + } + + @Override + public void saveAll(List characterExternalResponseDtoList) { + List characters = characterExternalResponseDtoList.stream() + .map(characterMapper::toModel) + .toList(); + characterRepository.saveAll(characters); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..a2dbb89f 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,7 @@ - +spring.datasource.url=jdbc:mysql://localhost:3306/rick_and_morty?serverTimeZone=UTC +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.username=root +spring.datasource.password=root +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.open-in-view=false diff --git a/src/test/java/mate/academy/rickandmorty/ApplicationTests.java b/src/test/java/mate/academy/rickandmorty/ApplicationTests.java index 8fec6af0..09b17308 100644 --- a/src/test/java/mate/academy/rickandmorty/ApplicationTests.java +++ b/src/test/java/mate/academy/rickandmorty/ApplicationTests.java @@ -6,8 +6,8 @@ @SpringBootTest class ApplicationTests { - @Test - void contextLoads() { - } + @Test + void contextLoads() { + } }