/* * PROPRIETARY and CONFIDENTIAL * * Copyright 2012 Magellan Distribution Corporation * * All rights reserved. */ package com.ajah.syndicate.fetch; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.logging.Level; import lombok.extern.java.Log; import org.apache.http.HttpResponse; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import org.joda.time.DateTime; import org.joda.time.Days; import org.springframework.beans.factory.annotation.Autowired; import com.ajah.spring.jdbc.err.DataOperationException; import com.ajah.syndicate.Feed; import com.ajah.syndicate.FeedEntry; import com.ajah.syndicate.FeedSource; import com.ajah.syndicate.PollStatus; import com.ajah.syndicate.SyndicationException; import com.ajah.syndicate.data.FeedEntryManager; import com.ajah.syndicate.data.FeedManager; import com.ajah.syndicate.data.FeedSourceManager; import com.ajah.syndicate.rome.RomeUtils; import com.ajah.util.StringUtils; import com.ajah.util.data.XmlString; import com.ajah.util.date.DateUtils; /** * Simple fetcher that will pull feeds and save entries. * * @author <a href="http://efsavage.com">Eric F. Savage</a>, <a * href="mailto:code@efsavage.com">code@efsavage.com</a>. */ @Log public class FeedFetcher { private static void tempError(final FeedSource feedSource) { if (feedSource.getPollStatus() == PollStatus.ERROR_TMP) { if (feedSource.getPollStatusSince() == null) { feedSource.setPollStatusSince(new Date()); } else if (Days.daysBetween(new DateTime(feedSource.getPollStatus()), new DateTime()).getDays() > 7) { // We've failed for over a week, kill it. feedSource.setNextPoll(null); feedSource.setPollStatus(PollStatus.ERROR_PERM); } } else if (feedSource.getPollStatus() == PollStatus.ACTIVE) { feedSource.setPollStatusSince(new Date()); feedSource.setPollStatus(PollStatus.ERROR_TMP); } else { log.severe("We shouldn't have gotten here!"); feedSource.setNextPoll(DateUtils.addHours(6)); } } @Autowired FeedSourceManager feedSourceManager; @Autowired FeedManager feedManager; @Autowired FeedEntryManager entryManager; private final List<EntryListener> entryListeners = new ArrayList<>(); /** * Adds a listener to the list of listeners to fire when an entry is found. * * @param entryListener * The listener to add. */ public void addListener(final EntryListener entryListener) { this.entryListeners.add(entryListener); } /** * Handles a feed based on its response, if necessary. * * @param feedSource * The feed source to handle. * @param response * The response from the fetch attempt. * @return true if processing should continue, false if it should be * aborted. * @throws DataOperationException * If a query could not be executed. */ private boolean handle(final FeedSource feedSource, final HttpResponse response) throws DataOperationException { final int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { if (feedSource.getPollStatus() != PollStatus.ACTIVE) { feedSource.setPollStatus(PollStatus.ACTIVE); feedSource.setPollStatusSince(null); this.feedSourceManager.save(feedSource); } return true; } else if (statusCode == 404 || statusCode == 500) { log.warning("404: " + feedSource.getFeedUrl()); tempError(feedSource); } else { log.severe(statusCode + ": " + feedSource.getFeedUrl()); } if (feedSource.getPollStatus().isActive()) { feedSource.setNextPoll(DateUtils.addHours(6)); } else { feedSource.setNextPoll(null); } this.feedSourceManager.save(feedSource); return false; } /** * Polls until there are no more stale feeds. * * @throws DataOperationException */ public void poll() throws DataOperationException { while (true) { final FeedSource feedSource = this.feedSourceManager.getStaleFeedSource(); if (feedSource == null) { log.finest("No feed source to poll"); return; } log.fine("Polling " + feedSource.getTitle() + " [" + feedSource.getId() + "]"); try (final CloseableHttpClient http = HttpClientBuilder.create().build()) { final HttpGet get = new HttpGet(feedSource.getFeedUrl()); try (final CloseableHttpResponse response = http.execute(get)) { final String rawFeed = EntityUtils.toString(response.getEntity()); // log.finest(rawFeed); EntityUtils.consume(response.getEntity()); if (!handle(feedSource, response)) { continue; } final Feed feed = RomeUtils.createFeed(new XmlString(rawFeed), feedSource); log.fine("Found " + feed.getEntries().size() + " entries"); this.feedManager.save(feed, true); for (final EntryListener entryListener : this.entryListeners) { for (final FeedEntry entry : feed.getEntries()) { entryListener.handle(entry); } } if (!StringUtils.isBlank(feed.getTitle())) { feedSource.setTitle(feed.getTitle()); } if (!StringUtils.isBlank(feed.getLink())) { feedSource.setHtmlUrl(feed.getLink()); } } feedSource.setNextPoll(DateUtils.addMinutes(feedSource.getFetchFrequency())); this.feedSourceManager.save(feedSource); } catch (SyndicationException | IOException | RuntimeException e) { log.log(Level.WARNING, e.getMessage(), e); tempError(feedSource); this.feedSourceManager.save(feedSource); } } } /** * Finds a stale feed and fetches it, saving it to the database and invoking * any listeners needed. This will run until interrupted by a serious * exception. * * @throws InterruptedException * If the thread was interrupted while sleeping (while waiting * for a feed to fetch). * @throws DataOperationException * If a database query could not be executed. */ public void run() throws InterruptedException, DataOperationException { while (true) { poll(); } } }