diff options
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/util')
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; |