Make It Easy to Schedule to Reddit – 使其易于安排到Reddit上

最后修改: 2015年 5月 30日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this article, we’re going to continue the case study and add a new feature to the Reddit application, with the goal of making it much simpler to schedule articles.

在本文中,我们将继续进行案例研究,并为Reddit应用程序添加一项新功能,目的是使文章的安排更加简单。

Instead of slowly adding in every article by hand in the schedule UI, the user can now just have some favorite sites to post articles to Reddit from. We’re going to use RSS to do that.

与其在日程表用户界面中慢慢地手工添加每一篇文章,用户现在只需有一些喜欢的网站来发布文章到Reddit。我们将使用RSS来做到这一点。

2. The Site Entity

2、网站实体

First – let’s create an entity to represent the 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;
}

Note that the url field represents the URL of the RSS feed of the site.

请注意,url字段代表该网站的RSS提要的URL。

3. The Repository and the Service

3.存储库和服务

Next – lets create the repository to work with the new Site entity:

下一步–让我们创建一个资源库,与新的网站实体一起工作。

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

And the service:

还有服务。

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. Load Data from the Feed

4.从饲料中加载数据

Now – let’s see how to load the articles details from website feed using the Rome Library.

现在–让我们看看如何使用罗马库从网站feed加载文章的细节。

We’ll first need to add Rome into our pom.xml:

我们首先需要将Rome添加到我们的pom.xml中。

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

And then use it to parse out the feeds of the sites:

然后用它来解析出各网站的feeds。

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

Finally – here’s the simple DTO that we’re going to use in the response:

最后–这是我们要在响应中使用的简单DTO。

public class SiteArticle {
    private String title;
    private String link;
    private Date publishDate;
}

5. Exception Handling

5.异常处理

Notice how, when parsing the feed, we’re wrapping the entire parsing logic into a try-catch block and – in case of an exception (any exception) – we’re wrapping it and throwing it.

请注意,在解析feed时,我们是如何将整个解析逻辑包裹在一个try-catch块中的,并且–在出现异常(任何异常)时–我们将它包裹起来并抛出。

The reason for that is simple – we need to control the type of exception that gets thrown out of the parsing process – so that we can then handle that exception and provide a proper response to the client of the API:

原因很简单–我们需要控制解析过程中抛出的异常类型–这样我们就可以处理该异常并向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. The Sites Page

6.网站页面

6.1. Display the Sites

6.1.显示网站

First, we will see how to show list of sites belonging to the logged in user:

首先,我们将看到如何显示属于登录用户的网站列表。

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

And here is the very simple front end piece:

而这里是非常简单的前端部分。

<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. Add a New Site

6.2.添加一个新网站

Next, let’s see how a user can create a new favorite site:

接下来,让我们看看用户如何创建一个新的收藏夹网站。

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

And here is the – again very simple – client side:

这里是–同样非常简单–客户端。

<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. Validating a Feed

6.3.验证饲料

The validation of a new feed is a bit of an expensive operation – we need to actually retrieve the feed and parse it out to validate it fully. Here is the simple service method:

验证一个新的饲料是一个有点昂贵的操作–我们需要实际检索饲料并将其解析出来以完全验证它。这里是简单的服务方法。

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

6.3. Delete a Site

6.3.删除一个网站

Now, let’s see how the user can 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);
}

And here the – again very simple – service level method:

这里是–同样非常简单–服务水平法。

public void deleteSiteById(Long siteId) {
    repo.delete(siteId);
}

7. Schedule a Post from a Site

7.安排一个网站的帖子

Now – let’s actually start using these sites and implement a basic way a user can schedule a new post to go out to Reddit not manually, but by loading in an article from an existing site.

现在–让我们真正开始使用这些网站,并实现一个基本的方法,让用户可以安排一个新的帖子去Reddit,而不是手动,而是通过从现有网站加载文章。

7.1. Modify Scheduling Form

7.1.修改日程安排表

Let’s start with the client site and modify the existing schedulePostForm.html – we’re going to add:

让我们从客户网站开始,修改现有的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>

Note that we’ve added:

请注意,我们已经添加了。

  • the button – “Load from my Sites” – to start the process
  • the pop-up – showing the list of sites and their articles

7.2. Load the Sites

7.2.加载网站

Loading the sites in the popup is relatively easy with a bit of javascript:

在弹出式窗口中加载网站是相对容易的,只需使用一点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. Load the Posts of a Site

7.3.加载一个网站的帖子

When the user select a website from the list, we need to show the articles of that site – again with some basic js:

当用户从列表中选择一个网站时,我们需要显示该网站的文章–还是用一些基本的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); 
    });
}

This of course hooks into a simple server side operation to load up the articles of a site:

当然,这与一个简单的服务器端操作挂钩,以加载一个网站的文章。

@RequestMapping(value = "/sites/articles")
@ResponseBody
public List<SiteArticle> getSiteArticles(@RequestParam("id") Long siteId) {
    return service.getArticlesFromSite(siteId);
}

Finally, we get the article data, fill in the form and schedule the article to go out to Reddit:

最后,我们得到文章数据,填写表格,安排文章发往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. Integration Tests

8.集成测试

Finally – let’s test our SiteService on two different feed formats:

最后–让我们在两种不同的饲料格式上测试我们的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());
        }
    }
}

There’s clearly a bit of duplication here, but we can take care of that later.

这里显然有一点重复,但我们可以稍后处理。

9. Conclusion

9.结论

In this installment we focused on a new, small feature – making the scheduling of the post to Reddit – simpler.

在这一期中,我们重点讨论了一个新的、小的功能–使发布到Reddit的时间安排更简单。