1. Overview
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.
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.
2. The Site Entity
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.
3. The Repository and the Service
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
Now – let’s see how to load the articles details from website feed using the Rome Library.
We’ll first need to add Rome into our 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:
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:
public class SiteArticle { private String title; private String link; private Date publishDate; }
5. Exception Handling
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.
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:
@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.1. Display the Sites
First, we will see how to show list of sites belonging to the logged in user:
@RequestMapping("/sites") public String getUserSites(Model model) { List<Site> sites = service.getSitesByUser(getCurrentUser()); model.addAttribute("sites", sites); return "siteListView"; }
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> <tr th:each="site : ${sites}"> <td th:text="${site.getName()}"></td> <td th:text="${site.getUrl()}"></td> <td><a href="#" th:onclick="'javascript:deleteSite(\'' +${site.getId()}+ '\') '">Delete</a></td> </tr> </table> <script> function deleteSite(id){ $.ajax({ url: 'sites/'+id, type: 'DELETE', success: function(result) { window.location.href="sites" } }); } </script>
6.2. Add A New Site
Next, let’s see how a user can create a new favorite site:
@RequestMapping(value = "/sites", method = RequestMethod.POST) public String addSite( Model model, @RequestParam("url") String url, @RequestParam("name") String name) throws ParseException { Site site = new Site(); site.setName(name); site.setUrl(url); site.setUser(getCurrentUser()); service.saveSite(site); List<Site> sites = service.getSitesByUser(getCurrentUser()); model.addAttribute("sites", sites); return "siteListView"; }
And here is the – again very simple – client side:
<form th:action="@{/sites}" method="post"> <input name="name"/> <input name="url" /> <button type="submit" id="submitBut">Add Site</button> </form> <script> $("#submitBut").click(function(event) { event.preventDefault(); $.get("sites/isValidUrl?url="+$("#url").val(), function(data){ if(data == true) $("form").submit(); else alert("Invalid Feed Url"); });
}); </script>
6.3. Validating a Feed
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.
So, in this first version – instead of having the validation as part of the create logic, it’s a separate operation in the API:
@RequestMapping(value = "/sites/isValidUrl") @ResponseBody public boolean isValidUrl(@RequestParam("url") final String url) { return service.isValidFeedUrl(url); }
And 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
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
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.
7.1. Modify Scheduling Form
Let’s start with the client site and modify the existing schedulePostForm.html – we’re going to add:
<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
Loading the sites in the popup is relatively easy with a bit of javascript:
$('#myModal').on('shown.bs.modal', function () { if($("#siteList").children().length > 0) return; $.get("sites/list", function(data){ $.each(data, function( index, site ) { $("#siteList").append('<li><a href="#" onclick="loadArticles('+ site.id+',\''+site.name+'\')">'+site.name+'</a></li>') }); }); });
This of course hooks into a simple server side operation to load the sites:
@RequestMapping(value = "/sites/list") @ResponseBody public List<Site> getSitesList() { return service.getSitesByUser(getCurrentUser()); }
7.3. Load the Posts of a Site
When the user select a website from the list, we need to show the articles of that site – again with some basic 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); }); }
And similarly to loading up the sites, we’re going to provide 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:
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
Finally – let’s test our SiteService on two different feed formats:
public class SiteIntegrationTest { private ISiteService service; @Before public void init() { service = new SiteService(); } @Test public void whenUsingServiceToReadWordpressFeed_thenCorrect() { Site site = new Site("http://www.baeldung.com/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
In this installment we focused on a new, small feature – making the scheduling of the post to Reddit – simpler.