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> fetchedData = fetchSubscriptionFeed(subscription); if (fetchedData.get(URL_KEY_MAP_INDEX).isEmpty()) { subscriptionDao.deleteById(subscription.getId()); return; } Subscription retrievedSubscription = subscriptionDao.save(subscription); List subscriptionEpisodes = retrievedSubscription.getEpisodes(); if (subscriptionEpisodes == null || subscriptionEpisodes.isEmpty()) { return; } List invalidEpisodes = new ArrayList<>(); List 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> fetchSubscriptionFeed( final Subscription subscription) { final List> 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 episodesByGuid = new HashMap<>(); Map episodesByUrl = new HashMap<>(); List 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 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 itunesTags = syndEntry.getForeignMarkup(); for (Element element : itunesTags) { if (!element.getName().equals("duration")) { continue; } List 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> 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; } }