diff --git a/README.md b/README.md index 4815bd91..c475ff23 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ "id": 1, "externalId": "1", "name": "Rick Sanchez", - "status": "Alive", + "status": "Alive", "gender": "Male" } ``` @@ -29,7 +29,7 @@ 1. You must use [public API](https://rickandmortyapi.com/documentation/#rest) (you should use REST API). 2. All data from the public API should be fetched once, and only once, when the Application context is created - + ### Tech Requirements - Use MySQL DB in your app. diff --git a/pom.xml b/pom.xml index 0c754f19..889e18c8 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,11 @@ test + + org.projectlombok + lombok + + org.springframework.boot spring-boot-starter-data-jpa @@ -41,6 +46,47 @@ com.h2database h2 + + + org.springframework.boot + spring-boot-starter-web + + + + mysql + mysql-connector-java + 8.0.33 + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + + + + org.mapstruct + mapstruct + 1.5.2.Final + + + + org.mapstruct + mapstruct-processor + 1.5.2.Final + provided + + + + org.hibernate.validator + hibernate-validator + + + + com.fasterxml.jackson.core + jackson-databind + 2.17.2 + @@ -66,6 +112,7 @@ true true false + main diff --git a/src/main/java/mate/academy/rickandmorty/Application.java b/src/main/java/mate/academy/rickandmorty/Application.java index cdea84fc..3b4e97e6 100644 --- a/src/main/java/mate/academy/rickandmorty/Application.java +++ b/src/main/java/mate/academy/rickandmorty/Application.java @@ -5,7 +5,6 @@ @SpringBootApplication public class Application { - public static void main(String[] args) { SpringApplication.run(Application.class, args); } 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..e44366d2 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/config/MapperConfig.java @@ -0,0 +1,14 @@ +package mate.academy.rickandmorty.config; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.NullValueCheckStrategy; + +@org.mapstruct.MapperConfig( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + implementationName = "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..4b99b01b --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -0,0 +1,34 @@ +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.CharacterDto; +import mate.academy.rickandmorty.service.CharacterService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RequiredArgsConstructor +@RestController +@RequestMapping("/characters") +@Tag(name = "Characters", description = "Operations related to characters") +public class CharacterController { + private final CharacterService characterService; + + @GetMapping("/random") + @Operation(summary = "Get a random character", + description = "Retrieve a random character from \"Rick and Morty\"") + public CharacterDto getRandomCharacter() { + return characterService.getRandomCharacter(); + } + + @GetMapping("/search/{name}") + @Operation(summary = "Get characters by name", + description = "Retrieve a list of characters name fragment") + public List getBookByName(@PathVariable String name) { + return characterService.searchCharacters(name); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java b/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java new file mode 100644 index 00000000..cf2ae582 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java @@ -0,0 +1,4 @@ +package mate.academy.rickandmorty.dto; + +public record CharacterDto(Long id, String externalId, String name, String status, String gender) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/CreateCharacterRequestDto.java b/src/main/java/mate/academy/rickandmorty/dto/CreateCharacterRequestDto.java new file mode 100644 index 00000000..d0c540bf --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/CreateCharacterRequestDto.java @@ -0,0 +1,20 @@ +package mate.academy.rickandmorty.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@AllArgsConstructor +public class CreateCharacterRequestDto { + @NotBlank + private String externalId; + @NotBlank + private String name; + @NotBlank + private String status; + @NotBlank + 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..30b9476d --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java @@ -0,0 +1,11 @@ +package mate.academy.rickandmorty.mapper; + +import mate.academy.rickandmorty.config.MapperConfig; +import mate.academy.rickandmorty.dto.CharacterDto; +import mate.academy.rickandmorty.model.Character; +import org.mapstruct.Mapper; + +@Mapper(config = MapperConfig.class) +public interface CharacterMapper { + 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..d69fe497 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/Character.java @@ -0,0 +1,23 @@ +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.Getter; +import lombok.Setter; + +@Entity +@Getter +@Setter +@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..c0465b04 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java @@ -0,0 +1,12 @@ +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 findByNameContaining(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..5fd1006d --- /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.CharacterDto; + +public interface CharacterService { + CharacterDto getRandomCharacter(); + + List searchCharacters(String name); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java b/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java new file mode 100644 index 00000000..89c7040d --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java @@ -0,0 +1,32 @@ +package mate.academy.rickandmorty.service; + +import java.util.List; +import java.util.Random; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.CharacterDto; +import mate.academy.rickandmorty.mapper.CharacterMapper; +import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.repository.CharacterRepository; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CharacterServiceImpl implements CharacterService { + private final CharacterRepository characterRepository; + private final CharacterMapper characterMapper; + private static final Random random = new Random(); + + @Override + public CharacterDto getRandomCharacter() { + List characters = characterRepository.findAll(); + return characterMapper + .toDto(characters.get(random.nextInt(characters.size()))); + } + + @Override + public List searchCharacters(String name) { + return characterRepository.findByNameContaining(name).stream() + .map(characterMapper::toDto) + .toList(); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/service/DataInitializer.java b/src/main/java/mate/academy/rickandmorty/service/DataInitializer.java new file mode 100644 index 00000000..53a8adca --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/DataInitializer.java @@ -0,0 +1,55 @@ +package mate.academy.rickandmorty.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import mate.academy.rickandmorty.model.Character; +import mate.academy.rickandmorty.repository.CharacterRepository; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +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; + +@Component +@AllArgsConstructor +public class DataInitializer implements CommandLineRunner { + private static final String URL = "https://rickandmortyapi.com/api/character"; + private final CharacterRepository characterRepository; + private final ObjectMapper objectMapper; + + @Override + public void run(String... args) throws IOException, InterruptedException { + HttpClient httpClient = HttpClient.newHttpClient(); + String nextPageUrl = URL; + + while (nextPageUrl != null) { + HttpRequest httpRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(nextPageUrl)) + .build(); + HttpResponse response = httpClient + .send(httpRequest, HttpResponse.BodyHandlers.ofString()); + + JsonNode rootNode = objectMapper.readTree(response.body()); + List characters = new ArrayList<>(); + JsonNode results = rootNode.get("results"); + + for (JsonNode result : results) { + Character character = new Character(); + character.setExternalId(result.get("id").asText()); + character.setName(result.get("name").asText()); + character.setGender(result.get("gender").asText()); + character.setStatus(result.get("status").asText()); + + characters.add(character); + } + characterRepository.saveAll(characters); + nextPageUrl = rootNode.get("info").get("next").asText(null); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..5304e22d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,5 @@ +spring.datasource.url=jdbc:mysql://localhost:3306/rick_and_morty +spring.datasource.username=root +spring.datasource.password=root12345 +spring.jpa.hibernate.ddl-auto=update diff --git a/src/test/java/mate/academy/rickandmorty/ApplicationTests.java b/src/test/java/mate/academy/rickandmorty/ApplicationTests.java index 8fec6af0..8697db13 100644 --- a/src/test/java/mate/academy/rickandmorty/ApplicationTests.java +++ b/src/test/java/mate/academy/rickandmorty/ApplicationTests.java @@ -5,9 +5,7 @@ @SpringBootTest class ApplicationTests { - @Test void contextLoads() { } - }