Сделать это легко по расписанию Reddit
1. обзор
В этой статье мы перейдем кcontinue the case study иadd a new feature to the Reddit application, чтобы упростить планирование статей.
Вместо того, чтобы медленно добавлять каждую статью вручную в пользовательском интерфейсе расписания, пользователь теперь может просто иметь несколько любимых сайтов для публикации статей в Reddit. Мы собираемся использовать для этого RSS.
2. СущностьSite
Во-первых, давайте создадим объект, представляющий сайт:
@Entity
public class Site {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String url;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
}
Обратите внимание, что полеurl представляет собой URL-адресthe RSS feed of the site.
3. Репозиторий и сервис
Далее - давайте создадим репозиторий для работы с новым объектом Site:
public interface SiteRepository extends JpaRepository {
List findByUser(User user);
}
И сервис:
public interface ISiteService {
List getSitesByUser(User user);
void saveSite(Site site);
Site findSiteById(Long siteId);
void deleteSiteById(Long siteId);
}
@Service
public class SiteService implements ISiteService {
@Autowired
private SiteRepository repo;
@Override
public List getSitesByUser(User user) {
return repo.findByUser(user);
}
@Override
public void saveSite(Site site) {
repo.save(site);
}
@Override
public Site findSiteById(Long siteId) {
return repo.findOne(siteId);
}
@Override
public void deleteSiteById(Long siteId) {
repo.delete(siteId);
}
}
4. Загрузить данные из фида
А теперь давайте посмотрим, как загрузить сведения о статьях из веб-канала с помощью Римской библиотеки.
Сначала нам нужно добавить Рим в нашpom.xml:
com.rometools
rome
1.5.0
А затем используйте его для разбора каналов сайтов:
public List getArticlesFromSite(Long siteId) {
Site site = repo.findOne(siteId);
return getArticlesFromSite(site);
}
List getArticlesFromSite(Site site) {
List entries;
try {
entries = getFeedEntries(site.getUrl());
} catch (Exception e) {
throw new FeedServerException("Error Occurred while parsing feed", e);
}
return parseFeed(entries);
}
private List getFeedEntries(String feedUrl)
throws IllegalArgumentException, FeedException, IOException {
URL url = new URL(feedUrl);
SyndFeed feed = new SyndFeedInput().build(new XmlReader(url));
return feed.getEntries();
}
private List parseFeed(List entries) {
List articles = new ArrayList();
for (SyndEntry entry : entries) {
articles.add(new SiteArticle(
entry.getTitle(), entry.getLink(), entry.getPublishedDate()));
}
return articles;
}
Наконец, вот простой DTO, который мы собираемся использовать в ответе:
public class SiteArticle {
private String title;
private String link;
private Date publishDate;
}
5. Обработка исключений
Обратите внимание, как при синтаксическом анализе фида мы упаковываем всю логику синтаксического анализа в блокtry-catch и - в случае исключения (любого исключения) - мы упаковываем его и бросаем.
Причина этого проста -we need to control the type of exception that gets thrown out of the parsing process - чтобы затем мы могли обработать это исключение и предоставить правильный ответ клиенту API:
@ExceptionHandler({ FeedServerException.class })
public ResponseEntity
6. Страница Сайтов
6.1. Показать сайты
Сначала мы увидим, как показать список сайтов, принадлежащих зарегистрированному пользователю:
@RequestMapping(value = "/sites")
@ResponseBody
public List getSitesList() {
return service.getSitesByUser(getCurrentUser());
}
А вот очень простая передняя часть:
Site Name Feed URL Actions
6.2. Добавить новый сайт
Затем давайте посмотрим, как пользователь может создать новый любимый сайт:
@RequestMapping(value = "/sites", method = RequestMethod.POST)
@ResponseStatus(HttpStatus.OK)
public void addSite(Site site) {
if (!service.isValidFeedUrl(site.getUrl())) {
throw new FeedServerException("Invalid Feed Url");
}
site.setUser(getCurrentUser());
service.saveSite(site);
}
И здесь - опять же очень просто - на стороне клиента:
6.3. Проверка фида
Проверка нового фида - это немного дорогая операция - нам нужно на самом деле получить фид и проанализировать его, чтобы полностью его проверить. Вот простой метод обслуживания:
public boolean isValidFeedUrl(String feedUrl) {
try {
return getFeedEntries(feedUrl).size() > 0;
} catch (Exception e) {
return false;
}
}
6.3. Удалить сайт
Теперь давайте посмотрим, как пользователь можетdelete a site from their list of favorite sites:
@RequestMapping(value = "/sites/{id}", method = RequestMethod.DELETE)
@ResponseStatus(HttpStatus.OK)
public void deleteSite(@PathVariable("id") Long id) {
service.deleteSiteById(id);
}
И здесь - опять же очень простой - метод уровня обслуживания:
public void deleteSiteById(Long siteId) {
repo.delete(siteId);
}
7. Запланировать публикацию с сайта
А теперь давайте начнем использовать эти сайты и реализовать основной способ, которым пользователь может запланировать публикацию нового сообщения на Reddit не вручную, а путем загрузки статьи с существующего сайта.
7.1. Изменить форму расписания
Давайте начнем с клиентского сайта и изменим существующийschedulePostForm.html - мы собираемся добавить:
Обратите внимание, что мы добавили:
-
кнопка - «Load from my Sites» - для запуска процесса
-
всплывающее окно - отображение списка сайтов и их статей
7.2. Загрузить сайты
Загрузка сайтов во всплывающем окне относительно проста с небольшим количеством JavaScript:
$('#myModal').on('shown.bs.modal', function () {
if($("#siteList").children().length > 0)
return;
$.get("sites", function(data){
$.each(data, function( index, site ) {
$("#siteList").append(''+site.name+' ')
});
});
});
7.3. Загрузить сообщения на сайте
Когда пользователь выбирает веб-сайт из списка, нам нужно показать статьи этого сайта - снова с некоторыми базовыми js:
function loadArticles(siteID,siteName){
$("#dropdownMenu1").html(siteName);
$.get("sites/articles?id="+siteID, function(data){
$("#articleList").html('');
$("#dropdownMenu2").html('Choose Article');
$.each(data, function( index, article ) {
$("#articleList").append(
''+article.title+' '+
new Date(article.publishDate).toUTCString()+' ')
});
}).fail(function(error){
alert(error.responseText);
});
}
Это, конечно, подключается к простой серверной операции для загрузки статей сайта:
@RequestMapping(value = "/sites/articles")
@ResponseBody
public List getSiteArticles(@RequestParam("id") Long siteId) {
return service.getArticlesFromSite(siteId);
}
Наконец, мы получаем данные статьи, заполняем форму и планируем отправку статьи в Reddit:
var title = "";
var link = "";
function chooseArticle(selectedTitle,selectedLink){
$("#dropdownMenu2").html(selectedTitle);
title=selectedTitle;
link = selectedLink;
}
function load(){
$("input[name='title']").val(title);
$("input[name='url']").val(link);
}
8. Интеграционные тесты
Наконец, давайте протестируем нашSiteService на двух разных форматах каналов:
public class SiteIntegrationTest {
private ISiteService service;
@Before
public void init() {
service = new SiteService();
}
@Test
public void whenUsingServiceToReadWordpressFeed_thenCorrect() {
Site site = new Site("/feed/");
List articles = service.getArticlesFromSite(site);
assertNotNull(articles);
for (SiteArticle article : articles) {
assertNotNull(article.getTitle());
assertNotNull(article.getLink());
}
}
@Test
public void whenUsingRomeToReadBloggerFeed_thenCorrect() {
Site site = new Site("http://blogname.blogspot.com/feeds/posts/default");
List articles = service.getArticlesFromSite(site);
assertNotNull(articles);
for (SiteArticle article : articles) {
assertNotNull(article.getTitle());
assertNotNull(article.getLink());
}
}
}
Здесь явно есть дублирование, но об этом мы позаботимся позже.
9. Заключение
В этой статье мы сфокусировались на новой небольшой функции - упрощении планирования публикации в Reddit.