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

Сделать это легко по расписанию 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 handleFeed(RuntimeException ex, WebRequest request) {
    logger.error("500 Status Code", ex);
    String bodyOfResponse = ex.getLocalizedMessage();
    return new ResponseEntity(bodyOfResponse, new HttpHeaders(),
      HttpStatus.INTERNAL_SERVER_ERROR);
}




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

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

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

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

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

Site NameFeed URLActions

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+'
    • ') }); }); });

      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.