diff --git a/pom.xml b/pom.xml index 0c754f19..319ad9db 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,8 @@ https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml + 1.5.5.Final + 0.2.0 @@ -41,6 +43,36 @@ com.h2database h2 + + + mysql + mysql-connector-java + 8.0.32 + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-web + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + + @@ -66,6 +98,33 @@ true true false + src/main + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok.mapstruck.binding.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + 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/RickAndMortyController.java b/src/main/java/mate/academy/rickandmorty/controller/RickAndMortyController.java new file mode 100644 index 00000000..9130b318 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/RickAndMortyController.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.CharacterDto; +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 = "Character management", description = "Endpoints for managing characters") +@RequiredArgsConstructor +@RestController +@RequestMapping("/characters") +public class RickAndMortyController { + private final CharacterService characterService; + + @GetMapping("/random") + @Operation(summary = "Get random character", description = "Returns a random character") + public CharacterDto getRandomCharacter() { + return characterService.getRandomCharacter(); + } + + @GetMapping + @Operation(summary = "Get characters by name", + description = "Returns a list of all characters with the given name") + public List getCharactersByName(@RequestParam String name) { + return characterService.getCharactersByName(name); + } +} 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..1d47e120 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDataDto.java @@ -0,0 +1,20 @@ +package mate.academy.rickandmorty.dto.external; + +import java.time.LocalDateTime; +import java.util.List; + +public record CharacterResponseDataDto( + int id, + String name, + String status, + String species, + String type, + String gender, + OriginResponseDto origin, + OriginResponseDto location, + String image, + List episode, + String url, + LocalDateTime created +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/InfoResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/InfoResponseDto.java new file mode 100644 index 00000000..354776bd --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/InfoResponseDto.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto.external; + +public record InfoResponseDto( + int count, + int pages, + String next, + String prev +) { +} 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..7d80f7b6 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/OriginResponseDto.java @@ -0,0 +1,7 @@ +package mate.academy.rickandmorty.dto.external; + +public record OriginResponseDto( + String name, + String url +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/ResultsResponseDataDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/ResultsResponseDataDto.java new file mode 100644 index 00000000..4c127c57 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/ResultsResponseDataDto.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto.external; + +import java.util.List; + +public record ResultsResponseDataDto( + List results, + InfoResponseDto infoResponseDto +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDto.java b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDto.java new file mode 100644 index 00000000..36f85ec9 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDto.java @@ -0,0 +1,12 @@ +package mate.academy.rickandmorty.dto.internal; + +import lombok.Data; + +@Data +public class CharacterDto { + private Long id; + private Long externalId; + private String name; + private String status; + private 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..f7121bb8 --- /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.CharacterResponseDataDto; +import mate.academy.rickandmorty.dto.internal.CharacterDto; +import mate.academy.rickandmorty.model.Character; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper(config = MapperConfig.class) +public interface CharacterMapper { + CharacterDto toDto(Character character); + + @Mapping(target = "externalId", source = "id") + Character toModel(CharacterResponseDataDto responseDataDto); +} 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..aa8db69d --- /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; + +@Data +@Entity +@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..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..ba606884 --- /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.CharacterDto; + +public interface CharacterService { + CharacterDto getRandomCharacter(); + + List getCharactersByName(String name); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/CharactersInitializer.java b/src/main/java/mate/academy/rickandmorty/service/CharactersInitializer.java new file mode 100644 index 00000000..23a022b4 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharactersInitializer.java @@ -0,0 +1,25 @@ +package mate.academy.rickandmorty.service; + +import jakarta.annotation.PostConstruct; +import java.util.List; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.mapper.CharacterMapper; +import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.repository.CharacterRepository; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class CharactersInitializer { + private final CharacterRepository characterRepository; + private final RickAndMortyClient rickAndMortyClient; + private final CharacterMapper characterMapper; + + @PostConstruct + public void fetchCharactersToDb() { + List characters = rickAndMortyClient.getCharacters().stream() + .map(characterMapper::toModel) + .toList(); + characterRepository.saveAll(characters); + } +} 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..adb49f8a --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java @@ -0,0 +1,50 @@ +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.CharacterResponseDataDto; +import mate.academy.rickandmorty.dto.external.ResultsResponseDataDto; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class RickAndMortyClient { + private static final String URL = "https://rickandmortyapi.com/api/character"; + private final ObjectMapper objectMapper; + + public List getCharacters() { + List characters = new ArrayList<>(); + HttpClient httpClient = HttpClient.newHttpClient(); + String page = URL; + while (page != null) { + HttpRequest httpRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(page)) + .build(); + try { + HttpResponse response = httpClient.send( + httpRequest, HttpResponse.BodyHandlers.ofString() + ); + ResultsResponseDataDto resultsResponseDataDto = objectMapper + .readValue(response.body(), ResultsResponseDataDto.class); + characters.addAll(resultsResponseDataDto.results()); + if (resultsResponseDataDto.infoResponseDto() != null + && resultsResponseDataDto.infoResponseDto().next() != null) { + page = resultsResponseDataDto.infoResponseDto().next(); + } else { + page = null; + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException("Can't send http request", e); + } + } + return characters; + } +} 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..d2b2d8a4 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java @@ -0,0 +1,37 @@ +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.internal.CharacterDto; +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 CharacterRepository characterRepository; + private final CharacterMapper characterMapper; + private final Random random = new Random(); + + @Override + public CharacterDto getRandomCharacter() { + long randomId = random.nextLong(characterRepository.count()); + Character character = characterRepository.findById(randomId) + .orElseThrow(() -> new EntityNotFoundException( + "Can't get character with id: " + randomId)); + return characterMapper.toDto(character); + } + + @Override + public List getCharactersByName(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..266b9d09 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,8 @@ +spring.datasource.url=jdbc:mysql://localhost:3306/rick_and_morty?serverTimeZone=UTC +spring.datasource.username=root +spring.datasource.password=123456 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.open-in-view=false