-
Notifications
You must be signed in to change notification settings - Fork 308
complited assigment #264
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
base: main
Are you sure you want to change the base?
complited assigment #264
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { | ||
gubber230 marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: the class implements CommandLineRunner which is okay, but ensure the execution is guarded. If you keep CommandLineRunner, use one of the conditional approaches described in the previous comment. Alternatively, consider @eventlistener(ApplicationReadyEvent.class) — the lifecycle hook is different but still requires guarding for tests. The main goal is to avoid executing the external import during test contexts. |
||
private final CharacterExternalApiService externalApiService; | ||
Comment on lines
+10
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dependency injection via Lombok's @requiredargsconstructor is fine here. Ensure Lombok is present in project dependencies so the final constructor for externalApiService is generated correctly at compile time. |
||
|
||
public static void main(String[] args) { | ||
SpringApplication.run(Application.class, args); | ||
Comment on lines
14
to
15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: The class implements CommandLineRunner and provides a main() that calls SpringApplication.run(...). This is the correct way to start the Spring Boot application. |
||
|
||
} | ||
|
||
@Override | ||
public void run(String... args) throws Exception { | ||
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: run declares throws Exception and currently delegates the fetch call directly. Consider catching and handling import-related exceptions here (or in the service) so a transient external API failure doesn't fail the entire application startup silently during development. Also log useful diagnostics if import fails. |
||
externalApiService.fetchCharacters(); | ||
gubber230 marked this conversation as resolved.
Show resolved
Hide resolved
gubber230 marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+19
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class implements CommandLineRunner and calls externalApiService.fetchCharacters() in run(...). That correctly triggers the required startup data load once per application context creation (satisfies checklist item 1.3). Keep this behavior to meet the requirement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Caution: Because the fetch is unconditional, every application context creation (including test contexts) will attempt to call the external API. If you experience slow or flaky tests, consider making the startup fetch configurable (e.g., enabled by a property or active only in a specific profile). The task requires a fetch on context creation, so only add such guards if you also ensure tests still satisfy the assignment expectations. See the external API loader for idempotency improvements. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: Calling externalApiService.fetchCharacters() here causes the application to fetch all characters on context creation, which fulfills the requirement that data must be loaded once at startup. Verify that CharacterExternalApiService.fetchCharacters() and CharacterServiceImpl.saveAll() remain idempotent to prevent duplicates on repeated starts.
Comment on lines
+20
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion (optional): If you prefer the application to start even when the external API is temporarily unavailable, consider adding error handling/logging here to catch exceptions from fetchCharacters() and decide whether to continue startup or fail fast. Current behavior (propagating exceptions) may be acceptable if you want startup to fail on missing data. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: calling externalApiService.fetchCharacters() here will run on every ApplicationContext creation (including tests/CI) and will perform network calls to the external API. That makes tests brittle and slow. Make the startup import conditional (e.g. use a profile like @Profile("!test"), or guard with a property such as import.enabled and set it to false in src/test/resources/application.properties). This preserves "fetch once at startup" in normal runs while preventing external calls during tests. See CharacterExternalApiService.fetchCharacters() for the import behavior and the task description regarding startup import. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package mate.academy.rickandmorty.config; | ||
|
||
import org.mapstruct.InjectionStrategy; | ||
import org.mapstruct.NullValueCheckStrategy; | ||
|
||
@org.mapstruct.MapperConfig( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stylistic: line uses fully-qualified annotation name. Consider importing the annotation and using @MapperConfig instead of @org.mapstruct.MapperConfig for readability and consistency. |
||
componentModel = "spring", | ||
injectionStrategy = InjectionStrategy.CONSTRUCTOR, | ||
nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, | ||
Comment on lines
+7
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: Keep the rest of the MapperConfig settings (componentModel, injectionStrategy, nullValueCheckStrategy). If you remove |
||
implementationPackage = "mate.academy.rickandmorty.mapper.impl" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: you set implementationPackage = "mate.academy.rickandmorty.mapper.impl". This is acceptable, but double-check that MapStruct is configured to generate implementations into that package and that generated sources are included in the build. If you don't need a custom package, removing this attribute avoids potential packaging mismatches. |
||
) | ||
public class MapperConfig { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
Comment on lines
+23
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Swagger docs are minimal; document responses/schema. You have
Comment on lines
+23
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: Each endpoint has an @operation annotation, so they will appear in the OpenAPI/Swagger documentation as required by the task. Ensure your OpenAPI configuration (if any) is active so the endpoints are visible in Swagger UI. |
||
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<CharacterDto> getAllCharactersByName( | ||
@Parameter(example = "?name=Rick+Sanchez") | ||
@RequestParam("name") | ||
@NotBlank | ||
Comment on lines
+34
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: The request parameter is validated with @notblank and an example is provided via @parameter. This satisfies the requirement to validate the presence of the search string (returns empty list when no matches — ensure service returns an empty list). |
||
String name) { | ||
return characterService.findByName(name); | ||
Comment on lines
+37
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The search endpoint returns a List (JPA entity) directly. Per the requirements, the response structure must include |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package mate.academy.rickandmorty.dto.external; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
|
||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: Using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: The class uses @JsonIgnoreProperties(ignoreUnknown = true), which is appropriate for external API DTOs and prevents failures if the API returns unexpected fields. |
||
public record CharacterInfoDto( | ||
String next) { | ||
Comment on lines
+6
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: The record
Comment on lines
+6
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion (optional): Add a short Javadoc above the record to explain the meaning of
Comment on lines
+6
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: The service calls info().next() during pagination. Consider that info could be null in unexpected responses — handle null-safety in the calling code (CharacterExternalApiService) to avoid a potential NullPointerException. The DTO itself is fine, but the caller should check info() before accessing next(). |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package mate.academy.rickandmorty.dto.external; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure the project's Jackson version and configuration support Java record deserialization (Jackson 2.12+). CharacterExternalApiService uses ObjectMapper.readValue(...) to parse this record — without record support you may get deserialization errors at runtime 【}. |
||
import java.util.List; | ||
|
||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: Using @JsonIgnoreProperties(ignoreUnknown = true) is correct — it makes deserialization resilient to extra fields returned by the public API. |
||
public record CharacterResponseDto( | ||
CharacterInfoDto info, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
List<CharacterResultsDto> results) { | ||
Comment on lines
+7
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: The record fields There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package mate.academy.rickandmorty.dto.external; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
|
||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @JsonIgnoreProperties(ignoreUnknown = true) is appropriate here to tolerate additional fields from the external API during deserialization. This prevents failures if the API adds fields in the future. |
||
public record CharacterResultsDto( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Declaration is correct: this record models a single result from the external API and matches the expected fields. Good use of a record for an immutable DTO. See CharacterResponseDto which uses a List for results. |
||
String id, | ||
String name, | ||
String status, | ||
String gender) { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package mate.academy.rickandmorty.dto.internal; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
|
||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure your Jackson version supports Java record deserialization (Jackson 2.12+). CharacterExternalApiService uses ObjectMapper.readValue(...) to parse external responses into DTOs; if Jackson does not support records you may see runtime deserialization errors. |
||
public record CharacterDto( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion (optional): Add a short javadoc to state that |
||
Long id, | ||
String externalId, | ||
String name, | ||
String status, | ||
String gender) { | ||
Comment on lines
+6
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This record correctly defines the response fields and keeps externalId as a String, matching the assignment example and requirements.
Comment on lines
+6
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: the app returns DTOs (not JPA entities) from controllers — verify CharacterController indeed returns CharacterDto to avoid exposing entity internals (controller already returns CharacterDto). |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package mate.academy.rickandmorty.exception; | ||
|
||
public class ExternalApiException extends RuntimeException { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class currently has only a two-argument constructor with an empty body. You may also add convenience constructors (e.g., message-only, cause-only) or declare a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: you may add a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: add a private static final long serialVersionUID for serialization stability (not required for normal use, but good practice if exceptions might be serialized). |
||
public ExternalApiException(String message, Throwable cause) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Class is fine as-is. Consider adding convenience constructors (e.g., ExternalApiException(String message) and ExternalApiException(Throwable cause)) so callers can create the exception with only a message or only a cause when appropriate. |
||
super(message, cause); | ||
Comment on lines
+4
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Constructor correctly passes the message and cause to the superclass, preserving the original exception and stack trace. This is the recommended behavior when wrapping exceptions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note for callers: when wrapping InterruptedException in this exception (as CharacterExternalApiService currently does), restore the interrupt status with Thread.currentThread().interrupt() before throwing/wrapping to avoid swallowing the interrupt signal. See CharacterExternalApiService.fetchCharacters() where ExternalApiException is used. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package mate.academy.rickandmorty.mapper; | ||
|
||
import java.util.List; | ||
import mate.academy.rickandmorty.config.MapperConfig; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: You import and reference MapperConfig here. That config currently contains a placeholder |
||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
@Mapping(target = "id", ignore = true) | ||
Character internalToModel(CharacterDto characterDto); | ||
Comment on lines
+17
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mapping(target = "id", ignore = true) prevents mapping a primary key back into the model. Because CharacterDto doesn't carry an
Comment on lines
+17
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct:
Comment on lines
+17
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: |
||
|
||
@Mappings({ | ||
@Mapping(target = "externalId", source = "id"), | ||
Comment on lines
+20
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: Mapper is configured to map |
||
@Mapping(target = "id", ignore = true) | ||
}) | ||
CharacterDto externalToDto(CharacterResultsDto characterResultsDto); | ||
|
||
@Mappings({ | ||
@Mapping(target = "externalId", source = "id"), | ||
@Mapping(target = "id", ignore = true) | ||
}) | ||
Comment on lines
+20
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: |
||
List<CharacterDto> externalToDtoList(List<CharacterResultsDto> externalDtos); | ||
|
||
void updateEntityFromDto(CharacterDto characterDto, @MappingTarget Character character); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Positive: |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommendation: Add a repository method like |
||
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: the table name is "characters", which matches the native query in CharacterRepository (SELECT * FROM characters ORDER BY RAND() LIMIT 1). This prevents table-name mismatches between entity and repository SQL. |
||
@Entity | ||
public class Character { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion (optional): The simple class name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The class name |
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
Comment on lines
+21
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: The entity defines an internal primary key There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: You currently expose the JPA entity in controllers. If you later return DTOs instead, ensure the DTO includes the internal
Comment on lines
+20
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Model correctness: The entity defines the internal primary key
Comment on lines
+21
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct: The primary key |
||
@Column(nullable = false, unique = true) | ||
@NotNull | ||
private String externalId; | ||
Comment on lines
+25
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
@Column(nullable = false) | ||
@NotBlank | ||
private String name; | ||
@Column(nullable = false) | ||
@NotBlank | ||
private String status; | ||
@Column(nullable = false) | ||
@NotBlank | ||
private String gender; | ||
Comment on lines
+27
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good:
Comment on lines
+27
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: |
||
} |
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.model.Character; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: importing and naming the entity as |
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.data.jpa.repository.Query; | ||
|
||
public interface CharacterRepository extends JpaRepository<Character, Long> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you plan to support a convenient repository-level random selection for |
||
List<Character> findByNameContainingIgnoreCase(String name); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: |
||
|
||
Optional<Character> findByExternalId(String externalId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note: findByExternalId(String) correctly matches the entity's externalId type (String) and supports the upsert logic used during startup import (repository.findByExternalId(...) is used in saveAll). Ensure the DB unique constraint on externalId is present (it is in the entity) so finds are consistent. |
||
|
||
@Query(value = "SELECT * FROM characters ORDER BY RAND() LIMIT 1", nativeQuery = true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High: the native query uses ORDER BY RAND(), which is fine for MySQL but can be brittle across different DBs/dialects (H2 used in tests). To make tests portable, either verify H2 supports this function in your test configuration or replace with a DB-agnostic approach (count + PageRequest offset) to select a random row. |
||
Character findRandomCharacter(); | ||
Comment on lines
+14
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: findRandomCharacter() returns a raw Character. If the table is empty this will return null; CharacterServiceImpl currently calls mapper.toDto(repository.findRandomCharacter()) without null-check and will throw a NullPointerException. Change the repository method to return Optional or handle null in the service and throw a meaningful exception (e.g., EntityNotFoundException). See CharacterServiceImpl#getRandomCharacter usage for context. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional approach to enforce "fetch once": instead of relying purely on upsert logic, you could check the repository (via characterService) at the start of fetchCharacters and skip fetching if characters already exist. This enforces a single import when DB is pre-populated. Be mindful of test setups where the DB may be empty and you do want to import (see Application.run). |
||
List<CharacterDto> characters = new ArrayList<>(); | ||
Comment on lines
+26
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Memory/use of batches: you collect all pages into
Comment on lines
+26
to
+27
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accumulating all pages into a single List before saving can cause high memory usage and makes the load non-idempotent in practice. Consider saving per page or in batches and deduplicating by externalId, or implement upsert logic in the service layer so repeated runs won't cause unique constraint violations. This is relevant to the overall idempotency requirement. |
||
String nextUrl = BASE_URL; | ||
|
||
HttpClient client = HttpClient.newHttpClient(); | ||
while (nextUrl != null) { | ||
HttpRequest request = HttpRequest.newBuilder() | ||
.uri(URI.create(nextUrl)) | ||
.build(); | ||
try { | ||
HttpResponse<String> response = client.send( | ||
request, HttpResponse.BodyHandlers.ofString()); | ||
if (response.statusCode() != 200) { | ||
throw new IOException("API returned non-successful status code: " | ||
+ response.statusCode()); | ||
} | ||
Comment on lines
+36
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion: consider adding basic retry/backoff or logging for non-200 responses (line 38-40) and set request timeouts to avoid long-hanging calls. This improves robustness against transient external API failures. |
||
CharacterResponseDto characterResponseDto = objectMapper.readValue( | ||
response.body(), CharacterResponseDto.class); | ||
Comment on lines
+42
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correctness: ObjectMapper.readValue(...) deserializes into Java records. Ensure your project uses a Jackson version that supports record deserialization (Jackson 2.12+), otherwise this call may fail at runtime. If necessary configure the ObjectMapper or upgrade Jackson. |
||
|
||
characters.addAll(characterMapper.externalToDtoList( | ||
characterResponseDto.results())); | ||
Comment on lines
+31
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good: the method implements paginated fetching by following the API |
||
nextUrl = characterResponseDto.info().next(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Null-safety:
Comment on lines
+45
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When advancing pages you call
Comment on lines
+31
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Positive: The code correctly uses a pagination loop with
Comment on lines
+42
to
+47
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommendation: Add a null-safety check for if (characterResponseDto.info() == null) {
nextUrl = null;
} else {
nextUrl = characterResponseDto.info().next();
} This makes the loader robust against unexpected/malformed responses. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug risk: nextUrl is set by characterResponseDto.info().next() without null checks. If info() or next() is null this will throw NPE. Add defensive checks for characterResponseDto.info() and characterResponseDto.info().next() before assigning nextUrl and decide how to handle (break loop or throw a descriptive exception). |
||
} catch (IOException | InterruptedException e) { | ||
throw new ExternalApiException( | ||
"An error occurred while trying to get a response from the server.", e); | ||
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exception wrapping bug downstream: you throw
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. InterruptedException handling: when catching
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Catching IOException | InterruptedException and wrapping into ExternalApiException is fine, but the ExternalApiException implementation currently discards the message and cause. Fix ExternalApiException to call
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important: The catch block lumps } catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new ExternalApiException("Interrupted while fetching characters", ie);
} catch (IOException e) {
throw new ExternalApiException("IO error while fetching characters", e);
} This change ensures correct thread behavior if the thread is interrupted.
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: InterruptedException is caught and wrapped but the thread interrupt status is not restored. When catching InterruptedException you should call Thread.currentThread().interrupt() before rethrowing or handling it to preserve interruption semantics. |
||
} | ||
Comment on lines
+48
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When catching InterruptedException, consider restoring the thread interrupt status via |
||
} | ||
characterService.saveAll(characters); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Idempotency issue: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You call
Comment on lines
+45
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommendation (performance/resilience): Currently all DTOs are accumulated in memory ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Positive: Delegation to
Comment on lines
+26
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High: fetchCharacters performs network calls and collects all pages into memory before saving. This can make tests slow/fragile (if invoked during test context) and cause high memory usage for many pages. Prefer persisting per page inside the loop or flushing periodically rather than accumulating the entire dataset in memory. Also ensure Application startup invocation is guarded for tests (property/profile). |
||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good: Using Lombok @requiredargsconstructor and a final service field ensures the external service is injected via constructor. This is the expected pattern for wiring the startup loader dependency.