-
Notifications
You must be signed in to change notification settings - Fork 240
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
init commit #151
base: main
Are you sure you want to change the base?
init commit #151
Changes from 2 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 |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package mate.academy.rickandmorty.config; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import mate.academy.rickandmorty.utils.DataFetcher; | ||
import org.springframework.boot.ApplicationArguments; | ||
import org.springframework.boot.ApplicationRunner; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class CustomApplicationRunner implements ApplicationRunner { | ||
private final DataFetcher dataFetcher; | ||
|
||
@Override | ||
public void run(ApplicationArguments args) { | ||
dataFetcher.populateCharactersDB(); | ||
} | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package mate.academy.rickandmorty.config; | ||
|
||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; | ||
import org.apache.hc.client5.http.impl.classic.HttpClients; | ||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; | ||
import org.springframework.web.client.RestTemplate; | ||
|
||
@Configuration | ||
public class RestTemplateConfig { | ||
|
||
@Bean | ||
public RestTemplate restTemplate() { | ||
HttpComponentsClientHttpRequestFactory httpRequestFactory | ||
= new HttpComponentsClientHttpRequestFactory(); | ||
httpRequestFactory.setHttpClient(httpClient()); | ||
return new RestTemplate(httpRequestFactory); | ||
} | ||
|
||
@Bean | ||
public CloseableHttpClient httpClient() { | ||
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); | ||
connManager.setMaxTotal(100); | ||
connManager.setDefaultMaxPerRoute(20); | ||
return HttpClients.custom() | ||
.setConnectionManager(connManager) | ||
.build(); | ||
} | ||
Comment on lines
+11
to
+30
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 beans should be created by spring, no need to create them manually, if you want to configure them somehow you can you application properties 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. In my case I add those, because spring said Action: Consider defining a bean of type 'org.springframework.web.client.RestTemplate' in your configuration. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
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.model.Character; | ||
import mate.academy.rickandmorty.service.CharacterService; | ||
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; | ||
|
||
@Tag(name = "Rick and Morty character management", | ||
description = "Endpoints for managing characters") | ||
@RestController | ||
@RequestMapping("/characters") | ||
@RequiredArgsConstructor | ||
public class CharacterController { | ||
private final CharacterService characterService; | ||
|
||
@Operation(summary = "Get one character", | ||
description = "Get random character from Rick and Morty universe") | ||
@GetMapping | ||
public Character getRandomCharacter() { | ||
return characterService.generateRandomCharacter(); | ||
} | ||
|
||
@Operation(summary = "Get list of characters by name contains 'row'", | ||
description = "Get list of character from Rick and Morty universe " | ||
+ "where character name like '%:row%'") | ||
@GetMapping("/byNameContains") | ||
public List<Character> findCharacterNameContains(@RequestParam String row) { | ||
return characterService.findCharacterNameContains(row); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package mate.academy.rickandmorty.model; | ||
|
||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; | ||
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.Getter; | ||
import lombok.Setter; | ||
import mate.academy.rickandmorty.utils.CharacterDeserializer; | ||
|
||
@Entity | ||
@Getter | ||
@Setter | ||
@Table(name = "characters") | ||
@JsonDeserialize(using = CharacterDeserializer.class) | ||
public class Character { | ||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
@Column(name = "external_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.repository; | ||
|
||
import java.util.List; | ||
import mate.academy.rickandmorty.model.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> { | ||
@Query("SELECT c FROM Character c WHERE LOWER(c.name) LIKE %:row%") | ||
List<Character> findByNameContainsIgnoreCase(String row); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package mate.academy.rickandmorty.service; | ||
|
||
import java.util.List; | ||
import mate.academy.rickandmorty.model.Character; | ||
|
||
public interface CharacterService { | ||
Character generateRandomCharacter(); | ||
|
||
List<Character> findCharacterNameContains(String row); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package mate.academy.rickandmorty.service.impl; | ||
|
||
import jakarta.transaction.Transactional; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.Random; | ||
import lombok.RequiredArgsConstructor; | ||
import mate.academy.rickandmorty.model.Character; | ||
import mate.academy.rickandmorty.repository.CharacterRepository; | ||
import mate.academy.rickandmorty.service.CharacterService; | ||
import org.springframework.stereotype.Service; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
public class CharacterServiceImpl implements CharacterService { | ||
private final CharacterRepository characterRepository; | ||
private Random random = new Random(); | ||
|
||
@Override | ||
@Transactional | ||
public Character generateRandomCharacter() { | ||
long charactersAmount = characterRepository.count(); | ||
long id = random.nextLong(charactersAmount); | ||
return Optional.of(characterRepository.findById(id)) | ||
.get() | ||
.orElseThrow(() -> new IllegalStateException("Character not found with id: " + 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. as far as I remember characterRepository.findById(id) returns optional. So you make Optional.of(Optional returned from repository).get (//Unboxing optional that you create) .orElseThrow (//Working with optional returned from repo) |
||
|
||
@Override | ||
public List<Character> findCharacterNameContains(String row) { | ||
return characterRepository.findByNameContainsIgnoreCase(row); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package mate.academy.rickandmorty.utils; | ||
|
||
import com.fasterxml.jackson.core.JacksonException; | ||
import com.fasterxml.jackson.core.JsonParser; | ||
import com.fasterxml.jackson.databind.DeserializationContext; | ||
import com.fasterxml.jackson.databind.JsonDeserializer; | ||
import com.fasterxml.jackson.databind.JsonNode; | ||
import java.io.IOException; | ||
import mate.academy.rickandmorty.model.Character; | ||
|
||
public class CharacterDeserializer extends JsonDeserializer<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. I think it is redundant, object mapper could deserialize your objects without additional configuration 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. It does, but it maps id from Json to Entity.id. I thought JsonProperty annotation will solve this problem, but had mapping exception with msg like (I want to set json.id to entity.id, but your annotation said that i should set json.id to other field) 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. That is why you need to create an externalDto to map response from API and then remap it to entity and save it, such approach will be more flexible because we have a separate class to represent characters in external API. In general better to avoid custom deserialization logic, because it is often confusing. Flow when we map external response as is and then work with it as we want is preferable, I could describe in more details on QnA |
||
private static final String ID = "id"; | ||
private static final String NAME = "name"; | ||
private static final String STATUS = "status"; | ||
private static final String GENDER = "gender"; | ||
|
||
@Override | ||
public Character deserialize(JsonParser jsonParser, | ||
DeserializationContext deserializationContext) | ||
throws IOException, JacksonException { | ||
JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser); | ||
|
||
String id = treeNode.get(ID).asText(); | ||
String name = treeNode.get(NAME).asText(); | ||
String status = treeNode.get(STATUS).asText(); | ||
String gender = treeNode.get(GENDER).asText(); | ||
|
||
Character character = new Character(); | ||
character.setExternalId(Long.valueOf(id)); | ||
character.setName(name); | ||
character.setStatus(status); | ||
character.setGender(gender); | ||
|
||
return character; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,61 @@ | ||||||
package mate.academy.rickandmorty.utils; | ||||||
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException; | ||||||
import com.fasterxml.jackson.databind.JsonNode; | ||||||
import com.fasterxml.jackson.databind.ObjectMapper; | ||||||
import java.util.List; | ||||||
import lombok.RequiredArgsConstructor; | ||||||
import mate.academy.rickandmorty.model.Character; | ||||||
import mate.academy.rickandmorty.repository.CharacterRepository; | ||||||
import org.springframework.beans.factory.annotation.Value; | ||||||
import org.springframework.stereotype.Component; | ||||||
import org.springframework.web.client.RestTemplate; | ||||||
|
||||||
@Component | ||||||
@RequiredArgsConstructor | ||||||
public class DataFetcher { | ||||||
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.
Suggested change
|
||||||
private static final String ROOT_INFO = "info"; | ||||||
private static final String ROOT_RESULTS = "results"; | ||||||
private static final String ROOT_NEXT = "next"; | ||||||
private static final String NULL_VALUE = "null"; | ||||||
private final RestTemplate restTemplate; | ||||||
private final ObjectMapper objectMapper; | ||||||
private final CharacterRepository characterRepository; | ||||||
@Value("${characters.url}") | ||||||
private String characterUrl; | ||||||
|
||||||
public void populateCharactersDB() { | ||||||
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.
Suggested change
|
||||||
String url = characterUrl; | ||||||
|
||||||
while (url != null) { | ||||||
String jsonResponse = restTemplate.getForObject(url, String.class); | ||||||
try { | ||||||
JsonNode rootNode = objectMapper.readTree(jsonResponse); | ||||||
List<Character> pageCharacters = deserializeCharacters(rootNode); | ||||||
characterRepository.saveAll(pageCharacters); | ||||||
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. I suggest to fetch all characters first and than save them all to not make request to db several times |
||||||
url = getNextPageUrl(rootNode); | ||||||
} catch (JsonProcessingException e) { | ||||||
throw new RuntimeException("Can't fetch all characters"); | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
private String getNextPageUrl(JsonNode rootNode) { | ||||||
JsonNode infoNode = rootNode.get(ROOT_INFO); | ||||||
if (infoNode != null) { | ||||||
JsonNode nextNode = infoNode.get(ROOT_NEXT); | ||||||
if (nextNode.asText() != NULL_VALUE) { | ||||||
return nextNode.asText(); | ||||||
} | ||||||
} | ||||||
return null; | ||||||
} | ||||||
|
||||||
private List<Character> deserializeCharacters(JsonNode rootNode) | ||||||
throws JsonProcessingException { | ||||||
JsonNode resultNode = rootNode.get(ROOT_RESULTS); | ||||||
return objectMapper.treeToValue(resultNode, | ||||||
objectMapper.getTypeFactory().constructCollectionType(List.class, Character.class)); | ||||||
|
||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,9 @@ | ||
|
||
spring.application.name=rickandmorty | ||
spring.datasource.url=jdbc:mysql://localhost:3306/rickandmorty | ||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver | ||
spring.datasource.username=${MYSQL_USERNAME} | ||
spring.datasource.password=${MYSQL_PASSWORD} | ||
spring.jpa.hibernate.ddl-auto=create-drop | ||
spring.jpa.show-sql=true | ||
spring.jpa.open-in-view=false | ||
characters.url=https://rickandmortyapi.com/api/character |
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.