Skip to content

Completed Rick and Morty task #208

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,28 @@
<maven.checkstyle.plugin.configLocation>
https://raw.githubusercontent.com/mate-academy/style-guides/master/java/checkstyle.xml
</maven.checkstyle.plugin.configLocation>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
Expand All @@ -40,6 +55,42 @@
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Expand All @@ -62,6 +113,9 @@
</execution>
</executions>
<configuration>
<sourceDirectories>
<sourceDirectory>${project.build.sourceDirectory}</sourceDirectory>
</sourceDirectories>
<configLocation>${maven.checkstyle.plugin.configLocation}</configLocation>
<consoleOutput>true</consoleOutput>
<failsOnError>true</failsOnError>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public class RickAndMortyApplication {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
SpringApplication.run(RickAndMortyApplication.class, args);
}
}
14 changes: 14 additions & 0 deletions src/main/java/mate/academy/rickandmorty/config/HttpConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package mate.academy.rickandmorty.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class HttpConfig {

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package mate.academy.rickandmorty.controller;

import io.swagger.v3.oas.annotations.Operation;
import java.util.List;
import lombok.RequiredArgsConstructor;
import mate.academy.rickandmorty.dto.CharacterResponseDto;
import mate.academy.rickandmorty.service.InternalCharacterService;
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("/api/characters")
public class CharacterController {
private final InternalCharacterService internalCharacterService;

@Operation(summary = "Get a random character",
description = "Returns a randomly selected character from the database, "
+ "including all available details such as: id, name, gender, and status")
@GetMapping("/random")
public CharacterResponseDto getRandomCharacter() {
return internalCharacterService.getRandomCharacter();
}

@Operation(summary = "Search for characters by name",
description = "Retrieves a list of characters whose names contain "
+ "the provided search string.")
@GetMapping("/search")
public List<CharacterResponseDto> searchCharacterByName(@RequestParam String name) {
if (!name.matches("[a-zA-Z]+")) {
Comment on lines +31 to +32

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex [a-zA-Z]+ will only match names consisting entirely of alphabetic characters without spaces or special characters. If the intention is to allow names with spaces or other characters, consider updating the regex accordingly.

throw new IllegalArgumentException("Invalid name format");
}

return internalCharacterService.searchCharacterByName(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package mate.academy.rickandmorty.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CharacterResponseDto {
private Long id;
private Long externalId;
private String name;
private String status;
private String gender;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package mate.academy.rickandmorty.dto;

import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExternalApiResponse {
private List<ExternalCharacterDto> results;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package mate.academy.rickandmorty.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExternalCharacterDto {
private Long id;
private String name;
private String status;
private String gender;
}
25 changes: 25 additions & 0 deletions src/main/java/mate/academy/rickandmorty/entity/Character.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package mate.academy.rickandmorty.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "characters")
public class Character {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long externalId;
private String name;
private String status;
private String gender;
}
14 changes: 14 additions & 0 deletions src/main/java/mate/academy/rickandmorty/entity/Gender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package mate.academy.rickandmorty.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum Gender {
MALE("Male"),
FEMALE("Female"),
UNKNOWN("Unknown");

private final String gender;
}
14 changes: 14 additions & 0 deletions src/main/java/mate/academy/rickandmorty/entity/Status.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package mate.academy.rickandmorty.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum Status {
ALIVE("Alive"),
DEAD("Dead"),
UNKNOWN("Unknown");

private final String status;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package mate.academy.rickandmorty.exception;

public class CharacterNotFoundException extends RuntimeException {
public CharacterNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package mate.academy.rickandmorty.exception;

public class ExternalApiException extends RuntimeException {
public ExternalApiException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package mate.academy.rickandmorty.exception;

import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CharacterNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public Map<String, String> handleCharacterNotFound(CharacterNotFoundException e) {
return Map.of("error", e.getMessage());
}

@ExceptionHandler(ExternalApiException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, String> handleExternalApiException(ExternalApiException e) {
return Map.of("error", e.getMessage());
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map<String, String> handleGenericException(Exception e) {
return Map.of("error", "Unexpected error occurred", "details", e.getMessage());
}

@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleMissingServletRequestParameter(
MissingServletRequestParameterException e) {
return Map.of("error", "Missing required parameter: " + e.getParameterName());
}

@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> handleIllegalArgumentException(IllegalArgumentException e) {
return Map.of("error", e.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package mate.academy.rickandmorty.initdata;

import java.util.List;
import lombok.RequiredArgsConstructor;
import mate.academy.rickandmorty.entity.Character;
import mate.academy.rickandmorty.service.ExternalCharacterService;
import mate.academy.rickandmorty.service.InternalCharacterService;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class DataInitializer {
private final ExternalCharacterService externalCharacterService;
private final InternalCharacterService internalCharacterService;

@EventListener(ApplicationReadyEvent.class)
public void initData() {
if (internalCharacterService.getAllCharacters().isEmpty()) {
List<Character> characters = externalCharacterService.fetchAllCharactersFromApi();
internalCharacterService.saveAllCharacters(characters);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package mate.academy.rickandmorty.mapper;

import java.util.Arrays;
import mate.academy.rickandmorty.dto.CharacterResponseDto;
import mate.academy.rickandmorty.dto.ExternalCharacterDto;
import mate.academy.rickandmorty.entity.Character;
import mate.academy.rickandmorty.entity.Gender;
import mate.academy.rickandmorty.entity.Status;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;

@Mapper(componentModel = "spring")
public interface CharacterMapper {
@Mapping(source = "externalId", target = "externalId")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mapping @Mapping(source = "externalId", target = "externalId") is redundant because the field names are the same in both the source and target. MapStruct automatically maps fields with the same name, so this line can be removed.

@Mapping(source = "status", target = "status", qualifiedByName = "stringToStatus")
@Mapping(source = "gender", target = "gender", qualifiedByName = "stringToGender")
CharacterResponseDto toDto(Character character);

@Mapping(source = "id", target = "externalId")
@Mapping(source = "status", target = "status", qualifiedByName = "stringToStatus")
@Mapping(source = "gender", target = "gender", qualifiedByName = "stringToGender")
Character toEntity(ExternalCharacterDto dto);

@Named("stringToStatus")
default Status stringToStatus(String status) {
return Arrays.stream(Status.values())
.filter(s -> s.getStatus().equalsIgnoreCase(status))
.findFirst()
.orElse(Status.UNKNOWN);
}

@Named("stringToGender")
default Gender stringToGender(String gender) {
return Arrays.stream(Gender.values())
.filter(g -> g.getGender().equalsIgnoreCase(gender))
.findFirst()
.orElse(Gender.UNKNOWN);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package mate.academy.rickandmorty.repository;

import java.util.List;
import java.util.Optional;
import mate.academy.rickandmorty.entity.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<Character, Long> {
Optional<Character> findByExternalId(Long externalId);

@Query("SELECT c FROM Character c WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :name, '%'))")
List<Character> findByNameContainingIgnoreCase(String name);
}
Loading