diff --git a/src/main/java/io/spring/JacksonCustomizations.java b/src/main/java/io/spring/JacksonCustomizations.java new file mode 100644 index 0000000..40ff87f --- /dev/null +++ b/src/main/java/io/spring/JacksonCustomizations.java @@ -0,0 +1,44 @@ +package io.spring; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.joda.time.DateTime; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.io.IOException; + +@Configuration +public class JacksonCustomizations { + + @Bean + public Module realWorldModules() { + return new RealWorldModules(); + } + + public static class RealWorldModules extends SimpleModule { + public RealWorldModules() { + addSerializer(DateTime.class, new DateTimeSerializer()); + } + } + + public static class DateTimeSerializer extends StdSerializer { + + protected DateTimeSerializer() { + super(DateTime.class); + } + + @Override + public void serialize(DateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException { + if (value == null) { + gen.writeNull(); + } else { + gen.writeString(value.toString()); + } + } + } + +} diff --git a/src/main/java/io/spring/api/ArticleApi.java b/src/main/java/io/spring/api/ArticleApi.java new file mode 100644 index 0000000..0e39610 --- /dev/null +++ b/src/main/java/io/spring/api/ArticleApi.java @@ -0,0 +1,30 @@ +package io.spring.api; + +import io.spring.api.exception.ResourceNotFoundException; +import io.spring.application.article.ArticleData; +import io.spring.application.article.ArticleQueryService; +import io.spring.core.user.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/articles/{slug}") +public class ArticleApi { + private ArticleQueryService articleQueryService; + + @Autowired + public ArticleApi(ArticleQueryService articleQueryService) { + this.articleQueryService = articleQueryService; + } + + @GetMapping + public ResponseEntity article(@PathVariable("slug") String slug, + @AuthenticationPrincipal User user) { + return articleQueryService.findBySlug(slug, user).map(ResponseEntity::ok).orElseThrow(ResourceNotFoundException::new); + } +} diff --git a/src/main/java/io/spring/api/exception/ResourceNotFoundException.java b/src/main/java/io/spring/api/exception/ResourceNotFoundException.java new file mode 100644 index 0000000..c734cc2 --- /dev/null +++ b/src/main/java/io/spring/api/exception/ResourceNotFoundException.java @@ -0,0 +1,8 @@ +package io.spring.api.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends RuntimeException { +} diff --git a/src/main/java/io/spring/api/security/WebSecurityConfig.java b/src/main/java/io/spring/api/security/WebSecurityConfig.java index 7b1f7bb..e8c267c 100644 --- a/src/main/java/io/spring/api/security/WebSecurityConfig.java +++ b/src/main/java/io/spring/api/security/WebSecurityConfig.java @@ -26,6 +26,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers(HttpMethod.POST, "/users", "/users/login").permitAll() + .antMatchers(HttpMethod.GET, "/articles/**").permitAll() .anyRequest().authenticated(); http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); diff --git a/src/main/java/io/spring/application/article/ArticleQueryService.java b/src/main/java/io/spring/application/article/ArticleQueryService.java index 1ba022f..c6add11 100644 --- a/src/main/java/io/spring/application/article/ArticleQueryService.java +++ b/src/main/java/io/spring/application/article/ArticleQueryService.java @@ -23,17 +23,35 @@ public class ArticleQueryService { } public Optional findById(String id, User user) { - ArticleData articleData = articleReadService.ofId(id); + ArticleData articleData = articleReadService.findById(id); if (articleData == null) { return Optional.empty(); } else { - articleData.setFavorited(articleFavoritesQueryService.isUserFavorite(user.getId(), id)); - articleData.setFavoritesCount(articleFavoritesQueryService.articleFavoriteCount(id)); - articleData.getProfileData().setFollowing( - userRelationshipQueryService.isUserFollowing( - user.getId(), - articleData.getProfileData().getId())); + if (user != null) { + fillExtraInfo(id, user, articleData); + } return Optional.of(articleData); } } + + public Optional findBySlug(String slug, User user) { + ArticleData articleData = articleReadService.findBySlug(slug); + if (articleData == null) { + return Optional.empty(); + } else { + if (user != null) { + fillExtraInfo(articleData.getId(), user, articleData); + } + return Optional.of(articleData); + } + } + + private void fillExtraInfo(String id, User user, ArticleData articleData) { + articleData.setFavorited(articleFavoritesQueryService.isUserFavorite(user.getId(), id)); + articleData.setFavoritesCount(articleFavoritesQueryService.articleFavoriteCount(id)); + articleData.getProfileData().setFollowing( + userRelationshipQueryService.isUserFollowing( + user.getId(), + articleData.getProfileData().getId())); + } } diff --git a/src/main/java/io/spring/application/article/ArticleReadService.java b/src/main/java/io/spring/application/article/ArticleReadService.java index 8291aca..5cb359f 100644 --- a/src/main/java/io/spring/application/article/ArticleReadService.java +++ b/src/main/java/io/spring/application/article/ArticleReadService.java @@ -7,5 +7,7 @@ import org.springframework.stereotype.Component; @Component @Mapper public interface ArticleReadService { - ArticleData ofId(@Param("id") String id); + ArticleData findById(@Param("id") String id); + + ArticleData findBySlug(@Param("slug") String slug); } diff --git a/src/main/resources/mapper/ArticleReadService.xml b/src/main/resources/mapper/ArticleReadService.xml index e226a69..95c2ab6 100644 --- a/src/main/resources/mapper/ArticleReadService.xml +++ b/src/main/resources/mapper/ArticleReadService.xml @@ -1,27 +1,35 @@ - + where A.id = #{id} + diff --git a/src/test/java/io/spring/api/ArticlesApiTest.java b/src/test/java/io/spring/api/ArticlesApiTest.java index 0e91f88..38f4d1f 100644 --- a/src/test/java/io/spring/api/ArticlesApiTest.java +++ b/src/test/java/io/spring/api/ArticlesApiTest.java @@ -24,6 +24,7 @@ import static io.restassured.RestAssured.given; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.*; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -131,4 +132,44 @@ public class ArticlesApiTest extends TestWithCurrentUser { }}); }}; } + + @Test + public void should_read_article_success() throws Exception { + String slug = "test-new-article"; + Article article = new Article(slug, "Test New Article", "Desc", "Body", new String[]{"java", "spring", "jpg"}, user.getId()); + + DateTime time = new DateTime(); + ArticleData articleData = new ArticleData( + article.getId(), + article.getSlug(), + article.getTitle(), + article.getDescription(), + article.getBody(), + false, + 0, + time, + time, + Arrays.asList("joda"), + new ProfileData(user.getId(), user.getUsername(), user.getBio(), user.getImage(), false)); + + when(articleQueryService.findBySlug(eq(slug), eq(null))).thenReturn(Optional.of(articleData)); + + RestAssured.when() + .get("/articles/{slug}", slug) + .then() + .statusCode(200) + .body("article.slug", equalTo(slug)) + .body("article.body", equalTo(articleData.getBody())) + .body("article.createdAt", equalTo(time.toDateTimeISO().toString())); + + } + + @Test + public void should_404_if_article_not_found() throws Exception { + when(articleQueryService.findBySlug(anyString(), any())).thenReturn(Optional.empty()); + RestAssured.when() + .get("/articles/not-exists") + .then() + .statusCode(404); + } } \ No newline at end of file