Skip to content

Commit

Permalink
Support multiple news locales (#17)
Browse files Browse the repository at this point in the history
* Support multiple news locales

* Update page header for multiple country news

* Update controller & templates

* Add tests

* Update VERSION
  • Loading branch information
garamb1 authored Apr 10, 2024
1 parent 2795950 commit b4b2c00
Show file tree
Hide file tree
Showing 16 changed files with 261 additions and 36 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.1
0.7
13 changes: 13 additions & 0 deletions src/main/java/it/garambo/retrosearch/configuration/BeanConfig.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package it.garambo.retrosearch.configuration;

import java.util.List;
import java.util.Locale;
import org.apache.commons.validator.routines.UrlValidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.util.StringUtils;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;

Expand Down Expand Up @@ -34,6 +37,16 @@ public ApplicationSettings applicationSettings(
return new ApplicationSettings(HTMLVersion.getByVersionName(htmlVersionValue), encodingValue);
}

@Bean
public NewsSettings newsSettings(
@Value("${retrosearch.news.enable}") boolean enabled,
@Value("${retrosearch.news.api.locales}") List<String> localeList,
@Value("${retrosearch.news.api.rate.limiter}") long rateLimiter) {
List<Locale> locales = localeList.stream().map(StringUtils::parseLocaleString).toList();

return new NewsSettings(enabled, rateLimiter, locales);
}

@Bean
public ThymeleafViewResolver thymeleafViewResolver(
@Autowired SpringTemplateEngine templateEngine,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package it.garambo.retrosearch.configuration;

import java.util.List;
import java.util.Locale;
import lombok.Getter;

@Getter
public class NewsSettings {

private final boolean enabled;
private final long rateLimiter;
private final List<Locale> locales;

public NewsSettings(boolean enabled, long rateLimiter, List<Locale> locales) {
this.enabled = enabled;
this.rateLimiter = rateLimiter;
this.locales = locales;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@ConditionalOnBean(NewsRepository.class)
Expand All @@ -14,9 +15,14 @@ public class NewsController {
@Autowired private NewsRepository newsRepository;

@GetMapping("/news")
public String news(Model model) {
public String news(@RequestParam String country, Model model) {
if (country.isEmpty() || !newsRepository.isCountrySupported(country)) {
return "redirect:/";
}

model.addAttribute("country", country);
model.addAttribute("updatedAt", newsRepository.getUpdatedAt());
model.addAttribute("articles", newsRepository.getAllArticles());
model.addAttribute("articles", newsRepository.getArticlesByCountry(country));
return "news";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@ public class GNewsApiClient {

@Autowired HttpService httpService;

public GNewsApiResponse fetchNews() throws URISyntaxException, IOException {
public GNewsApiResponse fetchNews(String language, String country)
throws URISyntaxException, IOException {
URI apiUri = new URI(API_URL);
Map<String, String> params =
Map.of(
"category", "general",
"max", "10",
"lang", "en",
"country", "us",
"lang", language,
"country", country,
"apikey", apiKey);

String response = httpService.get(apiUri, params);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package it.garambo.retrosearch.news.repository;

import static java.util.Objects.isNull;

import it.garambo.retrosearch.news.model.Article;
import java.util.Date;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
Expand All @@ -12,24 +15,24 @@
@ConditionalOnProperty(value = "retrosearch.news.enable", havingValue = "true")
public class InMemoryNewsRepository implements NewsRepository {

private List<Article> articles;
private Map<String, List<Article>> articles;
private Date updatedAt;

@Override
public List<Article> getAllArticles() {
return articles;
public List<Article> getArticlesByCountry(String country) {
return articles.get(country);
}

@Override
public Article getArticle(int index) {
return articles.get(index);
public boolean isCountrySupported(String country) {
return !isNull(articles) && articles.containsKey(country);
}

@Override
public void updateAll(List<Article> newArticles) {
public void updateAll(Map<String, List<Article>> newArticles) {
articles = newArticles;
updatedAt = new Date();
log.info("Article list updated");
log.info("Article list updated, news loaded for {}", articles.keySet());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
import it.garambo.retrosearch.news.model.Article;
import java.util.Date;
import java.util.List;
import java.util.Map;

public interface NewsRepository {

List<Article> getAllArticles();
List<Article> getArticlesByCountry(String country);

Article getArticle(int index);

void updateAll(List<Article> newArticles);
void updateAll(Map<String, List<Article>> newArticles);

Date getUpdatedAt();

boolean isCountrySupported(String country);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package it.garambo.retrosearch.news.scheduled;

import it.garambo.retrosearch.configuration.NewsSettings;
import it.garambo.retrosearch.news.client.GNewsApiClient;
import it.garambo.retrosearch.news.model.Article;
import it.garambo.retrosearch.news.repository.NewsRepository;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
Expand All @@ -14,19 +21,41 @@
@Component
@ConditionalOnProperty(value = "retrosearch.news.enable", havingValue = "true")
public class NewsScheduledTask {
@Autowired private NewsSettings settings;

@Autowired private GNewsApiClient apiClient;

@Autowired private NewsRepository repository;

@Scheduled(fixedRate = 60 * 60 * 1000)
private void updateNews() {
try {
log.info("Updating article lists...");
List<Article> articles = apiClient.fetchNews().articles();
repository.updateAll(articles);
} catch (Exception e) {
log.error("Article list update failed:", e);
}
private void updateNews() throws Exception {
List<Locale> locales = settings.getLocales();
log.info("Updating article lists... Requested Locales: {}", locales);

Map<String, List<Article>> newArticles = new HashMap<>();

Executors.newScheduledThreadPool(1)
.execute(
() -> {
for (Locale locale : locales) {
try {
List<Article> articles =
apiClient.fetchNews(locale.getLanguage(), locale.getCountry()).articles();
newArticles.put(locale.getDisplayCountry(), articles);
log.info("Fetched news for {}", locale);
Thread.sleep(settings.getRateLimiter());
} catch (IOException | URISyntaxException | InterruptedException e) {
log.error("Could not fetch news for {}", locale, e);
}
}

if (newArticles.keySet().size() < locales.size()) {
log.error(
"Could not retrieve news for all countries/locales. The current news map won't be updated.");
} else {
log.info("News fetching complete for requested locales: {}", locales);
repository.updateAll(newArticles);
}
});
}
}
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ retrosearch.encoding=UTF-8
retrosearch.html.version=3.2
retrosearch.news.enable=${NEWS_ACTIVE:false}
retrosearch.news.api.key=${NEWS_API_KEY:}
retrosearch.news.api.rate.limiter=${NEWS_API_RATE_LIMITER:3000}
retrosearch.news.api.locales=${NEWS_API_LOCALES:en_US,en_UK,it_IT,de_DE}

retrosearch.sports.football.enable=${FOOTBALL_API_ACTIVE:false}
retrosearch.sports.football.api.key=${FOOTBALL_API_KEY:}
Expand Down
23 changes: 19 additions & 4 deletions src/main/resources/templates/fragments/header.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
<a href="/"><img th:src="@{__${@logoPath}__}" alt="A RetroSearch Logo"></a>
<th:block th:if="${@environment.getProperty('retrosearch.news.enable') and

<th:block th:if="${@environment.getProperty('retrosearch.news.enable') or
@environment.getProperty('retrosearch.sports.football.enable')}">
<h4><a href="/">Search and Browse</a> - <a href="/news">News</a> - <a href="/football">Football scores</a></h4>
<h4>
<a href="/">Home</a>
<th:block th:if="${@environment.getProperty('retrosearch.news.enable')}">
- News (
<th:block th:each="locale,iteration: ${@newsSettings.getLocales()}">
<a th:href="@{ /news(country=${locale.getDisplayCountry()}) }" th:text="${locale.getDisplayCountry()}"></a>
<th:block th:text="${!iteration.last ? '|' : ''}"></th:block>
</th:block>
)
</th:block>

<th:block th:if="${@environment.getProperty('retrosearch.sports.football.enable')}">
- <a href="/football">Football scores</a>
</th:block>
</h4>
</th:block>

<form action="/search" method="get">
Search Query:<input type="text" th:value="${searchResults?.query}" name="query">
<input type="submit" value="Go!">
</form>
<br>
</form>
2 changes: 1 addition & 1 deletion src/main/resources/templates/news.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<body>
<th:block th:insert="fragments/header.html"></th:block>

<h2>Latest news</h2>
<h2>Latest news | <em th:text="${country}"></em></h2>
<p>Updated: <em th:text="${updatedAt}"></em></p>
<ul>
<li th:each="element: ${articles}">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package it.garambo.retrosearch;

import it.garambo.retrosearch.news.repository.InMemoryNewsRepository;
import it.garambo.retrosearch.news.repository.NewsRepository;
import it.garambo.retrosearch.sports.football.repository.FootballRepository;
import it.garambo.retrosearch.sports.football.repository.InMemoryFootballRepository;
import org.springframework.boot.test.context.TestConfiguration;
Expand All @@ -11,7 +13,13 @@ public class PrimaryTestConfiguration {

@Bean
@Primary
public FootballRepository repository() {
public FootballRepository footballRepository() {
return new InMemoryFootballRepository();
}

@Bean
@Primary
public NewsRepository newsRepository() {
return new InMemoryNewsRepository();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package it.garambo.retrosearch.news.client;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

import it.garambo.retrosearch.PrimaryTestConfiguration;
import it.garambo.retrosearch.http.HttpService;
import it.garambo.retrosearch.news.model.GNewsApiResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.Resource;
import org.springframework.test.util.ReflectionTestUtils;

@ExtendWith(MockitoExtension.class)
@SpringBootTest(classes = PrimaryTestConfiguration.class)
class GNewsApiClientTest {

@Mock private HttpService httpService;

@InjectMocks GNewsApiClient client;

@BeforeEach
void setup() {
ReflectionTestUtils.setField(client, "apiKey", "testKey");
}

@Test
void testFetchNews(@Value("classpath:news/response.json") Resource responseJson)
throws IOException, URISyntaxException {
URI uri = new URI("https://gnews.io/api/v4/top-headlines");

Map<String, String> params =
Map.of(
"category", "general",
"max", "10",
"lang", "it",
"country", "it",
"apikey", "testKey");

when(httpService.get(eq(uri), eq(params)))
.thenReturn(responseJson.getContentAsString(StandardCharsets.UTF_8));

GNewsApiResponse response = client.fetchNews("it", "it");
assertNotNull(response);
assertEquals(54904, response.totalArticles());
assertNotNull(response.articles());
assertEquals(1, response.articles().size());
}
}
Loading

0 comments on commit b4b2c00

Please sign in to comment.