diff --git a/pom.xml b/pom.xml index 0c754f19..812515bb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.4 + 3.3.1 mate.academy @@ -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,35 @@ com.h2database h2 + + + org.springframework.boot + spring-boot-starter-web + + + + com.mysql + mysql-connector-j + 8.4.0 + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.projectlombok + lombok + provided + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + @@ -49,6 +80,7 @@ org.springframework.boot spring-boot-maven-plugin + org.apache.maven.plugins maven-checkstyle-plugin @@ -66,6 +98,34 @@ true true false + src + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + diff --git a/src/main/java/mate/academy/rickandmorty/Application.java b/src/main/java/mate/academy/rickandmorty/Application.java index cdea84fc..28161d2a 100644 --- a/src/main/java/mate/academy/rickandmorty/Application.java +++ b/src/main/java/mate/academy/rickandmorty/Application.java @@ -1,12 +1,26 @@ package mate.academy.rickandmorty; +import java.util.List; +import mate.academy.rickandmorty.dto.external.CharacterResponseDto; +import mate.academy.rickandmorty.service.CharacterClient; +import mate.academy.rickandmorty.service.CharacterService; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { + private static CharacterService characterService; + private static CharacterClient characterClient; + + public Application(CharacterService service, CharacterClient client) { + Application.characterService = service; + Application.characterClient = client; + } public static void main(String[] args) { SpringApplication.run(Application.class, args); + + List characters = characterClient.findAllCharacters(); + characterService.saveAllCharacters(characters); } } 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..222fef3f --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -0,0 +1,39 @@ +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.http.HttpStatus; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/characters") +@RequiredArgsConstructor +@Tag(name = "Character management", + description = "Endpoint for getting Rick and Morty characters") +public class CharacterController { + private final CharacterService characterService; + + @GetMapping + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Get characters by names part", + description = "Getting Rick and Morty characters by names part") + public List getByNameContains(@RequestParam String name) { + return characterService.findCharacterByName(name); + } + + @GetMapping("/random") + @ResponseStatus(HttpStatus.OK) + @Operation(summary = "Get random character", + description = "Getting random character from Rick and Morty API") + public CharacterDto getRandomCharacter() { + return characterService.findRandomCharacter(); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDto.java new file mode 100644 index 00000000..c1b1c189 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDto.java @@ -0,0 +1,11 @@ +package mate.academy.rickandmorty.dto.external; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record CharacterResponseDto( + Long id, + String name, + String status, + String gender) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharactersDataResponseDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharactersDataResponseDto.java new file mode 100644 index 00000000..4e149d03 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharactersDataResponseDto.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto.external; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record CharactersDataResponseDto( + List results) { +} 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..ac41ca26 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDto.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto.internal; + +public record CharacterDto( + Long id, + Long externalId, + String name, + String status, + String gender) { +} diff --git a/src/main/java/mate/academy/rickandmorty/exception/DataProcessingException.java b/src/main/java/mate/academy/rickandmorty/exception/DataProcessingException.java new file mode 100644 index 00000000..a6c154de --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/exception/DataProcessingException.java @@ -0,0 +1,7 @@ +package mate.academy.rickandmorty.exception; + +public class DataProcessingException extends RuntimeException { + public DataProcessingException(String message, Throwable cause) { + super(message, cause); + } +} 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..8243fdb8 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java @@ -0,0 +1,22 @@ +package mate.academy.rickandmorty.mapper; + +import java.util.List; +import mate.academy.rickandmorty.config.MapperConfig; +import mate.academy.rickandmorty.dto.external.CharacterResponseDto; +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 { + @Mapping(source = "id", target = "externalId") + @Mapping(target = "id", ignore = true) + Character toModel(CharacterResponseDto responseDto); + + CharacterDto toDto(Character character); + + List toModelList(List responseDtoList); + + List toDtoList(List characterList); +} 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..4eaa90ba --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/Character.java @@ -0,0 +1,38 @@ +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.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +@Entity +@Table(name = "characters") +@SQLDelete(sql = "UPDATE books SET is_deleted = TRUE WHERE id = ?") +@SQLRestriction(value = "is_deleted = FALSE") +@Getter +@Setter +@ToString +@NoArgsConstructor +public class Character { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false, unique = true) + private Long externalId; + @Column(nullable = false) + private String name; + @Column(nullable = false) + private String status; + @Column(nullable = false) + private String gender; + @Column(nullable = false) + private boolean isDeleted = false; +} 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..aa1b886f --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java @@ -0,0 +1,15 @@ +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.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; + +public interface CharacterRepository extends JpaRepository, + JpaSpecificationExecutor { + List findByNameContaining(String name); + + @Query("FROM Character ORDER BY RAND() LIMIT 1") + Character getRandomCharacter(); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterClient.java b/src/main/java/mate/academy/rickandmorty/service/CharacterClient.java new file mode 100644 index 00000000..461a0d60 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterClient.java @@ -0,0 +1,8 @@ +package mate.academy.rickandmorty.service; + +import java.util.List; +import mate.academy.rickandmorty.dto.external.CharacterResponseDto; + +public interface CharacterClient { + List findAllCharacters(); +} 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..e00c1fca --- /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.CharacterResponseDto; +import mate.academy.rickandmorty.dto.internal.CharacterDto; + +public interface CharacterService { + void saveAllCharacters(List characters); + + CharacterDto findRandomCharacter(); + + List findCharacterByName(String name); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/impl/CharacterClientImpl.java b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterClientImpl.java new file mode 100644 index 00000000..4c4d1d98 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterClientImpl.java @@ -0,0 +1,58 @@ +package mate.academy.rickandmorty.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.CharacterResponseDto; +import mate.academy.rickandmorty.dto.external.CharactersDataResponseDto; +import mate.academy.rickandmorty.exception.DataProcessingException; +import mate.academy.rickandmorty.service.CharacterClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CharacterClientImpl implements CharacterClient { + private static final String PAGE_DELIMITER = "?page="; + private static final int PAGES_NUMBER = 42; + private static int CURRENT_PAGE = 1; + private final ObjectMapper objectMapper; + @Value("${rick-and-morty-url}") + private String characterUrl; + + @Override + public List findAllCharacters() { + List characters = new ArrayList<>(); + HttpClient client = HttpClient.newHttpClient(); + while (CURRENT_PAGE <= PAGES_NUMBER) { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(characterUrl + PAGE_DELIMITER + CURRENT_PAGE)) + .build(); + try { + HttpResponse response = client.send( + request, HttpResponse.BodyHandlers.ofString()); + characters.addAll(parseResponse(response.body()).results()); + CURRENT_PAGE++; + } catch (IOException | InterruptedException e) { + throw new DataProcessingException("Can't send request to get data", e); + } + } + return characters; + } + + private CharactersDataResponseDto parseResponse(String responseBody) { + try { + return objectMapper.readValue(responseBody, CharactersDataResponseDto.class); + } catch (JsonProcessingException e) { + throw new DataProcessingException("Can't parse response body.", 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..2cf9d092 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java @@ -0,0 +1,34 @@ +package mate.academy.rickandmorty.service.impl; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.external.CharacterResponseDto; +import mate.academy.rickandmorty.dto.internal.CharacterDto; +import mate.academy.rickandmorty.mapper.CharacterMapper; +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; + + @Override + public void saveAllCharacters(List responseDtoList) { + characterRepository.saveAll( + characterMapper.toModelList(responseDtoList)); + } + + @Override + public CharacterDto findRandomCharacter() { + return characterMapper.toDto(characterRepository.getRandomCharacter()); + } + + @Override + public List findCharacterByName(String name) { + return characterMapper.toDtoList( + characterRepository.findByNameContaining(name)); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..262526da 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,10 @@ +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.url=jdbc:mysql://localhost:3306/rick_and_morty_db?serverTimezone=UTC +spring.datasource.username=root +spring.datasource.password=Mate2023 +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.open-in-view=false + +rick-and-morty-url=https://rickandmortyapi.com/api/character diff --git a/src/main/resources/init_db.sql b/src/main/resources/init_db.sql new file mode 100644 index 00000000..2f72bc98 --- /dev/null +++ b/src/main/resources/init_db.sql @@ -0,0 +1 @@ +CREATE SCHEMA IF NOT EXISTS `rick_and_morty_db` DEFAULT CHARACTER SET UTF8MB4; 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() { + } } diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bc2fdde8..63824dda 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,3 +3,9 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true +spring.jpa.open-in-view=false + +rick-and-morty-url=https://rickandmortyapi.com/api/character