yuzurss

Feed aggregator microservice based on Spring
git clone https://git.neuralcrash.com/yuzurss.git
Log | Files | Refs | README | LICENSE

commit 2a4db4eb31d3de959b3b9131338587e3362b35b5
parent 2787d8245f7d591820da3dd7434f5efe46385a11
Author: Kebigon <git@kebigon.xyz>
Date:   Fri, 28 Dec 2018 22:03:05 +0900

Clean code

Diffstat:
Msrc/main/java/fr/lrgn/yuzurss/FeedClient.java | 68++++++++++++++++----------------------------------------------------
Asrc/main/java/fr/lrgn/yuzurss/YuzuRSSErrorAttributes.java | 23+++++++++++++++++++++++
Asrc/main/java/fr/lrgn/yuzurss/exception/DateParseException.java | 13+++++++++++++
Asrc/main/java/fr/lrgn/yuzurss/exception/NoParserFoundException.java | 13+++++++++++++
Asrc/main/java/fr/lrgn/yuzurss/exception/YuzuRSSException.java | 16++++++++++++++++
Asrc/main/java/fr/lrgn/yuzurss/parser/AtomFeedParser.java | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/java/fr/lrgn/yuzurss/parser/FeedParser.java | 17+++++++++++++++++
Asrc/main/java/fr/lrgn/yuzurss/parser/RSSFeedParser.java | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 227 insertions(+), 52 deletions(-)

