diff --git a/src/main/java/io/spring/api/ArticlesApi.java b/src/main/java/io/spring/api/ArticlesApi.java index 437e332..07defd0 100644 --- a/src/main/java/io/spring/api/ArticlesApi.java +++ b/src/main/java/io/spring/api/ArticlesApi.java @@ -56,13 +56,21 @@ public class ArticlesApi { }}); } + @GetMapping(path = "feed") + public ResponseEntity getFeed(@RequestParam(value = "offset", defaultValue = "0") int offset, + @RequestParam(value = "limit", defaultValue = "20") int limit, + @AuthenticationPrincipal User user) { + return ResponseEntity.ok(articleQueryService.findUserFeed(user, new Page(offset, limit))); + } + @GetMapping public ResponseEntity getArticles(@RequestParam(value = "offset", defaultValue = "0") int offset, @RequestParam(value = "limit", defaultValue = "20") int limit, @RequestParam(value = "tag", required = false) String tag, @RequestParam(value = "favorited", required = false) String favoritedBy, - @RequestParam(value = "author", required = false) String author) { - return ResponseEntity.ok(articleQueryService.findRecentArticles(tag, author, favoritedBy, new Page(offset, limit))); + @RequestParam(value = "author", required = false) String author, + @AuthenticationPrincipal User user) { + return ResponseEntity.ok(articleQueryService.findRecentArticles(tag, author, favoritedBy, new Page(offset, limit), user)); } } diff --git a/src/main/java/io/spring/api/TagsApi.java b/src/main/java/io/spring/api/TagsApi.java new file mode 100644 index 0000000..0c573cb --- /dev/null +++ b/src/main/java/io/spring/api/TagsApi.java @@ -0,0 +1,28 @@ +package io.spring.api; + +import io.spring.application.tag.TagsQueryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.HashMap; + +@RestController +@RequestMapping(path = "tags") +public class TagsApi { + private TagsQueryService tagsQueryService; + + @Autowired + public TagsApi(TagsQueryService tagsQueryService) { + this.tagsQueryService = tagsQueryService; + } + + @GetMapping + public ResponseEntity getTags() { + return ResponseEntity.ok(new HashMap() {{ + put("tags", tagsQueryService.allTags()); + }}); + } +} diff --git a/src/main/java/io/spring/api/security/WebSecurityConfig.java b/src/main/java/io/spring/api/security/WebSecurityConfig.java index 62c0093..361fd32 100644 --- a/src/main/java/io/spring/api/security/WebSecurityConfig.java +++ b/src/main/java/io/spring/api/security/WebSecurityConfig.java @@ -25,6 +25,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() + .antMatchers(HttpMethod.GET, "/articles/feed").authenticated() .antMatchers(HttpMethod.POST, "/users", "/users/login").permitAll() .antMatchers(HttpMethod.GET, "/articles/**", "/profiles/**").permitAll() .anyRequest().authenticated(); diff --git a/src/main/java/io/spring/application/article/ArticleFavoritesQueryService.java b/src/main/java/io/spring/application/article/ArticleFavoritesQueryService.java index 0d89c35..386bef6 100644 --- a/src/main/java/io/spring/application/article/ArticleFavoritesQueryService.java +++ b/src/main/java/io/spring/application/article/ArticleFavoritesQueryService.java @@ -1,13 +1,21 @@ package io.spring.application.article; +import io.spring.core.user.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Set; + @Mapper @Component public interface ArticleFavoritesQueryService { boolean isUserFavorite(@Param("userId") String userId, @Param("articleId") String articleId); int articleFavoriteCount(@Param("articleId") String articleId); + + List articlesFavoriteCount(@Param("ids") List ids); + + Set userFavorites(@Param("ids") List ids, @Param("currentUser") User currentUser); } diff --git a/src/main/java/io/spring/application/article/ArticleQueryService.java b/src/main/java/io/spring/application/article/ArticleQueryService.java index 79b3ebf..6f8a7e6 100644 --- a/src/main/java/io/spring/application/article/ArticleQueryService.java +++ b/src/main/java/io/spring/application/article/ArticleQueryService.java @@ -3,12 +3,19 @@ package io.spring.application.article; import io.spring.application.Page; import io.spring.application.profile.UserRelationshipQueryService; import io.spring.core.user.User; +import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; + +import static java.util.stream.Collectors.toList; @Service public class ArticleQueryService { @@ -49,6 +56,56 @@ public class ArticleQueryService { } } + public ArticleDataList findRecentArticles(String tag, String author, String favoritedBy, Page page, User currentUser) { + List articleIds = articleReadService.queryArticles(tag, author, favoritedBy, page); + int articleCount = articleReadService.countArticle(tag, author, favoritedBy); + if (articleIds.size() == 0) { + return new ArticleDataList(new ArrayList<>(), articleCount); + } else { + List articles = articleReadService.findArticles(articleIds); + fillExtraInfo(articles, currentUser); + return new ArticleDataList(articles, articleCount); + } + } + + private void fillExtraInfo(List articles, User currentUser) { + setFavoriteCount(articles); + if (currentUser != null) { + setIsFavorite(articles, currentUser); + setIsFollowingAuthor(articles, currentUser); + } + } + + private void setIsFollowingAuthor(List articles, User currentUser) { + Set followingAuthors = userRelationshipQueryService.followingAuthors( + currentUser.getId(), + articles.stream().map(articleData1 -> articleData1.getProfileData().getId()).collect(toList())); + articles.forEach(articleData -> { + if (followingAuthors.contains(articleData.getProfileData().getId())) { + articleData.getProfileData().setFollowing(true); + } + }); + } + + private void setFavoriteCount(List articles) { + List favoritesCounts = articleFavoritesQueryService.articlesFavoriteCount(articles.stream().map(ArticleData::getId).collect(toList())); + Map countMap = new HashMap<>(); + favoritesCounts.forEach(item -> { + countMap.put(item.getId(), item.getCount()); + }); + articles.forEach(articleData -> articleData.setFavoritesCount(countMap.get(articleData.getId()))); + } + + private void setIsFavorite(List articles, User currentUser) { + Set favoritedArticles = articleFavoritesQueryService.userFavorites(articles.stream().map(articleData -> articleData.getId()).collect(toList()), currentUser); + + articles.forEach(articleData -> { + if (favoritedArticles.contains(articleData.getId())) { + articleData.setFavorited(true); + } + }); + } + private void fillExtraInfo(String id, User user, ArticleData articleData) { articleData.setFavorited(articleFavoritesQueryService.isUserFavorite(user.getId(), id)); articleData.setFavoritesCount(articleFavoritesQueryService.articleFavoriteCount(id)); @@ -58,11 +115,22 @@ public class ArticleQueryService { articleData.getProfileData().getId())); } - public ArticleDataList findRecentArticles(String tag, String author, String favoritedBy, Page page) { - List articleIds = articleReadService.queryArticles(tag, author, favoritedBy, page); - int articleCount = articleReadService.countArticle(tag, author, favoritedBy); - return new ArticleDataList( - articleIds.size() == 0 ? new ArrayList<>() : articleReadService.findArticles(articleIds), - articleCount); + public ArticleDataList findUserFeed(User user, Page page) { + List followdUsers = userRelationshipQueryService.followedUsers(user.getId()); + if (followdUsers.size() == 0) { + return new ArticleDataList(new ArrayList<>(), 0); + } else { + List articles = articleReadService.findArticlesOfAuthors(followdUsers, page); + fillExtraInfo(articles, user); + int count = articleReadService.countFeedSize(followdUsers); + return new ArticleDataList(articles, count); + } } } + +@Data +@NoArgsConstructor +class ArticleFavoriteCount { + private String id; + private int count; +} \ No newline at end of file diff --git a/src/main/java/io/spring/application/article/ArticleReadService.java b/src/main/java/io/spring/application/article/ArticleReadService.java index 8ed9043..0e2274c 100644 --- a/src/main/java/io/spring/application/article/ArticleReadService.java +++ b/src/main/java/io/spring/application/article/ArticleReadService.java @@ -19,4 +19,8 @@ public interface ArticleReadService { int countArticle(@Param("tag") String tag, @Param("author") String author, @Param("favoritedBy") String favoritedBy); List findArticles(@Param("articleIds") List articleIds); + + List findArticlesOfAuthors(@Param("authors") List authors, @Param("page") Page page); + + int countFeedSize(@Param("authors") List authors); } diff --git a/src/main/java/io/spring/application/profile/UserRelationshipQueryService.java b/src/main/java/io/spring/application/profile/UserRelationshipQueryService.java index 34895a0..c533880 100644 --- a/src/main/java/io/spring/application/profile/UserRelationshipQueryService.java +++ b/src/main/java/io/spring/application/profile/UserRelationshipQueryService.java @@ -1,11 +1,19 @@ package io.spring.application.profile; +import io.spring.application.article.ArticleData; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Set; + @Component @Mapper public interface UserRelationshipQueryService { boolean isUserFollowing(@Param("userId") String userId, @Param("anotherUserId") String anotherUserId); + + Set followingAuthors(@Param("userId") String userId, @Param("ids") List ids); + + List followedUsers(@Param("userId") String userId); } diff --git a/src/main/java/io/spring/application/tag/TagReadService.java b/src/main/java/io/spring/application/tag/TagReadService.java new file mode 100644 index 0000000..5a77c52 --- /dev/null +++ b/src/main/java/io/spring/application/tag/TagReadService.java @@ -0,0 +1,12 @@ +package io.spring.application.tag; + +import org.apache.ibatis.annotations.Mapper; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Mapper +public interface TagReadService { + List all(); +} diff --git a/src/main/java/io/spring/application/tag/TagsQueryService.java b/src/main/java/io/spring/application/tag/TagsQueryService.java new file mode 100644 index 0000000..16968ab --- /dev/null +++ b/src/main/java/io/spring/application/tag/TagsQueryService.java @@ -0,0 +1,18 @@ +package io.spring.application.tag; + +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class TagsQueryService { + private TagReadService tagReadService; + + public TagsQueryService(TagReadService tagReadService) { + this.tagReadService = tagReadService; + } + + public List allTags() { + return tagReadService.all(); + } +} diff --git a/src/main/resources/mapper/ArticleFavoritesQueryService.xml b/src/main/resources/mapper/ArticleFavoritesQueryService.xml index e1e3d04..c3db49f 100644 --- a/src/main/resources/mapper/ArticleFavoritesQueryService.xml +++ b/src/main/resources/mapper/ArticleFavoritesQueryService.xml @@ -7,5 +7,30 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/ArticleReadService.xml b/src/main/resources/mapper/ArticleReadService.xml index 84c0ea3..c8ad921 100644 --- a/src/main/resources/mapper/ArticleReadService.xml +++ b/src/main/resources/mapper/ArticleReadService.xml @@ -82,6 +82,20 @@ #{id} + + diff --git a/src/main/resources/mapper/TagReadService.xml b/src/main/resources/mapper/TagReadService.xml new file mode 100644 index 0000000..5902be2 --- /dev/null +++ b/src/main/resources/mapper/TagReadService.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/mapper/UserRelationshipQueryService.xml b/src/main/resources/mapper/UserRelationshipQueryService.xml index b281ad7..4639631 100644 --- a/src/main/resources/mapper/UserRelationshipQueryService.xml +++ b/src/main/resources/mapper/UserRelationshipQueryService.xml @@ -4,4 +4,15 @@ + + \ No newline at end of file diff --git a/src/test/java/io/spring/TestHelper.java b/src/test/java/io/spring/TestHelper.java index 809c34e..2e16528 100644 --- a/src/test/java/io/spring/TestHelper.java +++ b/src/test/java/io/spring/TestHelper.java @@ -22,7 +22,6 @@ public class TestHelper { } public static ArticleData getArticleDataFromArticleAndUser(Article article, User user) { - DateTime time = new DateTime(); return new ArticleData( article.getId(), article.getSlug(), @@ -31,8 +30,8 @@ public class TestHelper { article.getBody(), false, 0, - time, - time, + article.getCreatedAt(), + article.getUpdatedAt(), Arrays.asList("joda"), new ProfileData(user.getId(), user.getUsername(), user.getBio(), user.getImage(), false)); } diff --git a/src/test/java/io/spring/api/ArticlesApiTest.java b/src/test/java/io/spring/api/ArticlesApiTest.java index 7b36c5d..ecd0792 100644 --- a/src/test/java/io/spring/api/ArticlesApiTest.java +++ b/src/test/java/io/spring/api/ArticlesApiTest.java @@ -46,6 +46,7 @@ public class ArticlesApiTest extends TestWithCurrentUser { @Before public void setUp() throws Exception { RestAssured.port = port; + userFixture(); } @Test @@ -114,9 +115,8 @@ public class ArticlesApiTest extends TestWithCurrentUser { @Test public void should_read_article_success() throws Exception { String slug = "test-new-article"; - Article article = new Article("Test New Article", "Desc", "Body", new String[]{"java", "spring", "jpg"}, user.getId()); - DateTime time = new DateTime(); + Article article = new Article("Test New Article", "Desc", "Body", new String[]{"java", "spring", "jpg"}, user.getId(), time); ArticleData articleData = TestHelper.getArticleDataFromArticleAndUser(article, user); when(articleQueryService.findBySlug(eq(slug), eq(null))).thenReturn(Optional.of(articleData)); diff --git a/src/test/java/io/spring/api/ListArticleApiTest.java b/src/test/java/io/spring/api/ListArticleApiTest.java index 4d6296c..f767a2c 100644 --- a/src/test/java/io/spring/api/ListArticleApiTest.java +++ b/src/test/java/io/spring/api/ListArticleApiTest.java @@ -12,6 +12,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit4.SpringRunner; +import static io.restassured.RestAssured.given; import static io.spring.TestHelper.articleDataFixture; import static java.util.Arrays.asList; import static org.mockito.Matchers.eq; @@ -36,11 +37,35 @@ public class ListArticleApiTest extends TestWithCurrentUser { public void should_get_default_article_list() throws Exception { ArticleDataList articleDataList = new ArticleDataList( asList(articleDataFixture("1", user), articleDataFixture("2", user)), 2); - when(articleQueryService.findRecentArticles(eq(null), eq(null), eq(null), eq(new Page(0, 20)))).thenReturn(articleDataList); + when(articleQueryService.findRecentArticles(eq(null), eq(null), eq(null), eq(new Page(0, 20)), eq(null))).thenReturn(articleDataList); RestAssured.when() .get("/articles") .prettyPeek() .then() .statusCode(200); } + + @Test + public void should_get_feeds_401_without_login() throws Exception { + RestAssured.when() + .get("/articles/feed") + .prettyPeek() + .then() + .statusCode(401); + } + + @Test + public void should_get_feeds_success() throws Exception { + ArticleDataList articleDataList = new ArticleDataList( + asList(articleDataFixture("1", user), articleDataFixture("2", user)), 2); + when(articleQueryService.findUserFeed(eq(user), eq(new Page(0, 20)))).thenReturn(articleDataList); + + given() + .header("Authorization", "Token " + token) + .when() + .get("/articles/feed") + .prettyPeek() + .then() + .statusCode(200); + } } diff --git a/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java b/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java index bc2bad7..7f72500 100644 --- a/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java +++ b/src/test/java/io/spring/application/article/ArticleQueryServiceTest.java @@ -5,6 +5,7 @@ import io.spring.core.article.Article; import io.spring.core.article.ArticleRepository; import io.spring.core.favorite.ArticleFavorite; import io.spring.core.favorite.ArticleFavoriteRepository; +import io.spring.core.user.FollowRelation; import io.spring.core.user.User; import io.spring.core.user.UserRepository; import io.spring.infrastructure.article.MyBatisArticleRepository; @@ -19,7 +20,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; import org.springframework.test.context.junit4.SpringRunner; -import java.util.Arrays; import java.util.Optional; import static org.hamcrest.CoreMatchers.anyOf; @@ -29,7 +29,11 @@ import static org.junit.Assert.*; @RunWith(SpringRunner.class) @MybatisTest -@Import({ArticleQueryService.class, MyBatisUserRepository.class, MyBatisArticleRepository.class, MyBatisArticleFavoriteRepository.class}) +@Import({ + ArticleQueryService.class, + MyBatisUserRepository.class, + MyBatisArticleRepository.class, + MyBatisArticleFavoriteRepository.class}) public class ArticleQueryServiceTest { @Autowired private ArticleQueryService queryService; @@ -83,12 +87,12 @@ public class ArticleQueryServiceTest { Article anotherArticle = new Article("new article", "desc", "body", new String[]{"test"}, user.getId(), new DateTime().minusHours(1)); articleRepository.save(anotherArticle); - ArticleDataList recentArticles = queryService.findRecentArticles(null, null, null, new Page()); + ArticleDataList recentArticles = queryService.findRecentArticles(null, null, null, new Page(), user); assertThat(recentArticles.getCount(), is(2)); assertThat(recentArticles.getArticleDatas().size(), is(2)); assertThat(recentArticles.getArticleDatas().get(0).getId(), is(article.getId())); - ArticleDataList nodata = queryService.findRecentArticles(null, null, null, new Page(2, 10)); + ArticleDataList nodata = queryService.findRecentArticles(null, null, null, new Page(2, 10), user); assertThat(nodata.getCount(), is(2)); assertThat(nodata.getArticleDatas().size(), is(0)); } @@ -101,7 +105,7 @@ public class ArticleQueryServiceTest { Article anotherArticle = new Article("new article", "desc", "body", new String[]{"test"}, anotherUser.getId()); articleRepository.save(anotherArticle); - ArticleDataList recentArticles = queryService.findRecentArticles(null, user.getId(), null, new Page()); + ArticleDataList recentArticles = queryService.findRecentArticles(null, user.getId(), null, new Page(), user); assertThat(recentArticles.getArticleDatas().size(), is(1)); assertThat(recentArticles.getCount(), is(1)); } @@ -117,10 +121,13 @@ public class ArticleQueryServiceTest { ArticleFavorite articleFavorite = new ArticleFavorite(article.getId(), anotherUser.getId()); articleFavoriteRepository.save(articleFavorite); - ArticleDataList recentArticles = queryService.findRecentArticles(null, null, anotherUser.getId(), new Page()); + ArticleDataList recentArticles = queryService.findRecentArticles(null, null, anotherUser.getId(), new Page(), anotherUser); assertThat(recentArticles.getArticleDatas().size(), is(1)); assertThat(recentArticles.getCount(), is(1)); - assertThat(recentArticles.getArticleDatas().get(0).getId(), is(article.getId())); + ArticleData articleData = recentArticles.getArticleDatas().get(0); + assertThat(articleData.getId(), is(article.getId())); + assertThat(articleData.getFavoritesCount(), is(1)); + assertThat(articleData.isFavorited(), is(true)); } @Test @@ -128,12 +135,43 @@ public class ArticleQueryServiceTest { Article anotherArticle = new Article("new article", "desc", "body", new String[]{"test"}, user.getId()); articleRepository.save(anotherArticle); - ArticleDataList recentArticles = queryService.findRecentArticles("spring", null, null, new Page()); + ArticleDataList recentArticles = queryService.findRecentArticles("spring", null, null, new Page(), user); assertThat(recentArticles.getArticleDatas().size(), is(1)); assertThat(recentArticles.getCount(), is(1)); assertThat(recentArticles.getArticleDatas().get(0).getId(), is(article.getId())); - ArticleDataList notag = queryService.findRecentArticles("notag", null, null, new Page()); + ArticleDataList notag = queryService.findRecentArticles("notag", null, null, new Page(), user); assertThat(notag.getCount(), is(0)); } + + @Test + public void should_show_following_if_user_followed_author() throws Exception { + User anotherUser = new User("other@email.com", "other", "123", "", ""); + userRepository.save(anotherUser); + + FollowRelation followRelation = new FollowRelation(anotherUser.getId(), user.getId()); + userRepository.saveRelation(followRelation); + + ArticleDataList recentArticles = queryService.findRecentArticles(null, null, null, new Page(), anotherUser); + assertThat(recentArticles.getCount(), is(1)); + ArticleData articleData = recentArticles.getArticleDatas().get(0); + assertThat(articleData.getProfileData().isFollowing(), is(true)); + } + + @Test + public void should_get_user_feed() throws Exception { + User anotherUser = new User("other@email.com", "other", "123", "", ""); + userRepository.save(anotherUser); + + FollowRelation followRelation = new FollowRelation(anotherUser.getId(), user.getId()); + userRepository.saveRelation(followRelation); + + ArticleDataList userFeed = queryService.findUserFeed(user, new Page()); + assertThat(userFeed.getCount(), is(0)); + + ArticleDataList anotherUserFeed = queryService.findUserFeed(anotherUser, new Page()); + assertThat(anotherUserFeed.getCount(), is(1)); + ArticleData articleData = anotherUserFeed.getArticleDatas().get(0); + assertThat(articleData.getProfileData().isFollowing(), is(true)); + } } \ No newline at end of file diff --git a/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java b/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java new file mode 100644 index 0000000..f21ce56 --- /dev/null +++ b/src/test/java/io/spring/application/tag/TagsQueryServiceTest.java @@ -0,0 +1,33 @@ +package io.spring.application.tag; + +import io.spring.core.article.Article; +import io.spring.core.article.ArticleRepository; +import io.spring.core.article.Tag; +import io.spring.infrastructure.article.ArticleMapper; +import io.spring.infrastructure.article.MyBatisArticleRepository; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mybatis.spring.boot.test.autoconfigure.MybatisTest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@MybatisTest +@Import({TagsQueryService.class, MyBatisArticleRepository.class}) +public class TagsQueryServiceTest { + @Autowired + private TagsQueryService tagsQueryService; + + @Autowired + private ArticleRepository articleRepository; + + @Test + public void should_get_all_tags() throws Exception { + articleRepository.save(new Article("test", "test", "test", new String[]{"java"}, "123")); + assertThat(tagsQueryService.allTags().contains("java"), is(true)); + } +} \ No newline at end of file