summaryrefslogtreecommitdiff
path: root/pse-server/src/main/java/org/psesquared/server/util/RssParser.java
diff options
context:
space:
mode:
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/util/RssParser.java')
-rw-r--r--pse-server/src/main/java/org/psesquared/server/util/RssParser.java257
1 files changed, 257 insertions, 0 deletions
diff --git a/pse-server/src/main/java/org/psesquared/server/util/RssParser.java b/pse-server/src/main/java/org/psesquared/server/util/RssParser.java
new file mode 100644
index 0000000..7647fee
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/util/RssParser.java
@@ -0,0 +1,257 @@
+package org.psesquared.server.util;
+
+import com.rometools.rome.feed.synd.SyndEnclosure;
+import com.rometools.rome.feed.synd.SyndEntry;
+import com.rometools.rome.feed.synd.SyndFeed;
+import com.rometools.rome.io.FeedException;
+import com.rometools.rome.io.SyndFeedInput;
+import com.rometools.rome.io.XmlReader;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.time.DateTimeException;
+import java.time.LocalTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.RequiredArgsConstructor;
+import org.jdom2.Content;
+import org.jdom2.Element;
+import org.psesquared.server.episode.actions.api.data.access.EpisodeDao;
+import org.psesquared.server.model.Episode;
+import org.psesquared.server.model.Subscription;
+import org.psesquared.server.subscriptions.api.data.access.SubscriptionDao;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * The class responsible for fetching data from RSS feeds.
+ */
+@Component
+@RequiredArgsConstructor
+public class RssParser {
+
+ /**
+ * The Index of the Map in the List of Maps that uses GUIDs as keys.
+ */
+ private static final int GUID_KEY_MAP_INDEX = 0;
+
+ /**
+ * The Index of the Map in the List of Maps that uses URLs as keys.
+ */
+ private static final int URL_KEY_MAP_INDEX = 1;
+
+ /**
+ * The JPA repository that handles all episode related database requests.
+ */
+ private final EpisodeDao episodeDao;
+
+ /**
+ * The JPA repository that handles all subscription related database requests.
+ */
+ private final SubscriptionDao subscriptionDao;
+
+ /**
+ * Validates that the RSS-Feed associated with the Subscription is of the
+ * expected Format and that the Episodes of the Subscription are part of the
+ * feed.
+ * If the Feed is invalid the Subscription is deleted using the
+ * SubscriptionDao.
+ * Otherwise, the Episodes that are Part of the Feed are saved using the
+ * EpisodeDao, those that are not are deleted using the EpisodeDao.
+ *
+ * @param subscription The Subscription to validate
+ */
+ @Async
+ @Transactional
+ public void validate(final Subscription subscription) {
+ if (subscription == null) {
+ return;
+ }
+
+ List<Map<String, Episode>> fetchedData
+ = fetchSubscriptionFeed(subscription);
+ if (fetchedData.get(URL_KEY_MAP_INDEX).isEmpty()) {
+ subscriptionDao.deleteById(subscription.getId());
+ return;
+ }
+
+ Subscription retrievedSubscription = subscriptionDao.save(subscription);
+ List<Episode> subscriptionEpisodes = retrievedSubscription.getEpisodes();
+ if (subscriptionEpisodes == null
+ || subscriptionEpisodes.isEmpty()) {
+ return;
+ }
+
+ List<Episode> invalidEpisodes = new ArrayList<>();
+ List<Episode> validEpisodes = new ArrayList<>();
+ for (Episode episode : subscriptionEpisodes) {
+ Episode fetchedEpisode
+ = getFetchedEpisode(episode, fetchedData);
+ if (fetchedEpisode == null) {
+ invalidEpisodes.add(episode);
+ } else {
+ fetchedEpisode.setId(episode.getId());
+ validEpisodes.add(fetchedEpisode);
+ }
+ }
+ if (!invalidEpisodes.isEmpty()) {
+ episodeDao.deleteAll(invalidEpisodes);
+ }
+ if (!validEpisodes.isEmpty()) {
+ episodeDao.saveAll(validEpisodes);
+ }
+ }
+
+ private List<Map<String, Episode>> fetchSubscriptionFeed(
+ final Subscription subscription) {
+ final List<Map<String, Episode>> empty
+ = List.of(new HashMap<>(), new HashMap<>());
+
+ if (subscription.getUrl() == null) {
+ return empty;
+ }
+ // fetch feed
+ URL feedUrl;
+ try {
+ feedUrl = new URL(subscription.getUrl());
+ } catch (MalformedURLException e) {
+ return empty;
+ }
+
+ SyndFeedInput input = new SyndFeedInput();
+ SyndFeed feed;
+ try {
+ feed = input.build(new XmlReader(feedUrl));
+ } catch (FeedException | IOException | IllegalArgumentException e) {
+ return empty;
+ }
+
+ String subscriptionTitle = feed.getTitle();
+ if (subscriptionTitle == null) {
+ return empty;
+ }
+ subscription.setTitle(subscriptionTitle);
+
+ Map<String, Episode> episodesByGuid = new HashMap<>();
+ Map<String, Episode> episodesByUrl = new HashMap<>();
+ List<SyndEntry> entries = feed.getEntries();
+ for (SyndEntry syndEntry : entries) {
+ // parse syndEntry to Episode
+ Episode episode = parseEpisode(syndEntry, subscription);
+ if (episode == null) {
+ return empty;
+ }
+ if (episode.getGuid() != null) {
+ episodesByGuid.put(episode.getGuid(), episode);
+ }
+ episodesByUrl.put(episode.getUrl(), episode);
+ }
+ return List.of(episodesByGuid, episodesByUrl);
+ }
+
+ private Episode parseEpisode(final SyndEntry syndEntry,
+ final Subscription subscription) {
+ if (syndEntry == null) {
+ return null;
+ }
+ final String title = syndEntry.getTitle();
+ final String guid = syndEntry.getUri();
+
+ List<SyndEnclosure> enclosureList = syndEntry.getEnclosures();
+ if (enclosureList.size() != 1) {
+ return null;
+ }
+ SyndEnclosure enclosure = enclosureList.get(0);
+ String url = enclosure.getUrl();
+ if (title == null || url == null) {
+ return null;
+ }
+
+ int total = 0;
+ List<Element> itunesTags = syndEntry.getForeignMarkup();
+ for (Element element : itunesTags) {
+ if (!element.getName().equals("duration")) {
+ continue;
+ }
+
+ List<Content> content = element.getContent();
+ if (content.size() != 1) {
+ return null;
+ }
+ String timeString = content.get(0).getValue();
+ total = parseTimeToSeconds(timeString);
+ }
+
+ return Episode.builder()
+ .guid(guid)
+ .url(url)
+ .title(title)
+ .total(total)
+ .subscription(subscription)
+ .build();
+ }
+
+ private Episode getFetchedEpisode(
+ final Episode episode,
+ final List<Map<String, Episode>> fetchedData) {
+ final String episodeUrl = episode.getUrl();
+ if (episodeUrl == null) {
+ return null;
+ }
+ final String episodeGuid = episode.getGuid();
+ if (fetchedData.get(GUID_KEY_MAP_INDEX).containsKey(episodeGuid)) {
+ return fetchedData.get(GUID_KEY_MAP_INDEX).get(episodeGuid);
+ }
+ return fetchedData.get(URL_KEY_MAP_INDEX).get(episodeUrl);
+ }
+
+ private static int parseTimeToSeconds(final String time) {
+ final String delim = ":";
+
+ if (time == null) {
+ // Returning default value
+ return 0;
+ }
+
+ if (!time.contains(delim)) {
+ try {
+ return Integer.parseInt(time);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ StringBuilder formattedTime = new StringBuilder();
+
+ String[] datetimeStrings = time.split(delim);
+
+ if (datetimeStrings.length == 2) {
+ formattedTime.append("00" + delim);
+ }
+
+ for (int i = 0; i < datetimeStrings.length; i++) {
+ String part = datetimeStrings[i];
+ if (part.length() < 2) {
+ String toAdd = "0";
+ part = toAdd.repeat(2 - part.length()) + part;
+ }
+ formattedTime.append(part);
+
+ if (i + 1 < datetimeStrings.length) {
+ formattedTime.append(delim);
+ }
+ }
+
+ int toReturn = 0;
+
+ try {
+ toReturn = LocalTime.parse(formattedTime.toString()).toSecondOfDay();
+ } catch (DateTimeException e) {
+ // Do nothing, default value has already been set
+ }
+ return toReturn;
+ }
+}