diff --git a/pom.xml b/pom.xml index 0c754f19..528764e4 100644 --- a/pom.xml +++ b/pom.xml @@ -19,6 +19,8 @@ https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml + 0.2.0 + 1.5.5.Final @@ -37,14 +39,71 @@ spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-web + + com.h2database h2 + + + mysql + mysql-connector-java + 8.0.33 + + + + org.projectlombok + lombok + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.liquibase + liquibase-core + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok.mapstruct.binding.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + org.springframework.boot spring-boot-maven-plugin @@ -68,6 +127,29 @@ false + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + + + compile + + check + + + + + ${maven.checkstyle.plugin.configLocation} + UTF-8 + UTF-8 + true + true + false + src + + + diff --git a/src/main/java/mate/academy/rickandmorty/api/util/ApiClientConfiguration.java b/src/main/java/mate/academy/rickandmorty/api/util/ApiClientConfiguration.java new file mode 100644 index 00000000..0efc95b3 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/api/util/ApiClientConfiguration.java @@ -0,0 +1,13 @@ +package mate.academy.rickandmorty.api.util; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class ApiClientConfiguration { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/api/util/PersonageDatabasePopulator.java b/src/main/java/mate/academy/rickandmorty/api/util/PersonageDatabasePopulator.java new file mode 100644 index 00000000..2a1f2022 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/api/util/PersonageDatabasePopulator.java @@ -0,0 +1,22 @@ +package mate.academy.rickandmorty.api.util; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.repository.PersonageRepository; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PersonageDatabasePopulator { + private final RickAndMortyClient rickAndMortyApiClient; + private final PersonageRepository personageRepository; + + @PostConstruct + public void init() { + if (personageRepository.count() != 0) { + personageRepository.deleteAll(); + } + personageRepository.saveAll(rickAndMortyApiClient.getAllPersonages()); + } + +} diff --git a/src/main/java/mate/academy/rickandmorty/api/util/RickAndMortyClient.java b/src/main/java/mate/academy/rickandmorty/api/util/RickAndMortyClient.java new file mode 100644 index 00000000..ef5d0b9e --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/api/util/RickAndMortyClient.java @@ -0,0 +1,39 @@ +package mate.academy.rickandmorty.api.util; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.external.ExternalResponseDto; +import mate.academy.rickandmorty.dto.external.Result; +import mate.academy.rickandmorty.mapper.PersonageMapper; +import mate.academy.rickandmorty.model.Personage; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +@RequiredArgsConstructor +public class RickAndMortyClient { + private final PersonageMapper personageMapper; + private final RestTemplate restTemplate; + @Value("${rick-and-morty.api.url}") + private String baseCharacterUrl; + + public List getAllPersonages() { + List resultList = new ArrayList<>(); + ExternalResponseDto externalResponseDto = + restTemplate.getForEntity(baseCharacterUrl, ExternalResponseDto.class).getBody(); + String nextUrl = externalResponseDto.info().next(); + while (nextUrl != null) { + resultList.addAll(externalResponseDto.resultList()); + externalResponseDto = restTemplate + .getForEntity(nextUrl, ExternalResponseDto.class) + .getBody(); + nextUrl = externalResponseDto.info().next(); + } + return resultList + .stream() + .map(personageMapper::toPersonage) + .toList(); + } +} 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..8a8ab40f --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/RickAndMortyController.java @@ -0,0 +1,41 @@ +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.PersonageResponseDto; +import mate.academy.rickandmorty.service.PersonageService; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +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 +@Tag(name = "Controller to get personages", + description = "In this controller you can get random personages and personages by name") +@RequiredArgsConstructor +@RequestMapping("/rick-and-morty") +public class RickAndMortyController { + private final PersonageService personageRepository; + + @GetMapping("/random-personage") + @Operation(summary = "Get random personage", + description = "Get random personage from Rick and Morty universe") + public PersonageResponseDto getRandomPersonage() { + return personageRepository.getRandomPersonage(); + } + + @GetMapping("/search") + @Operation(summary = "Get personages by name", + description = "Get from Rick and Morty universe " + + "personages witch has name in their name") + public List getPersonageByName( + @PageableDefault(page = 0, size = 10, sort = "name") Pageable pageable, + @RequestParam String name + ) { + return personageRepository.getPersonageByNameLike(pageable, name); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/ExternalResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/ExternalResponseDto.java new file mode 100644 index 00000000..4ef58a0f --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/ExternalResponseDto.java @@ -0,0 +1,10 @@ +package mate.academy.rickandmorty.dto.external; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public record ExternalResponseDto( + Info info, + @JsonProperty("results") List resultList +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/Info.java b/src/main/java/mate/academy/rickandmorty/dto/external/Info.java new file mode 100644 index 00000000..07658d24 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/Info.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto.external; + +public record Info( + Long count, + Integer pages, + String next, + String prev +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/Result.java b/src/main/java/mate/academy/rickandmorty/dto/external/Result.java new file mode 100644 index 00000000..e5a3ff1c --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/Result.java @@ -0,0 +1,11 @@ +package mate.academy.rickandmorty.dto.external; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record Result( + @JsonProperty("id") Long externalId, + String name, + String status, + String gender +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/internal/PersonageResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/internal/PersonageResponseDto.java new file mode 100644 index 00000000..783db6a7 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/internal/PersonageResponseDto.java @@ -0,0 +1,8 @@ +package mate.academy.rickandmorty.dto.internal; + +public record PersonageResponseDto(Long id, + Long externalId, + String name, + String status, + String gender) { +} diff --git a/src/main/java/mate/academy/rickandmorty/excpetion/CustomGlobalExceptionHandler.java b/src/main/java/mate/academy/rickandmorty/excpetion/CustomGlobalExceptionHandler.java new file mode 100644 index 00000000..b5ec28a6 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/excpetion/CustomGlobalExceptionHandler.java @@ -0,0 +1,16 @@ +package mate.academy.rickandmorty.excpetion; + +import jakarta.persistence.EntityNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@ControllerAdvice +public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler { + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity handleEntityNotFound(EntityNotFoundException ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/mapper/PersonageMapper.java b/src/main/java/mate/academy/rickandmorty/mapper/PersonageMapper.java new file mode 100644 index 00000000..6c0215b5 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/mapper/PersonageMapper.java @@ -0,0 +1,18 @@ +package mate.academy.rickandmorty.mapper; + +import mate.academy.rickandmorty.dto.external.Result; +import mate.academy.rickandmorty.dto.internal.PersonageResponseDto; +import mate.academy.rickandmorty.model.Personage; +import org.mapstruct.InjectionStrategy; +import org.mapstruct.Mapper; +import org.mapstruct.NullValueCheckStrategy; + +@Mapper(componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + implementationPackage = ".impl") +public interface PersonageMapper { + PersonageResponseDto toDto(Personage personage); + + Personage toPersonage(Result result); +} diff --git a/src/main/java/mate/academy/rickandmorty/model/Personage.java b/src/main/java/mate/academy/rickandmorty/model/Personage.java new file mode 100644 index 00000000..6b7e11c7 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/Personage.java @@ -0,0 +1,29 @@ +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 jakarta.persistence.Table; +import lombok.Data; + +@Entity +@Data +@Table(name = "personages") +public class Personage { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "external_id", nullable = false) + private Long externalId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String status; + + private String gender; +} diff --git a/src/main/java/mate/academy/rickandmorty/repository/PersonageRepository.java b/src/main/java/mate/academy/rickandmorty/repository/PersonageRepository.java new file mode 100644 index 00000000..98c665a4 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/repository/PersonageRepository.java @@ -0,0 +1,17 @@ +package mate.academy.rickandmorty.repository; + +import java.util.List; +import java.util.Optional; +import mate.academy.rickandmorty.model.Personage; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface PersonageRepository extends JpaRepository { + List findAllByNameContains(Pageable pageable, String name); + + @Query("from Personage p order by RAND() limit 1") + Optional findRandomPersonage(); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/PersonageService.java b/src/main/java/mate/academy/rickandmorty/service/PersonageService.java new file mode 100644 index 00000000..c7bdc7dd --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/PersonageService.java @@ -0,0 +1,11 @@ +package mate.academy.rickandmorty.service; + +import java.util.List; +import mate.academy.rickandmorty.dto.internal.PersonageResponseDto; +import org.springframework.data.domain.Pageable; + +public interface PersonageService { + List getPersonageByNameLike(Pageable pageable, String name); + + PersonageResponseDto getRandomPersonage(); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/impl/PersonageServiceImpl.java b/src/main/java/mate/academy/rickandmorty/service/impl/PersonageServiceImpl.java new file mode 100644 index 00000000..dd0458d7 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/impl/PersonageServiceImpl.java @@ -0,0 +1,34 @@ +package mate.academy.rickandmorty.service.impl; + +import jakarta.persistence.EntityNotFoundException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.internal.PersonageResponseDto; +import mate.academy.rickandmorty.mapper.PersonageMapper; +import mate.academy.rickandmorty.model.Personage; +import mate.academy.rickandmorty.repository.PersonageRepository; +import mate.academy.rickandmorty.service.PersonageService; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PersonageServiceImpl implements PersonageService { + private final PersonageRepository personageRepository; + private final PersonageMapper personageMapper; + + @Override + public List getPersonageByNameLike(Pageable pageable, String name) { + return personageRepository.findAllByNameContains(pageable, name) + .stream() + .map(personageMapper::toDto) + .toList(); + } + + @Override + public PersonageResponseDto getRandomPersonage() { + Personage randomPersonage = personageRepository.findRandomPersonage() + .orElseThrow(() -> new EntityNotFoundException("Can't get random personage")); + return personageMapper.toDto(randomPersonage); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..b03c6bf3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,11 @@ +spring.datasource.url=jdbc:mysql://localhost:3306/rick_and_morty +spring.datasource.username=root +spring.datasource.password=1234 +spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.show-sql=true + +spring.jpa.open-in-view=false + +rick-and-morty.api.url=https://rickandmortyapi.com/api/character diff --git a/src/main/resources/db/changelog/changes/01-create-characters-table.yaml b/src/main/resources/db/changelog/changes/01-create-characters-table.yaml new file mode 100644 index 00000000..6f566b42 --- /dev/null +++ b/src/main/resources/db/changelog/changes/01-create-characters-table.yaml @@ -0,0 +1,33 @@ +databaseChangeLog: + - changeSet: + id: create-characters-table + author: teract10s + changes: + - createTable: + tableName: characters + columns: + - column: + name: id + type: bigint + autoIncrement: true + constraints: + primaryKey: true + nullable: false + - column: + name: external_id + type: bigint + constraints: + nullable: false + - column: + name: name + type: varchar(255) + constraints: + nullable: false + - column: + name: status + type: varchar(255) + constraints: + nullable: false + - column: + name: gender + type: varchar(255) diff --git a/src/main/resources/db/changelog/changes/02-rename-characters-to-personages.yaml b/src/main/resources/db/changelog/changes/02-rename-characters-to-personages.yaml new file mode 100644 index 00000000..d3ae94a2 --- /dev/null +++ b/src/main/resources/db/changelog/changes/02-rename-characters-to-personages.yaml @@ -0,0 +1,8 @@ +databaseChangeLog: + - changeSet: + id: rename-characters-to-personages + author: teract10s + changes: + - renameTable: + oldTableName: characters + newTableName: personages diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 00000000..16f714e8 --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,5 @@ +databaseChangeLog: + - include: + file: db/changelog/changes/01-create-characters-table.yaml + - include: + file: db/changelog/changes/02-rename-characters-to-personages.yaml diff --git a/src/main/resources/liquibase.properties b/src/main/resources/liquibase.properties new file mode 100644 index 00000000..1187c950 --- /dev/null +++ b/src/main/resources/liquibase.properties @@ -0,0 +1,5 @@ +url=jdbc:mysql://localhost/rick_and_morty?serverTimezone=UTC +username=root +password=1234 +changeLogFile=db/changelog/db.changelog-master.yaml +driver=com.mysql.cj.jdbc.Driver diff --git a/src/test/java/mate/academy/rickandmorty/ApplicationTests.java b/src/test/java/mate/academy/rickandmorty/ApplicationTests.java deleted file mode 100644 index 8fec6af0..00000000 --- a/src/test/java/mate/academy/rickandmorty/ApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package mate.academy.rickandmorty; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ApplicationTests { - - @Test - void contextLoads() { - } - -}