summaryrefslogtreecommitdiff
path: root/pse-server/src/main/java/org/psesquared/server/subscriptions/api/service
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/subscriptions/api/service
Initial commitHEADmain
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/subscriptions/api/service')
-rw-r--r--pse-server/src/main/java/org/psesquared/server/subscriptions/api/service/SubscriptionService.java299
-rw-r--r--pse-server/src/main/java/org/psesquared/server/subscriptions/api/service/package-info.java13
2 files changed, 312 insertions, 0 deletions
diff --git a/pse-server/src/main/java/org/psesquared/server/subscriptions/api/service/SubscriptionService.java b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/service/SubscriptionService.java
new file mode 100644
index 0000000..08dc1f9
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/service/SubscriptionService.java
@@ -0,0 +1,299 @@
+package org.psesquared.server.subscriptions.api.service;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Map;
+import java.util.HashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import lombok.RequiredArgsConstructor;
+import org.psesquared.server.authentication.api.data.access.AuthenticationDao;
+import org.psesquared.server.episode.actions.api.controller.EpisodeActionPost;
+import org.psesquared.server.episode.actions.api.data.access.EpisodeActionDao;
+import org.psesquared.server.episode.actions.api.service.EpisodeActionService;
+import org.psesquared.server.model.Subscription;
+import org.psesquared.server.model.SubscriptionAction;
+import org.psesquared.server.model.User;
+import org.psesquared.server.subscriptions.api.controller.SubscriptionDelta;
+import org.psesquared.server.subscriptions.api.controller.SubscriptionTitles;
+import org.psesquared.server.subscriptions.api.data.access.SubscriptionActionDao;
+import org.psesquared.server.subscriptions.api.data.access.SubscriptionDao;
+import org.psesquared.server.util.RssParser;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * This service class manages all business logic associated with the
+ * episode action API.
+ * <br>
+ * It is called from the
+ * {@link
+ * org.psesquared.server.subscriptions.api.controller.SubscriptionController}
+ * and passes on requests concerning data access mainly to the
+ * {@link SubscriptionDao} and {@link SubscriptionActionDao}.
+ */
+@Service
+@Transactional
+@RequiredArgsConstructor
+public class SubscriptionService {
+
+ /**
+ * The error message that is logged if no subscription exists
+ * for a remove action.
+ */
+ private static final String NO_SUB_WARNING
+ = "Subscription for remove action does not exist!";
+
+ /**
+ * The logger for logging some warnings.
+ */
+ private static final Logger LOGGER
+ = Logger.getLogger(SubscriptionService.class.getName());
+
+ /**
+ * The class for fetching data from RSS feeds.
+ */
+ private final RssParser rssParser;
+
+ /**
+ * The JPA repository that handles all user related database requests.
+ */
+ private final AuthenticationDao authenticationDao;
+
+ /**
+ * The JPA repository that handles all subscription related database requests.
+ */
+ private final SubscriptionDao subscriptionDao;
+
+ /**
+ * The JPA repository that handles all subscription action related database
+ * requests.
+ */
+ private final SubscriptionActionDao subscriptionActionDao;
+
+ /**
+ * The JPA repository that handles all episode action related database
+ * requests.
+ */
+ private final EpisodeActionDao episodeActionDao;
+
+ /**
+ * The service class of the episode action API.
+ */
+ private final EpisodeActionService episodeActionService;
+
+ /**
+ * It takes a list of podcast URLs in the form of strings and checks if they
+ * exist in the database.
+ * If they do not exist yet, it creates them.
+ * <br>
+ * Then it checks, if the user already has a subscription action for each
+ * subscription, separately.
+ * If not, it creates one.
+ * If yes, it updates the action.
+ *
+ * @param username The username of the user
+ * @param subscriptionStrings List of Strings, each String is a URL of a
+ * podcast
+ * @return {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ */
+ public HttpStatus uploadSubscriptions(
+ final String username,
+ final List<String> subscriptionStrings) {
+ User user;
+ try {
+ user = authenticationDao.findByUsername(username)
+ .orElseThrow();
+ } catch (NoSuchElementException e) {
+ return HttpStatus.NOT_FOUND;
+ }
+
+ Subscription subscription;
+ for (String subscriptionString : subscriptionStrings) {
+
+ try {
+ subscription
+ = subscriptionDao.findByUrl(subscriptionString).orElseThrow();
+ } catch (NoSuchElementException e) {
+ subscription = Subscription.builder()
+ .url(subscriptionString)
+ .timestamp(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC))
+ .build();
+ subscriptionDao.save(subscription);
+ rssParser.validate(subscription);
+ }
+
+ try {
+ SubscriptionAction subscriptionAction
+ = subscriptionActionDao
+ .findByUserAndSubscription(user, subscription)
+ .orElseThrow();
+ subscriptionAction.setAdded(true);
+ subscriptionAction.setTimestamp(
+ LocalDateTime.now().toEpochSecond(ZoneOffset.UTC));
+ subscriptionActionDao.save(subscriptionAction);
+ } catch (NoSuchElementException e) {
+ SubscriptionAction subscriptionAction = SubscriptionAction.builder()
+ .user(user)
+ .added(true)
+ .subscription(subscription)
+ .timestamp(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC))
+ .build();
+ subscriptionActionDao.save(subscriptionAction);
+ }
+ }
+
+ return HttpStatus.OK;
+ }
+
+ /**
+ * It returns a URL List of all podcasts the user is subscribed to in the form
+ * of a String list.
+ *
+ * @param username The username of the user whose subscriptions are being
+ * requested
+ * @return A list of RSS-Feed URLs of all subscribed podcasts
+ */
+ public List<String> getSubscriptions(final String username) {
+ List<SubscriptionAction> subscriptionActions
+ = subscriptionActionDao.findByUserUsernameAndAddedTrue(username);
+ List<String> subscriptionUrls = new ArrayList<>();
+ for (SubscriptionAction subscriptionAction : subscriptionActions) {
+ subscriptionUrls.add(subscriptionAction.getSubscription().getUrl());
+ }
+ return subscriptionUrls;
+ }
+
+ /**
+ * All subscription changes of the user are uploaded to the database.
+ *
+ * @param username The username of the user uploading their subscription
+ * changes
+ * @param delta The subscription changes in the form of a SubscriptionDelta
+ * containing the added / removed subscriptions
+ */
+ public void applySubscriptionDelta(
+ final String username,
+ final SubscriptionDelta delta) {
+
+ SubscriptionDelta minimizedDelta = minimizeDelta(delta);
+
+ uploadSubscriptions(username, minimizedDelta.getAdd());
+ for (String removeSub : minimizedDelta.getRemove()) {
+ try {
+ SubscriptionAction subscriptionAction
+ = subscriptionActionDao
+ .findByUserUsernameAndSubscriptionUrl(username, removeSub)
+ .orElseThrow();
+ subscriptionAction.setAdded(false);
+ subscriptionAction.setTimestamp(
+ LocalDateTime.now().toEpochSecond(ZoneOffset.UTC));
+ subscriptionActionDao.save(subscriptionAction);
+ episodeActionDao
+ .deleteByUserUsernameAndEpisodeSubscriptionUrl(username, removeSub);
+ } catch (NoSuchElementException e) {
+ LOGGER.log(Level.WARNING, NO_SUB_WARNING);
+ }
+ }
+ }
+
+ private SubscriptionDelta minimizeDelta(SubscriptionDelta oldDelta){
+ SubscriptionDelta minimizedDelta = new SubscriptionDelta(new ArrayList<>(), new ArrayList<>());
+
+ Map<String, Integer> deltaMap = new HashMap<>();
+ for (String addString : oldDelta.getAdd()) {
+ if(deltaMap.containsKey(addString)) {
+ deltaMap.put(addString, deltaMap.get(addString) + 1);
+ }
+ else{
+ deltaMap.put(addString, 1);
+ }
+ }
+
+ for (String removeString : oldDelta.getRemove()) {
+ if(deltaMap.containsKey(removeString)) {
+ deltaMap.put(removeString, deltaMap.get(removeString) - 1);
+ }
+ else{
+ deltaMap.put(removeString, -1);
+ }
+ }
+
+ for(Map.Entry<String, Integer> deltaEntry : deltaMap.entrySet()) {
+ if(deltaEntry.getValue() > 0) {
+ minimizedDelta.getAdd().add(deltaEntry.getKey());
+ } else if(deltaEntry.getValue() < 0) {
+ minimizedDelta.getRemove().add(deltaEntry.getKey());
+ }
+ }
+
+ return minimizedDelta;
+ }
+
+ /**
+ * Returns a SubscriptionDelta of all changes made to a users subscriptions
+ * since a given point in time.
+ *
+ * @param username The username of the user whose subscription changes are
+ * being requested
+ * @param since The timestamp signifying how old the changes are allowed to
+ * be
+ * @return The SubscriptionDelta of all changes made since the given timestamp
+ */
+ public SubscriptionDelta getSubscriptionDelta(
+ final String username,
+ final long since) {
+ List<String> added = new ArrayList<>();
+ List<String> removed = new ArrayList<>();
+
+ List<SubscriptionAction> subscriptionActions = subscriptionActionDao
+ .findByUserUsernameAndTimestampGreaterThanEqual(username, since);
+ for (SubscriptionAction subscriptionAction : subscriptionActions) {
+ if (subscriptionAction.isAdded()) {
+ added.add(subscriptionAction.getSubscription().getUrl());
+ } else {
+ removed.add(subscriptionAction.getSubscription().getUrl());
+ }
+ }
+
+ return new SubscriptionDelta(added, removed);
+ }
+
+ /**
+ * Returns all Subscriptions and their 20 latest episodes of a given user as a
+ * List of SubscriptionTitles.
+ *
+ * @param username The username of the user whose subscriptions are being
+ * requested
+ * @return A list of SubscriptionTitles containing each Subscription and their
+ * 20 latest Episodes
+ */
+ public List<SubscriptionTitles> getTitles(final String username) {
+ List<SubscriptionAction> subscriptionActions
+ = subscriptionActionDao.findByUserUsernameAndAddedTrue(username);
+ List<Subscription> subscriptions = new ArrayList<>();
+ List<SubscriptionTitles> subscriptionTitlesList = new ArrayList<>();
+
+ for (SubscriptionAction subscriptionAction : subscriptionActions) {
+ subscriptions.add(subscriptionAction.getSubscription());
+ }
+
+ for (Subscription subscription : subscriptions) {
+ List<EpisodeActionPost> episodeActionPosts
+ = episodeActionService
+ .getEpisodeActionsOfPodcast(username, subscription.getUrl());
+ SubscriptionTitles subscriptionTitles
+ = new SubscriptionTitles(subscription, episodeActionPosts);
+ subscriptionTitlesList.add(subscriptionTitles);
+ }
+
+ return subscriptionTitlesList;
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/subscriptions/api/service/package-info.java b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/service/package-info.java
new file mode 100644
index 0000000..9e2a598
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/service/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * This package represents the logical middle layer of the subscription API
+ * ({@link org.psesquared.server.subscriptions.api}) - the service layer.
+ * <br>
+ * All business logic is handled here with the
+ * {@link
+ * org.psesquared.server.subscriptions.api.service.SubscriptionService}
+ * class.
+ *
+ * @author PSE-Squared Team
+ * @version 1.0
+ */
+package org.psesquared.server.subscriptions.api.service;