diff options
author | Orangerot <purple@orangerot.dev> | 2024-06-19 00:14:49 +0200 |
---|---|---|
committer | Orangerot <purple@orangerot.dev> | 2024-06-27 12:11:14 +0200 |
commit | 5b8851b6c268d0e93c158908fbfae9f8473db5ff (patch) | |
tree | 7010eb85d86fa2da06ea4ffbcdb01a685d502ae8 /pse-server/src/main/java/org/psesquared/server/subscriptions/api/service |
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/subscriptions/api/service')
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; |