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/episode |
Diffstat (limited to 'pse-server/src/main/java/org/psesquared/server/episode')
9 files changed, 779 insertions, 0 deletions
diff --git a/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/EpisodeActionController.java b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/EpisodeActionController.java new file mode 100644 index 0000000..9a0d898 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/EpisodeActionController.java @@ -0,0 +1,129 @@ +package org.psesquared.server.episode.actions.api.controller; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.psesquared.server.episode.actions.api.service.EpisodeActionService; +import org.psesquared.server.util.UpdateUrlsWrapper; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * This is a controller class for the Episode Action API that + * handles the requests from the client concerning the synchronization + * of episodes between clients. + * In the end an appropriate response is sent back to the user. + */ +@RequestMapping("/api/2/episodes/{username}.json") +@RestController +@RequiredArgsConstructor +public class EpisodeActionController { + + /** + * The service class that this controller calls to further process requests. + */ + private final EpisodeActionService episodeActionService; + + /** + * Takes a list of EpisodeActionPosts of a user and adds them to the database. + * + * @param username The username of the user uploading the + * EpisodeActions + * @param episodeActionPosts The list of EpisodeActionPosts to be uploaded + * @return The exit status of the function + */ + @PostMapping + public ResponseEntity<UpdateUrlsWrapper> addEpisodeActions( + @PathVariable final String username, + @RequestBody final List<EpisodeActionPost> episodeActionPosts) { + episodeActionService.addEpisodeActions(username, episodeActionPosts); + return ResponseEntity.ok(new UpdateUrlsWrapper()); + } + + /** + * Returns a list of all EpisodeActions a user has uploaded so far in the form + * of an EpisodeActionGetResponse. + * + * @param username The username of the user whose EpisodeActions are requested + * @return The exit status with a response body containing all requested + * EpisodeActions + */ + @GetMapping + public ResponseEntity<EpisodeActionGetResponse> getEpisodeActions( + @PathVariable final String username) { + EpisodeActionGetResponse responseBody + = new EpisodeActionGetResponse(episodeActionService + .getEpisodeActions(username)); + return ResponseEntity.ok(responseBody); + } + + /** + * Returns a list of EpisodeActions of a user for a given podcast in the form + * of an EpisodeActionGetResponse. + * + * @param username The username of the user whose EpisodeActions are + * requested + * @param podcastUrl The RSS-Feed URL of the podcast in question + * @return The exit status with a response body containing all requested + * EpisodeActions + */ + @GetMapping(params = {"podcast"}) + public ResponseEntity<EpisodeActionGetResponse> getEpisodeActionsOfPodcast( + @PathVariable final String username, + @RequestParam("podcastUrl") final String podcastUrl) { + EpisodeActionGetResponse responseBody + = new EpisodeActionGetResponse(episodeActionService + .getEpisodeActionsOfPodcast(username, podcastUrl)); + return ResponseEntity.ok(responseBody); + } + + /** + * Returns a list of EpisodeActions of a user since a given timestamp in the + * form of an EpisodeActionGetResponse. + * + * @param username The username of the user whose EpisodeActions are requested + * @param since The timestamp signifying how old the EpisodeActions are + * allowed to be + * @return The exit status with a response body containing all requested + * EpisodeActions + */ + @GetMapping(params = {"since"}) + public ResponseEntity<EpisodeActionGetResponse> getEpisodeActionsSince( + @PathVariable final String username, + @RequestParam("since") final long since) { + EpisodeActionGetResponse responseBody + = new EpisodeActionGetResponse(episodeActionService + .getEpisodeActionsSince(username, since)); + return ResponseEntity.ok(responseBody); + } + + /** + * Returns a list of EpisodeActions of a user for a given podcast, since a + * given time in the form of an EpisodeActionGetResponse. + * + * @param username The username of the user whose EpisodeActions are + * requested + * @param podcastUrl The RSS-Feed URL of the podcast in question + * @param since The timestamp signifying how old the EpisodeActions are + * allowed to be + * @return The exit status with a response body containing all requested + * EpisodeActions + */ + @GetMapping(params = {"podcast", "since"}) + public ResponseEntity<EpisodeActionGetResponse> + getEpisodeActionsOfPodcastSince( + @PathVariable final String username, + @RequestParam("podcastUrl") final String podcastUrl, + @RequestParam("since") final long since) { + EpisodeActionGetResponse responseBody + = new EpisodeActionGetResponse(episodeActionService + .getEpisodeActionsOfPodcastSince(username, podcastUrl, since)); + return ResponseEntity.ok(responseBody); + } + +} diff --git a/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/EpisodeActionGetResponse.java b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/EpisodeActionGetResponse.java new file mode 100644 index 0000000..d1facac --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/EpisodeActionGetResponse.java @@ -0,0 +1,37 @@ +package org.psesquared.server.episode.actions.api.controller; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.List; +import lombok.Data; + +/** + * The Response Object for a GET-Request concerning an EpisodeAction. + * <br> + * May contain multiple EpisodeActions. + */ +@Data +public class EpisodeActionGetResponse { + + /** + * The list of EpisodeActionPosts. + */ + private final List<EpisodeActionPost> actions; + + /** + * The timestamp of the response. + */ + private final long timestamp; + + /** + * Instantiates a new EpisodeActionGetResponse with the current timestamp. + * + * @param episodeActionPosts A list of EpisodeActionPosts + */ + public EpisodeActionGetResponse( + final List<EpisodeActionPost> episodeActionPosts) { + this.actions = episodeActionPosts; + this.timestamp = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC); + } + +} diff --git a/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/EpisodeActionPost.java b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/EpisodeActionPost.java new file mode 100644 index 0000000..28613f2 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/EpisodeActionPost.java @@ -0,0 +1,61 @@ +package org.psesquared.server.episode.actions.api.controller; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.psesquared.server.model.EpisodeAction; + +/** + * An Episode Action that is being sent to the server via a POST Request. + * <br> + * If the user listened to an episode or did another action, an + * EpisodeActionPOST is uploaded. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EpisodeActionPost { + + /** + * The URL of the podcast the posted episode action belongs to. + */ + @JsonProperty(value = "podcast", required = true) + @NotBlank + private String podcastUrl; + + /** + * The URL of the corresponding episode. + */ + @JsonProperty(value = "episode", required = true) + @NotBlank + private String episodeUrl; + + /** + * The title of the corresponding episode. + */ + private String title; + + /** + * The GUID of the corresponding episode. + */ + private String guid; + + /** + * The total length of the corresponding episode in milliseconds. + */ + private int total; + + /** + * The actual episode action whose attributes are presented unwrapped. + * + * @see JsonUnwrapped + */ + @JsonUnwrapped + private EpisodeAction episodeAction; + +} diff --git a/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/package-info.java b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/package-info.java new file mode 100644 index 0000000..3115012 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/controller/package-info.java @@ -0,0 +1,13 @@ +/** + * This package represents the highest logical layer of the episode action API + * ({@link org.psesquared.server.episode.actions.api}) - the controller layer. + * <br> + * It contains the + * {@link + * org.psesquared.server.episode.actions.api.controller.EpisodeActionController} + * along with some wrapper classes for JSON request and response bodies. + * + * @author PSE-Squared Team + * @version 1.0 + */ +package org.psesquared.server.episode.actions.api.controller; diff --git a/pse-server/src/main/java/org/psesquared/server/episode/actions/api/data/access/EpisodeActionDao.java b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/data/access/EpisodeActionDao.java new file mode 100644 index 0000000..7f23312 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/data/access/EpisodeActionDao.java @@ -0,0 +1,107 @@ +package org.psesquared.server.episode.actions.api.data.access; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import org.psesquared.server.model.Action; +import org.psesquared.server.model.EpisodeAction; +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 EpisodeActions. + */ +@Repository +public interface EpisodeActionDao extends JpaRepository<EpisodeAction, Long> { + + /** + * Find all EpisodeActions a user has uploaded. + * + * @param username The username of the user who uploaded the EpisodeActions + * @return The list of EpisodeActions regarding the user + */ + List<EpisodeAction> findByUserUsername(String username); + + /** + * Find all EpisodeActions of a user that concern a certain podcast identified + * by its RSS-Feed URL. + * + * @param username The username of the user who uploaded the EpisodeActions + * @param url The RSS-Feed URL of the podcast in question + * @return The list of EpisodeActions regarding the user and the given podcast + */ + List<EpisodeAction> findByUserUsernameAndEpisodeSubscriptionUrl( + String username, + String url); + + /** + * Deletes all EpisodeActions of a podcast for a user. + * + * @param username The username of the user + * @param url The podcast URL + */ + void deleteByUserUsernameAndEpisodeSubscriptionUrl( + String username, + String url); + + /** + * Checks if an EpisodeAction of the specified action type for a given user + * and episode already exists. + * + * @param user The user + * @param url The episode URL + * @param action The type of action + * @return {@code true} if such an EpisodeAction exists, + * <br> + * {@code false} otherwise + */ + boolean existsByUserAndEpisodeUrlAndAction(User user, + String url, + Action action); + + /** + * Finds the EpisodeAction of the specified action type and of an Episode + * for a User. + * + * @param user The user + * @param url The episode URL + * @param action The type of action + * @return An {@link Optional} containing the EpisodeAction if present + */ + Optional<EpisodeAction> findByUserAndEpisodeUrlAndAction(User user, + String url, + Action action); + + /** + * Find all EpisodeActions of a user since a given timestamp. + * + * @param username The username of the user + * @param timestamp The timestamp signifying how old an EpisodeAction is + * allowed + * to be + * @return A list containing all EpisodeActions not older than the timestamp + */ + List<EpisodeAction> findByUserUsernameAndTimestampGreaterThanEqual( + String username, + LocalDateTime timestamp); + + /** + * Find all EpisodeActions of a user since a given timestamp of a given + * podcast. + * + * @param username The username of the user + * @param timestamp The timestamp signifying how old an EpisodeAction is + * allowed to be + * @param url The RSS-Feed URL of the podcast whose EpisodeActions are + * requested + * @return A list containing all EpisodeActions of the given podcast not older + * than the timestamp + */ + List<EpisodeAction> + findByUserUsernameAndTimestampGreaterThanEqualAndEpisodeSubscriptionUrl( + String username, + LocalDateTime timestamp, + String url); + +} diff --git a/pse-server/src/main/java/org/psesquared/server/episode/actions/api/data/access/EpisodeDao.java b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/data/access/EpisodeDao.java new file mode 100644 index 0000000..16c647f --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/data/access/EpisodeDao.java @@ -0,0 +1,46 @@ +package org.psesquared.server.episode.actions.api.data.access; + +import java.util.Optional; +import org.psesquared.server.model.Episode; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * A DAO interface responsible for transactions involving Episodes. + */ +@Repository +public interface EpisodeDao extends JpaRepository<Episode, Long> { + + /** + * Find an episode by its URL. + * + * @param url The URL of the episode + * @return The matching episode / NULL, if there was no match. + */ + Optional<Episode> findByUrl(String url); + + /** + * Returns true if there is an episode that matches a given URL. + * + * @param url The URL of the episode + * @return A boolean value signifying whether the episode exists + */ + boolean existsByUrl(String url); + + /** + * Returns true if there is an episode that matches a given GUID. + * + * @param guid The GUID of the episode + * @return A boolean value signifying whether the episode exists + */ + boolean existsByGuid(String guid); + + /** + * Find an episode by its GUID. + * + * @param guid The GUID of the episode + * @return The matching episode / NULL, if there was no match. + */ + Optional<Episode> findByGuid(String guid); + +} diff --git a/pse-server/src/main/java/org/psesquared/server/episode/actions/api/data/access/package-info.java b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/data/access/package-info.java new file mode 100644 index 0000000..b32f249 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/data/access/package-info.java @@ -0,0 +1,13 @@ +/** + * This package represents the lowest logical layer of the episode action API + * ({@link org.psesquared.server.episode.actions.api}) - the data-access layer. + * <br> + * It features the interfaces {@link + * org.psesquared.server.episode.actions.api.data.access.EpisodeActionDao} + * and {@link + * org.psesquared.server.episode.actions.api.data.access.EpisodeDao}. + * + * @author PSE-Squared Team + * @version 1.0 + */ +package org.psesquared.server.episode.actions.api.data.access; diff --git a/pse-server/src/main/java/org/psesquared/server/episode/actions/api/service/EpisodeActionService.java b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/service/EpisodeActionService.java new file mode 100644 index 0000000..8c4f93a --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/service/EpisodeActionService.java @@ -0,0 +1,360 @@ +package org.psesquared.server.episode.actions.api.service; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.data.access.EpisodeDao; +import org.psesquared.server.model.Action; +import org.psesquared.server.model.Episode; +import org.psesquared.server.model.EpisodeAction; +import org.psesquared.server.model.Subscription; +import org.psesquared.server.model.SubscriptionAction; +import org.psesquared.server.model.User; +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.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.episode.actions.api.controller.EpisodeActionController} + * and passes on requests concerning data access mainly to the + * {@link EpisodeDao} and {@link EpisodeActionDao}. + */ +@Service +@Transactional +@RequiredArgsConstructor +public class EpisodeActionService { + + /** + * The nano of second default value for + * {@link LocalDateTime#ofEpochSecond(long, int, ZoneOffset)}. + */ + private static final int NANO_OF_SECOND_DEFAULT = 0; + + /** + * The JPA repository that handles all episode action related database + * requests. + */ + private final EpisodeActionDao episodeActionDao; + + /** + * The JPA repository that handles all episode related database requests. + */ + private final EpisodeDao episodeDao; + + /** + * 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 class for asynchronously fetching data from RSS feeds. + */ + private final RssParser rssParser; + + /** + * A map of subscription that need to be fetched with the {@link RssParser}. + */ + private final Map<String, Subscription> subscriptionsToFetch + = new HashMap<>(); + + /** + * Takes a list of EpisodeActionPosts, converts them to EpisodeActions and + * saves them to the database. + * + * @param username The username of the user who uploads these + * EpisodeActions + * @param episodeActionPosts List of EpisodeActionPosts that were sent via the + * POST request + */ + public void addEpisodeActions( + final String username, + final List<EpisodeActionPost> episodeActionPosts) { + User user = authenticationDao.findByUsername(username).orElseThrow(); + List<EpisodeActionPost> filteredEpisodeActionPosts + = new ArrayList<>(filterNewestAction(episodeActionPosts)); + List<EpisodeAction> episodeActions + = episodeActionPostsToEpisodeActions(user, filteredEpisodeActionPosts); + addEpisodeActionsToDatabase(user, episodeActions); + validateDummyEpisodes(); + } + + private Collection<EpisodeActionPost> filterNewestAction( + final List<EpisodeActionPost> episodeActionPosts) { + Map<String, EpisodeActionPost> relevantEpisodeActionPosts = new HashMap<>(); + for (EpisodeActionPost episodeActionPost : episodeActionPosts) { + if (episodeActionPost.getEpisodeAction().getAction() != Action.PLAY) { + continue; + } + String url = episodeActionPost.getEpisodeUrl(); + if (relevantEpisodeActionPosts.containsKey(url)) { + EpisodeActionPost currentEpisodeActionPost + = relevantEpisodeActionPosts.get(url); + if (episodeActionPost.getEpisodeAction().getTimestamp() + .isAfter( + currentEpisodeActionPost.getEpisodeAction().getTimestamp())) { + relevantEpisodeActionPosts.put(url, episodeActionPost); + } + } else { + relevantEpisodeActionPosts.put(url, episodeActionPost); + } + } + return relevantEpisodeActionPosts.values(); + } + + private List<EpisodeAction> episodeActionPostsToEpisodeActions( + final User user, + final List<EpisodeActionPost> episodeActionPosts) { + List<EpisodeAction> episodeActions = new ArrayList<>(); + for (EpisodeActionPost episodeActionPost : episodeActionPosts) { + if (episodeActionPost.getEpisodeAction().getAction() == Action.PLAY) { + episodeActions.add(episodeActionPostToEpisodeAction( + user, + episodeActionPost)); + } + } + return episodeActions; + } + + private EpisodeAction episodeActionPostToEpisodeAction( + final User user, + final EpisodeActionPost episodeActionPost) { + EpisodeAction episodeAction = episodeActionPost.getEpisodeAction(); + episodeAction.setUser(user); + // If Subscription does not exist, create dummy Subscription + Subscription subscription = null; + if (!subscriptionDao.existsByUrl(episodeActionPost.getPodcastUrl())) { + subscription = new Subscription(); + subscription.setTimestamp( + LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)); + subscription.setUrl(episodeActionPost.getPodcastUrl()); + subscription = subscriptionDao.save(subscription); + // create Subscription Action + SubscriptionAction subscriptionAction = SubscriptionAction.builder() + .user(user) + .added(true) + .subscription( + subscriptionDao.findByUrl( + episodeActionPost.getPodcastUrl()).orElseThrow()) + .timestamp(LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)) + .build(); + subscriptionActionDao.save(subscriptionAction); + } else { + subscription = subscriptionDao + .findByUrl(episodeActionPost.getPodcastUrl()).orElseThrow(); + } + Episode episode = getEpisodeFromDatabase(episodeActionPost); + episodeAction.setEpisode(episode); + subscription.addEpisode(episode); + return episodeAction; + } + + private Episode getEpisodeFromDatabase( + final EpisodeActionPost episodeActionPost) { + Episode episode; + String episodeUrl = episodeActionPost.getEpisodeUrl(); + String episodeGuid = episodeActionPost.getGuid(); + // If guid is passed and a matching episode exists get it + if (episodeGuid != null && episodeDao.existsByGuid(episodeGuid)) { + episode = episodeDao.findByGuid(episodeGuid).orElseThrow(); + } else if (episodeDao.existsByUrl(episodeUrl)) { + // No episode with matching guid found -> search by url + episode = episodeDao.findByUrl(episodeUrl).orElseThrow(); + // If guid was passed, pass it along to the database + if (episodeGuid != null) { + episode.setGuid(episodeGuid); + episodeDao.save(episode); + } + } else { + // Episode does not exist, so construct a new one + episode = createEpisode(episodeActionPost); + } + return episode; + } + + private Episode createEpisode(final EpisodeActionPost episodeActionPost) { + Episode episode = Episode.builder() + .title(episodeActionPost.getTitle()) + .url(episodeActionPost.getEpisodeUrl()) + .total(episodeActionPost.getTotal()) + .subscription(subscriptionDao + .findByUrl(episodeActionPost.getPodcastUrl()).orElseThrow()) + .build(); + if (episodeActionPost.getGuid() != null) { + episode.setGuid(episodeActionPost.getGuid()); + } + episodeDao.save(episode); + Subscription subscription = episode.getSubscription(); + subscriptionsToFetch.put(subscription.getUrl(), subscription); + return episode; + } + + private void addEpisodeActionsToDatabase( + final User user, + final List<EpisodeAction> episodeActions) { + for (EpisodeAction episodeAction : episodeActions) { + addEpisodeActionToDatabase(user, episodeAction); + } + } + + private void addEpisodeActionToDatabase( + final User user, + final EpisodeAction episodeAction) { + if (episodeActionDao.existsByUserAndEpisodeUrlAndAction( + user, + episodeAction.getEpisode().getUrl(), + episodeAction.getAction())) { + addNewestEpisodeActionToDatabase(user, episodeAction); + } else { + episodeActionDao.save(episodeAction); + } + } + + private void addNewestEpisodeActionToDatabase( + final User user, + final EpisodeAction episodeAction) { + EpisodeAction oldEpisodeAction + = episodeActionDao.findByUserAndEpisodeUrlAndAction( + user, + episodeAction.getEpisode().getUrl(), + episodeAction.getAction()).orElseThrow(); + if (episodeAction.getTimestamp().isAfter(oldEpisodeAction.getTimestamp())) { + episodeActionDao.delete(oldEpisodeAction); + episodeActionDao.save(episodeAction); + } + } + + private void validateDummyEpisodes() { + Collection<Subscription> subscriptions = subscriptionsToFetch.values(); + for (Subscription subscription : subscriptions) { + rssParser.validate(subscription); + } + subscriptionsToFetch.clear(); + } + + /** + * Gets all EpisodeActions of a user and converts them to EpisodeActionPosts + * before returning them. + * + * @param username The username of the user whose EpisodeActions are requested + * @return A list containing the requested EpisodeActions as + * EpisodeActionPosts + */ + public List<EpisodeActionPost> getEpisodeActions(final String username) { + List<EpisodeAction> episodeActions + = episodeActionDao.findByUserUsername(username); + return episodeActionsToEpisodeActionPosts(episodeActions); + } + + /** + * Gets all EpisodeActions of a user that correspond to a given podcast. + * Returns the EpisodeActions after converting them to EpisodeActionPosts. + * + * @param username The username of the user whose EpisodeActions are + * requested + * @param podcastUrl The RSS-Feed URL of the podcast + * @return A list containing the requested EpisodeActions as + * EpisodeActionPosts + */ + public List<EpisodeActionPost> getEpisodeActionsOfPodcast( + final String username, + final String podcastUrl) { + List<EpisodeAction> episodeActions + = episodeActionDao.findByUserUsernameAndEpisodeSubscriptionUrl( + username, + podcastUrl); + return episodeActionsToEpisodeActionPosts(episodeActions); + } + + /** + * Gets all EpisodeActions of a user since a given timestamp and converts them + * to EpisodeActionPosts before returning them. + * + * @param username The username of the user whose EpisodeActions are requested + * @param since the timestamp signifying how old the EpisodeActions are + * allowed to be + * @return A list containing the requested EpisodeActions as + * EpisodeActionPosts + */ + public List<EpisodeActionPost> getEpisodeActionsSince( + final String username, + final long since) { + LocalDateTime sinceTimestamp + = LocalDateTime.ofEpochSecond( + since, + NANO_OF_SECOND_DEFAULT, + ZoneOffset.UTC); + List<EpisodeAction> episodeActions + = episodeActionDao.findByUserUsernameAndTimestampGreaterThanEqual( + username, + sinceTimestamp); + return episodeActionsToEpisodeActionPosts(episodeActions); + } + + /** + * Gets all EpisodeActions of a user concerning a certain podcast since a + * given timestamp and converts them to EpisodeActionPosts before returning + * them. + * + * @param username The username of the user whose EpisodeActions are + * requested + * @param podcastUrl The RSS-Feed URL of the podcast + * @param since The timestamp signifying how old the EpisodeActions are + * allowed to be + * @return A list containing the requested EpisodeActions as + * EpisodeActionPosts + */ + public List<EpisodeActionPost> getEpisodeActionsOfPodcastSince( + final String username, + final String podcastUrl, + final long since) { + LocalDateTime sinceTimestamp + = LocalDateTime.ofEpochSecond( + since, + NANO_OF_SECOND_DEFAULT, + ZoneOffset.UTC); + List<EpisodeAction> episodeActions = episodeActionDao + .findByUserUsernameAndTimestampGreaterThanEqualAndEpisodeSubscriptionUrl( + username, + sinceTimestamp, + podcastUrl); + return episodeActionsToEpisodeActionPosts(episodeActions); + } + + private List<EpisodeActionPost> episodeActionsToEpisodeActionPosts( + final List<EpisodeAction> episodeActions) { + List<EpisodeActionPost> episodeActionPosts = new ArrayList<>(); + + for (EpisodeAction episodeAction : episodeActions) { + episodeActionPosts.add(episodeAction.toEpisodeActionPost()); + } + + return episodeActionPosts; + } + +} diff --git a/pse-server/src/main/java/org/psesquared/server/episode/actions/api/service/package-info.java b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/service/package-info.java new file mode 100644 index 0000000..c431539 --- /dev/null +++ b/pse-server/src/main/java/org/psesquared/server/episode/actions/api/service/package-info.java @@ -0,0 +1,13 @@ +/** + * This package represents the logical middle layer of the episode action API + * ({@link org.psesquared.server.episode.actions.api}) - the service layer. + * <br> + * All business logic is handled here with the + * {@link + * org.psesquared.server.episode.actions.api.service.EpisodeActionService} + * class. + * + * @author PSE-Squared Team + * @version 1.0 + */ +package org.psesquared.server.episode.actions.api.service; |