summaryrefslogtreecommitdiff
path: root/pse-server/src/main/java/org/psesquared/server/subscriptions
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
Initial commitHEADmain
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/subscriptions')
-rw-r--r--pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionController.java155
-rw-r--r--pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionDelta.java80
-rw-r--r--pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionTitles.java17
-rw-r--r--pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/package-info.java13
-rw-r--r--pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/SubscriptionActionDao.java91
-rw-r--r--pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/SubscriptionDao.java34
-rw-r--r--pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/package-info.java13
-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
9 files changed, 715 insertions, 0 deletions
diff --git a/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionController.java b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionController.java
new file mode 100644
index 0000000..1ffe137
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionController.java
@@ -0,0 +1,155 @@
+package org.psesquared.server.subscriptions.api.controller;
+
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.psesquared.server.subscriptions.api.service.SubscriptionService;
+import org.psesquared.server.util.UpdateUrlsWrapper;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * This is a controller class for the Subscription API that handles the requests
+ * from the client concerning adding subscriptions, removing subscriptions and
+ * getting all current subscriptions.
+ * In the end an appropriate response is sent back to the user.
+ */
+@RestController
+@RequiredArgsConstructor
+public class SubscriptionController {
+
+ /**
+ * The response for uploading subscriptions successfully.
+ */
+ private static final String UPLOAD_SUCCESS = "";
+
+ /**
+ * The service class that this controller calls to further process requests.
+ */
+ private final SubscriptionService subscriptionService;
+
+ /**
+ * It takes a list of strings containing the URLs of all subscribed podcasts,
+ * and saves them to the database.
+ *
+ * @param username The username of the user
+ * @param deviceId The device ID of the device that is uploading the
+ * subscriptions (will be ignored in this implementation)
+ * @param subscriptions A list of strings, each string is the URL to a podcast
+ * RSS-Feed that was subscribed
+ * @return The response containing an empty String with
+ * <br>
+ * {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#NOT_FOUND} user not found
+ */
+ @PutMapping(path = "/subscriptions/{username}/{deviceId}.json")
+ public ResponseEntity<String> uploadSubscriptions(
+ @PathVariable final String username,
+ @PathVariable final String deviceId,
+ @RequestBody final List<String> subscriptions) {
+ HttpStatus status
+ = subscriptionService.uploadSubscriptions(username, subscriptions);
+ return new ResponseEntity<>(UPLOAD_SUCCESS, status);
+ }
+
+ /**
+ * This function returns a list of subscriptions for a given user.
+ *
+ * @param username The username of the user whose subscriptions you want
+ * to retrieve
+ * @param deviceId This is the unique identifier for the device of the
+ * user whose subscriptions are asked for
+ * (will be ignored in this implementation)
+ * @param functionJsonp This parameter is not supported in this implementation
+ * and is thus ignored
+ * @return A list of strings containing the RSS-Feed URLs of all subscribed
+ * podcasts
+ */
+ @GetMapping(path = {"/subscriptions/{username}.json",
+ "/subscriptions/{username}/{deviceId}.json"})
+ public ResponseEntity<List<String>> getSubscriptions(
+ @PathVariable final String username,
+ @PathVariable(required = false) final String deviceId,
+ @RequestParam(value = "jsonp",
+ required = false) final String functionJsonp) {
+ List<String> subscriptions = subscriptionService.getSubscriptions(username);
+ return ResponseEntity.ok(subscriptions);
+ }
+
+ /**
+ * This function takes the information of added and removed podcasts in the
+ * form of a SubcriptionDelta as a JSON object.
+ * <br>
+ * After that, it applies the changes to the given user in the database.
+ *
+ * @param username The username of the user who is making changes to their
+ * subscriptions
+ * @param deviceId The device ID of the device that is requesting the update
+ * (will be ignored in this implementation)
+ * @param delta Contains all the changes that were made to the
+ * subscriptions of the user
+ * @return The response containing a placeholder for not
+ * supported function with
+ * <br>
+ * {@link HttpStatus#OK} on success,
+ * <br>
+ * {@link HttpStatus#NOT_FOUND} user or subscription not found
+ */
+ @PostMapping(path = "/api/2/subscriptions/{username}/{deviceId}.json")
+ public ResponseEntity<UpdateUrlsWrapper> applySubscriptionDelta(
+ @PathVariable final String username,
+ @PathVariable final String deviceId,
+ @RequestBody final SubscriptionDelta delta) {
+ subscriptionService.applySubscriptionDelta(username, delta);
+ return ResponseEntity.ok(new UpdateUrlsWrapper());
+ }
+
+ /**
+ * It returns a list of all the changes to the subscriptions of a user since a
+ * given time.
+ *
+ * @param username The username of the user whose SubscriptionDeltas are being
+ * requested
+ * @param deviceId The device ID of the device that is requesting the delta
+ * (will be ignored in this implementation)
+ * @param since The timestamp of the last time the client checked for
+ * updates
+ * @return A response containing the SubscriptionDelta of all changes that
+ * were made in a JSON format
+ */
+ @GetMapping(path = "/api/2/subscriptions/{username}/{deviceId}.json")
+ public ResponseEntity<SubscriptionDelta> getSubscriptionDelta(
+ @PathVariable final String username,
+ @PathVariable final String deviceId,
+ @RequestParam("since") final long since) {
+ SubscriptionDelta delta
+ = subscriptionService.getSubscriptionDelta(username, since);
+ return ResponseEntity.ok(delta);
+ }
+
+ /**
+ * This function returns a list of podcasts a user is subscribed to.
+ * <br>
+ * This includes not only the podcast itself, but also the latest 20 Episodes
+ * of the podcast.
+ *
+ * @param username The username of the user whose podcasts are being requested
+ * @return A response containing a List of podcasts and their episodes the
+ * user is subscribed to
+ */
+ @GetMapping(path = "/subscriptions/titles/{username}.json")
+ public ResponseEntity<List<SubscriptionTitles>> getTitles(
+ @PathVariable final String username) {
+ List<SubscriptionTitles> responseBody
+ = subscriptionService.getTitles(username);
+ return ResponseEntity.ok(responseBody);
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionDelta.java b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionDelta.java
new file mode 100644
index 0000000..7611b05
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionDelta.java
@@ -0,0 +1,80 @@
+package org.psesquared.server.subscriptions.api.controller;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.List;
+import lombok.NonNull;
+
+/**
+ * SubscriptionDeltas contain all changes that were made to the subscriptions
+ * of a user (added / removed podcasts) at a certain time.
+ */
+public class SubscriptionDelta {
+
+ /**
+ * The list of recently subscribed podcasts.
+ */
+ @JsonProperty(required = true)
+ @NonNull
+ private final List<String> add;
+
+ /**
+ * The list of recently unsubscribed podcasts.
+ */
+ @JsonProperty(required = true)
+ @NonNull
+ private final List<String> remove;
+
+ /**
+ * The timestamp of the delta.
+ */
+ @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+ private final long timestamp;
+
+ /**
+ * Instantiates a new SubscriptionDelta with a current timestamp.
+ *
+ * @param addedPodcastUrls List of Strings containing the RSS-Feed URLs of
+ * all added podcasts
+ * @param removedPodcastUrls List of Strings containing the RSS-Feed URLs of
+ * all removed podcasts
+ */
+ public SubscriptionDelta(
+ @org.springframework.lang.NonNull final List<String> addedPodcastUrls,
+ @org.springframework.lang.NonNull final List<String> removedPodcastUrls) {
+ this.add = addedPodcastUrls;
+ this.remove = removedPodcastUrls;
+ this.timestamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC);
+ }
+
+ /**
+ * Returns the list of RSS-Feed URLs of all added podcasts.
+ *
+ * @return RSS-Feed URLs of all added podcasts
+ */
+ @org.springframework.lang.NonNull
+ public List<String> getAdd() {
+ return add;
+ }
+
+ /**
+ * Returns the list of RSS-Feed URLs of all removed podcasts.
+ *
+ * @return RSS-Feed URLs of all removed podcasts
+ */
+ @org.springframework.lang.NonNull
+ public List<String> getRemove() {
+ return remove;
+ }
+
+ /**
+ * Returns the timestamp of when this Subscription Delta was uploaded.
+ *
+ * @return The timestamp of when this Subscription Delta was uploaded
+ */
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionTitles.java b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionTitles.java
new file mode 100644
index 0000000..682497b
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/SubscriptionTitles.java
@@ -0,0 +1,17 @@
+package org.psesquared.server.subscriptions.api.controller;
+
+import com.fasterxml.jackson.annotation.JsonUnwrapped;
+import java.util.List;
+import org.psesquared.server.episode.actions.api.controller.EpisodeActionPost;
+import org.psesquared.server.model.Subscription;
+
+/**
+ * Contains a podcast and its latest 20 Episodes.
+ *
+ * @param subscription The podcast
+ * @param episodes The episodes of the podcast
+ */
+public record SubscriptionTitles(
+ @JsonUnwrapped Subscription subscription,
+ List<EpisodeActionPost> episodes) {
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/package-info.java b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/package-info.java
new file mode 100644
index 0000000..0238039
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/controller/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * This package represents the highest logical layer of the subscription API
+ * ({@link org.psesquared.server.subscriptions.api}) - the controller layer.
+ * <br>
+ * It contains the
+ * {@link
+ * org.psesquared.server.subscriptions.api.controller.SubscriptionController}
+ * along with some wrapper classes for JSON request and response bodies.
+ *
+ * @author PSE-Squared Team
+ * @version 1.0
+ */
+package org.psesquared.server.subscriptions.api.controller;
diff --git a/pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/SubscriptionActionDao.java b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/SubscriptionActionDao.java
new file mode 100644
index 0000000..5d91453
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/SubscriptionActionDao.java
@@ -0,0 +1,91 @@
+package org.psesquared.server.subscriptions.api.data.access;
+
+import java.util.List;
+import java.util.Optional;
+import org.psesquared.server.model.Subscription;
+import org.psesquared.server.model.SubscriptionAction;
+import org.psesquared.server.model.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * A DAO interface responsible for transactions involving SubscriptionActions.
+ */
+@Repository
+public interface SubscriptionActionDao
+ extends JpaRepository<SubscriptionAction, Long> {
+
+ /**
+ * True, if the given user is already subscribed to the given Subscription.
+ *
+ * @param user The user that could be subscribed
+ * @param subscription The subscription the user could be subscribed to
+ * @return A boolean value signifying whether the user is subscribed to the
+ * given subscription
+ */
+ boolean existsByUserAndSubscription(User user, Subscription subscription);
+
+ /**
+ * Find the SubscriptionAction signifying that the user is subscribed to the
+ * given Subscription.
+ *
+ * @param user The user who is subscribed to the subscription
+ * @param subscription The subscription that the user is subscribed to
+ * @return Contains the relevant SubscriptionAction. Could also be NULL if
+ * none was found.
+ */
+ Optional<SubscriptionAction> findByUserAndSubscription(
+ User user,
+ Subscription subscription);
+
+ /**
+ * Find the SubscriptionAction for a {@link User} with the given username
+ * and for a {@link Subscription} with the given URL.
+ *
+ * @param username The username of the user who is subscribed to
+ * the subscription
+ * @param subscriptionUrl The URL of the subscription that the user is
+ * subscribed to
+ * @return Contains the relevant SubscriptionAction. Could also be NULL if
+ * none was found.
+ */
+ Optional<SubscriptionAction> findByUserUsernameAndSubscriptionUrl(
+ String username, String subscriptionUrl);
+
+ /**
+ * All SubscriptionActions of a given user that were applied since a given
+ * timestamp are searched for and returned.
+ *
+ * @param username The username of the user whose SubscriptionActions are
+ * requested
+ * @param timestamp The timestamp signifying how old the SubscriptionActions
+ * are allowed to be
+ * @return A list of SubscriptionActions that have since been applied
+ */
+ List<SubscriptionAction> findByUserUsernameAndTimestampGreaterThanEqual(
+ String username,
+ long timestamp);
+
+ /**
+ * Returns a List of all Subscriptions the user is subscribed to.
+ *
+ * @param username The username of the user whose subscriptions are requested
+ * @return A list of subscriptions the user is subscribed to
+ */
+ List<SubscriptionAction> findByUserUsernameAndAddedTrue(String username);
+
+ /**
+ * Returns a List of RSS-Feed URLs of all podcasts the given user is
+ * subscribed to since a given timestamp.
+ *
+ * @param username The username of the user whose subscriptions are requested
+ * @param timestamp The timestamp signifying the time since when the user must
+ * have been subscribed
+ * @return List of RSS-Feed URLs
+ */
+ List<SubscriptionAction>
+ findByUserUsernameAndAddedTrueAndTimestampGreaterThanEqual(
+ String username,
+ long timestamp);
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/SubscriptionDao.java b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/SubscriptionDao.java
new file mode 100644
index 0000000..d3d1fbf
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/SubscriptionDao.java
@@ -0,0 +1,34 @@
+package org.psesquared.server.subscriptions.api.data.access;
+
+import java.util.Optional;
+import org.psesquared.server.model.Subscription;
+import org.springframework.data.jpa.repository.EntityGraph;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+/**
+ * A DAO interface responsible for transactions involving Subscriptions.
+ */
+@Repository
+public interface SubscriptionDao extends JpaRepository<Subscription, Long> {
+
+ /**
+ * Find a subscription by its URL.
+ *
+ * @param url The URL of the subscription
+ * @return The found subscription (could be NULL, if there was no match)
+ */
+ @EntityGraph(value = "graph.Subscription.episodes")
+ Optional<Subscription> findByUrl(String url);
+
+ /**
+ * Returns true if the database already has a Subscription that has the given
+ * URL.
+ *
+ * @param url The URL of the Subscription that could already exist in the
+ * database
+ * @return A boolean value signifying the existence of the Subscription
+ */
+ boolean existsByUrl(String url);
+
+}
diff --git a/pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/package-info.java b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/package-info.java
new file mode 100644
index 0000000..db23d5f
--- /dev/null
+++ b/pse-server/src/main/java/org/psesquared/server/subscriptions/api/data/access/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * This package represents the lowest logical layer of the subscription API
+ * ({@link org.psesquared.server.subscriptions.api}) - the data-access layer.
+ * <br>
+ * It features the interfaces {@link
+ * org.psesquared.server.subscriptions.api.data.access.SubscriptionActionDao}
+ * and {@link
+ * org.psesquared.server.subscriptions.api.data.access.SubscriptionDao}.
+ *
+ * @author PSE-Squared Team
+ * @version 1.0
+ */
+package org.psesquared.server.subscriptions.api.data.access;
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;