diff --git a/pom.xml b/pom.xml index 0c754f19..58bf662d 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.4 + 3.3.4 mate.academy @@ -19,6 +19,9 @@ https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml + 1.18.34 + 0.2.0 + 1.5.5.Final @@ -37,10 +40,35 @@ spring-boot-starter-data-jpa + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + + com.h2database h2 + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok + lombok + + + com.mysql + mysql-connector-j + runtime + @@ -48,24 +76,38 @@ org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + org.apache.maven.plugins - maven-checkstyle-plugin - 3.3.0 - - - compile - - check - - - + maven-compiler-plugin - ${maven.checkstyle.plugin.configLocation} - true - true - false + ${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/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..b5872dbe --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/controller/CharacterController.java @@ -0,0 +1,27 @@ +package mate.academy.rickandmorty.controller; + +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; +import java.util.List; + +@RequiredArgsConstructor +@RestController +@RequestMapping(value = "/character") +public class CharacterController { + private final CharacterService characterService; + + @GetMapping + public CharacterDto getRandomCharacter() { + return characterService.getRandomCharacter(); + } + + @GetMapping("/{name}") + public List getCharactersByName(@PathVariable String name) { + return characterService.findCharacterByName(name); + } +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/ApiPageInfo.java b/src/main/java/mate/academy/rickandmorty/dto/ApiPageInfo.java new file mode 100644 index 00000000..1ef22a41 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/ApiPageInfo.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto; + +public record ApiPageInfo( + int pages, + int count, + String next, + String prev +) { +} diff --git a/src/main/java/mate/academy/rickandmorty/dto/ApiResponse.java b/src/main/java/mate/academy/rickandmorty/dto/ApiResponse.java new file mode 100644 index 00000000..5d23428f --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/ApiResponse.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.dto; + +import java.util.List; + +public record ApiResponse( + ApiPageInfo info, + List results +) { +} 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..db943632 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/dto/CharacterDto.java @@ -0,0 +1,8 @@ +package mate.academy.rickandmorty.dto; + +public record CharacterDto( + Long id, + String name, + String status, + 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..be43977b --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/mapper/CharacterMapper.java @@ -0,0 +1,22 @@ +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; +import org.mapstruct.Mapping; +import java.util.List; + +@Mapper(config = MapperConfig.class) +public interface CharacterMapper { + + @Mapping(source = "id", target = "externalId") + Character toModel(CharacterDto characterDto); + + @Mapping(source = "id", target = "externalId") + List toModelList(List characterDtoList); + + CharacterDto toDto(Character character); + + 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..1342dacd --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/model/Character.java @@ -0,0 +1,26 @@ +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.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@Table(name = "results") + +public class Character { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private Long 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..dab6d4e8 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/repository/CharacterRepository.java @@ -0,0 +1,11 @@ +package mate.academy.rickandmorty.repository; + +import mate.academy.rickandmorty.model.Character; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import java.util.List; + +public interface CharacterRepository extends JpaRepository, JpaSpecificationExecutor { + + List findByNameContainingIgnoreCase(String name); +} diff --git a/src/main/java/mate/academy/rickandmorty/service/CharacterInitializer.java b/src/main/java/mate/academy/rickandmorty/service/CharacterInitializer.java new file mode 100644 index 00000000..c25d65b1 --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterInitializer.java @@ -0,0 +1,61 @@ +package mate.academy.rickandmorty.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; +import mate.academy.rickandmorty.dto.ApiResponse; +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.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +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; + +@Service +@RequiredArgsConstructor +public class CharacterInitializer { + @Value("${api.url}") + private String apiUrl; + private final CharacterRepository characterRepository; + private final ObjectMapper objectMapper; + private final CharacterMapper characterMapper; + + @PostConstruct + public void init() { + if(characterRepository.count() == 0) { + initCharactersFromExternalApi(apiUrl); + } + } + private void initCharactersFromExternalApi(String apiUrl) { + HttpClient client = HttpClient.newHttpClient(); + List characterDtoList = new ArrayList<>(); + try { + while (apiUrl != null) { + HttpRequest getRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(apiUrl)) + .build(); + HttpResponse response = client.send(getRequest, + HttpResponse.BodyHandlers.ofString()); + ApiResponse apiResponse = objectMapper.readValue( + response.body(), + ApiResponse.class + ); + characterDtoList.addAll(apiResponse.results()); + apiUrl = apiResponse.info().next(); + } + List characters = characterMapper.toModelList(characterDtoList); + characterRepository.saveAll(characters); + } catch (IOException | InterruptedException e) { + throw new EntityNotFoundException("Can't access results from external API", e); + } + } +} 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..a4b0342f --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterService.java @@ -0,0 +1,9 @@ +package mate.academy.rickandmorty.service; + +import mate.academy.rickandmorty.dto.CharacterDto; +import java.util.List; + +public interface CharacterService { + CharacterDto getRandomCharacter(); + List findCharacterByName(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..bb31fecd --- /dev/null +++ b/src/main/java/mate/academy/rickandmorty/service/CharacterServiceImpl.java @@ -0,0 +1,37 @@ +package mate.academy.rickandmorty.service; + +import jakarta.persistence.EntityNotFoundException; +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; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +@Service +@RequiredArgsConstructor +public class CharacterServiceImpl implements CharacterService { + + private final CharacterRepository characterRepository; + private final CharacterMapper characterMapper; + + @Override + public CharacterDto getRandomCharacter() { + long count = characterRepository.count(); + if(count == 0) { + throw new EntityNotFoundException("No character found"); + } + long randomNumber = ThreadLocalRandom.current().nextLong(count); + return characterRepository.findById(randomNumber) + .map(characterMapper::toDto) + .orElseThrow(() -> new EntityNotFoundException("Character not found for ID: " + randomNumber)); + } + + @Override + public List findCharacterByName(String name) { + List character = characterRepository.findByNameContainingIgnoreCase(name); + return characterMapper.toDtoList(character); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8b137891..cc5dbc13 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,10 @@ +spring.datasource.url=jdbc:mysql://localhost:3306/rick-and-morty?createDatabaseIfNotExist=true +spring.datasource.username=root +spring.datasource.password=Styczen10! +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.jpa.open-in-view=false +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true + +api.url=https://rickandmortyapi.com/api/character \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bc2fdde8..ca131643 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -3,3 +3,5 @@ spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=password spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +api.url=https://rickandmortyapi.com/api/character