Перейти к основному содержимому

Упростите планирование для Reddit

· 6 мин. чтения

1. Обзор

В этой статье мы продолжим изучение примера и добавим новую функцию в приложение Reddit с целью значительно упростить планирование статей.

Вместо того, чтобы медленно добавлять каждую статью вручную в пользовательском интерфейсе расписания, теперь пользователь может просто иметь несколько любимых сайтов для публикации статей в Reddit. Для этого мы будем использовать RSS.

2. Объект сайта

Во-первых, давайте создадим объект для представления сайта:

@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-адрес RSS-канала сайта .

3. Репозиторий и Сервис

Далее — давайте создадим репозиторий для работы с новым объектом Site:

public interface SiteRepository extends JpaRepository<Site, Long> {
List<Site> findByUser(User user);
}

И обслуживание:

public interface ISiteService {

List<Site> 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<Site> 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. Загрузить данные из фида

Теперь давайте посмотрим, как загрузить информацию о статьях из ленты веб-сайта с помощью Римской библиотеки.

Сначала нам нужно добавить Rome в наш pom.xml :

<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.5.0</version>
</dependency>

А затем используйте его для анализа фидов сайтов:

public List<SiteArticle> getArticlesFromSite(Long siteId) {
Site site = repo.findOne(siteId);
return getArticlesFromSite(site);
}

List<SiteArticle> getArticlesFromSite(Site site) {
List<SyndEntry> entries;
try {
entries = getFeedEntries(site.getUrl());
} catch (Exception e) {
throw new FeedServerException("Error Occurred while parsing feed", e);
}
return parseFeed(entries);
}

private List<SyndEntry> 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<SiteArticle> parseFeed(List<SyndEntry> entries) {
List<SiteArticle> articles = new ArrayList<SiteArticle>();
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 и — в случае исключения (любого исключения) — мы упаковываем его и выбрасываем.

Причина этого проста — нам нужно контролировать тип исключения, которое выбрасывается в процессе синтаксического анализа, чтобы мы могли затем обработать это исключение и предоставить правильный ответ клиенту API:

@ExceptionHandler({ FeedServerException.class })
public ResponseEntity<Object> handleFeed(RuntimeException ex, WebRequest request) {
logger.error("500 Status Code", ex);
String bodyOfResponse = ex.getLocalizedMessage();
return new ResponseEntity<Object>(bodyOfResponse, new HttpHeaders(),
HttpStatus.INTERNAL_SERVER_ERROR);
}

6. Страница сайтов

6.1. Показать сайты

Во-первых, мы увидим, как показать список сайтов, принадлежащих вошедшему в систему пользователю:

@RequestMapping(value = "/sites")
@ResponseBody
public List<Site> getSitesList() {
return service.getSitesByUser(getCurrentUser());
}

А вот и очень простая передняя часть:

<table>
<thead>
<tr><th>Site Name</th><th>Feed URL</th><th>Actions</th></tr>
</thead>
</table>
<script>
$(function(){
$.get("sites", function(data){
$.each(data, function( index, site ) {
$('.table').append('<tr><td>'+site.name+'</td><td>'+site.url+
'</td><td><a href="#" onclick="deleteSite('+site.id+') ">Delete</a> </td></tr>');
});
});
});

function deleteSite(id){
$.ajax({ url: 'sites/'+id, type: 'DELETE', success: function(result) {
window.location.href="mysites"
}
});
}
</script>

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

А вот — опять же очень просто — клиентская часть:

<form>
<input name="name" />
<input id="url" name="url" />
<button type="submit" onclick="addSite()">Add Site</button>
</form>

<script>
function addSite(){
$.post("sites",$('form').serialize(), function(data){
window.location.href="mysites";
}).fail(function(error){
alert(error.responseText);
});
}
</script>

6.3. Проверка фида

Проверка нового фида — довольно затратная операция: нам нужно получить фид и проанализировать его, чтобы полностью проверить. Вот простой метод обслуживания:

public boolean isValidFeedUrl(String feedUrl) {
try {
return getFeedEntries(feedUrl).size() > 0;
} catch (Exception e) {
return false;
}
}

6.3. Удалить сайт

Теперь давайте посмотрим, как пользователь может удалить сайт из своего списка любимых сайтов :

@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 — мы собираемся добавить:

<button data-target="#myModal">Load from My Sites</button>
<div id="myModal">
<button id="dropdownMenu1">Choose Site</button><ul id="siteList"></ul>
<button id="dropdownMenu2">Choose Article</button><ul id="articleList"></ul>
<button onclick="load()">Load</button>
</div>

Обратите внимание, что мы добавили:

  • кнопка — « Загрузить с моих сайтов » — для запуска процесса
  • всплывающее окно — показывает список сайтов и их статьи

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('<li><a href="#" onclick="loadArticles('+
site.id+',\''+site.name+'\')">'+site.name+'</a></li>')
});
});
});

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(
'<li><a href="#" onclick="chooseArticle(\''+article.title+
'\',\''+article.link+'\')"><b>'+article.title+'</b> <small>'+
new Date(article.publishDate).toUTCString()+'</small></li>')
});
}).fail(function(error){
alert(error.responseText);
});
}

Это, конечно, связано с простой операцией на стороне сервера для загрузки статей сайта:

@RequestMapping(value = "/sites/articles")
@ResponseBody
public List<SiteArticle> 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<SiteArticle> 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<SiteArticle> articles = service.getArticlesFromSite(site);

assertNotNull(articles);
for (SiteArticle article : articles) {
assertNotNull(article.getTitle());
assertNotNull(article.getLink());
}
}
}

Здесь явно есть некоторое дублирование, но мы можем позаботиться об этом позже.

9. Заключение

В этом выпуске мы сосредоточились на новой небольшой функции — упрощении планирования публикации в Reddit.