package io.kaif.web.v1; import static io.kaif.model.clientapp.ClientAppScope.VOTE; import static java.util.Arrays.asList; import static java.util.stream.Collectors.*; import java.util.Collections; import java.util.List; import java.util.Optional; import javax.validation.Valid; import javax.validation.constraints.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; 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.RestController; import io.swagger.annotations.Api; import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import io.kaif.flake.FlakeId; import io.kaif.model.clientapp.ClientAppUserAccessToken; import io.kaif.model.vote.ArticleVoter; import io.kaif.model.vote.DebateVoter; import io.kaif.model.vote.VoteState; import io.kaif.service.ArticleService; import io.kaif.service.VoteService; import io.kaif.web.v1.dto.V1VoteDto; @Api(tags = "vote", description = "Vote on articles or debates") @RestController @RequestMapping(value = "/v1/vote", produces = MediaType.APPLICATION_JSON_VALUE) public class V1VoteResource { static class VoteArticleEntry { @ApiModelProperty(required = true) @NotNull public FlakeId articleId; @ApiModelProperty(value = "note that articles only support EMPTY and UP state", required = true) @NotNull public VoteState voteState; } static class VoteDebateEntry { @ApiModelProperty(required = true) @NotNull public FlakeId debateId; @ApiModelProperty(required = true) @NotNull public VoteState voteState; } private static List<FlakeId> toFlakeIds(List<String> ids) { return Optional.ofNullable(ids) .orElse(Collections.emptyList()) .stream() .map(FlakeId::fromString) .collect(toList()); } @Autowired private VoteService voteService; @Autowired private ArticleService articleService; @ApiOperation(value = "[vote] List votes of the user on multiple articles", notes = "List votes of the user on multiple articles. When you paging articles, " + "you may want to know what user voted for those articles in the page. " + "You can send multiple articleIds to obtain votes in batch.") @RequiredScope(VOTE) @RequestMapping(value = "/article", method = RequestMethod.GET) public List<V1VoteDto> votesOfArticles(ClientAppUserAccessToken token, @ApiParam(value = "comma separated articleIds", allowMultiple = true) @RequestParam("article-id") List<String> articleIds) { return voteService.listArticleVoters(token, toFlakeIds(articleIds)) .stream() .map(ArticleVoter::toV1Dto) .collect(toList()); } @ApiOperation(value = "[vote] List votes of the user on multiple debates", notes = "List votes of the user on multiple debates. When you paging debates, " + "you may want to know what user voted for those debates in the page. " + "You can send multiple debateIds to obtain votes in batch.") @RequiredScope(VOTE) @RequestMapping(value = "/debate", method = RequestMethod.GET) public List<V1VoteDto> votesOfDebates(ClientAppUserAccessToken token, @ApiParam(value = "comma separated debateIds", allowMultiple = true) @RequestParam("debate-id") List<String> debateIds) { return voteService.listDebateVotersByIds(token, toFlakeIds(debateIds)) .stream() .map(DebateVoter::toV1Dto) .collect(toList()); } @ApiOperation(value = "[vote] List all votes of the user for all debates in an article", notes = "List all votes of the user on all debates of the article, this is recommend method when " + "you want all votes of the user in a large debate tree.") @RequiredScope(VOTE) @RequestMapping(value = "/debate/article/{articleId}", method = RequestMethod.GET) public List<V1VoteDto> votesOfDebatesOfArticle(ClientAppUserAccessToken token, @PathVariable("articleId") FlakeId articleId) { return voteService.listDebateVoters(token, articleId) .stream() .map(DebateVoter::toV1Dto) .collect(toList()); } @ApiOperation(value = "[vote] Vote on an article", notes = "Vote on an article. Note that kaif do not support DOWN vote on article.") @RequiredScope(VOTE) @RequestMapping(value = "/article", method = RequestMethod.POST, consumes = { MediaType.APPLICATION_JSON_VALUE }) public void article(ClientAppUserAccessToken token, @Valid @RequestBody VoteArticleEntry entry) { ignoreDuplicateVote(() -> { VoteState previousState = voteService.listArticleVoters(token, asList(entry.articleId)) .stream() .map(ArticleVoter::getVoteState) .findAny() .orElse(VoteState.EMPTY); //for api, we don't hack previousCount. this may cause user see stale total counting int previousCount = 0; voteService.voteArticle(entry.voteState, entry.articleId, token, previousState, previousCount); }); } private void ignoreDuplicateVote(Runnable runnable) { try { runnable.run(); } catch (DuplicateKeyException ignore) { // user duplicate vote, this mostly happened when user press browser back. // this typically is fine, we safely ignore } } @ApiOperation(value = "[vote] Vote on a debate", notes = "Vote on a debate.") @RequiredScope(VOTE) @RequestMapping(value = "/debate", method = RequestMethod.POST, consumes = { MediaType.APPLICATION_JSON_VALUE }) public void debate(ClientAppUserAccessToken token, @Valid @RequestBody VoteDebateEntry entry) { ignoreDuplicateVote(() -> { VoteState previousState = voteService.listDebateVotersByIds(token, asList(entry.debateId)) .stream() .map(DebateVoter::getVoteState) .findAny() .orElse(VoteState.EMPTY); //for api, we don't hack previousCount. this may cause user see stale total counting int previousCount = 0; voteService.voteDebate(entry.voteState, entry.debateId, token, previousState, previousCount); }); } }