diff --git a/src/main/java/fr/lrgn/yuzurss/FeedClient.java b/src/main/java/fr/lrgn/yuzurss/FeedClient.java @@ -1,12 +1,7 @@ package fr.lrgn.yuzurss; import java.net.URI; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import org.json.JSONException; import org.json.JSONObject; import org.json.XML; import org.slf4j.Logger; @@ -17,19 +12,19 @@ import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient.RequestHeadersSpec; import org.springframework.web.reactive.function.client.WebClient.ResponseSpec; +import fr.lrgn.yuzurss.exception.NoParserFoundException; +import fr.lrgn.yuzurss.parser.AtomFeedParser; +import fr.lrgn.yuzurss.parser.FeedParser; +import fr.lrgn.yuzurss.parser.RSSFeedParser; import reactor.core.publisher.Flux; @Component public class FeedClient { - private static final String ATOM_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX"; // 2018-11-03T18:12:15+00:00 - private static final String RSS_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss Z"; // Sun, 09 Dec 2018 09:22:00 +0000 - - private final SimpleDateFormat atomDateFormat = new SimpleDateFormat(ATOM_DATE_FORMAT); - private final SimpleDateFormat rssDateFormat = new SimpleDateFormat(RSS_DATE_FORMAT, Locale.ROOT); - private final Logger log = LoggerFactory.getLogger(getClass()); + private final FeedParser[] parsers = new FeedParser[] { new AtomFeedParser(), new RSSFeedParser() }; + @Cacheable("feeds") public Flux<FeedEntry> getFeed(URI uri) { @@ -37,6 +32,8 @@ public class FeedClient final RequestHeadersSpec<?> request = client.get().uri(uri); final ResponseSpec response = request.retrieve(); + log.info("Downloading {}", uri); + return response.bodyToMono(String.class).flatMapMany(xml -> { log.info("Downloaded {}", uri); @@ -44,9 +41,9 @@ public class FeedClient try { - return isAtom(root) ? parseAtomFeed(root) : parseRSSFeed(root); + return getFeedParser(uri, root).parseFeed(root); } - catch (final JSONException | ParseException e) + catch (final Throwable e) { log.info("Exception while parsing {}", uri, e); return Flux.empty(); @@ -54,44 +51,12 @@ public class FeedClient }).cache(); } - private static boolean isAtom(JSONObject root) - { - return root.has("feed"); - } - - private Flux<FeedEntry> parseAtomFeed(JSONObject root) throws JSONException, ParseException - { - Flux<FeedEntry> entries = Flux.empty(); - - for (final Object entry : root.getJSONObject("feed").getJSONArray("entry")) - { - final String author = ((JSONObject) entry).getJSONObject("author").getString("name"); - final String link = ((JSONObject) entry).getJSONObject("link").getString("href"); - final String title = ((JSONObject) entry).getString("title"); - final Date published = atomDateFormat.parse(((JSONObject) entry).getString("published")); - - entries = entries.mergeWith(Flux.just(new FeedEntry(title, link, published, author))); - } - - return entries; - } - - private Flux<FeedEntry> parseRSSFeed(JSONObject root) throws JSONException, ParseException + private FeedParser getFeedParser(URI uri, JSONObject root) { - Flux<FeedEntry> entries = Flux.empty(); - - final JSONObject channel = root.getJSONObject("rss").getJSONObject("channel"); - final String author = channel.getString("title"); - - for (final Object entry : channel.getJSONArray("item")) - { - final String link = ((JSONObject) entry).getString("link"); - final String title = ((JSONObject) entry).getString("title"); - final Date published = rssDateFormat.parse(((JSONObject) entry).getString("pubDate")); - - entries = entries.mergeWith(Flux.just(new FeedEntry(title, link, published, author))); - } + for (final FeedParser parser : parsers) + if (parser.acceptFeed(root)) + return parser; - return entries; + throw new NoParserFoundException(uri); } -} -\ No newline at end of file +} diff --git a/src/main/java/fr/lrgn/yuzurss/YuzuRSSErrorAttributes.java b/src/main/java/fr/lrgn/yuzurss/YuzuRSSErrorAttributes.java @@ -0,0 +1,23 @@ +package fr.lrgn.yuzurss; + +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; + +@Component +public class YuzuRSSErrorAttributes extends DefaultErrorAttributes +{ + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Override + public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) + { + log.error("Error while processsing request {}", request.uri(), getError(request)); + + return super.getErrorAttributes(request, includeStackTrace); + } +} diff --git a/src/main/java/fr/lrgn/yuzurss/exception/DateParseException.java b/src/main/java/fr/lrgn/yuzurss/exception/DateParseException.java @@ -0,0 +1,13 @@ +package fr.lrgn.yuzurss.exception; + +import java.text.ParseException; + +public class DateParseException extends YuzuRSSException +{ + private static final long serialVersionUID = 1L; + + public DateParseException(String date, String format, ParseException cause) + { + super("Unable to parse date " + date + " with format " + format, cause); + } +} diff --git a/src/main/java/fr/lrgn/yuzurss/exception/NoParserFoundException.java b/src/main/java/fr/lrgn/yuzurss/exception/NoParserFoundException.java @@ -0,0 +1,13 @@ +package fr.lrgn.yuzurss.exception; + +import java.net.URI; + +public class NoParserFoundException extends YuzuRSSException +{ + private static final long serialVersionUID = 1L; + + public NoParserFoundException(URI uri) + { + super("No parser found for feed " + uri); + } +} diff --git a/src/main/java/fr/lrgn/yuzurss/exception/YuzuRSSException.java b/src/main/java/fr/lrgn/yuzurss/exception/YuzuRSSException.java @@ -0,0 +1,16 @@ +package fr.lrgn.yuzurss.exception; + +abstract class YuzuRSSException extends RuntimeException +{ + private static final long serialVersionUID = 1L; + + protected YuzuRSSException(String message) + { + super(message); + } + + protected YuzuRSSException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/src/main/java/fr/lrgn/yuzurss/parser/AtomFeedParser.java b/src/main/java/fr/lrgn/yuzurss/parser/AtomFeedParser.java @@ -0,0 +1,63 @@ +package fr.lrgn.yuzurss.parser; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.json.JSONObject; + +import fr.lrgn.yuzurss.FeedEntry; +import fr.lrgn.yuzurss.exception.DateParseException; +import reactor.core.publisher.Flux; + +public class AtomFeedParser extends FeedParser +{ + private static final String ATOM_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssXXX"; // 2018-11-03T18:12:15+00:00 + + private static final ThreadLocal<SimpleDateFormat> atomDateFormat = new ThreadLocal<SimpleDateFormat>() + { + @Override + protected SimpleDateFormat initialValue() + { + return new SimpleDateFormat(ATOM_DATE_FORMAT); + }; + }; + + @Override + public boolean acceptFeed(JSONObject root) + { + return root.has("feed"); + } + + @Override + public Flux<FeedEntry> parseFeed(JSONObject root) + { + Flux<FeedEntry> entries = Flux.empty(); + + for (final Object entry : root.getJSONObject("feed").getJSONArray("entry")) + { + log.debug("Parsing entry {}", entry); + + final String author = ((JSONObject) entry).getJSONObject("author").getString("name"); + final String link = ((JSONObject) entry).getJSONObject("link").getString("href"); + final String title = ((JSONObject) entry).getString("title"); + final Date published = parseDate(((JSONObject) entry).getString("published")); + + entries = entries.mergeWith(Flux.just(new FeedEntry(title, link, published, author))); + } + + return entries; + } + + public Date parseDate(String date) + { + try + { + return atomDateFormat.get().parse(date); + } + catch (final ParseException ex) + { + throw new DateParseException(date, ATOM_DATE_FORMAT, ex); + } + } +} diff --git a/src/main/java/fr/lrgn/yuzurss/parser/FeedParser.java b/src/main/java/fr/lrgn/yuzurss/parser/FeedParser.java @@ -0,0 +1,17 @@ +package fr.lrgn.yuzurss.parser; + +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import fr.lrgn.yuzurss.FeedEntry; +import reactor.core.publisher.Flux; + +public abstract class FeedParser +{ + protected final Logger log = LoggerFactory.getLogger(getClass()); + + public abstract boolean acceptFeed(JSONObject root); + + public abstract Flux<FeedEntry> parseFeed(JSONObject root); +} diff --git a/src/main/java/fr/lrgn/yuzurss/parser/RSSFeedParser.java b/src/main/java/fr/lrgn/yuzurss/parser/RSSFeedParser.java @@ -0,0 +1,66 @@ +package fr.lrgn.yuzurss.parser; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import org.json.JSONObject; + +import fr.lrgn.yuzurss.FeedEntry; +import fr.lrgn.yuzurss.exception.DateParseException; +import reactor.core.publisher.Flux; + +public class RSSFeedParser extends FeedParser +{ + private static final String RSS_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss Z"; // Sun, 09 Dec 2018 09:22:00 +0000 + + private static final ThreadLocal<SimpleDateFormat> rssDateFormat = new ThreadLocal<SimpleDateFormat>() + { + @Override + protected SimpleDateFormat initialValue() + { + return new SimpleDateFormat(RSS_DATE_FORMAT, Locale.ROOT); + }; + }; + + @Override + public boolean acceptFeed(JSONObject root) + { + return root.has("rss"); + } + + @Override + public Flux<FeedEntry> parseFeed(JSONObject root) + { + Flux<FeedEntry> entries = Flux.empty(); + + final JSONObject channel = root.getJSONObject("rss").getJSONObject("channel"); + final String author = channel.getString("title"); + + for (final Object entry : channel.getJSONArray("item")) + { + log.debug("Parsing entry {}", entry); + + final String link = ((JSONObject) entry).getString("link"); + final String title = ((JSONObject) entry).getString("title"); + final Date published = parseDate(((JSONObject) entry).getString("pubDate")); + + entries = entries.mergeWith(Flux.just(new FeedEntry(title, link, published, author))); + } + + return entries; + } + + public Date parseDate(String date) + { + try + { + return rssDateFormat.get().parse(date); + } + catch (final ParseException ex) + { + throw new DateParseException(date, RSS_DATE_FORMAT, ex); + } + } +}