diff --git a/pom.xml b/pom.xml index 0c754f19..d495ffc6 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,8 @@ jv-rick-and-morty jv-rick-and-morty + 1.6.3 + 0.2.0 17 3.1.1 @@ -32,6 +34,23 @@ test + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.projectlombok + lombok-mapstruct-binding + ${lombok-mapstruct-binding.version} + + + org.projectlombok + lombok + 1.18.30 + + org.springframework.boot spring-boot-starter-data-jpa @@ -41,14 +60,48 @@ 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.2.0 + - - - org.springframework.boot - spring-boot-maven-plugin - + + 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.apache.maven.plugins maven-checkstyle-plugin @@ -66,8 +119,21 @@ true true false + src/main + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + diff --git a/src/main/java/mate/academy/rickandmorty/Application.java b/src/main/java/mate/academy/rickandmorty/Application.java index cdea84fc..ca5761f8 100644 --- a/src/main/java/mate/academy/rickandmorty/Application.java +++ b/src/main/java/mate/academy/rickandmorty/Application.java @@ -1,12 +1,23 @@ package mate.academy.rickandmorty; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.service.CharacterExternalApiService; +import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class Application { +@RequiredArgsConstructor +public class Application implements CommandLineRunner { + private final CharacterExternalApiService externalApiService; public static void main(String[] args) { SpringApplication.run(Application.class, args); + + } + + @Override + public void run(String... args) throws Exception { + externalApiService.fetchCharacters(); } } 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..d757f88a --- /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 = "mate.academy.rickandmorty.mapper.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..a1bf72c1 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -0,0 +1,40 @@ +package mate.academy.rickandmorty.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.constraints.NotBlank; +import java.util.List; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.internal.CharacterDto; +import mate.academy.rickandmorty.service.CharacterService; +import org.springframework.validation.annotation.Validated; +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 +@RequiredArgsConstructor +@RequestMapping("/characters") +@Validated +public class CharacterController { + private final CharacterService characterService; + + @Operation(description = "The request randomly generates a wiki " + + "about one character in the universe the animated series Rick & Morty") + @GetMapping("/random") + public CharacterDto getRandomCharacter() { + return characterService.getRandomCharacter(); + } + + @Operation(description = "The request takes a string as an argument," + + " and returns a list of all characters whose name contains the search string.") + @GetMapping + public List getAllCharactersByName( + @Parameter(example = "?name=Rick+Sanchez") + @RequestParam("name") + @NotBlank + String name) { + return characterService.findByName(name); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterInfoDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterInfoDto.java new file mode 100644 index 00000000..f1491582 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterInfoDto.java @@ -0,0 +1,8 @@ +package mate.academy.rickandmorty.dto.external; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record CharacterInfoDto( + String next) { +} 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..6fc334db --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResponseDto.java @@ -0,0 +1,10 @@ +package mate.academy.rickandmorty.dto.external; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record CharacterResponseDto( + CharacterInfoDto info, + List results) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResultsDto.java b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResultsDto.java new file mode 100644 index 00000000..92992be3 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/external/CharacterResultsDto.java @@ -0,0 +1,11 @@ +package mate.academy.rickandmorty.dto.external; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record CharacterResultsDto( + String id, + String name, + String status, + String gender) { +} 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..5ea4214f --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/internal/CharacterDto.java @@ -0,0 +1,12 @@ +package mate.academy.rickandmorty.dto.internal; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record CharacterDto( + Long id, + String externalId, + String name, + String status, + String gender) { +} diff --git a/src/main/java/mate/academy/rickandmorty/exception/ExternalApiException.java b/src/main/java/mate/academy/rickandmorty/exception/ExternalApiException.java new file mode 100644 index 00000000..a0bdda77 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/exception/ExternalApiException.java @@ -0,0 +1,7 @@ +package mate.academy.rickandmorty.exception; + +public class ExternalApiException extends RuntimeException { + public ExternalApiException(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..77b70507 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java @@ -0,0 +1,33 @@ +package mate.academy.rickandmorty.mapper; + +import java.util.List; +import mate.academy.rickandmorty.config.MapperConfig; +import mate.academy.rickandmorty.dto.external.CharacterResultsDto; +import mate.academy.rickandmorty.dto.internal.CharacterDto; +import mate.academy.rickandmorty.model.Character; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.Mappings; + +@Mapper(config = MapperConfig.class) +public interface CharacterMapper { + CharacterDto toDto(Character character); + + @Mapping(target = "id", ignore = true) + Character internalToModel(CharacterDto characterDto); + + @Mappings({ + @Mapping(target = "externalId", source = "id"), + @Mapping(target = "id", ignore = true) + }) + CharacterDto externalToDto(CharacterResultsDto characterResultsDto); + + @Mappings({ + @Mapping(target = "externalId", source = "id"), + @Mapping(target = "id", ignore = true) + }) + List externalToDtoList(List externalDtos); + + void updateEntityFromDto(CharacterDto characterDto, @MappingTarget 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..0b68b47d --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/Character.java @@ -0,0 +1,36 @@ +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 jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Table(name = "characters") +@Entity +public class Character { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false, unique = true) + @NotNull + private String externalId; + @Column(nullable = false) + @NotBlank + private String name; + @Column(nullable = false) + @NotBlank + private String status; + @Column(nullable = false) + @NotBlank + 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..4db25bc0 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java @@ -0,0 +1,16 @@ +package mate.academy.rickandmorty.repository; + +import java.util.List; +import java.util.Optional; +import mate.academy.rickandmorty.model.Character; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface CharacterRepository extends JpaRepository { + List findByNameContainingIgnoreCase(String name); + + Optional findByExternalId(String externalId); + + @Query(value = "SELECT * FROM characters ORDER BY RAND() LIMIT 1", nativeQuery = true) + Character findRandomCharacter(); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterExternalApiService.java b/src/main/java/mate/academy/rickandmorty/service/CharacterExternalApiService.java new file mode 100644 index 00000000..e96eecd7 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterExternalApiService.java @@ -0,0 +1,56 @@ +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.CharacterResponseDto; +import mate.academy.rickandmorty.dto.internal.CharacterDto; +import mate.academy.rickandmorty.exception.ExternalApiException; +import mate.academy.rickandmorty.mapper.CharacterMapper; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CharacterExternalApiService { + private static final String BASE_URL = "https://rickandmortyapi.com/api/character"; + private final ObjectMapper objectMapper; + private final CharacterMapper characterMapper; + private final CharacterService characterService; + + public void fetchCharacters() { + List characters = new ArrayList<>(); + String nextUrl = BASE_URL; + + HttpClient client = HttpClient.newHttpClient(); + while (nextUrl != null) { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(nextUrl)) + .build(); + try { + HttpResponse response = client.send( + request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() != 200) { + throw new IOException("API returned non-successful status code: " + + response.statusCode()); + } + CharacterResponseDto characterResponseDto = objectMapper.readValue( + response.body(), CharacterResponseDto.class); + + characters.addAll(characterMapper.externalToDtoList( + characterResponseDto.results())); + nextUrl = characterResponseDto.info().next(); + } catch (IOException | InterruptedException e) { + throw new ExternalApiException( + "An error occurred while trying to get a response from the server.", e); + } + } + characterService.saveAll(characters); + } + +} 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..f6bb9e12 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java @@ -0,0 +1,14 @@ +package mate.academy.rickandmorty.service; + +import java.util.List; +import mate.academy.rickandmorty.dto.internal.CharacterDto; + +public interface CharacterService { + void saveAll(List dtos); + + CharacterDto getById(Long id); + + List findByName(String name); + + CharacterDto getRandomCharacter(); +} 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..69e1370a --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java @@ -0,0 +1,55 @@ +package mate.academy.rickandmorty.service; + +import jakarta.persistence.EntityNotFoundException; +import java.util.List; +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 org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CharacterServiceImpl implements CharacterService { + private final CharacterMapper mapper; + private final CharacterRepository repository; + + @Override + @Transactional + public void saveAll(List dtos) { + List characters = dtos.stream() + .map(characterDto -> { + Character newCharacter = repository + .findByExternalId(characterDto.externalId()) + .orElse(mapper.internalToModel(characterDto)); + mapper.updateEntityFromDto(characterDto, newCharacter); + return newCharacter; + }) + .toList(); + repository.saveAll(characters); + } + + @Override + public CharacterDto getById(Long id) { + return repository.findById(id) + .map(mapper::toDto) + .orElseThrow( + () -> new EntityNotFoundException( + "Could not find Character with id: " + id) + ); + } + + @Override + public List findByName(String name) { + return repository.findByNameContainingIgnoreCase(name).stream() + .map(mapper::toDto) + .toList(); + } + + @Override + public CharacterDto getRandomCharacter() { + return mapper.toDto(repository.findRandomCharacter()); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..2d1822bf 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,6 @@ - +spring.datasource.url=jdbc:mysql://localhost:3306/rick_and_morty_db +spring.datasource.username=root +spring.datasource.password=12345678 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.jooq.sql-dialect=MYSQL +spring.jpa.hibernate.ddl-auto=create