diff --git a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeControllerTests.java b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeControllerTests.java
index 214502f..d1d44ab 100644
--- a/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeControllerTests.java
+++ b/src/integrationTest/java/app/mealsmadeeasy/api/recipe/RecipeControllerTests.java
@@ -35,7 +35,7 @@ public class RecipeControllerTests {
}
private Recipe createTestRecipe(User owner) {
- return this.recipeService.create(owner, "Test Recipe", "Hello, World!");
+ return this.recipeService.create(owner, "Test Recipe", "# Hello, World!");
}
@Test
@@ -46,7 +46,12 @@ public class RecipeControllerTests {
this.mockMvc.perform(get("/recipe/{id}", recipe.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
- .andExpect(jsonPath("$.title").value("Test Recipe"));
+ .andExpect(jsonPath("$.title").value("Test Recipe"))
+ .andExpect(jsonPath("$.text").value("
Hello, World!
"))
+ .andExpect(jsonPath("$.ownerId").value(owner.getId()))
+ .andExpect(jsonPath("$.ownerUsername").value(owner.getUsername()))
+ .andExpect(jsonPath("$.starCount").value(0))
+ .andExpect(jsonPath("$.viewerCount").value(0));
}
}
diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeController.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeController.java
index b70800a..8b4a9ea 100644
--- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeController.java
+++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeController.java
@@ -1,7 +1,7 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.recipe.view.RecipeExceptionView;
-import app.mealsmadeeasy.api.recipe.view.RecipeGetView;
+import app.mealsmadeeasy.api.recipe.view.RecipePageView;
import app.mealsmadeeasy.api.user.User;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
@@ -26,15 +26,9 @@ public class RecipeController {
}
@GetMapping("/{id}")
- public ResponseEntity getById(@PathVariable long id, @AuthenticationPrincipal User user)
+ public ResponseEntity getById(@PathVariable long id, @AuthenticationPrincipal User user)
throws RecipeException {
- final Recipe recipe;
- if (user != null) {
- recipe = this.recipeService.getById(id, user);
- } else {
- recipe = this.recipeService.getById(id);
- }
- return ResponseEntity.ok(new RecipeGetView(recipe.getId(), recipe.getTitle()));
+ return ResponseEntity.ok(this.recipeService.getPageViewById(id, user));
}
}
diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java
index dcfbc8f..b3f2643 100644
--- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java
+++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeRepository.java
@@ -28,4 +28,10 @@ public interface RecipeRepository extends JpaRepository {
@EntityGraph(attributePaths = { "stars" })
Optional findByIdWithStars(long id);
+ @Query("SELECT size(r.stars) FROM Recipe r WHERE r.id = ?1")
+ int getStarCount(long recipeId);
+
+ @Query("SELECT size(r.viewers) FROM Recipe r WHERE r.id = ?1")
+ int getViewerCount(long recipeId);
+
}
diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurity.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurity.java
index 52eedc8..13eb23d 100644
--- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurity.java
+++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurity.java
@@ -1,9 +1,11 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.user.User;
+import org.jetbrains.annotations.Nullable;
public interface RecipeSecurity {
boolean isOwner(Recipe recipe, User user);
boolean isOwner(long recipeId, User user) throws RecipeException;
- boolean isViewableBy(Recipe recipe, User user);
+ boolean isViewableBy(Recipe recipe, @Nullable User user);
+ boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException;
}
diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurityImpl.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurityImpl.java
index ea9a9c3..6c2bcbd 100644
--- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurityImpl.java
+++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeSecurityImpl.java
@@ -1,6 +1,7 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.user.User;
+import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component;
import java.util.Objects;
@@ -29,10 +30,18 @@ public class RecipeSecurityImpl implements RecipeSecurity {
}
@Override
- public boolean isViewableBy(Recipe recipe, User user) {
- if (Objects.equals(recipe.getOwner().getId(), user.getId())) {
+ public boolean isViewableBy(Recipe recipe, @Nullable User user) {
+ if (recipe.isPublic()) {
+ // public recipe
+ return true;
+ } else if (user == null) {
+ // a non-public recipe with no principal
+ return false;
+ } else if (Objects.equals(recipe.getOwner().getId(), user.getId())) {
+ // is owner
return true;
} else {
+ // check if viewer
final RecipeEntity withViewers = this.recipeRepository.getByIdWithViewers(recipe.getId());
for (final User viewer : withViewers.getViewers()) {
if (viewer.getId() != null && viewer.getId().equals(user.getId())) {
@@ -40,7 +49,17 @@ public class RecipeSecurityImpl implements RecipeSecurity {
}
}
}
+ // non-public recipe and not viewer
return false;
}
+ @Override
+ public boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException {
+ final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
+ RecipeException.Type.INVALID_ID,
+ "No such Recipe with id " + recipeId
+ ));
+ return this.isViewableBy(recipe, user);
+ }
+
}
diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java
index fbedba8..6ecd181 100644
--- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java
+++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeService.java
@@ -2,7 +2,9 @@ package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.recipe.comment.RecipeComment;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
+import app.mealsmadeeasy.api.recipe.view.RecipePageView;
import app.mealsmadeeasy.api.user.User;
+import org.jetbrains.annotations.Nullable;
import java.util.List;
@@ -17,6 +19,8 @@ public interface RecipeService {
Recipe getByIdWithStars(long id) throws RecipeException;
Recipe getByIdWithStars(long id, User viewer) throws RecipeException;
+ RecipePageView getPageViewById(long id, @Nullable User viewer) throws RecipeException;
+
List getByMinimumStars(long minimumStars);
List getByMinimumStars(long minimumStars, User viewer);
@@ -33,12 +37,14 @@ public interface RecipeService {
RecipeStar addStar(Recipe recipe, User giver) throws RecipeException;
void deleteStarByUser(Recipe recipe, User giver) throws RecipeException;
void deleteStar(RecipeStar recipeStar);
+ int getStarCount(Recipe recipe, @Nullable User viewer);
Recipe setPublic(Recipe recipe, User owner, boolean isPublic);
Recipe addViewer(Recipe recipe, User user);
Recipe removeViewer(Recipe recipe, User user);
Recipe clearViewers(Recipe recipe);
+ int getViewerCount(Recipe recipe, @Nullable User viewer);
RecipeComment getCommentById(long id) throws RecipeException;
RecipeComment addComment(Recipe recipe, String rawCommentText, User commenter);
diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java
index e9aa255..457d7c4 100644
--- a/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java
+++ b/src/main/java/app/mealsmadeeasy/api/recipe/RecipeServiceImpl.java
@@ -6,11 +6,13 @@ import app.mealsmadeeasy.api.recipe.comment.RecipeCommentRepository;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.recipe.star.RecipeStarEntity;
import app.mealsmadeeasy.api.recipe.star.RecipeStarRepository;
+import app.mealsmadeeasy.api.recipe.view.RecipePageView;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
import app.mealsmadeeasy.api.user.UserRepository;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
+import org.jetbrains.annotations.Nullable;
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import org.springframework.security.access.prepost.PostAuthorize;
@@ -82,7 +84,7 @@ public class RecipeServiceImpl implements RecipeService {
}
@Override
- @PostAuthorize("returnObject.isPublic || @recipeSecurity.isViewableBy(returnObject, #viewer)")
+ @PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
public Recipe getById(long id, User viewer) throws RecipeException {
return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID,
@@ -100,7 +102,7 @@ public class RecipeServiceImpl implements RecipeService {
}
@Override
- @PostAuthorize("returnObject.isPublic || @recipeSecurity.isViewableBy(returnObject, #viewer)")
+ @PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
public Recipe getByIdWithStars(long id, User viewer) throws RecipeException {
return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID,
@@ -108,6 +110,23 @@ public class RecipeServiceImpl implements RecipeService {
));
}
+ @Override
+ @PostAuthorize("@recipeSecurity.isViewableBy(#id, #viewer)")
+ public RecipePageView getPageViewById(long id, @Nullable User viewer) throws RecipeException {
+ final Recipe recipe = this.recipeRepository.getReferenceById(id);
+ final RecipePageView view = new RecipePageView();
+ view.setId(recipe.getId());
+ view.setCreated(recipe.getCreated());
+ view.setModified(recipe.getModified());
+ view.setTitle(recipe.getTitle());
+ view.setText(this.getRenderedMarkdown(recipe, viewer));
+ view.setOwnerId(recipe.getOwner().getId());
+ view.setOwnerUsername(recipe.getOwner().getUsername());
+ view.setStarCount(this.getStarCount(recipe, viewer));
+ view.setViewerCount(this.getViewerCount(recipe, viewer));
+ return view;
+ }
+
@Override
public List getByMinimumStars(long minimumStars) {
return List.copyOf(this.recipeRepository.findAllPublicByStarsGreaterThanEqual(minimumStars));
@@ -136,7 +155,7 @@ public class RecipeServiceImpl implements RecipeService {
}
@Override
- @PreAuthorize("#recipe.isPublic || @recipeSecurity.isViewableBy(#recipe, #viewer)")
+ @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #viewer)")
public String getRenderedMarkdown(Recipe recipe, User viewer) {
RecipeEntity entity = (RecipeEntity) recipe;
if (entity.getCachedRenderedText() == null) {
@@ -164,7 +183,7 @@ public class RecipeServiceImpl implements RecipeService {
}
@Override
- @PreAuthorize("#recipe.isPublic || @recipeSecurity.isViewableBy(#recipe, #giver)")
+ @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #giver)")
public RecipeStar addStar(Recipe recipe, User giver) {
final RecipeStarEntity star = new RecipeStarEntity();
star.setOwner((UserEntity) giver);
@@ -189,6 +208,12 @@ public class RecipeServiceImpl implements RecipeService {
this.recipeStarRepository.delete(star);
}
+ @Override
+ @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #viewer)")
+ public int getStarCount(Recipe recipe, @Nullable User viewer) {
+ return this.recipeRepository.getStarCount(recipe.getId());
+ }
+
@Override
@PreAuthorize("@recipeSecurity.isOwner(#recipe, #owner)")
public Recipe setPublic(Recipe recipe, User owner, boolean isPublic) {
@@ -222,6 +247,12 @@ public class RecipeServiceImpl implements RecipeService {
return this.recipeRepository.save(entity);
}
+ @Override
+ @PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #viewer)")
+ public int getViewerCount(Recipe recipe, User viewer) {
+ return this.recipeRepository.getViewerCount(recipe.getId());
+ }
+
@Override
public RecipeComment getCommentById(long id) throws RecipeException {
return this.recipeCommentRepository.findById(id)
diff --git a/src/main/java/app/mealsmadeeasy/api/recipe/view/RecipePageView.java b/src/main/java/app/mealsmadeeasy/api/recipe/view/RecipePageView.java
new file mode 100644
index 0000000..cc6a676
--- /dev/null
+++ b/src/main/java/app/mealsmadeeasy/api/recipe/view/RecipePageView.java
@@ -0,0 +1,91 @@
+package app.mealsmadeeasy.api.recipe.view;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.time.LocalDateTime;
+
+public class RecipePageView {
+
+ private long id;
+ private LocalDateTime created;
+ private LocalDateTime modified;
+ private String title;
+ private String text;
+ private long ownerId;
+ private String ownerUsername;
+ private int starCount;
+ private int viewerCount;
+
+ public long getId() {
+ return this.id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public LocalDateTime getCreated() {
+ return this.created;
+ }
+
+ public void setCreated(LocalDateTime created) {
+ this.created = created;
+ }
+
+ public LocalDateTime getModified() {
+ return this.modified;
+ }
+
+ public void setModified(LocalDateTime modified) {
+ this.modified = modified;
+ }
+
+ public String getTitle() {
+ return this.title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public @Nullable String getText() {
+ return this.text;
+ }
+
+ public void setText(String text) {
+ this.text = text;
+ }
+
+ public long getOwnerId() {
+ return this.ownerId;
+ }
+
+ public void setOwnerId(long ownerId) {
+ this.ownerId = ownerId;
+ }
+
+ public String getOwnerUsername() {
+ return this.ownerUsername;
+ }
+
+ public void setOwnerUsername(String ownerUsername) {
+ this.ownerUsername = ownerUsername;
+ }
+
+ public int getStarCount() {
+ return this.starCount;
+ }
+
+ public void setStarCount(int starCount) {
+ this.starCount = starCount;
+ }
+
+ public int getViewerCount() {
+ return this.viewerCount;
+ }
+
+ public void setViewerCount(int viewerCount) {
+ this.viewerCount = viewerCount;
+ }
+
+}