diff --git a/src/main/java/dev/dobicinaitis/feedreader/services/FeedReaderService.java b/src/main/java/dev/dobicinaitis/feedreader/services/FeedReaderService.java index 716d368..5a324d5 100644 --- a/src/main/java/dev/dobicinaitis/feedreader/services/FeedReaderService.java +++ b/src/main/java/dev/dobicinaitis/feedreader/services/FeedReaderService.java @@ -4,17 +4,31 @@ import com.apptasticsoftware.rssreader.RssReader; import com.apptasticsoftware.rssreader.util.ItemComparator; import dev.dobicinaitis.feedreader.exceptions.FeedReaderRuntimeException; -import lombok.RequiredArgsConstructor; +import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import javax.net.ssl.SSLContext; import java.io.IOException; +import java.net.http.HttpClient; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; import java.util.List; @Slf4j -@RequiredArgsConstructor +@AllArgsConstructor public class FeedReaderService { + private static final int CONNECTION_TIMEOUT_IN_SECONDS = 10; + private static final int FEED_READ_RETRY_COUNT = 3; + private final String url; + private final HttpClient httpClient; + + public FeedReaderService(String url) { + this.url = url; + this.httpClient = createHttpClient(CONNECTION_TIMEOUT_IN_SECONDS); + } /** @@ -23,14 +37,55 @@ public class FeedReaderService { * @return list of RSS feed items */ public List getItems() { - final RssReader rssReader = new RssReader(); + return loadItems(FEED_READ_RETRY_COUNT).stream() + .sorted(ItemComparator.oldestItemFirst()) + .toList(); + } + + /** + * Loads items from the RSS feed URL. + * + * @param retriesLeft number of attempts to load the RSS feed + * @return list of RSS feed items + */ + protected List loadItems(int retriesLeft) { + final RssReader rssReader = new RssReader(httpClient); + log.debug("Loading items from the RSS feed."); + while (true) { + try { + return rssReader.read(url).toList(); + } catch (IOException e) { + log.error("Could not load the RSS feed, reason {}", e.getMessage()); + retriesLeft--; + if (retriesLeft == 0) { + throw new FeedReaderRuntimeException(e); + } + } + log.info("Retrying to load the RSS feed, retries left: {}", retriesLeft); + } + } + + /** + * Creates a new HTTP client with a custom connection timeout. + * + * @return HTTP client + */ + protected static HttpClient createHttpClient(int timeoutInSeconds) { + HttpClient client; try { - return rssReader.read(url) - .sorted(ItemComparator.oldestItemFirst()) - .toList(); - } catch (IOException e) { - log.error("Could not load items from the RSS feed."); - throw new FeedReaderRuntimeException(e); + var sslContext = SSLContext.getInstance("TLSv1.3"); + sslContext.init(null, null, null); + client = HttpClient.newBuilder() + .sslContext(sslContext) + .connectTimeout(Duration.ofSeconds(timeoutInSeconds)) + .followRedirects(HttpClient.Redirect.ALWAYS) + .build(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { + client = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(timeoutInSeconds)) + .followRedirects(HttpClient.Redirect.ALWAYS) + .build(); } + return client; } } diff --git a/src/test/java/dev/dobicinaitis/feedreader/services/FeedReaderServiceTest.java b/src/test/java/dev/dobicinaitis/feedreader/services/FeedReaderServiceTest.java index 94b1076..c27ed47 100644 --- a/src/test/java/dev/dobicinaitis/feedreader/services/FeedReaderServiceTest.java +++ b/src/test/java/dev/dobicinaitis/feedreader/services/FeedReaderServiceTest.java @@ -4,11 +4,15 @@ import dev.dobicinaitis.feedreader.helpers.TestFeedServer; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import java.net.http.HttpClient; import static org.junit.jupiter.api.Assertions.*; class FeedReaderServiceTest { + private static final String NON_ROUTABLE_IP = "10.255.255.1"; private static final TestFeedServer feedServer = new TestFeedServer(); @AfterAll @@ -36,5 +40,33 @@ void shouldThrowFeedReaderRuntimeExceptionWhenFeedIsUnavailable() { final FeedReaderService feedReader = new FeedReaderService(feedUrl); assertThrows(FeedReaderRuntimeException.class, feedReader::getItems, "Should throw FeedReaderRuntimeException"); } + + @Test + @Timeout(3) + void shouldRespectHttpRequestTimeoutSettings() { + // given + final int timeoutInSeconds = 1; + final int retryCount = 1; + final String feedUrl = "https://" + NON_ROUTABLE_IP; + final HttpClient httpClient = FeedReaderService.createHttpClient(timeoutInSeconds); + final FeedReaderService feedReader = new FeedReaderService(feedUrl, httpClient); + // when, then + assertThrows(FeedReaderRuntimeException.class, () -> feedReader.loadItems(retryCount), "Should throw FeedReaderRuntimeException"); + // the timeout should occur after 1 second, so anything under 3 seconds is fine + } + + @Test + @Timeout(5) + void shouldAttemptToLoadFeedMultipleTimes(){ + // given + final int timeoutPerRequestInSeconds = 1; + final int retryCount = 3; + final String feedUrl = "https://" + NON_ROUTABLE_IP; + final HttpClient httpClient = FeedReaderService.createHttpClient(timeoutPerRequestInSeconds); + final FeedReaderService feedReader = new FeedReaderService(feedUrl, httpClient); + // when, then + assertThrows(FeedReaderRuntimeException.class, () -> feedReader.loadItems(retryCount), "Should throw FeedReaderRuntimeException"); + // the timeout should occur after 3 seconds, so anything under 5 seconds is fine + } }