summaryrefslogtreecommitdiff
path: root/pse-server/src/main/java/org/psesquared/server/util
diff options
context:
space:
mode:
authorOrangerot <purple@orangerot.dev>2024-06-19 00:14:49 +0200
committerOrangerot <purple@orangerot.dev>2024-06-27 12:11:14 +0200
commit5b8851b6c268d0e93c158908fbfae9f8473db5ff (patch)
tree7010eb85d86fa2da06ea4ffbcdb01a685d502ae8 /pse-server/src/main/java/org/psesquared/server/util
Initial commitHEADmain
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/util')
-rw-r--r--pse-server/src/main/java/org/psesquared/server/util/RssParser.java257
-rw-r--r--pse-server/src/main/java/org/psesquared/server/util/Scheduler.java41
-rw-r--r--pse-server/src/main/java/org/psesquared/server/util/UpdateUrlsWrapper.java35
-rw-r--r--pse-server/src/main/java/org/psesquared/server/util/package-info.java11
4 files changed, 344 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;
+ }
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/util/Scheduler.java b/pse-server/src/main/java/org/psesquared/server/util/Scheduler.java
new file mode 100644
index 0000000..fbe8194
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/util/Scheduler.java
@@ -0,0 +1,41 @@
+package org.psesquared.server.util;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import lombok.RequiredArgsConstructor;
+import org.psesquared.server.authentication.api.service.AuthenticationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * A scheduler responsible for running scheduled actions.
+ */
+@Component
+@RequiredArgsConstructor
+public class Scheduler {
+
+ /**
+ * The seconds of one day.
+ */
+ private static final long ONE_DAY = 24 * 60 * (long) 60;
+
+ /**
+ * The service class of the authentication API.
+ */
+ @Autowired
+ private final AuthenticationService authenticationService;
+
+ /**
+ * A scheduled operation that removes all non-verified users from the server,
+ * that haven't been verified since at least 24 hours.
+ * <br>
+ * Standard: Runs every day at 3 AM.
+ */
+ @Scheduled(cron = "0 0 3 * * *")
+ public void clean() {
+ authenticationService.deleteInvalidUsersOlderThan(
+ LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) - ONE_DAY);
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/util/UpdateUrlsWrapper.java b/pse-server/src/main/java/org/psesquared/server/util/UpdateUrlsWrapper.java
new file mode 100644
index 0000000..004df12
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/util/UpdateUrlsWrapper.java
@@ -0,0 +1,35 @@
+package org.psesquared.server.util;
+
+import ch.qos.logback.core.joran.sanity.Pair;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Data;
+
+/**
+ * Placeholder for a function this implementation does not support.
+ */
+@Data
+public class UpdateUrlsWrapper {
+
+ /**
+ * The timestamp of this response-wrapper.
+ */
+ private final long timestamp;
+
+ /**
+ * An empty list of URL pairs.
+ */
+ @JsonProperty(value = "update_urls")
+ private final List<Pair<String, String>> updateUrls = new ArrayList<>();
+
+ /**
+ * Creates a placeholder for a function this implementation does not support.
+ */
+ public UpdateUrlsWrapper() {
+ this.timestamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/util/package-info.java b/pse-server/src/main/java/org/psesquared/server/util/package-info.java
new file mode 100644
index 0000000..169098c
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/util/package-info.java
@@ -0,0 +1,11 @@
+/**
+ * This package features the following utility classes:
+ * <br>
+ * {@link org.psesquared.server.util.RssParser},
+ * {@link org.psesquared.server.util.Scheduler},
+ * {@link org.psesquared.server.util.UpdateUrlsWrapper}.
+ *
+ * @author PSE-Squared Team
+ * @version 1.0
+ */
+package org.psesquared.server.util;