diff --git a/pom.xml b/pom.xml index 0c754f19..4203abe3 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot @@ -16,6 +16,8 @@ 17 3.1.1 + 1.5.5.Final + 0.2.0 https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml @@ -41,10 +43,71 @@ com.h2database h2 - + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + org.projectlombok + lombok + true + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + + + org.springframework.boot + spring-boot-starter-web + + + com.mysql + mysql-connector-j + runtime + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + org.springframework.boot spring-boot-maven-plugin @@ -68,7 +131,37 @@ false + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.1.1 + + + compile + + check + + + + + + ${project.build.sourceDirectory} + + ${maven.checkstyle.plugin.configLocation} + UTF-8 + true + true + false + + + + com.puppycrawl.tools + checkstyle + 9.2 + + + - + \ No newline at end of file diff --git a/src/main/java/mate/academy/rickandmorty/Application.java b/src/main/java/mate/academy/rickandmorty/Application.java index cdea84fc..550ca0ed 100644 --- a/src/main/java/mate/academy/rickandmorty/Application.java +++ b/src/main/java/mate/academy/rickandmorty/Application.java @@ -1,12 +1,19 @@ package mate.academy.rickandmorty; +import mate.academy.rickandmorty.service.RickAndMortyClient; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { + private static RickAndMortyClient rickAndMortyClient; + + public Application(RickAndMortyClient client) { + Application.rickAndMortyClient = client; + } public static void main(String[] args) { SpringApplication.run(Application.class, args); + rickAndMortyClient.getCharacters(); } } 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..b3ed21ee --- /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..3ca0b26e --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -0,0 +1,25 @@ +package mate.academy.rickandmorty.controller; + +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.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class CharacterController { + private final CharacterService characterService; + + @GetMapping("/random") + public CharacterDto getRandomCharacter() { + return characterService.getRandomCharacter(); + } + + @GetMapping("/search") + public List searchCharacter(@RequestParam String name) { + return characterService.findByName(name); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterMetaDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterMetaDto.java new file mode 100644 index 00000000..bdba8b29 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterMetaDto.java @@ -0,0 +1,6 @@ +package mate.academy.rickandmorty.dto.external; + +import java.util.Map; + +public record CharacterMetaDto(Map info) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CreateCharacterDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CreateCharacterDto.java new file mode 100644 index 00000000..a19c6621 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CreateCharacterDto.java @@ -0,0 +1,13 @@ +package mate.academy.rickandmorty.dto.external; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class CreateCharacterDto { + private Long id; + private String name; + private String status; + private String gender; +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CreateCharacterDtoList.java b/src/main/java/mate/academy/rickandmorty/dto/external/CreateCharacterDtoList.java new file mode 100644 index 00000000..616e2439 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CreateCharacterDtoList.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto.external; + +import java.util.List; +import lombok.Data; + +@Data +public class CreateCharacterDtoList { + private 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..7d2bc6fc --- /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 String 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..b8f09220 --- /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.CreateCharacterDto; +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") + Character toModel(CreateCharacterDto createCharacterDto); + + CharacterDto 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..803fc71a --- /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 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..3113ed33 --- /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.Query; +import org.springframework.stereotype.Repository; + +@Repository +public interface CharacterRepository extends JpaRepository { + List findByNameContaining(String name); + + @Query("SELECT COUNT(*) FROM Character ") + Long getCountOfRecords(); +} 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..e8b78a97 --- /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.CreateCharacterDto; +import mate.academy.rickandmorty.dto.internal.CharacterDto; + +public interface CharacterService { + void saveAll(List characterDtoList); + + CharacterDto getRandomCharacter(); + + List findByName(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..29b3b367 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/RickAndMortyClient.java @@ -0,0 +1,53 @@ +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 lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.external.CharacterMetaDto; +import mate.academy.rickandmorty.dto.external.CreateCharacterDtoList; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RickAndMortyClient { + private static final String URL = "https://rickandmortyapi.com/api/character"; + + private final ObjectMapper objectMapper; + + private final CharacterService characterService; + + public void getCharacters() { + HttpClient httpClient = HttpClient.newHttpClient(); + + HttpRequest httpRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(URL)) + .build(); + + try { + HttpResponse response = httpClient + .send(httpRequest, HttpResponse.BodyHandlers.ofString()); + CharacterMetaDto metaDto = objectMapper.readValue( + response.body(), CharacterMetaDto.class); + int countOfPages = Integer.parseInt(metaDto.info().get("pages")); + for (int i = 1; i <= countOfPages; i++) { + httpRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(URL + "/?page=" + i)) + .build(); + response = httpClient + .send(httpRequest, HttpResponse.BodyHandlers.ofString()); + CreateCharacterDtoList dtoList = objectMapper.readValue( + response.body(), CreateCharacterDtoList.class); + characterService.saveAll(dtoList.getResults()); + } + + } catch (IOException | InterruptedException e) { + throw new RuntimeException(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..fbd0f0e9 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/impl/CharacterServiceImpl.java @@ -0,0 +1,49 @@ +package mate.academy.rickandmorty.service.impl; + +import java.util.List; +import java.util.Optional; +import java.util.Random; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.external.CreateCharacterDto; +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(); + private int size; + + @Override + public void saveAll(List characterDtoList) { + List characters = characterDtoList.stream() + .map(characterMapper::toModel) + .toList(); + size = characters.size(); + characterRepository.saveAll(characters); + } + + @Override + public CharacterDto getRandomCharacter() { + Long countOfRecords = characterRepository.getCountOfRecords(); + Long id = random.nextLong(1L, countOfRecords); + Optional optionalCharacter = characterRepository.findById(id); + if (optionalCharacter.isPresent()) { + return characterMapper.toDto(optionalCharacter.get()); + } + throw new RuntimeException("Can't find character by id: " + id); + } + + @Override + public List findByName(String name) { + return characterRepository.findByNameContaining(name).stream() + .map(characterMapper::toDto) + .toList(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..d55dd1a0 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=root1234 +#spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect +server.port=8090 +spring.jpa.hibernate.ddl-auto=create-drop +server.servlet.context-path=/api +spring.jpa.show-sql=true +spring.jpa.open-in-view=false diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bc2fdde8..bd211685 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,5 +1,5 @@ spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver -spring.datasource.username=sa -spring.datasource.password=password +spring.datasource.username=root +spring.datasource.password=root1234 spring.jpa.database-platform=org.hibernate.dialect.H2Dialect