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;  | 
