-
Notifications
You must be signed in to change notification settings - Fork 308
Done Rick and Morty task #266
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?
Changes from 1 commit
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 |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package mate.academy.rickandmorty.client; | ||
|
||
import com.fasterxml.jackson.databind.DeserializationFeature; | ||
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 mate.academy.rickandmorty.dto.external.ExternalCharacterDto; | ||
import mate.academy.rickandmorty.dto.external.RickAndMortyResponseDto; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class RickAndMortyClient { | ||
private static final String BASE_URL = "https://rickandmortyapi.com/api/character"; | ||
private final ObjectMapper objectMapper; | ||
|
||
public RickAndMortyClient(ObjectMapper objectMapper) { | ||
this.objectMapper = objectMapper | ||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | ||
|
||
} | ||
|
||
public List<ExternalCharacterDto> fetchAllCharacters() { | ||
|
||
List<ExternalCharacterDto> allCharacters = new ArrayList<>(); | ||
|
||
HttpClient httpClient = HttpClient.newHttpClient(); | ||
|
||
HttpRequest httpRequest = HttpRequest.newBuilder() | ||
.GET() | ||
.uri(URI.create(BASE_URL)) | ||
.build(); | ||
|
||
try { | ||
HttpResponse<String> response = httpClient | ||
.send(httpRequest, HttpResponse.BodyHandlers.ofString()); | ||
|
||
System.out.println(response.body()); | ||
|
||
|
||
RickAndMortyResponseDto apiResponse = objectMapper | ||
.readValue(response.body(), RickAndMortyResponseDto.class); | ||
|
||
|
||
allCharacters.addAll(apiResponse.results()); | ||
|
||
|
||
} catch (IOException | InterruptedException e) { | ||
throw new RuntimeException("Failed to fetch information from Rick and Morty API ", e); | ||
|
||
} | ||
return allCharacters; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package mate.academy.rickandmorty.config; | ||
|
||
import jakarta.annotation.PostConstruct; | ||
import lombok.RequiredArgsConstructor; | ||
import mate.academy.rickandmorty.service.CharacterService; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
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 loader runs unconditionally because the class is annotated with @component. To avoid network calls during tests and satisfy test stability requirements, make the loader conditional. For example add: @ConditionalOnProperty(name = "app.initial-load.enabled", havingValue = "true", matchIfMissing = true) Then set app.initial-load.enabled=false in src/test/resources/application.properties and true (or omitted) in main properties. This addresses checklist item #2 (initial data fetch once on startup) while preventing test-time external calls and ensures property keys match between main and test configurations (checklist item #7). |
||
@RequiredArgsConstructor | ||
public class DataLoader { | ||
private final CharacterService characterService; | ||
|
||
@PostConstruct | ||
|
||
public void init() { | ||
System.out.println("DataLoader запускається..."); | ||
|
||
characterService.loadInitialData(); | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
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.CharacterResponseDto;import mate.academy.rickandmorty.service.CharacterService;import org.springframework.http.HttpStatus;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.ResponseStatus;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/characters")@RequiredArgsConstructor@Tag(name = "Rick and Morty API", description = "Endpoints for Rick and Morty characters")public class CharacterController { private final CharacterService characterService; @GetMapping("/random") @ResponseStatus(HttpStatus.OK) @Operation(summary = "Get random character", description = "Returns one of the character from local database") public CharacterResponseDto getRandomCharacter() { return characterService.getRandomCharacter(); } @GetMapping("/search") @ResponseStatus(HttpStatus.OK) @Operation(summary = "Search characters by name", description = "Finds all characters for the specific name") public List<CharacterResponseDto> searchCharacters(@RequestParam String name) { return characterService.searchByName(name); }} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package mate.academy.rickandmorty.dto; | ||
|
||
public record CharacterResponseDto( | ||
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 already contains the exact required response fields. Optional suggestion: if OpenAPI generation doesn't pick up Java records in your setup, consider annotating the record/components with @Schema to ensure the response schema is visible in Swagger UI (helps guarantee requirement 3.6). |
||
Long id, | ||
String externalId, | ||
String name, | ||
String status, | ||
String gender | ||
) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package mate.academy.rickandmorty.dto.external; | ||
|
||
public record ExternalCharacterDto( | ||
Integer id, | ||
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. Using Integer for |
||
String name, | ||
String status, | ||
String species, | ||
String type, | ||
String gender | ||
) {} | ||
Comment on lines
+3
to
+10
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 models the external API character shape and matches how the service maps fields (id -> externalId via dto.id().toString()). No change required.
Comment on lines
+3
to
+10
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 improvement: add @JsonProperty annotations if you want explicit mapping or add a short javadoc explaining this DTO's purpose. Not required for functionality. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package mate.academy.rickandmorty.dto.external; | ||
|
||
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 choose to use @JsonProperty("prev") Adding the import should be done on the blank/import area after the package declaration. |
||
public record InfoDto( | ||
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. Consider adding a short JavaDoc describing that this record maps the external API 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. Record declaration is otherwise fine. If you choose to annotate the component, add the import |
||
int count, | ||
int pages, | ||
String next, | ||
String previous | ||
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. The public Rick & Morty API uses the 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. The external API returns the previous page link under the 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 external API uses |
||
) {} | ||
Comment on lines
1
to
8
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 response wrapper |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package mate.academy.rickandmorty.dto.external; | ||
|
||
import io.swagger.v3.oas.models.info.Info; | ||
|
||
import java.util.List; | ||
|
||
public record RickAndMortyResponseDto( | ||
Info info, | ||
|
||
List<ExternalCharacterDto> results | ||
) {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
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 lombok.AllArgsConstructor; | ||
import lombok.Builder; | ||
import lombok.Data; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Entity | ||
@Table(name = "characters") | ||
@Data | ||
@NoArgsConstructor | ||
@AllArgsConstructor | ||
@Builder | ||
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. The 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. 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. Note: the class is named |
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@Column(name = "external_id", nullable = false) | ||
private String externalId; | ||
|
||
|
||
@Column(nullable = false) | ||
private String name; | ||
|
||
private String status; | ||
|
||
private String gender; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package mate.academy.rickandmorty.repository; | ||
|
||
import java.util.List; | ||
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 note: keep in mind the entity is named |
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
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. 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. To support efficient random selection (checklist item #3.15) you can use JpaRepository.count() together with 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 interface correctly extends JpaRepository<Character, Long> and provides the CRUD/paging methods needed by the service (count(), findAll(PageRequest), etc.). This satisfies the technical needs for random selection and persistence operations. |
||
|
||
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: this method implements the contains search required by the task (SQL LIKE %query%) and is case-insensitive, satisfying checklist item #3.5. Keep this method as-is. 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 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,12 @@ | ||
package mate.academy.rickandmorty.service; | ||
|
||
import java.util.List; | ||
import mate.academy.rickandmorty.dto.CharacterResponseDto; | ||
|
||
public interface CharacterService { | ||
void loadInitialData(); | ||
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. Add JavaDoc to describe expected semantics of loadInitialData(): that it must fetch all pages from the external API once during application startup and be idempotent (no duplicate records based on externalId). This documents checklist requirements that the implementation must satisfy. |
||
|
||
List<CharacterResponseDto> searchByName(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. Document searchByName behaviour: clarify that the method performs case-insensitive containment (SQL LIKE %query%) and should return an empty list for null/blank input instead of throwing. The implementation currently returns empty for blank; documenting it here makes the contract explicit. |
||
|
||
CharacterResponseDto getRandomCharacter(); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package mate.academy.rickandmorty.service.impl; | ||
|
||
import java.util.List; | ||
import java.util.Random; | ||
import lombok.RequiredArgsConstructor; | ||
import mate.academy.rickandmorty.client.RickAndMortyClient; | ||
import mate.academy.rickandmorty.dto.CharacterResponseDto; | ||
import mate.academy.rickandmorty.dto.external.ExternalCharacterDto; | ||
import mate.academy.rickandmorty.model.Character; | ||
import mate.academy.rickandmorty.repository.CharacterRepository; | ||
import mate.academy.rickandmorty.service.CharacterService; | ||
import org.springframework.stereotype.Service; | ||
import org.springframework.transaction.annotation.Transactional; | ||
|
||
@Service | ||
@Transactional | ||
|
||
@RequiredArgsConstructor | ||
public class CharacterServiceImpl implements CharacterService { | ||
|
||
private final CharacterRepository characterRepository; | ||
private final RickAndMortyClient rickAndMortyClient; | ||
private final Random random = new Random(); | ||
|
||
@Override | ||
public void loadInitialData() { | ||
System.out.println("loadInitialData() виконується..."); | ||
|
||
List<ExternalCharacterDto> externalCharacterDto = rickAndMortyClient.fetchAllCharacters(); | ||
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 call delegates to the HTTP client to fetch characters. The current RickAndMortyClient.fetchAllCharacters implementation only requests the base URL (first page) and does not iterate over pagination. That means you'll only persist the first page of characters — the task requires fetching all pages once at startup. Implement pagination in the client (follow info.next until null) and add retry/backoff for transient failures. |
||
System.out.println("Отримано персонажів: " + externalCharacterDto.size()); | ||
|
||
List<Character> toSaveDataEntity = externalCharacterDto.stream() | ||
.map(dto -> { | ||
Character character = new Character(); | ||
character.setExternalId(dto.id().toString()); | ||
character.setName(dto.name()); | ||
character.setStatus(dto.status()); | ||
character.setGender(dto.gender()); | ||
return character; | ||
}) | ||
.toList(); | ||
|
||
|
||
characterRepository.saveAll(toSaveDataEntity); | ||
|
||
System.out.println("Збережено у БД " + toSaveDataEntity.size() + " персонажів."); | ||
} | ||
|
||
@Override | ||
public List<CharacterResponseDto> searchByName(String name) { | ||
return (name == null || name.isBlank()) | ||
? List.of() : | ||
characterRepository.findByNameContainingIgnoreCase(name).stream() | ||
.map(this::toDto) | ||
.toList(); | ||
Comment on lines
59
to
64
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. searchByName returns an empty list when name is null or blank — this is acceptable per requirements. Ensure the controller parameter is optional (@RequestParam(required = false)) so requests without the parameter don't produce 400. The controller currently requires the parameter by default; consider aligning controller and service behavior. |
||
} | ||
|
||
@Override | ||
public CharacterResponseDto getRandomCharacter() { | ||
List<Character> characters = characterRepository.findAll(); | ||
if (characters.isEmpty()) { | ||
return null; | ||
} | ||
Character characterEntity = characters.get(random.nextInt(characters.size())); | ||
|
||
return toDto(characterEntity); | ||
} | ||
|
||
private CharacterResponseDto toDto(Character character) { | ||
return new CharacterResponseDto(character.getId(), | ||
character.getExternalId(), character.getName(), | ||
character.getStatus(), character.getGender()); | ||
} | ||
} |
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.
externalId
before saving (e.g., query existing externalIds and insert only new ones, or use unique constraint on theexternal_id
column). This prevents duplicates on repeated startups. See CharacterServiceImpl and Character entity for where this will be used.