Massive refactor of RecipeService and related. All tests passing.

This commit is contained in:
Jesse Brault 2024-07-28 14:52:00 -05:00
parent 2565e63a7d
commit 341133f779
27 changed files with 593 additions and 487 deletions

View File

@ -1,5 +1,6 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserCreateException;
import app.mealsmadeeasy.api.user.UserService;
@ -36,16 +37,19 @@ public class RecipeControllerTests {
}
}
private Recipe createTestRecipe(User owner) {
return this.recipeService.create(owner, "Test Recipe", "# Hello, World!");
private Recipe createTestRecipe(User owner, boolean isPublic) {
final RecipeCreateSpec spec = new RecipeCreateSpec();
spec.setTitle("Test Recipe");
spec.setRawText("# Hello, World!");
spec.setPublic(isPublic);
return this.recipeService.create(owner, spec);
}
@Test
@DirtiesContext
public void getRecipePageViewByIdPublicRecipeNoPrincipal() throws Exception {
final User owner = this.createTestUser("owner");
final Recipe recipe = this.createTestRecipe(owner);
this.recipeService.setPublic(recipe, owner, true);
final Recipe recipe = this.createTestRecipe(owner, true);
this.mockMvc.perform(get("/recipes/{id}", recipe.getId()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
@ -61,8 +65,7 @@ public class RecipeControllerTests {
@DirtiesContext
public void getRecipeInfoViewsNoPrincipal() throws Exception {
final User owner = this.createTestUser("owner");
final Recipe recipe = this.createTestRecipe(owner);
this.recipeService.setPublic(recipe, owner, true);
final Recipe recipe = this.createTestRecipe(owner, true);
this.mockMvc.perform(get("/recipes"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.slice.number").value(0))

View File

@ -1,10 +1,13 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.user.IsUserMatcher;
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.recipe.star.RecipeStarService;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
import app.mealsmadeeasy.api.user.UserRepository;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@ -14,16 +17,20 @@ import org.springframework.test.annotation.DirtiesContext;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
// TODO: test mainImage included
@SpringBootTest
public class RecipeServiceTests {
@Autowired
private RecipeService recipeService;
@Autowired
private RecipeStarService recipeStarService;
@Autowired
private UserRepository userRepository;
@ -35,25 +42,26 @@ public class RecipeServiceTests {
return this.userRepository.save(draft);
}
private Recipe createTestRecipe(User owner) {
return this.recipeService.create(owner, "My Recipe" , "Hello!");
private Recipe createTestRecipe(@Nullable User owner) {
return this.createTestRecipe(owner, false);
}
private Recipe createTestRecipe(@Nullable User owner, boolean isPublic) {
final RecipeCreateSpec spec = new RecipeCreateSpec();
spec.setTitle("My Recipe");
spec.setRawText("Hello!");
spec.setPublic(isPublic);
return this.recipeService.create(owner, spec);
}
@Test
@DirtiesContext
public void createViaUsername() throws RecipeException {
final User user = this.createTestUser("recipeOwner");
final Recipe recipe = this.recipeService.create(user.getUsername(), "My Recipe" , "Hello!");
assertThat(recipe.getOwner(), is(user));
assertThat(recipe.getTitle(), is("My Recipe"));
assertThat(recipe.getRawText(), is("Hello!"));
}
public void smokeScreen() {}
@Test
@DirtiesContext
public void createViaUser() {
public void create() {
final User user = this.createTestUser("recipeOwner");
final Recipe recipe = this.recipeService.create(user, "My Recipe", "Hello!");
final Recipe recipe = this.createTestRecipe(user);
assertThat(recipe.getOwner().getUsername(), is(user.getUsername()));
assertThat(recipe.getTitle(), is("My Recipe"));
assertThat(recipe.getRawText(), is("Hello!"));
@ -61,22 +69,36 @@ public class RecipeServiceTests {
@Test
@DirtiesContext
public void getByIdPublic() throws RecipeException {
final User owner = this.createTestUser("recipeOwner");
Recipe recipe = this.createTestRecipe(owner);
recipe = this.recipeService.setPublic(recipe, owner, true);
final Recipe byId = this.recipeService.getById(recipe.getId());
assertThat(byId.getId(), is(recipe.getId()));
assertThat(byId.getTitle(), is("My Recipe"));
assertThat(byId.getRawText(), is("Hello!"));
public void createWithoutOwnerThrowsAccessDenied() {
assertThrows(AccessDeniedException.class, () -> this.recipeService.create(null, new RecipeCreateSpec()));
}
@Test
@DirtiesContext
public void getByIdThrowsWhenNotPublic() {
public void getByIdPublicNoViewerDoesNotThrow() {
final User owner = this.createTestUser("recipeOwner");
final Recipe recipe = this.createTestRecipe(owner);
assertThrows(AccessDeniedException.class, () -> this.recipeService.getById(recipe.getId()));
final Recipe recipe = this.createTestRecipe(owner, true);
assertDoesNotThrow(() -> this.recipeService.getById(recipe.getId(), null));
}
@Test
@DirtiesContext
public void getByIdHasCorrectProperties() throws RecipeException {
final User owner = this.createTestUser("recipeOwner");
final Recipe recipe = this.createTestRecipe(owner, true);
final Recipe byId = this.recipeService.getById(recipe.getId(), null);
assertThat(byId.getId(), is(recipe.getId()));
assertThat(byId.getTitle(), is("My Recipe"));
assertThat(byId.getRawText(), is("Hello!"));
assertThat(byId.isPublic(), is(true));
}
@Test
@DirtiesContext
public void getByIdThrowsWhenNotPublicAndNoViewer() {
final User owner = this.createTestUser("recipeOwner");
final Recipe recipe = this.createTestRecipe(owner, false); // not public
assertThrows(AccessDeniedException.class, () -> this.recipeService.getById(recipe.getId(), null));
}
@Test
@ -90,11 +112,10 @@ public class RecipeServiceTests {
@Test
@DirtiesContext
public void getByIdOkayWhenPublic() {
public void getByIdOkayWhenPublicAndNoViewer() {
final User owner = this.createTestUser("recipeOwner");
final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, owner, true);
assertDoesNotThrow(() -> this.recipeService.getById(publicRecipe.getId()));
final Recipe recipe = this.createTestRecipe(owner, true);
assertDoesNotThrow(() -> this.recipeService.getById(recipe.getId(), null));
}
@Test
@ -102,25 +123,25 @@ public class RecipeServiceTests {
public void getByIdOkayWhenPublicRecipeWithViewer() {
final User owner = this.createTestUser("recipeOwner");
final User viewer = this.createTestUser("viewer");
final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, owner, true);
assertDoesNotThrow(() -> this.recipeService.getById(publicRecipe.getId(), viewer));
final Recipe recipe = this.createTestRecipe(owner, true);
assertDoesNotThrow(() -> this.recipeService.getById(recipe.getId(), viewer));
}
@Test
@DirtiesContext
public void getByIdWithStarsPublic() throws RecipeException {
public void getByIdOkayWithStarsPublicAndNoViewer() {
final User owner = this.createTestUser("recipeOwner");
Recipe recipe = this.createTestRecipe(owner);
recipe = this.recipeService.setPublic(recipe, owner, true);
final RecipeStar star = this.recipeService.addStar(recipe, owner);
final Recipe byIdWithStars = this.recipeService.getByIdWithStars(recipe.getId());
final Recipe recipe = this.createTestRecipe(owner, true);
final RecipeStar star = this.recipeStarService.create(recipe.getId(), owner.getUsername());
final Recipe byIdWithStars = assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(
recipe.getId(), null
));
assertThat(byIdWithStars.getStars(), ContainsRecipeStarsMatcher.containsStars(star));
}
@Test
@DirtiesContext
public void getByIdWithStarsThrowsWhenNotViewer() {
public void getByIdOkayWithStarsThrowsWhenNotViewer() {
final User owner = this.createTestUser("recipeOwner");
final User notViewer = this.createTestUser("notViewer");
final Recipe recipe = this.createTestRecipe(owner);
@ -129,46 +150,35 @@ public class RecipeServiceTests {
@Test
@DirtiesContext
public void getByIdWithStarsOkayWhenPublic() {
final User owner = this.createTestUser("recipeOwner");
final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, owner, true);
assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(publicRecipe.getId()));
}
@Test
@DirtiesContext
public void getByIdWithStarsOkayWhenPublicRecipeWithViewer() {
public void getByIdWithStarsOkayWhenPublicRecipeWithViewer() throws RecipeException {
final User owner = this.createTestUser("recipeOwner");
final User viewer = this.createTestUser("viewer");
final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
final Recipe publicRecipe = this.recipeService.setPublic(notYetPublicRecipe, owner, true);
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec();
updateSpec.setPublic(true);
final Recipe publicRecipe = this.recipeService.update(notYetPublicRecipe.getId(), updateSpec, owner);
assertDoesNotThrow(() -> this.recipeService.getByIdWithStars(publicRecipe.getId(), viewer));
}
@Test
@DirtiesContext
public void getByMinimumStarsAllPublic() throws RecipeException {
public void getByMinimumStarsAllPublic() {
final User owner = this.createTestUser("recipeOwner");
final User u0 = this.createTestUser("u0");
final User u1 = this.createTestUser("u1");
Recipe r0 = this.createTestRecipe(owner);
Recipe r1 = this.createTestRecipe(owner);
Recipe r2 = this.createTestRecipe(owner);
r0 = this.recipeService.setPublic(r0, owner, true);
r1 = this.recipeService.setPublic(r1, owner, true);
r2 = this.recipeService.setPublic(r2, owner, true);
final Recipe r0 = this.createTestRecipe(owner, true);
final Recipe r1 = this.createTestRecipe(owner, true);
final Recipe r2 = this.createTestRecipe(owner, true);
// r0.stars = 0, r1.stars = 1, r2.stars = 2
this.recipeService.addStar(r1, u0);
this.recipeService.addStar(r2, u0);
this.recipeService.addStar(r2, u1);
this.recipeStarService.create(r1.getId(), u0.getUsername());
this.recipeStarService.create(r2.getId(), u0.getUsername());
this.recipeStarService.create(r2.getId(), u1.getUsername());
final List<Recipe> zeroStars = this.recipeService.getByMinimumStars(0);
final List<Recipe> oneStar = this.recipeService.getByMinimumStars(1);
final List<Recipe> twoStars = this.recipeService.getByMinimumStars(2);
final List<Recipe> zeroStars = this.recipeService.getByMinimumStars(0, null);
final List<Recipe> oneStar = this.recipeService.getByMinimumStars(1, null);
final List<Recipe> twoStars = this.recipeService.getByMinimumStars(2, null);
assertThat(zeroStars.size(), is(3));
assertThat(oneStar.size(), is(2));
@ -187,20 +197,20 @@ public class RecipeServiceTests {
final User u1 = this.createTestUser("u1");
final User viewer = this.createTestUser("recipeViewer");
Recipe r0 = this.createTestRecipe(owner);
Recipe r0 = this.createTestRecipe(owner); // not public
Recipe r1 = this.createTestRecipe(owner);
Recipe r2 = this.createTestRecipe(owner);
for (final User starer : List.of(u0, u1)) {
r0 = this.recipeService.addViewer(r0, starer);
r1 = this.recipeService.addViewer(r1, starer);
r2 = this.recipeService.addViewer(r2, starer);
r0 = this.recipeService.addViewer(r0.getId(), owner, starer);
r1 = this.recipeService.addViewer(r1.getId(), owner, starer);
r2 = this.recipeService.addViewer(r2.getId(), owner, starer);
}
// r0.stars = 0, r1.stars = 1, r2.stars = 2
this.recipeService.addStar(r1, u0);
this.recipeService.addStar(r2, u0);
this.recipeService.addStar(r2, u1);
this.recipeStarService.create(r1.getId(), u0.getUsername());
this.recipeStarService.create(r2.getId(), u0.getUsername());
this.recipeStarService.create(r2.getId(), u1.getUsername());
final List<Recipe> zeroStarsNoneViewable = this.recipeService.getByMinimumStars(0, viewer);
final List<Recipe> oneStarNoneViewable = this.recipeService.getByMinimumStars(1, viewer);
@ -211,9 +221,9 @@ public class RecipeServiceTests {
assertThat(twoStarsNoneViewable.size(), is(0));
// Now make them viewable
r0 = this.recipeService.addViewer(r0, viewer);
r1 = this.recipeService.addViewer(r1, viewer);
r2 = this.recipeService.addViewer(r2, viewer);
r0 = this.recipeService.addViewer(r0.getId(), owner, viewer);
r1 = this.recipeService.addViewer(r1.getId(), owner, viewer);
r2 = this.recipeService.addViewer(r2.getId(), owner, viewer);
final List<Recipe> zeroStarsViewable = this.recipeService.getByMinimumStars(0, viewer);
final List<Recipe> oneStarViewable = this.recipeService.getByMinimumStars(1, viewer);
@ -230,14 +240,11 @@ public class RecipeServiceTests {
@Test
@DirtiesContext
public void getPublicRecipes() throws RecipeException {
public void getPublicRecipes() {
final User owner = this.createTestUser("recipeOwner");
Recipe r0 = this.createTestRecipe(owner);
Recipe r1 = this.createTestRecipe(owner);
r0 = this.recipeService.setPublic(r0, owner, true);
r1 = this.recipeService.setPublic(r1, owner, true);
Recipe r0 = this.createTestRecipe(owner, true);
Recipe r1 = this.createTestRecipe(owner, true);
final List<Recipe> publicRecipes = this.recipeService.getPublicRecipes();
assertThat(publicRecipes.size(), is(2));
@ -251,7 +258,7 @@ public class RecipeServiceTests {
final User viewer = this.createTestUser("recipeViewer");
Recipe r0 = this.createTestRecipe(owner);
r0 = this.recipeService.addViewer(r0, viewer);
r0 = this.recipeService.addViewer(r0.getId(), owner, viewer);
final List<Recipe> viewableRecipes = this.recipeService.getRecipesViewableBy(viewer);
assertThat(viewableRecipes.size(), is(1));
assertThat(viewableRecipes, ContainsRecipesMatcher.containsRecipes(r0));
@ -269,33 +276,16 @@ public class RecipeServiceTests {
@Test
@DirtiesContext
public void getRenderedMarkdown() {
public void updateRawText() throws RecipeException {
final User owner = this.createTestUser("recipeOwner");
final Recipe recipe = this.recipeService.create(
owner, "My Recipe", "# A Heading"
);
final String rendered = this.recipeService.getRenderedMarkdown(recipe, owner);
assertThat(rendered, is("<h1>A Heading</h1>"));
}
@Test
@DirtiesContext
public void getRenderedMarkThrowsIfNotViewable() {
final User owner = this.createTestUser("recipeOwner");
final User notViewer = this.createTestUser("notViewer");
final Recipe recipe = this.createTestRecipe(owner);
assertThrows(AccessDeniedException.class, () -> this.recipeService.getRenderedMarkdown(recipe, notViewer));
}
@Test
@DirtiesContext
public void updateRawText() {
final User owner = this.createTestUser("recipeOwner");
Recipe recipe = this.recipeService.create(
owner, "My Recipe", "# A Heading"
);
final RecipeCreateSpec createSpec = new RecipeCreateSpec();
createSpec.setTitle("My Recipe");
createSpec.setRawText("# A Heading");
Recipe recipe = this.recipeService.create(owner, createSpec);
final String newRawText = "# A Heading\n## A Subheading";
recipe = this.recipeService.updateRawText(recipe, owner, newRawText);
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec();
updateSpec.setRawText(newRawText);
recipe = this.recipeService.update(recipe.getId(), updateSpec, owner);
assertThat(recipe.getRawText(), is(newRawText));
}
@ -305,71 +295,20 @@ public class RecipeServiceTests {
final User owner = this.createTestUser("recipeOwner");
final User notOwner = this.createTestUser("notOwner");
final Recipe recipe = this.createTestRecipe(owner);
final RecipeUpdateSpec updateSpec = new RecipeUpdateSpec();
updateSpec.setRawText("should fail");
assertThrows(
AccessDeniedException.class,
() -> this.recipeService.updateRawText(recipe, notOwner, "should fail")
() -> this.recipeService.update(recipe.getId(), updateSpec, notOwner)
);
}
@Test
@DirtiesContext
public void updateOwnerViaUser() throws RecipeException {
final User firstOwner = this.createTestUser("firstOwner");
final User secondOwner = this.createTestUser("secondOwner");
Recipe recipe = this.createTestRecipe(firstOwner);
recipe = this.recipeService.updateOwner(recipe, firstOwner, secondOwner);
assertThat(recipe.getOwner(), IsUserMatcher.isUser(secondOwner));
}
@Test
@DirtiesContext
public void updateOwnerViaUserThrowsIfNotOwner() {
final User actualOwner = this.createTestUser("u0");
final User notOwner = this.createTestUser("u1");
final User target = this.createTestUser("u2");
final Recipe recipe = this.createTestRecipe(actualOwner);
assertThrows(AccessDeniedException.class, () -> this.recipeService.updateOwner(recipe, notOwner, target));
}
@Test
@DirtiesContext
public void addStar() throws RecipeException {
final User owner = this.createTestUser("recipeOwner");
final User starer = this.createTestUser("starer");
Recipe recipe = this.createTestRecipe(owner);
recipe = this.recipeService.addViewer(recipe, starer);
final RecipeStar star = this.recipeService.addStar(recipe, starer);
assertThat(star.getRecipe(), IsRecipeMatcher.isRecipe(recipe));
assertThat(star.getOwner(), IsUserMatcher.isUser(starer));
}
@Test
@DirtiesContext
public void addStarWhenNotViewableThrows() {
final User notViewer = this.createTestUser("notViewer");
final Recipe recipe = this.createTestRecipe(this.createTestUser("recipeOwner"));
assertThrows(AccessDeniedException.class, () -> this.recipeService.addStar(recipe, notViewer));
}
@Test
@DirtiesContext
public void deleteStar() throws RecipeException {
final User owner = this.createTestUser("recipeOwner");
final User starer = this.createTestUser("starer");
Recipe recipe = this.createTestRecipe(owner);
recipe = this.recipeService.addViewer(recipe, starer);
final RecipeStar star = this.recipeService.addStar(recipe, starer);
this.recipeService.deleteStar(star);
recipe = this.recipeService.getByIdWithStars(recipe.getId(), owner);
assertThat(recipe.getStars(), is(empty()));
}
@Test
@DirtiesContext
public void deleteRecipe() {
final User owner = this.createTestUser("recipeOwner");
final Recipe toDelete = this.createTestRecipe(owner);
this.recipeService.deleteRecipe(toDelete, owner);
this.recipeService.deleteRecipe(toDelete.getId(), owner);
assertThrows(RecipeException.class, () -> this.recipeService.getById(toDelete.getId(), owner));
}
@ -379,7 +318,7 @@ public class RecipeServiceTests {
final User owner = this.createTestUser("recipeOwner");
final User notOwner = this.createTestUser("notOwner");
final Recipe toDelete = this.createTestRecipe(owner);
assertThrows(AccessDeniedException.class, () -> this.recipeService.deleteRecipe(toDelete, notOwner));
assertThrows(AccessDeniedException.class, () -> this.recipeService.deleteRecipe(toDelete.getId(), notOwner));
}
}

View File

@ -6,6 +6,7 @@ import app.mealsmadeeasy.api.image.ImageService;
import app.mealsmadeeasy.api.image.spec.ImageCreateInfoSpec;
import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.recipe.RecipeService;
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserService;
import org.slf4j.Logger;
@ -43,19 +44,22 @@ public class DevConfiguration {
);
logger.info("Created {}", testUser);
final Recipe recipe = this.recipeService.create(testUser, "Test Recipe", "Hello, World!");
this.recipeService.setPublic(recipe, testUser, true);
final RecipeCreateSpec recipeCreateSpec = new RecipeCreateSpec();
recipeCreateSpec.setTitle("Test Recipe");
recipeCreateSpec.setRawText("Hello, World!");
recipeCreateSpec.setPublic(true);
final Recipe recipe = this.recipeService.create(testUser, recipeCreateSpec);
logger.info("Created {}", recipe);
try (final InputStream inputStream = DevConfiguration.class.getResourceAsStream("HAL9000.svg")) {
final ImageCreateInfoSpec spec = new ImageCreateInfoSpec();
spec.setPublic(true);
final ImageCreateInfoSpec imageCreateSpec = new ImageCreateInfoSpec();
imageCreateSpec.setPublic(true);
final Image image = this.imageService.create(
testUser,
"HAL9000.svg",
inputStream,
27881L,
spec
imageCreateSpec
);
logger.info("Created {}", image);
} catch (IOException | ImageException e) {

View File

@ -1,5 +1,6 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.image.Image;
import app.mealsmadeeasy.api.recipe.comment.RecipeComment;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.user.User;
@ -19,4 +20,5 @@ public interface Recipe {
boolean isPublic();
Set<User> getViewers();
Set<RecipeComment> getComments();
Image getMainImage();
}

View File

@ -1,8 +1,8 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
import app.mealsmadeeasy.api.recipe.view.RecipeExceptionView;
import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
import app.mealsmadeeasy.api.recipe.view.RecipePageView;
import app.mealsmadeeasy.api.user.User;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
@ -32,9 +32,9 @@ public class RecipeController {
}
@GetMapping("/{id}")
public ResponseEntity<RecipePageView> getById(@PathVariable long id, @AuthenticationPrincipal User user)
public ResponseEntity<FullRecipeView> getById(@PathVariable long id, @AuthenticationPrincipal User user)
throws RecipeException {
return ResponseEntity.ok(this.recipeService.getPageViewById(id, user));
return ResponseEntity.ok(this.recipeService.getFullViewById(id, user));
}
@GetMapping

View File

@ -1,5 +1,6 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.image.S3ImageEntity;
import app.mealsmadeeasy.api.recipe.comment.RecipeComment;
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentEntity;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
@ -42,7 +43,8 @@ public final class RecipeEntity implements Recipe {
@JoinColumn(name = "owner_id", nullable = false)
private UserEntity owner;
@OneToMany(mappedBy = "recipe")
@OneToMany
@JoinColumn(name = "recipeId")
private Set<RecipeStarEntity> stars = new HashSet<>();
@OneToMany(mappedBy = "recipe")
@ -54,6 +56,9 @@ public final class RecipeEntity implements Recipe {
@ManyToMany
private Set<UserEntity> viewers = new HashSet<>();
@ManyToOne
private S3ImageEntity mainImage;
@Override
public Long getId() {
return this.id;
@ -169,4 +174,13 @@ public final class RecipeEntity implements Recipe {
return "RecipeEntity(" + this.id + ", " + this.title + ")";
}
@Override
public S3ImageEntity getMainImage() {
return this.mainImage;
}
public void setMainImage(S3ImageEntity image) {
this.mainImage = image;
}
}

View File

@ -3,7 +3,7 @@ package app.mealsmadeeasy.api.recipe;
public class RecipeException extends Exception {
public enum Type {
INVALID_OWNER_USERNAME, INVALID_STAR, NOT_VIEWABLE, INVALID_ID
INVALID_OWNER_USERNAME, INVALID_STAR, NOT_VIEWABLE, INVALID_COMMENT_ID, INVALID_ID
}
private final Type type;

View File

@ -13,18 +13,17 @@ import java.util.Optional;
public interface RecipeRepository extends JpaRepository<RecipeEntity, Long> {
List<RecipeEntity> findAllByIsPublicIsTrue();
List<RecipeEntity> findAllByViewersContaining(UserEntity viewer);
List<RecipeEntity> findAllByOwner(UserEntity owner);
@Query("SELECT r FROM Recipe r WHERE size(r.stars) >= ?1 AND r.isPublic")
List<RecipeEntity> findAllPublicByStarsGreaterThanEqual(long stars);
List<RecipeEntity> findAllByViewersContaining(UserEntity viewer);
List<RecipeEntity> findAllByOwner(UserEntity owner);
@Query("SELECT r FROM Recipe r WHERE size(r.stars) >= ?1 AND (r.isPublic OR ?2 MEMBER OF r.viewers)")
List<RecipeEntity> findAllViewableByStarsGreaterThanEqual(long stars, UserEntity viewer);
@Query("SELECT r FROM Recipe r WHERE r.id = ?1")
@EntityGraph(attributePaths = { "viewers" })
RecipeEntity getByIdWithViewers(long id);
Optional<RecipeEntity> findByIdWithViewers(long id);
@Query("SELECT r FROM Recipe r WHERE r.id = ?1")
@EntityGraph(attributePaths = { "stars" })

View File

@ -6,6 +6,6 @@ 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, @Nullable User user);
boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException;
boolean isViewableBy(long recipeId, @Nullable User user) throws RecipeException;
}

View File

@ -30,7 +30,7 @@ public class RecipeSecurityImpl implements RecipeSecurity {
}
@Override
public boolean isViewableBy(Recipe recipe, @Nullable User user) {
public boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException {
if (recipe.isPublic()) {
// public recipe
return true;
@ -42,7 +42,10 @@ public class RecipeSecurityImpl implements RecipeSecurity {
return true;
} else {
// check if viewer
final RecipeEntity withViewers = this.recipeRepository.getByIdWithViewers(recipe.getId());
final RecipeEntity withViewers = this.recipeRepository.findByIdWithViewers(recipe.getId())
.orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + recipe.getId()
));
for (final User viewer : withViewers.getViewers()) {
if (viewer.getId() != null && viewer.getId().equals(user.getId())) {
return true;
@ -57,7 +60,7 @@ public class RecipeSecurityImpl implements RecipeSecurity {
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
"No such Recipe with id: " + recipeId
));
return this.isViewableBy(recipe, user);
}

View File

@ -1,9 +1,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.spec.RecipeCreateSpec;
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
import app.mealsmadeeasy.api.recipe.view.RecipePageView;
import app.mealsmadeeasy.api.user.User;
import org.jetbrains.annotations.Nullable;
import org.springframework.data.domain.Pageable;
@ -13,51 +13,24 @@ import java.util.List;
public interface RecipeService {
Recipe create(String ownerUsername, String title, String rawText) throws RecipeException;
Recipe create(User user, String title, String rawText);
Recipe create(@Nullable User owner, RecipeCreateSpec spec);
Recipe getById(long id) throws RecipeException;
Recipe getById(long id, User viewer) throws RecipeException;
Recipe getById(long id, @Nullable User viewer) throws RecipeException;
Recipe getByIdWithStars(long id, @Nullable User viewer) throws RecipeException;
FullRecipeView getFullViewById(long id, @Nullable User viewer) throws RecipeException;
Recipe getByIdWithStars(long id) throws RecipeException;
Recipe getByIdWithStars(long id, User viewer) throws RecipeException;
RecipePageView getPageViewById(long id, @Nullable User viewer) throws RecipeException;
Slice<RecipeInfoView> getInfoViewsViewableBy(Pageable pageable, @Nullable User viewer);
List<Recipe> getByMinimumStars(long minimumStars);
List<Recipe> getByMinimumStars(long minimumStars, User viewer);
List<Recipe> getByMinimumStars(long minimumStars, @Nullable User viewer);
List<Recipe> getPublicRecipes();
List<Recipe> getRecipesViewableBy(User user);
List<Recipe> getRecipesOwnedBy(User user);
List<Recipe> getRecipesViewableBy(User viewer);
List<Recipe> getRecipesOwnedBy(User owner);
String getRenderedMarkdown(Recipe recipe, User viewer);
Recipe update(long id, RecipeUpdateSpec spec, User modifier) throws RecipeException;
Recipe updateRawText(Recipe recipe, User owner, String newRawText);
Recipe addViewer(long id, User modifier, User viewer) throws RecipeException;
Recipe removeViewer(long id, User modifier, User viewer) throws RecipeException;
Recipe clearAllViewers(long id, User modifier) throws RecipeException;
Recipe updateOwner(Recipe recipe, User oldOwner, User newOwner) throws RecipeException;
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);
RecipeComment updateComment(RecipeComment comment, String newRawCommentText);
String getRenderedMarkdown(RecipeComment recipeComment);
void deleteComment(RecipeComment comment);
Recipe clearComments(Recipe recipe);
void deleteRecipe(Recipe recipe, User owner);
void deleteById(long id, User owner);
void deleteRecipe(long id, User modifier);
}

View File

@ -1,16 +1,12 @@
package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.recipe.comment.RecipeComment;
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentEntity;
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.image.S3ImageEntity;
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
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;
@ -18,10 +14,12 @@ import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -38,95 +36,79 @@ public class RecipeServiceImpl implements RecipeService {
}
private final RecipeRepository recipeRepository;
private final UserRepository userRepository;
private final RecipeStarRepository recipeStarRepository;
private final RecipeCommentRepository recipeCommentRepository;
public RecipeServiceImpl(
RecipeRepository recipeRepository,
UserRepository userRepository,
RecipeStarRepository recipeStarRepository,
RecipeCommentRepository recipeCommentRepository
) {
public RecipeServiceImpl(RecipeRepository recipeRepository) {
this.recipeRepository = recipeRepository;
this.userRepository = userRepository;
this.recipeStarRepository = recipeStarRepository;
this.recipeCommentRepository = recipeCommentRepository;
}
@Override
public Recipe create(String ownerUsername, String title, String rawText) throws RecipeException {
public Recipe create(@Nullable User owner, RecipeCreateSpec spec) {
if (owner == null) {
throw new AccessDeniedException("Must be logged in.");
}
final RecipeEntity draft = new RecipeEntity();
final UserEntity userEntity = this.userRepository.findByUsername(ownerUsername)
.orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_OWNER_USERNAME,
"No such ownerUsername " + ownerUsername
));
draft.setOwner(userEntity);
draft.setTitle(title);
draft.setRawText(rawText);
draft.setCreated(LocalDateTime.now());
draft.setOwner((UserEntity) owner);
draft.setTitle(spec.getTitle());
draft.setRawText(spec.getRawText());
draft.setMainImage((S3ImageEntity) spec.getMainImage());
draft.setPublic(spec.isPublic());
return this.recipeRepository.save(draft);
}
@Override
public Recipe create(User user, String title, String rawText) {
final RecipeEntity draft = new RecipeEntity();
draft.setOwner((UserEntity) user);
draft.setTitle(title);
draft.setRawText(rawText);
return this.recipeRepository.save(draft);
}
@Override
@PostAuthorize("returnObject.isPublic")
public Recipe getById(long id) throws RecipeException {
private RecipeEntity findRecipeEntity(long id) throws RecipeException {
return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID,
"No such recipe for id " + id
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id
));
}
@Override
@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,
"No such recipe for id " + id
));
}
@Override
@PostAuthorize("returnObject.isPublic")
public Recipe getByIdWithStars(long id) throws RecipeException {
return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID,
"No such recipe for id " + id
));
return this.findRecipeEntity(id);
}
@Override
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
public Recipe getByIdWithStars(long id, User viewer) throws RecipeException {
public Recipe getByIdWithStars(long id, @Nullable User viewer) throws RecipeException {
return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID,
"No such recipe for id " + id
"No such Recipe with id: " + id
));
}
private String getRenderedMarkdown(RecipeEntity entity) {
if (entity.getCachedRenderedText() == null) {
entity.setCachedRenderedText(renderAndCleanMarkdown(entity.getRawText()));
entity = this.recipeRepository.save(entity);
}
return entity.getCachedRenderedText();
}
private int getStarCount(Recipe recipe) {
return this.recipeRepository.getStarCount(recipe.getId());
}
private int getViewerCount(long recipeId) {
return this.recipeRepository.getViewerCount(recipeId);
}
@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();
public FullRecipeView getFullViewById(long id, @Nullable User viewer) throws RecipeException {
final RecipeEntity recipe = this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, "No such Recipe for id: " + id
));
final FullRecipeView view = new FullRecipeView();
view.setId(recipe.getId());
view.setCreated(recipe.getCreated());
view.setModified(recipe.getModified());
view.setTitle(recipe.getTitle());
view.setText(this.getRenderedMarkdown(recipe, viewer));
view.setText(this.getRenderedMarkdown(recipe));
view.setOwnerId(recipe.getOwner().getId());
view.setOwnerUsername(recipe.getOwner().getUsername());
view.setStarCount(this.getStarCount(recipe, viewer));
view.setViewerCount(this.getViewerCount(recipe, viewer));
view.setStarCount(this.getStarCount(recipe));
view.setViewerCount(this.getViewerCount(recipe.getId()));
return view;
}
@ -144,16 +126,11 @@ public class RecipeServiceImpl implements RecipeService {
view.setOwnerId(entity.getOwner().getId());
view.setOwnerUsername(entity.getOwner().getUsername());
view.setPublic(entity.isPublic());
view.setStarCount(this.getStarCount(entity, viewer));
view.setStarCount(this.getStarCount(entity));
return view;
});
}
@Override
public List<Recipe> getByMinimumStars(long minimumStars) {
return List.copyOf(this.recipeRepository.findAllPublicByStarsGreaterThanEqual(minimumStars));
}
@Override
public List<Recipe> getByMinimumStars(long minimumStars, User viewer) {
return List.copyOf(
@ -167,169 +144,75 @@ public class RecipeServiceImpl implements RecipeService {
}
@Override
public List<Recipe> getRecipesViewableBy(User user) {
return List.copyOf(this.recipeRepository.findAllByViewersContaining((UserEntity) user));
public List<Recipe> getRecipesViewableBy(User viewer) {
return List.copyOf(this.recipeRepository.findAllByViewersContaining((UserEntity) viewer));
}
@Override
public List<Recipe> getRecipesOwnedBy(User user) {
return List.copyOf(this.recipeRepository.findAllByOwner((UserEntity) user));
public List<Recipe> getRecipesOwnedBy(User owner) {
return List.copyOf(this.recipeRepository.findAllByOwner((UserEntity) owner));
}
@Override
@PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #viewer)")
public String getRenderedMarkdown(Recipe recipe, User viewer) {
RecipeEntity entity = (RecipeEntity) recipe;
if (entity.getCachedRenderedText() == null) {
entity.setCachedRenderedText(renderAndCleanMarkdown(entity.getRawText()));
entity = this.recipeRepository.save(entity);
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
public Recipe update(long id, RecipeUpdateSpec spec, User modifier) throws RecipeException {
final RecipeEntity entity = this.findRecipeEntity(id);
boolean didModify = false;
if (spec.getTitle() != null) {
entity.setTitle(spec.getTitle());
didModify = true;
}
if (spec.getRawText() != null) {
entity.setRawText(spec.getRawText());
didModify = true;
}
if (spec.getPublic() != null) {
entity.setPublic(spec.getPublic());
didModify = true;
}
if (spec.getMainImage() != null) {
entity.setMainImage((S3ImageEntity) spec.getMainImage());
didModify = true;
}
if (didModify) {
entity.setModified(LocalDateTime.now());
}
return entity.getCachedRenderedText();
}
@Override
@PreAuthorize("@recipeSecurity.isOwner(#recipe, #owner)")
public Recipe updateRawText(Recipe recipe, User owner, String newRawText) {
final RecipeEntity entity = (RecipeEntity) recipe;
entity.setCachedRenderedText(null);
entity.setRawText(newRawText);
return this.recipeRepository.save(entity);
}
@Override
@PreAuthorize("@recipeSecurity.isOwner(#recipe, #oldOwner)")
public Recipe updateOwner(Recipe recipe, User oldOwner, User newOwner) {
final RecipeEntity entity = (RecipeEntity) recipe;
entity.setOwner((UserEntity) newOwner);
return this.recipeRepository.save(entity);
}
@Override
@PreAuthorize("@recipeSecurity.isViewableBy(#recipe, #giver)")
public RecipeStar addStar(Recipe recipe, User giver) {
final RecipeStarEntity star = new RecipeStarEntity();
star.setOwner((UserEntity) giver);
star.setRecipe((RecipeEntity) recipe);
return this.recipeStarRepository.save(star);
}
@Override
public void deleteStar(RecipeStar recipeStar) {
this.recipeStarRepository.delete((RecipeStarEntity) recipeStar);
}
@Override
public void deleteStarByUser(Recipe recipe, User giver) throws RecipeException {
final RecipeStarEntity star = this.recipeStarRepository.findByOwnerAndRecipe(
(UserEntity) giver,
(RecipeEntity) recipe
).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_STAR,
"No such star for user " + giver.getUsername() + " and recipe " + recipe.getId()
));
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) {
final RecipeEntity entity = (RecipeEntity) recipe;
entity.setPublic(isPublic);
return this.recipeRepository.save(entity);
}
@Override
public Recipe addViewer(Recipe recipe, User user) {
final RecipeEntity entity = (RecipeEntity) recipe;
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
public Recipe addViewer(long id, User modifier, User viewer) throws RecipeException {
final RecipeEntity entity = this.recipeRepository.findByIdWithViewers(id).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id
));
final Set<UserEntity> viewers = new HashSet<>(entity.getViewerEntities());
viewers.add((UserEntity) user);
viewers.add((UserEntity) viewer);
entity.setViewers(viewers);
return this.recipeRepository.save(entity);
}
@Override
public Recipe removeViewer(Recipe recipe, User user) {
final RecipeEntity entity = (RecipeEntity) recipe;
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
public Recipe removeViewer(long id, User modifier, User viewer) throws RecipeException {
final RecipeEntity entity = this.findRecipeEntity(id);
final Set<UserEntity> viewers = new HashSet<>(entity.getViewerEntities());
viewers.remove((UserEntity) user);
viewers.remove((UserEntity) viewer);
entity.setViewers(viewers);
return this.recipeRepository.save(entity);
}
@Override
public Recipe clearViewers(Recipe recipe) {
final RecipeEntity entity = (RecipeEntity) recipe;
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
public Recipe clearAllViewers(long id, User modifier) throws RecipeException {
final RecipeEntity entity = this.findRecipeEntity(id);
entity.setViewers(new HashSet<>());
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)
.orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID,
"No such RecipeComment for id " + id
));
}
@Override
public RecipeComment addComment(Recipe recipe, String rawCommentText, User commenter) {
final RecipeCommentEntity draft = new RecipeCommentEntity();
draft.setRawText(rawCommentText);
draft.setOwner((UserEntity) commenter);
return this.recipeCommentRepository.save(draft);
}
@Override
public RecipeComment updateComment(RecipeComment comment, String newRawCommentText) {
final RecipeCommentEntity entity = (RecipeCommentEntity) comment;
entity.setCachedRenderedText(null);
entity.setRawText(newRawCommentText);
return this.recipeCommentRepository.save(entity);
}
@Override
public String getRenderedMarkdown(RecipeComment recipeComment) {
RecipeCommentEntity entity = (RecipeCommentEntity) recipeComment;
if (entity.getCachedRenderedText() == null) {
entity.setCachedRenderedText(renderAndCleanMarkdown(entity.getRawText()));
entity = this.recipeCommentRepository.save(entity);
}
return entity.getCachedRenderedText();
}
@Override
public void deleteComment(RecipeComment comment) {
this.recipeCommentRepository.delete((RecipeCommentEntity) comment);
}
@Override
public Recipe clearComments(Recipe recipe) {
this.recipeCommentRepository.deleteAllByRecipe((RecipeEntity) recipe);
return this.recipeRepository.getReferenceById(recipe.getId());
}
@Override
@PreAuthorize("@recipeSecurity.isOwner(#recipe, #owner)")
public void deleteRecipe(Recipe recipe, User owner) {
this.recipeRepository.delete((RecipeEntity) recipe);
}
@Override
@PreAuthorize("@recipeSecurity.isOwner(#id, #owner)")
public void deleteById(long id, User owner) {
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
public void deleteRecipe(long id, User modifier) {
this.recipeRepository.deleteById(id);
}

View File

@ -0,0 +1,15 @@
package app.mealsmadeeasy.api.recipe.comment;
public class RecipeCommentCreateSpec {
private String rawText;
public String getRawText() {
return this.rawText;
}
public void setRawText(String rawText) {
this.rawText = rawText;
}
}

View File

@ -0,0 +1,11 @@
package app.mealsmadeeasy.api.recipe.comment;
import app.mealsmadeeasy.api.recipe.RecipeException;
import app.mealsmadeeasy.api.user.User;
public interface RecipeCommentService {
RecipeComment create(long recipeId, User owner, RecipeCommentCreateSpec spec) throws RecipeException;
RecipeComment get(long commentId, User viewer) throws RecipeException;
RecipeComment update(long commentId, User viewer, RecipeCommentUpdateSpec spec) throws RecipeException;
void delete(long commentId, User modifier) throws RecipeException;
}

View File

@ -0,0 +1,75 @@
package app.mealsmadeeasy.api.recipe.comment;
import app.mealsmadeeasy.api.recipe.RecipeEntity;
import app.mealsmadeeasy.api.recipe.RecipeException;
import app.mealsmadeeasy.api.recipe.RecipeRepository;
import app.mealsmadeeasy.api.user.User;
import app.mealsmadeeasy.api.user.UserEntity;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import static java.util.Objects.requireNonNull;
@Service
public class RecipeCommentServiceImpl implements RecipeCommentService {
private final RecipeCommentRepository recipeCommentRepository;
private final RecipeRepository recipeRepository;
public RecipeCommentServiceImpl(
RecipeCommentRepository recipeCommentRepository,
RecipeRepository recipeRepository
) {
this.recipeCommentRepository = recipeCommentRepository;
this.recipeRepository = recipeRepository;
}
@Override
public RecipeComment create(long recipeId, User owner, RecipeCommentCreateSpec spec) throws RecipeException {
requireNonNull(owner);
final RecipeCommentEntity draft = new RecipeCommentEntity();
draft.setCreated(LocalDateTime.now());
draft.setRawText(spec.getRawText());
draft.setOwner((UserEntity) owner);
final RecipeEntity recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_ID, "No such Recipe for id: " + recipeId
));
draft.setRecipe(recipe);
return this.recipeCommentRepository.save(draft);
}
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject.recipe, #viewer)")
private RecipeCommentEntity loadCommentEntity(long commentId, User viewer) throws RecipeException {
return this.recipeCommentRepository.findById(commentId).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_COMMENT_ID, "No such RecipeComment for id: " + commentId
));
}
@Override
public RecipeComment get(long commentId, User viewer) throws RecipeException {
return this.loadCommentEntity(commentId, viewer);
}
@Override
public RecipeComment update(long commentId, User viewer, RecipeCommentUpdateSpec spec) throws RecipeException {
final RecipeCommentEntity entity = this.loadCommentEntity(commentId, viewer);
entity.setRawText(spec.getRawText());
return this.recipeCommentRepository.save(entity);
}
@PostAuthorize("@recipeSecurity.isOwner(returnObject.recipe, #modifier)")
private RecipeCommentEntity loadForDelete(long commentId, User modifier) throws RecipeException {
return this.recipeCommentRepository.findById(commentId).orElseThrow(() -> new RecipeException(
RecipeException.Type.INVALID_COMMENT_ID, "No such RecipeComment for id: " + commentId
));
}
@Override
public void delete(long commentId, User modifier) throws RecipeException {
final RecipeCommentEntity entityToDelete = this.loadForDelete(commentId, modifier);
this.recipeCommentRepository.delete(entityToDelete);
}
}

View File

@ -0,0 +1,15 @@
package app.mealsmadeeasy.api.recipe.comment;
public class RecipeCommentUpdateSpec {
private String rawText;
public String getRawText() {
return this.rawText;
}
public void setRawText(String rawText) {
this.rawText = rawText;
}
}

View File

@ -0,0 +1,45 @@
package app.mealsmadeeasy.api.recipe.spec;
import app.mealsmadeeasy.api.image.Image;
import org.jetbrains.annotations.Nullable;
public class RecipeCreateSpec {
private String title;
private String rawText;
private boolean isPublic;
private @Nullable Image mainImage;
public String getTitle() {
return this.title;
}
public void setTitle(String title) {
this.title = title;
}
public String getRawText() {
return this.rawText;
}
public void setRawText(String rawText) {
this.rawText = rawText;
}
public boolean isPublic() {
return this.isPublic;
}
public void setPublic(boolean isPublic) {
this.isPublic = isPublic;
}
public @Nullable Image getMainImage() {
return this.mainImage;
}
public void setMainImage(@Nullable Image mainImage) {
this.mainImage = mainImage;
}
}

View File

@ -0,0 +1,45 @@
package app.mealsmadeeasy.api.recipe.spec;
import app.mealsmadeeasy.api.image.Image;
import org.jetbrains.annotations.Nullable;
public class RecipeUpdateSpec {
private @Nullable String title;
private @Nullable String rawText;
private @Nullable Boolean isPublic;
private @Nullable Image mainImage;
public @Nullable String getTitle() {
return this.title;
}
public void setTitle(@Nullable String title) {
this.title = title;
}
public @Nullable String getRawText() {
return this.rawText;
}
public void setRawText(@Nullable String rawText) {
this.rawText = rawText;
}
public @Nullable Boolean getPublic() {
return this.isPublic;
}
public void setPublic(@Nullable Boolean isPublic) {
this.isPublic = isPublic;
}
public @Nullable Image getMainImage() {
return this.mainImage;
}
public void setMainImage(@Nullable Image mainImage) {
this.mainImage = mainImage;
}
}

View File

@ -1,13 +1,7 @@
package app.mealsmadeeasy.api.recipe.star;
import app.mealsmadeeasy.api.recipe.Recipe;
import app.mealsmadeeasy.api.user.User;
import java.time.LocalDateTime;
public interface RecipeStar {
Long getId();
User getOwner();
LocalDateTime getDate();
Recipe getRecipe();
}

View File

@ -1,48 +1,28 @@
package app.mealsmadeeasy.api.recipe.star;
import app.mealsmadeeasy.api.recipe.RecipeEntity;
import app.mealsmadeeasy.api.user.UserEntity;
import jakarta.persistence.*;
import jakarta.persistence.Column;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import java.time.LocalDateTime;
@Entity(name = "RecipeStar")
public final class RecipeStarEntity implements RecipeStar {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable = false)
private Long id;
@ManyToOne
@JoinColumn(name = "owner_id", nullable = false, updatable = false)
private UserEntity owner;
@EmbeddedId
private RecipeStarId id;
@Column(nullable = false, updatable = false)
private LocalDateTime date = LocalDateTime.now();
@ManyToOne
@JoinColumn(name = "recipe_id", nullable = false, updatable = false)
private RecipeEntity recipe;
@Override
public Long getId() {
public RecipeStarId getId() {
return this.id;
}
public void setId(long id) {
public void setId(RecipeStarId id) {
this.id = id;
}
@Override
public UserEntity getOwner() {
return this.owner;
}
public void setOwner(UserEntity owner) {
this.owner = owner;
}
@Override
public LocalDateTime getDate() {
return this.date;
@ -52,15 +32,6 @@ public final class RecipeStarEntity implements RecipeStar {
this.date = date;
}
@Override
public RecipeEntity getRecipe() {
return this.recipe;
}
public void setRecipe(RecipeEntity recipe) {
this.recipe = recipe;
}
@Override
public String toString() {
return "RecipeStarEntity(" + this.id + ")";

View File

@ -0,0 +1,36 @@
package app.mealsmadeeasy.api.recipe.star;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
@Embeddable
public class RecipeStarId {
@Column(nullable = false)
private String ownerUsername;
@Column(nullable = false)
private Long recipeId;
public String getOwnerUsername() {
return this.ownerUsername;
}
public void setOwnerUsername(String ownerUsername) {
this.ownerUsername = ownerUsername;
}
public Long getRecipeId() {
return this.recipeId;
}
public void setRecipeId(Long recipeId) {
this.recipeId = recipeId;
}
@Override
public String toString() {
return "RecipeStarId(" + this.recipeId + ", " + this.ownerUsername + ")";
}
}

View File

@ -1,14 +1,16 @@
package app.mealsmadeeasy.api.recipe.star;
import app.mealsmadeeasy.api.recipe.RecipeEntity;
import app.mealsmadeeasy.api.user.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
public interface RecipeStarRepository extends JpaRepository<RecipeStarEntity, Long> {
List<RecipeStarEntity> findAllByOwner(UserEntity user);
long countAllByOwner(UserEntity user);
Optional<RecipeStarEntity> findByOwnerAndRecipe(UserEntity user, RecipeEntity recipe);
@Query("SELECT star FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerUsername = ?2")
Optional<RecipeStarEntity> findByRecipeIdAndOwnerUsername(Long recipeId, String username);
@Query("DELETE FROM RecipeStar star WHERE star.id.recipeId = ?1 AND star.id.ownerUsername = ?2")
void deleteByRecipeIdAndOwnerUsername(Long recipeId, String username);
}

View File

@ -0,0 +1,9 @@
package app.mealsmadeeasy.api.recipe.star;
import app.mealsmadeeasy.api.recipe.RecipeException;
public interface RecipeStarService {
RecipeStar create(long recipeId, String ownerUsername);
RecipeStar get(long recipeId, String ownerUsername) throws RecipeException;
void delete(long recipeId, String ownerUsername);
}

View File

@ -0,0 +1,43 @@
package app.mealsmadeeasy.api.recipe.star;
import app.mealsmadeeasy.api.recipe.RecipeException;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Service
public class RecipeStarServiceImpl implements RecipeStarService {
private final RecipeStarRepository recipeStarRepository;
public RecipeStarServiceImpl(RecipeStarRepository recipeStarRepository) {
this.recipeStarRepository = recipeStarRepository;
}
@Override
public RecipeStar create(long recipeId, String ownerUsername) {
final RecipeStarEntity draft = new RecipeStarEntity();
final RecipeStarId id = new RecipeStarId();
id.setRecipeId(recipeId);
id.setOwnerUsername(ownerUsername);
draft.setId(id);
draft.setDate(LocalDateTime.now());
return this.recipeStarRepository.save(draft);
}
@Override
public RecipeStar get(long recipeId, String ownerUsername) throws RecipeException {
return this.recipeStarRepository.findByRecipeIdAndOwnerUsername(recipeId, ownerUsername).orElseThrow(
() -> new RecipeException(
RecipeException.Type.INVALID_ID,
"No such RecipeStar for recipeId: " + recipeId + " and ownerUsername: " + ownerUsername
)
);
}
@Override
public void delete(long recipeId, String ownerUsername) {
this.recipeStarRepository.deleteByRecipeIdAndOwnerUsername(recipeId, ownerUsername);
}
}

View File

@ -4,7 +4,7 @@ import org.jetbrains.annotations.Nullable;
import java.time.LocalDateTime;
public class RecipePageView {
public class FullRecipeView {
private long id;
private LocalDateTime created;

View File

@ -5,6 +5,7 @@ import org.hamcrest.Description;
import java.util.List;
import java.util.Objects;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
@ -13,11 +14,26 @@ public class ContainsItemsMatcher<T, ID> extends BaseMatcher<Iterable<T>> {
private final List<T> allExpected;
private final Predicate<Object> isT;
private final Function<T, ID> idFunction;
private final BiPredicate<ID, ID> equalsFunction;
public ContainsItemsMatcher(List<T> allExpected, Predicate<Object> isT, Function<T, ID> idFunction) {
public ContainsItemsMatcher(
List<T> allExpected,
Predicate<Object> isT,
Function<T, ID> idFunction,
BiPredicate<ID, ID> equalsFunction
) {
this.allExpected = allExpected;
this.isT = isT;
this.idFunction = idFunction;
this.equalsFunction = equalsFunction;
}
public ContainsItemsMatcher(
List<T> allExpected,
Predicate<Object> isT,
Function<T, ID> idFunction
) {
this(allExpected, isT, idFunction, Objects::equals);
}
@SuppressWarnings("unchecked")
@ -28,7 +44,7 @@ public class ContainsItemsMatcher<T, ID> extends BaseMatcher<Iterable<T>> {
for (final T expected : this.allExpected) {
for (final Object item : iterable) {
if (
this.isT.test(item) && Objects.equals(
this.isT.test(item) && this.equalsFunction.test(
this.idFunction.apply((T) item),
this.idFunction.apply(expected)
)

View File

@ -2,17 +2,26 @@ package app.mealsmadeeasy.api.recipe;
import app.mealsmadeeasy.api.matchers.ContainsItemsMatcher;
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
import app.mealsmadeeasy.api.recipe.star.RecipeStarEntity;
import app.mealsmadeeasy.api.recipe.star.RecipeStarId;
import java.util.List;
import java.util.Objects;
public class ContainsRecipeStarsMatcher extends ContainsItemsMatcher<RecipeStar, Long> {
public class ContainsRecipeStarsMatcher extends ContainsItemsMatcher<RecipeStar, RecipeStarId> {
public static ContainsRecipeStarsMatcher containsStars(RecipeStar... expected) {
return new ContainsRecipeStarsMatcher(expected);
}
private ContainsRecipeStarsMatcher(RecipeStar[] allExpected) {
super(List.of(allExpected), o -> o instanceof RecipeStar, RecipeStar::getId);
super(
List.of(allExpected),
o -> o instanceof RecipeStar,
recipeStar -> ((RecipeStarEntity) recipeStar).getId(),
(id0, id1) -> Objects.equals(id0.getRecipeId(), id1.getRecipeId())
&& Objects.equals(id0.getOwnerUsername(), id1.getOwnerUsername())
);
}
}