package io.kaif.web.v1; import static java.util.stream.Collectors.*; import java.util.List; import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.URL; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import io.swagger.annotations.Api; import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiOperation; import io.kaif.flake.FlakeId; import io.kaif.model.article.Article; import io.kaif.model.clientapp.ClientAppScope; import io.kaif.model.clientapp.ClientAppUserAccessToken; import io.kaif.model.zone.Zone; import io.kaif.service.ArticleService; import io.kaif.service.VoteService; import io.kaif.web.v1.dto.V1ArticleDto; @Api(tags = "article", description = "Articles") @RestController @RequestMapping(value = "/v1/article", produces = MediaType.APPLICATION_JSON_VALUE) public class V1ArticleResource { static class ExternalLinkEntry { @Size(max = Article.URL_MAX) @NotNull @URL(regexp = Article.URL_PATTERN) @ApiModelProperty(required = true) public String url; @Size(min = Article.TITLE_MIN, max = Article.TITLE_MAX) @NotNull @ApiModelProperty(value = "title of article, min 3 chars", required = true) public String title; @NotNull @ApiModelProperty(required = true) public Zone zone; } static class SpeakEntry { @Size(max = Article.CONTENT_MAX, min = Article.CONTENT_MIN) @NotNull @ApiModelProperty(value = "content of article, min 10 chars", required = true) public String content; @Size(min = Article.TITLE_MIN, max = Article.TITLE_MAX) @NotNull @ApiModelProperty(value = "title of article, min 3 chars", required = true) public String title; @NotNull @ApiModelProperty(required = true) public Zone zone; } @Autowired private ArticleService articleService; @Autowired private VoteService voteService; @ApiOperation(value = "[public] Get an article", notes = "Get an article by articleId") @RequiredScope(ClientAppScope.PUBLIC) @RequestMapping(value = "/{articleId}", method = RequestMethod.GET) public V1ArticleDto article(ClientAppUserAccessToken accessToken, @PathVariable("articleId") FlakeId articleId) { return articleService.loadArticle(articleId).toV1Dto(); } @ApiOperation(value = "[public] List hot articles of all zones", notes = "List hot articles of all zones, 25 articles a page. " + "To retrieve next page, passing last article id of previous page in parameter start-article-id") @RequiredScope(ClientAppScope.PUBLIC) @RequestMapping(value = "/hot", method = RequestMethod.GET) public List<V1ArticleDto> hot(ClientAppUserAccessToken accessToken, @RequestParam(value = "start-article-id", required = false) FlakeId startArticleId) { return toDtos(articleService.listTopArticles(startArticleId)); } private List<V1ArticleDto> toDtos(List<Article> articles) { return articles.stream().map(Article::toV1Dto).collect(toList()); } @ApiOperation(value = "[public] List latest articles of all zones", notes = "List latest articles of all zones, 25 articles a page. " + "To retrieve next page, passing last article id of previous page in parameter start-article-id") @RequiredScope(ClientAppScope.PUBLIC) @RequestMapping(value = "/latest", method = RequestMethod.GET) public List<V1ArticleDto> latest(ClientAppUserAccessToken accessToken, @RequestParam(value = "start-article-id", required = false) FlakeId startArticleId) { return toDtos(articleService.listLatestArticles(startArticleId)); } @ApiOperation(value = "[public] List latest articles for the zone", notes = "List latest articles for the zone, 25 articles a page. " + "To retrieve next page, passing last article id of previous page in parameter start-article-id") @RequiredScope(ClientAppScope.PUBLIC) @RequestMapping(value = "/zone/{zone}/latest", method = RequestMethod.GET) public List<V1ArticleDto> latestByZone(ClientAppUserAccessToken accessToken, @PathVariable("zone") String zone, @RequestParam(value = "start-article-id", required = false) FlakeId startArticleId) { return toDtos(articleService.listLatestZoneArticles(Zone.valueOf(zone), startArticleId)); } @ApiOperation(value = "[public] List hot articles for the zone", notes = "List hot articles for the zone, 25 articles a page. " + "To retrieve next page, passing last article id of previous page in parameter start-article-id") @RequiredScope(ClientAppScope.PUBLIC) @RequestMapping(value = "/zone/{zone}/hot", method = RequestMethod.GET) public List<V1ArticleDto> hotByZone(ClientAppUserAccessToken accessToken, @PathVariable("zone") String zone, @RequestParam(value = "start-article-id", required = false) FlakeId startArticleId) { return toDtos(articleService.listHotZoneArticles(Zone.valueOf(zone), startArticleId)); } @ApiOperation(value = "[public] List submitted articles of the user", notes = "List submitted articles of the user, 25 articles a page. " + "To retrieve next page, passing last article id of previous page in parameter start-article-id") @RequiredScope(ClientAppScope.PUBLIC) @RequestMapping(value = "/user/{username}/submitted", method = RequestMethod.GET) public List<V1ArticleDto> submitted(ClientAppUserAccessToken accessToken, @PathVariable("username") String username, @RequestParam(value = "start-article-id", required = false) FlakeId startArticleId) { return toDtos(articleService.listArticlesByAuthor(username, startArticleId)); } @ApiOperation(value = "[vote] List voted articles of the user", notes = "List voted articles of the user, 25 articles a page. " + "To retrieve next page, passing last article id of previous page in parameter start-article-id") @RequiredScope(ClientAppScope.VOTE) @RequestMapping(value = "/voted", method = RequestMethod.GET) public List<V1ArticleDto> voted(ClientAppUserAccessToken accessToken, @RequestParam(value = "start-article-id", required = false) FlakeId startArticleId) { return toDtos(voteService.listUpVotedArticles(accessToken, startArticleId)); } @ApiOperation(value = "[article] Create an article with external link", notes = "Create an article with external link in specified zone. Before calling the method, " + "it is recommended check the link already submitted by other users. check API - " + "'GET /zone/{zone}/external-link'") @ResponseStatus(HttpStatus.CREATED) @RequiredScope(ClientAppScope.ARTICLE) @RequestMapping(value = "/external-link", method = RequestMethod.PUT, consumes = { MediaType.APPLICATION_JSON_VALUE }) public V1ArticleDto externalLink(ClientAppUserAccessToken token, @Valid @RequestBody ExternalLinkEntry entry) { return articleService.createExternalLink(token, entry.zone, entry.title.trim(), entry.url.trim()).toV1Dto(); } @ApiOperation(value = "[public] List articles for external link", notes = "List most 3 external-url-articles for a specific url." + "You are encouraged to show exist articles to end user before creating the article " + "to prevent article duplication in zone.") @RequiredScope(ClientAppScope.PUBLIC) @RequestMapping(value = "/zone/{zone}/external-link", method = RequestMethod.GET) public List<V1ArticleDto> externalLink(ClientAppUserAccessToken accessToken, @PathVariable(value = "zone") String zone, @RequestParam("url") String url) { return toDtos(articleService.listArticlesByExternalLink(Zone.valueOf(zone), url)); } @ApiOperation(value = "[public] check url already submitted in zone", notes = "Check url already submitted as external-url-article in zone") @RequiredScope(ClientAppScope.PUBLIC) @RequestMapping(value = "/zone/{zone}/external-link/exist", method = RequestMethod.GET) public boolean isExternalLinkExist(ClientAppUserAccessToken accessToken, @PathVariable(value = "zone") String zone, @RequestParam("url") String url) { return articleService.isExternalLinkExist(Zone.valueOf(zone), url); } @ApiOperation(value = "[public] check the article can be deleted", notes = "Check the article can be deleted by user. Note that there is time-constraint " + "on article deletion. If article is old enough, the method return false.") @RequiredScope(ClientAppScope.PUBLIC) @RequestMapping(value = "/{articleId}/can-delete", method = RequestMethod.GET) public boolean canDeleteArticle(ClientAppUserAccessToken accessToken, @PathVariable(value = "articleId") FlakeId articleId, @RequestParam("username") String username) { return articleService.canDeleteArticle(username, articleId); } @ApiOperation(value = "[article] Create an article with content", notes = "Create an article with content in specified zone") @ResponseStatus(HttpStatus.CREATED) @RequiredScope(ClientAppScope.ARTICLE) @RequestMapping(value = "/speak", method = RequestMethod.PUT, consumes = { MediaType.APPLICATION_JSON_VALUE }) public V1ArticleDto speak(ClientAppUserAccessToken token, @Valid @RequestBody SpeakEntry entry) { return articleService.createSpeak(token, entry.zone, entry.title.trim(), entry.content.trim()) .toV1Dto(); } @ApiOperation(value = "[article] Delete an article", notes = "Delete an article, return status 200 if success, response as error if sign-in user " + "not allow to delete the article.") @RequiredScope(ClientAppScope.ARTICLE) @RequestMapping(value = "/{articleId}", method = RequestMethod.DELETE, consumes = { MediaType.APPLICATION_JSON_VALUE }) public void deleteArticle(ClientAppUserAccessToken token, @PathVariable("articleId") FlakeId articleId) { articleService.deleteArticle(token, articleId); } }