1概要

この記事では、/case-study-a-reddit-app-with-spring[ケーススタディを続ける]と

Redditアプリケーションに新しい機能を追加

することを目標にしています。記事をスケジュールするのが簡単です。

スケジュールのUIにすべての記事を手でゆっくり追加する代わりに、ユーザーは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

フィールドは、

サイトのRSSフィード

のURLを表します。

** 3リポジトリとサービス

**

Next – 新しい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フィードからデータを読み込む

それでは、ローマ図書館を使ってウェブサイトのフィードから記事の詳細を読み込む方法を見てみましょう。

最初にローマを

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. サイトの投稿を読み込む

**

ユーザーがリストからWebサイトを選択したら、そのサイトの記事を表示する必要があります。

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統合テスト

最後に、2つの異なるフィード形式で

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への投稿のスケジューリングが簡単になりました。