Refactoring: less reliance on entity-specific exceptions, more converters, etc.
This commit is contained in:
parent
db9e9eca07
commit
0a83a032c8
@ -9,6 +9,7 @@ import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
|
|||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import app.mealsmadeeasy.api.user.UserCreateException;
|
import app.mealsmadeeasy.api.user.UserCreateException;
|
||||||
import app.mealsmadeeasy.api.user.UserService;
|
import app.mealsmadeeasy.api.user.UserService;
|
||||||
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@ -393,7 +394,7 @@ public class ImageControllerTests {
|
|||||||
.header("Authorization", "Bearer " + accessToken)
|
.header("Authorization", "Bearer " + accessToken)
|
||||||
)
|
)
|
||||||
.andExpect(status().isNoContent());
|
.andExpect(status().isNoContent());
|
||||||
assertThrows(ImageException.class, () -> this.imageService.getById(image.getId(), owner));
|
assertThrows(NoSuchEntityWithIdException.class, () -> this.imageService.getById(image.getId(), owner));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
|
|||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import app.mealsmadeeasy.api.user.UserCreateException;
|
import app.mealsmadeeasy.api.user.UserCreateException;
|
||||||
import app.mealsmadeeasy.api.user.UserService;
|
import app.mealsmadeeasy.api.user.UserService;
|
||||||
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -261,7 +262,7 @@ public class S3ImageServiceTests {
|
|||||||
final User owner = this.seedUser();
|
final User owner = this.seedUser();
|
||||||
final Image image = this.seedImage(owner);
|
final Image image = this.seedImage(owner);
|
||||||
this.imageService.deleteImage(image, owner);
|
this.imageService.deleteImage(image, owner);
|
||||||
assertThrows(ImageException.class, () -> this.imageService.getById(image.getId(), owner));
|
assertThrows(NoSuchEntityWithIdException.class, () -> this.imageService.getById(image.getId(), owner));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@ -46,7 +47,7 @@ public class RecipeRepositoryTests {
|
|||||||
publicRecipe.setRawText("Hello, World!");
|
publicRecipe.setRawText("Hello, World!");
|
||||||
final Recipe savedRecipe = this.recipeRepository.save(publicRecipe);
|
final Recipe savedRecipe = this.recipeRepository.save(publicRecipe);
|
||||||
|
|
||||||
final List<Recipe> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue();
|
final List<Recipe> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue(Pageable.unpaged()).toList();
|
||||||
assertThat(publicRecipes).anyMatch(recipeEntity -> recipeEntity.getId().equals(savedRecipe.getId()));
|
assertThat(publicRecipes).anyMatch(recipeEntity -> recipeEntity.getId().equals(savedRecipe.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +61,7 @@ public class RecipeRepositoryTests {
|
|||||||
nonPublicRecipe.setRawText("Hello, World!");
|
nonPublicRecipe.setRawText("Hello, World!");
|
||||||
final Recipe savedRecipe = this.recipeRepository.save(nonPublicRecipe);
|
final Recipe savedRecipe = this.recipeRepository.save(nonPublicRecipe);
|
||||||
|
|
||||||
final List<Recipe> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue();
|
final List<Recipe> publicRecipes = this.recipeRepository.findAllByIsPublicIsTrue(Pageable.unpaged()).toList();
|
||||||
assertThat(publicRecipes).noneMatch(recipeEntity -> recipeEntity.getId().equals(savedRecipe.getId()));
|
assertThat(publicRecipes).noneMatch(recipeEntity -> recipeEntity.getId().equals(savedRecipe.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
package app.mealsmadeeasy.api.recipe;
|
package app.mealsmadeeasy.api.recipe;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.IntegrationTestsExtension;
|
import app.mealsmadeeasy.api.IntegrationTestsExtension;
|
||||||
import app.mealsmadeeasy.api.image.ImageException;
|
|
||||||
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
|
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
|
||||||
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
|
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
|
||||||
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
|
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
|
||||||
import app.mealsmadeeasy.api.recipe.star.RecipeStarService;
|
import app.mealsmadeeasy.api.recipe.star.RecipeStarService;
|
||||||
import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import app.mealsmadeeasy.api.user.UserRepository;
|
import app.mealsmadeeasy.api.user.UserRepository;
|
||||||
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@ -21,7 +20,6 @@ import org.springframework.security.access.AccessDeniedException;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static app.mealsmadeeasy.api.recipe.ContainsRecipeInfoViewsForRecipesMatcher.containsRecipeInfoViewsForRecipes;
|
|
||||||
import static app.mealsmadeeasy.api.recipe.ContainsRecipesMatcher.containsRecipes;
|
import static app.mealsmadeeasy.api.recipe.ContainsRecipesMatcher.containsRecipes;
|
||||||
import static org.hamcrest.MatcherAssert.assertThat;
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
import static org.hamcrest.Matchers.*;
|
import static org.hamcrest.Matchers.*;
|
||||||
@ -78,13 +76,6 @@ public class RecipeServiceTests {
|
|||||||
assertThat(recipe.getRawText(), is("Hello!"));
|
assertThat(recipe.getRawText(), is("Hello!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
public void createWithoutOwnerThrowsAccessDenied() {
|
|
||||||
assertThrows(AccessDeniedException.class, () -> this.recipeService.create(
|
|
||||||
null, RecipeCreateSpec.builder().build()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getByIdPublicNoViewerDoesNotThrow() {
|
public void getByIdPublicNoViewerDoesNotThrow() {
|
||||||
final User owner = this.seedUser();
|
final User owner = this.seedUser();
|
||||||
@ -93,7 +84,7 @@ public class RecipeServiceTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getByIdHasCorrectProperties() throws RecipeException {
|
public void getByIdHasCorrectProperties() {
|
||||||
final User owner = this.seedUser();
|
final User owner = this.seedUser();
|
||||||
final Recipe recipe = this.createTestRecipe(owner, true);
|
final Recipe recipe = this.createTestRecipe(owner, true);
|
||||||
final Recipe byId = this.recipeService.getById(recipe.getId(), null);
|
final Recipe byId = this.recipeService.getById(recipe.getId(), null);
|
||||||
@ -154,7 +145,7 @@ public class RecipeServiceTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getByIdWithStarsOkayWhenPublicRecipeWithViewer() throws RecipeException, ImageException {
|
public void getByIdWithStarsOkayWhenPublicRecipeWithViewer() {
|
||||||
final User owner = this.seedUser();
|
final User owner = this.seedUser();
|
||||||
final User viewer = this.seedUser();
|
final User viewer = this.seedUser();
|
||||||
final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
|
final Recipe notYetPublicRecipe = this.createTestRecipe(owner);
|
||||||
@ -195,7 +186,7 @@ public class RecipeServiceTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getByMinimumStarsOnlySomeViewable() throws RecipeException {
|
public void getByMinimumStarsOnlySomeViewable() {
|
||||||
final User owner = this.seedUser();
|
final User owner = this.seedUser();
|
||||||
final User u0 = this.seedUser();
|
final User u0 = this.seedUser();
|
||||||
final User u1 = this.seedUser();
|
final User u1 = this.seedUser();
|
||||||
@ -245,7 +236,7 @@ public class RecipeServiceTests {
|
|||||||
Recipe r0 = this.createTestRecipe(owner, true);
|
Recipe r0 = this.createTestRecipe(owner, true);
|
||||||
Recipe r1 = this.createTestRecipe(owner, true);
|
Recipe r1 = this.createTestRecipe(owner, true);
|
||||||
|
|
||||||
final List<Recipe> publicRecipes = this.recipeService.getPublicRecipes();
|
final List<Recipe> publicRecipes = this.recipeService.getPublicRecipes(Pageable.unpaged()).toList();
|
||||||
assertThat(publicRecipes, containsRecipes(r0, r1));
|
assertThat(publicRecipes, containsRecipes(r0, r1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,37 +246,13 @@ public class RecipeServiceTests {
|
|||||||
Recipe r0 = this.createTestRecipe(owner, true);
|
Recipe r0 = this.createTestRecipe(owner, true);
|
||||||
Recipe r1 = this.createTestRecipe(owner, false);
|
Recipe r1 = this.createTestRecipe(owner, false);
|
||||||
|
|
||||||
final Slice<RecipeInfoView> viewableInfoViewsSlice = this.recipeService.getInfoViewsViewableBy(
|
final Slice<Recipe> viewableInfoViewsSlice = this.recipeService.getViewableBy(Pageable.unpaged(), owner);
|
||||||
Pageable.ofSize(20),
|
final List<Recipe> viewableInfos = viewableInfoViewsSlice.toList();
|
||||||
owner
|
assertThat(viewableInfos, containsRecipes(r0, r1));
|
||||||
);
|
|
||||||
final List<RecipeInfoView> viewableInfos = viewableInfoViewsSlice.getContent();
|
|
||||||
assertThat(viewableInfos, containsRecipeInfoViewsForRecipes(r0, r1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void getRecipesViewableByUser() throws RecipeException {
|
public void updateRawText() {
|
||||||
final User owner = this.seedUser();
|
|
||||||
final User viewer = this.seedUser();
|
|
||||||
|
|
||||||
Recipe r0 = this.createTestRecipe(owner);
|
|
||||||
r0 = this.recipeService.addViewer(r0.getId(), owner, viewer);
|
|
||||||
final List<Recipe> viewableRecipes = this.recipeService.getRecipesViewableBy(viewer);
|
|
||||||
assertThat(viewableRecipes.size(), is(1));
|
|
||||||
assertThat(viewableRecipes, containsRecipes(r0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getRecipesOwnedByUser() {
|
|
||||||
final User owner = this.seedUser();
|
|
||||||
final Recipe r0 = this.createTestRecipe(owner);
|
|
||||||
final List<Recipe> ownedRecipes = this.recipeService.getRecipesOwnedBy(owner);
|
|
||||||
assertThat(ownedRecipes.size(), is(1));
|
|
||||||
assertThat(ownedRecipes, containsRecipes(r0));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void updateRawText() throws RecipeException, ImageException {
|
|
||||||
final User owner = this.seedUser();
|
final User owner = this.seedUser();
|
||||||
final RecipeCreateSpec createSpec = RecipeCreateSpec.builder()
|
final RecipeCreateSpec createSpec = RecipeCreateSpec.builder()
|
||||||
.slug(UUID.randomUUID().toString())
|
.slug(UUID.randomUUID().toString())
|
||||||
@ -330,7 +297,7 @@ public class RecipeServiceTests {
|
|||||||
final User owner = this.seedUser();
|
final User owner = this.seedUser();
|
||||||
final Recipe toDelete = this.createTestRecipe(owner);
|
final Recipe toDelete = this.createTestRecipe(owner);
|
||||||
this.recipeService.deleteRecipe(toDelete.getId(), owner);
|
this.recipeService.deleteRecipe(toDelete.getId(), owner);
|
||||||
assertThrows(RecipeException.class, () -> this.recipeService.getById(toDelete.getId(), owner));
|
assertThrows(NoSuchEntityWithIdException.class, () -> this.recipeService.getById(toDelete.getId(), owner));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package app.mealsmadeeasy.api.recipe.star;
|
|||||||
|
|
||||||
import app.mealsmadeeasy.api.IntegrationTestsExtension;
|
import app.mealsmadeeasy.api.IntegrationTestsExtension;
|
||||||
import app.mealsmadeeasy.api.recipe.Recipe;
|
import app.mealsmadeeasy.api.recipe.Recipe;
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeException;
|
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeService;
|
import app.mealsmadeeasy.api.recipe.RecipeService;
|
||||||
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
|
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
@ -63,7 +62,6 @@ public class RecipeStarServiceTests {
|
|||||||
recipe.getSlug(),
|
recipe.getSlug(),
|
||||||
starer
|
starer
|
||||||
));
|
));
|
||||||
//noinspection DataFlowIssue
|
|
||||||
assertThat(star.getTimestamp(), is(notNullValue()));
|
assertThat(star.getTimestamp(), is(notNullValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,12 +74,11 @@ public class RecipeStarServiceTests {
|
|||||||
recipe.getId(),
|
recipe.getId(),
|
||||||
starer.getId()
|
starer.getId()
|
||||||
));
|
));
|
||||||
//noinspection DataFlowIssue
|
|
||||||
assertThat(star.getTimestamp(), is(notNullValue()));
|
assertThat(star.getTimestamp(), is(notNullValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void find() throws RecipeException {
|
public void find() {
|
||||||
final User owner = this.seedUser();
|
final User owner = this.seedUser();
|
||||||
final User starer = this.seedUser();
|
final User starer = this.seedUser();
|
||||||
final Recipe recipe = this.seedRecipe(owner);
|
final Recipe recipe = this.seedRecipe(owner);
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
package app.mealsmadeeasy.api.image;
|
package app.mealsmadeeasy.api.image;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.image.body.ImageUpdateBody;
|
import app.mealsmadeeasy.api.image.body.ImageUpdateBody;
|
||||||
|
import app.mealsmadeeasy.api.image.converter.ImageToViewConverter;
|
||||||
import app.mealsmadeeasy.api.image.spec.ImageCreateSpec;
|
import app.mealsmadeeasy.api.image.spec.ImageCreateSpec;
|
||||||
import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
|
import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
|
||||||
import app.mealsmadeeasy.api.image.view.ImageView;
|
import app.mealsmadeeasy.api.image.view.ImageView;
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import app.mealsmadeeasy.api.user.UserService;
|
import app.mealsmadeeasy.api.user.UserService;
|
||||||
import app.mealsmadeeasy.api.util.AccessDeniedView;
|
import app.mealsmadeeasy.api.util.AccessDeniedView;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.core.io.InputStreamResource;
|
import org.springframework.core.io.InputStreamResource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@ -24,15 +26,12 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/images")
|
@RequestMapping("/images")
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class ImageController {
|
public class ImageController {
|
||||||
|
|
||||||
private final ImageService imageService;
|
private final ImageService imageService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
private final ImageToViewConverter imageToViewConverter;
|
||||||
public ImageController(ImageService imageService, UserService userService) {
|
|
||||||
this.imageService = imageService;
|
|
||||||
this.userService = userService;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ImageUpdateSpec getImageUpdateSpec(ImageUpdateBody body) {
|
private ImageUpdateSpec getImageUpdateSpec(ImageUpdateBody body) {
|
||||||
final var builder = ImageUpdateSpec.builder()
|
final var builder = ImageUpdateSpec.builder()
|
||||||
@ -73,7 +72,7 @@ public class ImageController {
|
|||||||
@AuthenticationPrincipal User principal,
|
@AuthenticationPrincipal User principal,
|
||||||
@PathVariable String username,
|
@PathVariable String username,
|
||||||
@PathVariable String filename
|
@PathVariable String filename
|
||||||
) throws ImageException, IOException {
|
) throws IOException {
|
||||||
final User owner = this.userService.getUser(username);
|
final User owner = this.userService.getUser(username);
|
||||||
final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal);
|
final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal);
|
||||||
final InputStream imageInputStream = this.imageService.getImageContent(image, principal);
|
final InputStream imageInputStream = this.imageService.getImageContent(image, principal);
|
||||||
@ -110,7 +109,7 @@ public class ImageController {
|
|||||||
image.getSize(),
|
image.getSize(),
|
||||||
specBuilder.build()
|
specBuilder.build()
|
||||||
);
|
);
|
||||||
return ResponseEntity.status(201).body(this.imageService.toImageView(saved, principal));
|
return ResponseEntity.status(201).body(this.imageToViewConverter.convert(saved, principal, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{username}/{filename}")
|
@PostMapping("/{username}/{filename}")
|
||||||
@ -119,14 +118,14 @@ public class ImageController {
|
|||||||
@PathVariable String username,
|
@PathVariable String username,
|
||||||
@PathVariable String filename,
|
@PathVariable String filename,
|
||||||
@RequestBody ImageUpdateBody body
|
@RequestBody ImageUpdateBody body
|
||||||
) throws ImageException {
|
) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
throw new AccessDeniedException("Must be logged in.");
|
throw new AccessDeniedException("Must be logged in.");
|
||||||
}
|
}
|
||||||
final User owner = this.userService.getUser(username);
|
final User owner = this.userService.getUser(username);
|
||||||
final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal);
|
final Image image = this.imageService.getByOwnerAndFilename(owner, filename, principal);
|
||||||
final Image updated = this.imageService.update(image, principal, this.getImageUpdateSpec(body));
|
final Image updated = this.imageService.update(image, principal, this.getImageUpdateSpec(body));
|
||||||
return ResponseEntity.ok(this.imageService.toImageView(updated, principal));
|
return ResponseEntity.ok(this.imageToViewConverter.convert(updated, principal, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{username}/{filename}")
|
@DeleteMapping("/{username}/{filename}")
|
||||||
@ -134,7 +133,7 @@ public class ImageController {
|
|||||||
@AuthenticationPrincipal User principal,
|
@AuthenticationPrincipal User principal,
|
||||||
@PathVariable String username,
|
@PathVariable String username,
|
||||||
@PathVariable String filename
|
@PathVariable String filename
|
||||||
) throws ImageException, IOException {
|
) throws IOException {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
throw new AccessDeniedException("Must be logged in.");
|
throw new AccessDeniedException("Must be logged in.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,6 @@ package app.mealsmadeeasy.api.image;
|
|||||||
public class ImageException extends Exception {
|
public class ImageException extends Exception {
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
INVALID_ID,
|
|
||||||
INVALID_USERNAME_OR_FILENAME,
|
|
||||||
IMAGE_NOT_FOUND,
|
|
||||||
UNSUPPORTED_IMAGE_TYPE,
|
UNSUPPORTED_IMAGE_TYPE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package app.mealsmadeeasy.api.image;
|
package app.mealsmadeeasy.api.image;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -16,7 +17,7 @@ public class ImageSecurityImpl implements ImageSecurity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isViewableBy(Image image, @Nullable User viewer) {
|
public boolean isViewableBy(@NotNull Image image, @Nullable User viewer) {
|
||||||
if (image.getIsPublic()) {
|
if (image.getIsPublic()) {
|
||||||
// public image
|
// public image
|
||||||
return true;
|
return true;
|
||||||
@ -40,7 +41,7 @@ public class ImageSecurityImpl implements ImageSecurity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isOwner(Image image, @Nullable User user) {
|
public boolean isOwner(@NotNull Image image, @Nullable User user) {
|
||||||
return image.getOwner() != null && user != null && Objects.equals(image.getOwner().getId(), user.getId());
|
return image.getOwner() != null && user != null && Objects.equals(image.getOwner().getId(), user.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package app.mealsmadeeasy.api.image;
|
|||||||
|
|
||||||
import app.mealsmadeeasy.api.image.spec.ImageCreateSpec;
|
import app.mealsmadeeasy.api.image.spec.ImageCreateSpec;
|
||||||
import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
|
import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
|
||||||
import app.mealsmadeeasy.api.image.view.ImageView;
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
@ -15,9 +14,9 @@ public interface ImageService {
|
|||||||
Image create(User owner, String userFilename, InputStream inputStream, long objectSize, ImageCreateSpec infoSpec)
|
Image create(User owner, String userFilename, InputStream inputStream, long objectSize, ImageCreateSpec infoSpec)
|
||||||
throws IOException, ImageException;
|
throws IOException, ImageException;
|
||||||
|
|
||||||
Image getById(Integer id, @Nullable User viewer) throws ImageException;
|
Image getById(Integer id, @Nullable User viewer);
|
||||||
Image getByOwnerAndFilename(User owner, String filename, User viewer) throws ImageException;
|
Image getByOwnerAndFilename(User owner, String filename, User viewer);
|
||||||
Image getByUsernameAndFilename(String username, String filename, User viewer) throws ImageException;
|
Image getByUsernameAndFilename(String username, String filename, User viewer);
|
||||||
|
|
||||||
InputStream getImageContent(Image image, @Nullable User viewer) throws IOException;
|
InputStream getImageContent(Image image, @Nullable User viewer) throws IOException;
|
||||||
List<Image> getImagesOwnedBy(User user);
|
List<Image> getImagesOwnedBy(User user);
|
||||||
@ -26,6 +25,4 @@ public interface ImageService {
|
|||||||
|
|
||||||
void deleteImage(Image image, User modifier) throws IOException;
|
void deleteImage(Image image, User modifier) throws IOException;
|
||||||
|
|
||||||
ImageView toImageView(Image image, @Nullable User viewer);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,11 @@ package app.mealsmadeeasy.api.image;
|
|||||||
|
|
||||||
import app.mealsmadeeasy.api.image.spec.ImageCreateSpec;
|
import app.mealsmadeeasy.api.image.spec.ImageCreateSpec;
|
||||||
import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
|
import app.mealsmadeeasy.api.image.spec.ImageUpdateSpec;
|
||||||
import app.mealsmadeeasy.api.image.view.ImageView;
|
|
||||||
import app.mealsmadeeasy.api.s3.S3Manager;
|
import app.mealsmadeeasy.api.s3.S3Manager;
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import app.mealsmadeeasy.api.util.MimeTypeService;
|
import app.mealsmadeeasy.api.util.MimeTypeService;
|
||||||
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
|
||||||
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithUsernameAndFilenameException;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.access.prepost.PostAuthorize;
|
import org.springframework.security.access.prepost.PostAuthorize;
|
||||||
@ -27,20 +28,17 @@ public class S3ImageService implements ImageService {
|
|||||||
private final S3Manager s3Manager;
|
private final S3Manager s3Manager;
|
||||||
private final ImageRepository imageRepository;
|
private final ImageRepository imageRepository;
|
||||||
private final String imageBucketName;
|
private final String imageBucketName;
|
||||||
private final String baseUrl;
|
|
||||||
private final MimeTypeService mimeTypeService;
|
private final MimeTypeService mimeTypeService;
|
||||||
|
|
||||||
public S3ImageService(
|
public S3ImageService(
|
||||||
S3Manager s3Manager,
|
S3Manager s3Manager,
|
||||||
ImageRepository imageRepository,
|
ImageRepository imageRepository,
|
||||||
@Value("${app.mealsmadeeasy.api.images.bucketName}") String imageBucketName,
|
@Value("${app.mealsmadeeasy.api.images.bucketName}") String imageBucketName,
|
||||||
@Value("${app.mealsmadeeasy.api.baseUrl}") String baseUrl,
|
|
||||||
MimeTypeService mimeTypeService
|
MimeTypeService mimeTypeService
|
||||||
) {
|
) {
|
||||||
this.s3Manager = s3Manager;
|
this.s3Manager = s3Manager;
|
||||||
this.imageRepository = imageRepository;
|
this.imageRepository = imageRepository;
|
||||||
this.imageBucketName = imageBucketName;
|
this.imageBucketName = imageBucketName;
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.mimeTypeService = mimeTypeService;
|
this.mimeTypeService = mimeTypeService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,42 +162,38 @@ public class S3ImageService implements ImageService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
@PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)")
|
@PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)")
|
||||||
public Image getById(Integer id, @Nullable User viewer) throws ImageException {
|
public Image getById(Integer id, @Nullable User viewer) {
|
||||||
return this.imageRepository.findById(id).orElseThrow(() -> new ImageException(
|
return this.imageRepository.findById(id).orElseThrow(() -> new NoSuchEntityWithIdException(Image.class, id));
|
||||||
ImageException.Type.INVALID_ID, "No Image with id: " + id
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)")
|
@PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)")
|
||||||
public Image getByOwnerAndFilename(User owner, String filename, User viewer) throws ImageException {
|
public Image getByOwnerAndFilename(User owner, String filename, User viewer) {
|
||||||
return this.imageRepository.findByOwnerAndUserFilename((User) owner, filename)
|
return this.imageRepository.findByOwnerAndUserFilename(owner, filename)
|
||||||
.orElseThrow(() -> new ImageException(
|
.orElseThrow(() -> new NoSuchEntityWithUsernameAndFilenameException(
|
||||||
ImageException.Type.IMAGE_NOT_FOUND,
|
Image.class,
|
||||||
"No such image for owner " + owner + " with filename " + filename
|
owner.getUsername(),
|
||||||
|
filename
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)")
|
@PostAuthorize("@imageSecurity.isViewableBy(returnObject, #viewer)")
|
||||||
public Image getByUsernameAndFilename(String username, String filename, User viewer) throws ImageException {
|
public Image getByUsernameAndFilename(String username, String filename, User viewer) {
|
||||||
return this.imageRepository.findByOwnerUsernameAndFilename(username, filename).orElseThrow(
|
return this.imageRepository.findByOwnerUsernameAndFilename(username, filename).orElseThrow(
|
||||||
() -> new ImageException(
|
() -> new NoSuchEntityWithUsernameAndFilenameException(Image.class, username, filename)
|
||||||
ImageException.Type.INVALID_USERNAME_OR_FILENAME,
|
|
||||||
"No such Image for username " + username + " and filename " + filename
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@PreAuthorize("@imageSecurity.isViewableBy(#image, #viewer)")
|
@PreAuthorize("@imageSecurity.isViewableBy(#image, #viewer)")
|
||||||
public InputStream getImageContent(Image image, User viewer) throws IOException {
|
public InputStream getImageContent(Image image, User viewer) throws IOException {
|
||||||
return this.s3Manager.load(this.imageBucketName, ((Image) image).getObjectName());
|
return this.s3Manager.load(this.imageBucketName, image.getObjectName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Image> getImagesOwnedBy(User user) {
|
public List<Image> getImagesOwnedBy(User user) {
|
||||||
return new ArrayList<>(this.imageRepository.findAllByOwner((User) user));
|
return new ArrayList<>(this.imageRepository.findAllByOwner(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -219,17 +213,4 @@ public class S3ImageService implements ImageService {
|
|||||||
this.s3Manager.delete("images", image.getObjectName());
|
this.s3Manager.delete("images", image.getObjectName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getImageUrl(Image image) {
|
|
||||||
return this.baseUrl + "/images/" + image.getOwner().getUsername() + "/" + image.getUserFilename();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ImageView toImageView(Image image, @Nullable User viewer) {
|
|
||||||
if (viewer != null && image.getOwner().getUsername().equals(viewer.getUsername())) {
|
|
||||||
return ImageView.from(image, this.getImageUrl(image), true);
|
|
||||||
} else {
|
|
||||||
return ImageView.from(image, this.getImageUrl(image), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
package app.mealsmadeeasy.api.image.converter;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.image.Image;
|
||||||
|
import app.mealsmadeeasy.api.image.view.ImageView;
|
||||||
|
import app.mealsmadeeasy.api.user.User;
|
||||||
|
import app.mealsmadeeasy.api.user.view.UserInfoView;
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class ImageToViewConverter {
|
||||||
|
|
||||||
|
private final String baseUrl;
|
||||||
|
|
||||||
|
public ImageToViewConverter(@Value("${app.mealsmadeeasy.api.baseUrl}") String baseUrl) {
|
||||||
|
this.baseUrl = baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getImageUrl(Image image) {
|
||||||
|
return this.baseUrl + "/images/" + image.getOwner().getUsername() + "/" + image.getUserFilename();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract("null, _, _ -> null")
|
||||||
|
public @Nullable ImageView convert(@Nullable Image image, @Nullable User viewer, boolean includeViewers) {
|
||||||
|
if (image == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final var builder = ImageView.builder()
|
||||||
|
.url(this.getImageUrl(image))
|
||||||
|
.created(image.getCreated())
|
||||||
|
.modified(image.getModified())
|
||||||
|
.filename(image.getUserFilename())
|
||||||
|
.mimeType(image.getMimeType())
|
||||||
|
.alt(image.getAlt())
|
||||||
|
.caption(image.getCaption())
|
||||||
|
.owner(UserInfoView.from(image.getOwner()))
|
||||||
|
.isPublic(image.getIsPublic())
|
||||||
|
.height(image.getHeight())
|
||||||
|
.width(image.getWidth());
|
||||||
|
if (includeViewers) {
|
||||||
|
builder.viewers(
|
||||||
|
image.getViewers().stream()
|
||||||
|
.map(UserInfoView::from)
|
||||||
|
.collect(Collectors.toSet())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,6 +1,5 @@
|
|||||||
package app.mealsmadeeasy.api.image.view;
|
package app.mealsmadeeasy.api.image.view;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.image.Image;
|
|
||||||
import app.mealsmadeeasy.api.user.view.UserInfoView;
|
import app.mealsmadeeasy.api.user.view.UserInfoView;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
@ -8,35 +7,10 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
|
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Value
|
@Value
|
||||||
@Builder
|
@Builder
|
||||||
public class ImageView {
|
public class ImageView {
|
||||||
|
|
||||||
public static ImageView from(Image image, String url, boolean includeViewers) {
|
|
||||||
final var builder = ImageView.builder()
|
|
||||||
.url(url)
|
|
||||||
.created(image.getCreated())
|
|
||||||
.modified(image.getModified())
|
|
||||||
.filename(image.getUserFilename())
|
|
||||||
.mimeType(image.getMimeType())
|
|
||||||
.alt(image.getAlt())
|
|
||||||
.caption(image.getCaption())
|
|
||||||
.owner(UserInfoView.from(image.getOwner()))
|
|
||||||
.isPublic(image.getIsPublic())
|
|
||||||
.height(image.getHeight())
|
|
||||||
.width(image.getWidth());
|
|
||||||
if (includeViewers) {
|
|
||||||
builder.viewers(
|
|
||||||
image.getViewers().stream()
|
|
||||||
.map(UserInfoView::from)
|
|
||||||
.collect(Collectors.toSet())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
String url;
|
String url;
|
||||||
OffsetDateTime created;
|
OffsetDateTime created;
|
||||||
@Nullable OffsetDateTime modified;
|
@Nullable OffsetDateTime modified;
|
||||||
@ -49,5 +23,4 @@ public class ImageView {
|
|||||||
@Nullable Integer height;
|
@Nullable Integer height;
|
||||||
@Nullable Integer width;
|
@Nullable Integer width;
|
||||||
@Nullable Set<UserInfoView> viewers;
|
@Nullable Set<UserInfoView> viewers;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@ public class Recipe {
|
|||||||
joinColumns = @JoinColumn(name = "recipe_id"),
|
joinColumns = @JoinColumn(name = "recipe_id"),
|
||||||
inverseJoinColumns = @JoinColumn(name = "viewer_id")
|
inverseJoinColumns = @JoinColumn(name = "viewer_id")
|
||||||
)
|
)
|
||||||
private Set<User> viewers = new HashSet<>();
|
private Set<User> viewers = new HashSet<>(); // todo: see if we can get rid of this init
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "main_image_id")
|
@JoinColumn(name = "main_image_id")
|
||||||
@ -81,4 +81,14 @@ public class Recipe {
|
|||||||
@OneToOne(mappedBy = "recipe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToOne(mappedBy = "recipe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private RecipeEmbedding embedding;
|
private RecipeEmbedding embedding;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
public void prePersist() {
|
||||||
|
this.created = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
public void preUpdate() {
|
||||||
|
this.modified = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,18 +2,16 @@ package app.mealsmadeeasy.api.recipe;
|
|||||||
|
|
||||||
import app.mealsmadeeasy.api.file.File;
|
import app.mealsmadeeasy.api.file.File;
|
||||||
import app.mealsmadeeasy.api.file.FileService;
|
import app.mealsmadeeasy.api.file.FileService;
|
||||||
import app.mealsmadeeasy.api.image.ImageException;
|
|
||||||
import app.mealsmadeeasy.api.image.ImageService;
|
|
||||||
import app.mealsmadeeasy.api.image.view.ImageView;
|
|
||||||
import app.mealsmadeeasy.api.recipe.body.RecipeDraftUpdateBody;
|
import app.mealsmadeeasy.api.recipe.body.RecipeDraftUpdateBody;
|
||||||
|
import app.mealsmadeeasy.api.recipe.converter.RecipeDraftToViewConverter;
|
||||||
import app.mealsmadeeasy.api.recipe.converter.RecipeDraftUpdateBodyToSpecConverter;
|
import app.mealsmadeeasy.api.recipe.converter.RecipeDraftUpdateBodyToSpecConverter;
|
||||||
|
import app.mealsmadeeasy.api.recipe.converter.RecipeToFullViewConverter;
|
||||||
import app.mealsmadeeasy.api.recipe.spec.RecipeDraftUpdateSpec;
|
import app.mealsmadeeasy.api.recipe.spec.RecipeDraftUpdateSpec;
|
||||||
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
||||||
import app.mealsmadeeasy.api.recipe.view.RecipeDraftView;
|
import app.mealsmadeeasy.api.recipe.view.RecipeDraftView;
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import app.mealsmadeeasy.api.util.MustBeLoggedInException;
|
import app.mealsmadeeasy.api.util.MustBeLoggedInException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
@ -31,14 +29,9 @@ public class RecipeDraftsController {
|
|||||||
|
|
||||||
private final RecipeService recipeService;
|
private final RecipeService recipeService;
|
||||||
private final FileService fileService;
|
private final FileService fileService;
|
||||||
private final ImageService imageService;
|
|
||||||
private final RecipeDraftUpdateBodyToSpecConverter updateBodyToSpecConverter;
|
private final RecipeDraftUpdateBodyToSpecConverter updateBodyToSpecConverter;
|
||||||
|
private final RecipeDraftToViewConverter draftToViewConverter;
|
||||||
private @Nullable ImageView getImageView(RecipeDraft recipeDraft, User viewer) {
|
private final RecipeToFullViewConverter recipeToFullViewConverter;
|
||||||
return recipeDraft.getMainImage() != null
|
|
||||||
? this.imageService.toImageView(recipeDraft.getMainImage(), viewer)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<List<RecipeDraftView>> getAllDraftsForUser(@AuthenticationPrincipal User user) {
|
public ResponseEntity<List<RecipeDraftView>> getAllDraftsForUser(@AuthenticationPrincipal User user) {
|
||||||
@ -46,10 +39,10 @@ public class RecipeDraftsController {
|
|||||||
throw new MustBeLoggedInException();
|
throw new MustBeLoggedInException();
|
||||||
}
|
}
|
||||||
final List<RecipeDraft> recipeDrafts = this.recipeService.getDrafts(user);
|
final List<RecipeDraft> recipeDrafts = this.recipeService.getDrafts(user);
|
||||||
return ResponseEntity.ok(recipeDrafts.stream().map(recipeDraft -> {
|
return ResponseEntity.ok(recipeDrafts.stream()
|
||||||
final @Nullable ImageView mainImage = this.getImageView(recipeDraft, user);
|
.map(recipeDraft -> this.draftToViewConverter.convert(recipeDraft, user))
|
||||||
return RecipeDraftView.from(recipeDraft, mainImage);
|
.toList()
|
||||||
}).toList());
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@ -61,8 +54,7 @@ public class RecipeDraftsController {
|
|||||||
throw new MustBeLoggedInException();
|
throw new MustBeLoggedInException();
|
||||||
}
|
}
|
||||||
final RecipeDraft recipeDraft = this.recipeService.getDraftByIdWithViewer(id, viewer);
|
final RecipeDraft recipeDraft = this.recipeService.getDraftByIdWithViewer(id, viewer);
|
||||||
final @Nullable ImageView imageView = this.getImageView(recipeDraft, viewer);
|
return ResponseEntity.ok(this.draftToViewConverter.convert(recipeDraft, viewer));
|
||||||
return ResponseEntity.ok(RecipeDraftView.from(recipeDraft, imageView));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/manual")
|
@PostMapping("/manual")
|
||||||
@ -71,8 +63,7 @@ public class RecipeDraftsController {
|
|||||||
throw new MustBeLoggedInException();
|
throw new MustBeLoggedInException();
|
||||||
}
|
}
|
||||||
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
final RecipeDraft recipeDraft = this.recipeService.createDraft(owner);
|
||||||
final ImageView imageView = this.getImageView(recipeDraft, owner);
|
return ResponseEntity.ok(this.draftToViewConverter.convert(recipeDraft, owner));
|
||||||
return ResponseEntity.ok(RecipeDraftView.from(recipeDraft, imageView));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/ai")
|
@PostMapping("/ai")
|
||||||
@ -91,8 +82,7 @@ public class RecipeDraftsController {
|
|||||||
owner
|
owner
|
||||||
);
|
);
|
||||||
final RecipeDraft recipeDraft = this.recipeService.createAiDraft(file, owner);
|
final RecipeDraft recipeDraft = this.recipeService.createAiDraft(file, owner);
|
||||||
final @Nullable ImageView mainImageView = this.getImageView(recipeDraft, owner);
|
return ResponseEntity.status(HttpStatus.CREATED).body(this.draftToViewConverter.convert(recipeDraft, owner));
|
||||||
return ResponseEntity.status(HttpStatus.CREATED).body(RecipeDraftView.from(recipeDraft, mainImageView));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@ -100,14 +90,13 @@ public class RecipeDraftsController {
|
|||||||
@AuthenticationPrincipal User modifier,
|
@AuthenticationPrincipal User modifier,
|
||||||
@PathVariable UUID id,
|
@PathVariable UUID id,
|
||||||
@RequestBody RecipeDraftUpdateBody updateBody
|
@RequestBody RecipeDraftUpdateBody updateBody
|
||||||
) throws ImageException {
|
) {
|
||||||
if (modifier == null) {
|
if (modifier == null) {
|
||||||
throw new MustBeLoggedInException();
|
throw new MustBeLoggedInException();
|
||||||
}
|
}
|
||||||
final RecipeDraftUpdateSpec spec = this.updateBodyToSpecConverter.convert(updateBody, modifier);
|
final RecipeDraftUpdateSpec spec = this.updateBodyToSpecConverter.convert(updateBody, modifier);
|
||||||
final RecipeDraft updated = this.recipeService.updateDraft(id, spec, modifier);
|
final RecipeDraft updated = this.recipeService.updateDraft(id, spec, modifier);
|
||||||
final @Nullable ImageView imageView = this.getImageView(updated, modifier);
|
return ResponseEntity.ok(this.draftToViewConverter.convert(updated, modifier));
|
||||||
return ResponseEntity.ok(RecipeDraftView.from(updated, imageView));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
@ -131,7 +120,7 @@ public class RecipeDraftsController {
|
|||||||
throw new MustBeLoggedInException();
|
throw new MustBeLoggedInException();
|
||||||
}
|
}
|
||||||
final Recipe recipe = this.recipeService.publishDraft(id, modifier);
|
final Recipe recipe = this.recipeService.publishDraft(id, modifier);
|
||||||
final FullRecipeView view = this.recipeService.toFullRecipeView(recipe, false, modifier);
|
final FullRecipeView view = this.recipeToFullViewConverter.convert(recipe, false, modifier);
|
||||||
return ResponseEntity.status(HttpStatus.CREATED).body(view);
|
return ResponseEntity.status(HttpStatus.CREATED).body(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
package app.mealsmadeeasy.api.recipe;
|
|
||||||
|
|
||||||
public class RecipeException extends Exception {
|
|
||||||
|
|
||||||
public enum Type {
|
|
||||||
INVALID_USERNAME_OR_SLUG,
|
|
||||||
INVALID_ID,
|
|
||||||
INVALID_COMMENT_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Type type;
|
|
||||||
|
|
||||||
public RecipeException(Type type, String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RecipeException(Type type, String message) {
|
|
||||||
super(message);
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type getType() {
|
|
||||||
return this.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -12,7 +12,7 @@ import java.util.Optional;
|
|||||||
|
|
||||||
public interface RecipeRepository extends JpaRepository<Recipe, Integer> {
|
public interface RecipeRepository extends JpaRepository<Recipe, Integer> {
|
||||||
|
|
||||||
List<Recipe> findAllByIsPublicIsTrue();
|
Slice<Recipe> findAllByIsPublicIsTrue(Pageable pageable);
|
||||||
|
|
||||||
List<Recipe> findAllByViewersContaining(User viewer);
|
List<Recipe> findAllByViewersContaining(User viewer);
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package app.mealsmadeeasy.api.recipe;
|
package app.mealsmadeeasy.api.recipe;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
|
||||||
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithUsernameAndSlugException;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@ -19,25 +21,21 @@ public class RecipeSecurity {
|
|||||||
return recipe.getOwner() != null && recipe.getOwner().getId().equals(user.getId());
|
return recipe.getOwner() != null && recipe.getOwner().getId().equals(user.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOwner(Integer recipeId, User user) throws RecipeException {
|
public boolean isOwner(Integer recipeId, User user) {
|
||||||
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
|
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(
|
||||||
RecipeException.Type.INVALID_ID,
|
() -> new NoSuchEntityWithIdException(Recipe.class, recipeId)
|
||||||
"No such Recipe with id " + recipeId
|
|
||||||
));
|
|
||||||
return this.isOwner(recipe, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isOwner(String username, String slug, @Nullable User user) throws RecipeException {
|
|
||||||
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(
|
|
||||||
() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
|
||||||
"No such Recipe for username " + username + " and slug " + slug
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
return this.isOwner(recipe, user);
|
return this.isOwner(recipe, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException {
|
public boolean isOwner(String username, String slug, @Nullable User user) {
|
||||||
|
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(
|
||||||
|
() -> new NoSuchEntityWithUsernameAndSlugException(Recipe.class, username, slug)
|
||||||
|
);
|
||||||
|
return this.isOwner(recipe, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isViewableBy(Recipe recipe, @Nullable User user) {
|
||||||
if (recipe.getIsPublic()) {
|
if (recipe.getIsPublic()) {
|
||||||
// public recipe
|
// public recipe
|
||||||
return true;
|
return true;
|
||||||
@ -50,9 +48,7 @@ public class RecipeSecurity {
|
|||||||
} else {
|
} else {
|
||||||
// check if viewer
|
// check if viewer
|
||||||
final Recipe withViewers = this.recipeRepository.findByIdWithViewers(recipe.getId())
|
final Recipe withViewers = this.recipeRepository.findByIdWithViewers(recipe.getId())
|
||||||
.orElseThrow(() -> new RecipeException(
|
.orElseThrow(() -> new NoSuchEntityWithIdException(Recipe.class, recipe.getId()));
|
||||||
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + recipe.getId()
|
|
||||||
));
|
|
||||||
for (final User viewer : withViewers.getViewers()) {
|
for (final User viewer : withViewers.getViewers()) {
|
||||||
if (viewer.getId() != null && viewer.getId().equals(user.getId())) {
|
if (viewer.getId() != null && viewer.getId().equals(user.getId())) {
|
||||||
return true;
|
return true;
|
||||||
@ -63,20 +59,16 @@ public class RecipeSecurity {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) throws RecipeException {
|
public boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) {
|
||||||
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(ownerUsername, slug)
|
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(ownerUsername, slug)
|
||||||
.orElseThrow(() -> new RecipeException(
|
.orElseThrow(() -> new NoSuchEntityWithUsernameAndSlugException(Recipe.class, ownerUsername, slug));
|
||||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
|
||||||
"No such Recipe for username " + ownerUsername + " and slug: " + slug
|
|
||||||
));
|
|
||||||
return this.isViewableBy(recipe, user);
|
return this.isViewableBy(recipe, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isViewableBy(Integer recipeId, @Nullable User user) throws RecipeException {
|
public boolean isViewableBy(Integer recipeId, @Nullable User user) {
|
||||||
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
|
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(
|
||||||
RecipeException.Type.INVALID_ID,
|
() -> new NoSuchEntityWithIdException(Recipe.class, recipeId)
|
||||||
"No such Recipe with id: " + recipeId
|
);
|
||||||
));
|
|
||||||
return this.isViewableBy(recipe, user);
|
return this.isViewableBy(recipe, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,7 @@ package app.mealsmadeeasy.api.recipe;
|
|||||||
|
|
||||||
import app.mealsmadeeasy.api.file.File;
|
import app.mealsmadeeasy.api.file.File;
|
||||||
import app.mealsmadeeasy.api.image.Image;
|
import app.mealsmadeeasy.api.image.Image;
|
||||||
import app.mealsmadeeasy.api.image.ImageException;
|
|
||||||
import app.mealsmadeeasy.api.image.ImageService;
|
import app.mealsmadeeasy.api.image.ImageService;
|
||||||
import app.mealsmadeeasy.api.image.view.ImageView;
|
|
||||||
import app.mealsmadeeasy.api.job.JobService;
|
import app.mealsmadeeasy.api.job.JobService;
|
||||||
import app.mealsmadeeasy.api.markdown.MarkdownService;
|
import app.mealsmadeeasy.api.markdown.MarkdownService;
|
||||||
import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody;
|
import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody;
|
||||||
@ -14,18 +12,16 @@ import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
|
|||||||
import app.mealsmadeeasy.api.recipe.spec.RecipeDraftUpdateSpec;
|
import app.mealsmadeeasy.api.recipe.spec.RecipeDraftUpdateSpec;
|
||||||
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
|
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
|
||||||
import app.mealsmadeeasy.api.recipe.star.RecipeStarRepository;
|
import app.mealsmadeeasy.api.recipe.star.RecipeStarRepository;
|
||||||
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
|
||||||
import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
|
||||||
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithUsernameAndSlugException;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.jetbrains.annotations.Contract;
|
import org.jetbrains.annotations.Contract;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.springframework.ai.embedding.EmbeddingModel;
|
import org.springframework.ai.embedding.EmbeddingModel;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Slice;
|
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.PostAuthorize;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -37,6 +33,7 @@ import java.util.Set;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class RecipeService {
|
public class RecipeService {
|
||||||
|
|
||||||
private final RecipeRepository recipeRepository;
|
private final RecipeRepository recipeRepository;
|
||||||
@ -47,28 +44,7 @@ public class RecipeService {
|
|||||||
private final RecipeDraftRepository recipeDraftRepository;
|
private final RecipeDraftRepository recipeDraftRepository;
|
||||||
private final JobService jobService;
|
private final JobService jobService;
|
||||||
|
|
||||||
public RecipeService(
|
public Recipe create(User owner, RecipeCreateSpec spec) {
|
||||||
RecipeRepository recipeRepository,
|
|
||||||
RecipeStarRepository recipeStarRepository,
|
|
||||||
ImageService imageService,
|
|
||||||
MarkdownService markdownService,
|
|
||||||
EmbeddingModel embeddingModel,
|
|
||||||
RecipeDraftRepository recipeDraftRepository,
|
|
||||||
JobService jobService
|
|
||||||
) {
|
|
||||||
this.recipeRepository = recipeRepository;
|
|
||||||
this.recipeStarRepository = recipeStarRepository;
|
|
||||||
this.imageService = imageService;
|
|
||||||
this.markdownService = markdownService;
|
|
||||||
this.embeddingModel = embeddingModel;
|
|
||||||
this.recipeDraftRepository = recipeDraftRepository;
|
|
||||||
this.jobService = jobService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Recipe create(@Nullable User owner, RecipeCreateSpec spec) {
|
|
||||||
if (owner == null) {
|
|
||||||
throw new AccessDeniedException("Must be logged in.");
|
|
||||||
}
|
|
||||||
final Recipe draft = new Recipe();
|
final Recipe draft = new Recipe();
|
||||||
draft.setCreated(OffsetDateTime.now());
|
draft.setCreated(OffsetDateTime.now());
|
||||||
draft.setOwner(owner);
|
draft.setOwner(owner);
|
||||||
@ -80,34 +56,33 @@ public class RecipeService {
|
|||||||
return this.recipeRepository.save(draft);
|
return this.recipeRepository.save(draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Recipe findRecipeEntity(Integer id) throws RecipeException {
|
private Recipe getById(Integer id) {
|
||||||
return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
|
return this.recipeRepository.findById(id).orElseThrow(() -> new NoSuchEntityWithIdException(Recipe.class, id));
|
||||||
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id
|
}
|
||||||
));
|
|
||||||
|
private Recipe getByUsernameAndSlug(String username, String slug) {
|
||||||
|
return this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(
|
||||||
|
() -> new NoSuchEntityWithUsernameAndSlugException(Recipe.class, username, slug)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
||||||
public Recipe getById(Integer id, @Nullable User viewer) throws RecipeException {
|
public Recipe getById(Integer id, @Nullable User viewer) {
|
||||||
return this.findRecipeEntity(id);
|
return this.getById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
||||||
public Recipe getByIdWithStars(Integer id, @Nullable User viewer) throws RecipeException {
|
public Recipe getByIdWithStars(Integer id, @Nullable User viewer) {
|
||||||
return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException(
|
return this.recipeRepository.findByIdWithStars(id).orElseThrow(
|
||||||
RecipeException.Type.INVALID_ID,
|
() -> new NoSuchEntityWithIdException(Recipe.class, id)
|
||||||
"No such Recipe with id: " + id
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
||||||
public Recipe getByUsernameAndSlug(String username, String slug, @Nullable User viewer) throws RecipeException {
|
public Recipe getByUsernameAndSlug(String username, String slug, @Nullable User viewer) {
|
||||||
return this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(() -> new RecipeException(
|
return this.getByUsernameAndSlug(username, slug);
|
||||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
|
||||||
"No such Recipe for username " + username + " and slug " + slug
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiStatus.Internal
|
|
||||||
public String getRenderedMarkdown(Recipe entity) {
|
public String getRenderedMarkdown(Recipe entity) {
|
||||||
if (entity.getCachedRenderedText() == null) {
|
if (entity.getCachedRenderedText() == null) {
|
||||||
entity.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(entity.getRawText()));
|
entity.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(entity.getRawText()));
|
||||||
@ -116,69 +91,16 @@ public class RecipeService {
|
|||||||
return entity.getCachedRenderedText();
|
return entity.getCachedRenderedText();
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getStarCount(Recipe recipe) {
|
public int getStarCount(Recipe recipe) {
|
||||||
return this.recipeRepository.getStarCount(recipe.getId());
|
return this.recipeRepository.getStarCount(recipe.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getViewerCount(long recipeId) {
|
public int getViewerCount(Recipe recipe) {
|
||||||
return this.recipeRepository.getViewerCount(recipeId);
|
return this.recipeRepository.getViewerCount(recipe.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Contract("null, _ -> null")
|
public Slice<Recipe> getViewableBy(Pageable pageable, User viewer) {
|
||||||
private @Nullable ImageView getImageView(@Nullable Image image, @Nullable User viewer) {
|
return this.recipeRepository.findAllViewableBy(viewer, pageable);
|
||||||
if (image != null) {
|
|
||||||
return this.imageService.toImageView(image, viewer);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private FullRecipeView getFullView(Recipe recipe, boolean includeRawText, @Nullable User viewer) {
|
|
||||||
return FullRecipeView.from(
|
|
||||||
recipe,
|
|
||||||
this.getRenderedMarkdown(recipe),
|
|
||||||
includeRawText,
|
|
||||||
this.getStarCount(recipe),
|
|
||||||
this.getViewerCount(recipe.getId()),
|
|
||||||
this.getImageView(recipe.getMainImage(), viewer)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private RecipeInfoView getInfoView(Recipe recipe, @Nullable User viewer) {
|
|
||||||
return RecipeInfoView.from(
|
|
||||||
recipe,
|
|
||||||
this.getStarCount(recipe),
|
|
||||||
this.getImageView(recipe.getMainImage(), viewer)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreAuthorize("@recipeSecurity.isViewableBy(#id, #viewer)")
|
|
||||||
public FullRecipeView getFullViewById(Integer id, @Nullable User viewer) throws RecipeException {
|
|
||||||
final Recipe recipe = this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_ID, "No such Recipe for id: " + id
|
|
||||||
));
|
|
||||||
return this.getFullView(recipe, false, viewer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreAuthorize("@recipeSecurity.isViewableBy(#username, #slug, #viewer)")
|
|
||||||
public FullRecipeView getFullViewByUsernameAndSlug(
|
|
||||||
String username,
|
|
||||||
String slug,
|
|
||||||
boolean includeRawText,
|
|
||||||
@Nullable User viewer
|
|
||||||
) throws RecipeException {
|
|
||||||
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug)
|
|
||||||
.orElseThrow(() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
|
||||||
"No such Recipe for username " + username + " and slug: " + slug
|
|
||||||
));
|
|
||||||
return this.getFullView(recipe, includeRawText, viewer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Slice<RecipeInfoView> getInfoViewsViewableBy(Pageable pageable, @Nullable User viewer) {
|
|
||||||
return this.recipeRepository.findAllViewableBy(viewer, pageable).map(recipe ->
|
|
||||||
this.getInfoView(recipe, viewer)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Recipe> getByMinimumStars(long minimumStars, @Nullable User viewer) {
|
public List<Recipe> getByMinimumStars(long minimumStars, @Nullable User viewer) {
|
||||||
@ -187,19 +109,11 @@ public class RecipeService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Recipe> getPublicRecipes() {
|
public Slice<Recipe> getPublicRecipes(Pageable pageable) {
|
||||||
return List.copyOf(this.recipeRepository.findAllByIsPublicIsTrue());
|
return this.recipeRepository.findAllByIsPublicIsTrue(pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Recipe> getRecipesViewableBy(User viewer) {
|
public List<Recipe> aiSearch(RecipeAiSearchBody searchSpec, @Nullable User viewer) {
|
||||||
return List.copyOf(this.recipeRepository.findAllByViewersContaining(viewer));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Recipe> getRecipesOwnedBy(User owner) {
|
|
||||||
return List.copyOf(this.recipeRepository.findAllByOwner(owner));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<RecipeInfoView> aiSearch(RecipeAiSearchBody searchSpec, @Nullable User viewer) {
|
|
||||||
final float[] queryEmbedding = this.embeddingModel.embed(searchSpec.getPrompt());
|
final float[] queryEmbedding = this.embeddingModel.embed(searchSpec.getPrompt());
|
||||||
final List<Recipe> results;
|
final List<Recipe> results;
|
||||||
if (viewer == null) {
|
if (viewer == null) {
|
||||||
@ -207,12 +121,10 @@ public class RecipeService {
|
|||||||
} else {
|
} else {
|
||||||
results = this.recipeRepository.searchByEmbeddingAndViewableBy(queryEmbedding, 0.5f, viewer.getId());
|
results = this.recipeRepository.searchByEmbeddingAndViewableBy(queryEmbedding, 0.5f, viewer.getId());
|
||||||
}
|
}
|
||||||
return results.stream()
|
return results;
|
||||||
.map(recipeEntity -> this.getInfoView(recipeEntity, viewer))
|
|
||||||
.toList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareForUpdate(RecipeUpdateSpec spec, Recipe recipe, User modifier) throws ImageException {
|
private void prepareForUpdate(RecipeUpdateSpec spec, Recipe recipe, User modifier) {
|
||||||
boolean didUpdate = false;
|
boolean didUpdate = false;
|
||||||
if (spec.getTitle() != null) {
|
if (spec.getTitle() != null) {
|
||||||
recipe.setTitle(spec.getTitle());
|
recipe.setTitle(spec.getTitle());
|
||||||
@ -258,23 +170,17 @@ public class RecipeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("@recipeSecurity.isOwner(#username, #slug, #modifier)")
|
@PreAuthorize("@recipeSecurity.isOwner(#username, #slug, #modifier)")
|
||||||
public Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier)
|
public Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier) {
|
||||||
throws RecipeException, ImageException {
|
final Recipe recipe = this.getByUsernameAndSlug(username, slug);
|
||||||
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(() ->
|
|
||||||
new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
|
||||||
"No such Recipe for username " + username + " and slug: " + slug
|
|
||||||
)
|
|
||||||
);
|
|
||||||
this.prepareForUpdate(spec, recipe, modifier);
|
this.prepareForUpdate(spec, recipe, modifier);
|
||||||
return this.recipeRepository.save(recipe);
|
return this.recipeRepository.save(recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
||||||
public Recipe addViewer(Integer id, User modifier, User viewer) throws RecipeException {
|
public Recipe addViewer(Integer id, User modifier, User viewer) {
|
||||||
final Recipe entity = this.recipeRepository.findByIdWithViewers(id).orElseThrow(() -> new RecipeException(
|
final Recipe entity = this.recipeRepository.findByIdWithViewers(id).orElseThrow(
|
||||||
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id
|
() -> new NoSuchEntityWithIdException(Recipe.class, id)
|
||||||
));
|
);
|
||||||
final Set<User> viewers = new HashSet<>(entity.getViewers());
|
final Set<User> viewers = new HashSet<>(entity.getViewers());
|
||||||
viewers.add(viewer);
|
viewers.add(viewer);
|
||||||
entity.setViewers(viewers);
|
entity.setViewers(viewers);
|
||||||
@ -282,8 +188,8 @@ public class RecipeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
||||||
public Recipe removeViewer(Integer id, User modifier, User viewer) throws RecipeException {
|
public Recipe removeViewer(Integer id, User modifier, User viewer) {
|
||||||
final Recipe entity = this.findRecipeEntity(id);
|
final Recipe entity = this.getById(id);
|
||||||
final Set<User> viewers = new HashSet<>(entity.getViewers());
|
final Set<User> viewers = new HashSet<>(entity.getViewers());
|
||||||
viewers.remove(viewer);
|
viewers.remove(viewer);
|
||||||
entity.setViewers(viewers);
|
entity.setViewers(viewers);
|
||||||
@ -291,8 +197,8 @@ public class RecipeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
||||||
public Recipe clearAllViewers(Integer id, User modifier) throws RecipeException {
|
public Recipe clearAllViewers(Integer id, User modifier) {
|
||||||
final Recipe entity = this.findRecipeEntity(id);
|
final Recipe entity = this.getById(id);
|
||||||
entity.setViewers(new HashSet<>());
|
entity.setViewers(new HashSet<>());
|
||||||
return this.recipeRepository.save(entity);
|
return this.recipeRepository.save(entity);
|
||||||
}
|
}
|
||||||
@ -302,14 +208,6 @@ public class RecipeService {
|
|||||||
this.recipeRepository.deleteById(id);
|
this.recipeRepository.deleteById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FullRecipeView toFullRecipeView(Recipe recipe, boolean includeRawText, @Nullable User viewer) {
|
|
||||||
return this.getFullView(recipe, includeRawText, viewer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer) {
|
|
||||||
return this.getInfoView(recipe, viewer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreAuthorize("@recipeSecurity.isViewableBy(#username, #slug, #viewer)")
|
@PreAuthorize("@recipeSecurity.isViewableBy(#username, #slug, #viewer)")
|
||||||
@Contract("_, _, null -> null")
|
@Contract("_, _, null -> null")
|
||||||
public @Nullable Boolean isStarer(String username, String slug, @Nullable User viewer) {
|
public @Nullable Boolean isStarer(String username, String slug, @Nullable User viewer) {
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
package app.mealsmadeeasy.api.recipe;
|
package app.mealsmadeeasy.api.recipe;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.image.ImageException;
|
|
||||||
import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody;
|
import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody;
|
||||||
import app.mealsmadeeasy.api.recipe.body.RecipeSearchBody;
|
import app.mealsmadeeasy.api.recipe.body.RecipeSearchBody;
|
||||||
import app.mealsmadeeasy.api.recipe.body.RecipeUpdateBody;
|
import app.mealsmadeeasy.api.recipe.body.RecipeUpdateBody;
|
||||||
@ -8,15 +7,17 @@ import app.mealsmadeeasy.api.recipe.comment.RecipeComment;
|
|||||||
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentCreateBody;
|
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentCreateBody;
|
||||||
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentService;
|
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentService;
|
||||||
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentView;
|
import app.mealsmadeeasy.api.recipe.comment.RecipeCommentView;
|
||||||
|
import app.mealsmadeeasy.api.recipe.converter.RecipeToFullViewConverter;
|
||||||
|
import app.mealsmadeeasy.api.recipe.converter.RecipeToInfoViewConverter;
|
||||||
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
|
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
|
||||||
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
|
import app.mealsmadeeasy.api.recipe.star.RecipeStar;
|
||||||
import app.mealsmadeeasy.api.recipe.star.RecipeStarService;
|
import app.mealsmadeeasy.api.recipe.star.RecipeStarService;
|
||||||
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
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.RecipeInfoView;
|
||||||
import app.mealsmadeeasy.api.sliceview.SliceViewService;
|
import app.mealsmadeeasy.api.sliceview.SliceViewService;
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Slice;
|
import org.springframework.data.domain.Slice;
|
||||||
@ -32,6 +33,7 @@ import java.util.Map;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/recipes")
|
@RequestMapping("/recipes")
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class RecipesController {
|
public class RecipesController {
|
||||||
|
|
||||||
private final RecipeService recipeService;
|
private final RecipeService recipeService;
|
||||||
@ -39,32 +41,8 @@ public class RecipesController {
|
|||||||
private final RecipeCommentService recipeCommentService;
|
private final RecipeCommentService recipeCommentService;
|
||||||
private final SliceViewService sliceViewService;
|
private final SliceViewService sliceViewService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final RecipeToFullViewConverter recipeToFullViewConverter;
|
||||||
public RecipesController(
|
private final RecipeToInfoViewConverter recipeToInfoViewConverter;
|
||||||
RecipeService recipeService,
|
|
||||||
RecipeStarService recipeStarService,
|
|
||||||
RecipeCommentService recipeCommentService,
|
|
||||||
SliceViewService sliceViewService,
|
|
||||||
ObjectMapper objectMapper
|
|
||||||
) {
|
|
||||||
this.recipeService = recipeService;
|
|
||||||
this.recipeStarService = recipeStarService;
|
|
||||||
this.recipeCommentService = recipeCommentService;
|
|
||||||
this.sliceViewService = sliceViewService;
|
|
||||||
this.objectMapper = objectMapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExceptionHandler(RecipeException.class)
|
|
||||||
public ResponseEntity<RecipeExceptionView> onRecipeException(RecipeException recipeException) {
|
|
||||||
final HttpStatus status = switch (recipeException.getType()) {
|
|
||||||
case INVALID_ID, INVALID_USERNAME_OR_SLUG -> HttpStatus.NOT_FOUND;
|
|
||||||
case INVALID_COMMENT_ID -> HttpStatus.BAD_REQUEST;
|
|
||||||
};
|
|
||||||
return ResponseEntity.status(status.value()).body(new RecipeExceptionView(
|
|
||||||
recipeException.getType().toString(),
|
|
||||||
recipeException.getMessage()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<String, Object> getFullViewWrapper(String username, String slug, FullRecipeView view, @Nullable User viewer) {
|
private Map<String, Object> getFullViewWrapper(String username, String slug, FullRecipeView view, @Nullable User viewer) {
|
||||||
Map<String, Object> wrapper = new HashMap<>();
|
Map<String, Object> wrapper = new HashMap<>();
|
||||||
@ -80,13 +58,9 @@ public class RecipesController {
|
|||||||
@PathVariable String slug,
|
@PathVariable String slug,
|
||||||
@RequestParam(defaultValue = "false") boolean includeRawText,
|
@RequestParam(defaultValue = "false") boolean includeRawText,
|
||||||
@AuthenticationPrincipal User viewer
|
@AuthenticationPrincipal User viewer
|
||||||
) throws RecipeException {
|
) {
|
||||||
final FullRecipeView view = this.recipeService.getFullViewByUsernameAndSlug(
|
final Recipe recipe = this.recipeService.getByUsernameAndSlug(username, slug, viewer);
|
||||||
username,
|
final FullRecipeView view = this.recipeToFullViewConverter.convert(recipe, includeRawText, viewer);
|
||||||
slug,
|
|
||||||
includeRawText,
|
|
||||||
viewer
|
|
||||||
);
|
|
||||||
return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, viewer));
|
return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, viewer));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,10 +71,10 @@ public class RecipesController {
|
|||||||
@RequestParam(defaultValue = "true") boolean includeRawText,
|
@RequestParam(defaultValue = "true") boolean includeRawText,
|
||||||
@RequestBody RecipeUpdateBody updateBody,
|
@RequestBody RecipeUpdateBody updateBody,
|
||||||
@AuthenticationPrincipal User principal
|
@AuthenticationPrincipal User principal
|
||||||
) throws ImageException, RecipeException {
|
) {
|
||||||
final RecipeUpdateSpec spec = RecipeUpdateSpec.from(updateBody);
|
final RecipeUpdateSpec spec = RecipeUpdateSpec.from(updateBody);
|
||||||
final Recipe updated = this.recipeService.update(username, slug, spec, principal);
|
final Recipe updated = this.recipeService.update(username, slug, spec, principal);
|
||||||
final FullRecipeView view = this.recipeService.toFullRecipeView(updated, includeRawText, principal);
|
final FullRecipeView view = this.recipeToFullViewConverter.convert(updated, includeRawText, principal);
|
||||||
return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, principal));
|
return ResponseEntity.ok(this.getFullViewWrapper(username, slug, view, principal));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,8 +83,19 @@ public class RecipesController {
|
|||||||
Pageable pageable,
|
Pageable pageable,
|
||||||
@AuthenticationPrincipal User user
|
@AuthenticationPrincipal User user
|
||||||
) {
|
) {
|
||||||
final Slice<RecipeInfoView> slice = this.recipeService.getInfoViewsViewableBy(pageable, user);
|
if (user == null) {
|
||||||
return ResponseEntity.ok(this.sliceViewService.getSliceView(slice));
|
final Slice<Recipe> publicRecipes = this.recipeService.getPublicRecipes(pageable);
|
||||||
|
final Slice<RecipeInfoView> publicRecipeInfoViews = publicRecipes.map(
|
||||||
|
recipe -> this.recipeToInfoViewConverter.convert(recipe, null)
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok(this.sliceViewService.getSliceView(publicRecipeInfoViews));
|
||||||
|
} else {
|
||||||
|
final Slice<Recipe> recipes = this.recipeService.getViewableBy(pageable, user);
|
||||||
|
final Slice<RecipeInfoView> recipeInfoViews = recipes.map(
|
||||||
|
recipe -> this.recipeToInfoViewConverter.convert(recipe, user)
|
||||||
|
);
|
||||||
|
return ResponseEntity.ok(this.sliceViewService.getSliceView(recipeInfoViews));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@ -123,7 +108,10 @@ public class RecipesController {
|
|||||||
recipeSearchBody.getData(),
|
recipeSearchBody.getData(),
|
||||||
RecipeAiSearchBody.class
|
RecipeAiSearchBody.class
|
||||||
);
|
);
|
||||||
final List<RecipeInfoView> results = this.recipeService.aiSearch(spec, user);
|
final List<RecipeInfoView> results = this.recipeService.aiSearch(spec, user)
|
||||||
|
.stream()
|
||||||
|
.map(recipe -> this.recipeToInfoViewConverter.convert(recipe, user))
|
||||||
|
.toList();
|
||||||
return ResponseEntity.ok(Map.of("results", results));
|
return ResponseEntity.ok(Map.of("results", results));
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Invalid recipeSearchBody type: " + recipeSearchBody.getType());
|
throw new IllegalArgumentException("Invalid recipeSearchBody type: " + recipeSearchBody.getType());
|
||||||
@ -135,7 +123,7 @@ public class RecipesController {
|
|||||||
@PathVariable String username,
|
@PathVariable String username,
|
||||||
@PathVariable String slug,
|
@PathVariable String slug,
|
||||||
@Nullable @AuthenticationPrincipal User principal
|
@Nullable @AuthenticationPrincipal User principal
|
||||||
) throws RecipeException {
|
) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
throw new AccessDeniedException("Must be logged in to star a recipe.");
|
throw new AccessDeniedException("Must be logged in to star a recipe.");
|
||||||
}
|
}
|
||||||
@ -147,7 +135,7 @@ public class RecipesController {
|
|||||||
@PathVariable String username,
|
@PathVariable String username,
|
||||||
@PathVariable String slug,
|
@PathVariable String slug,
|
||||||
@Nullable @AuthenticationPrincipal User principal
|
@Nullable @AuthenticationPrincipal User principal
|
||||||
) throws RecipeException {
|
) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
throw new AccessDeniedException("Must be logged in to get a recipe star.");
|
throw new AccessDeniedException("Must be logged in to get a recipe star.");
|
||||||
}
|
}
|
||||||
@ -164,7 +152,7 @@ public class RecipesController {
|
|||||||
@PathVariable String username,
|
@PathVariable String username,
|
||||||
@PathVariable String slug,
|
@PathVariable String slug,
|
||||||
@Nullable @AuthenticationPrincipal User principal
|
@Nullable @AuthenticationPrincipal User principal
|
||||||
) throws RecipeException {
|
) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
throw new AccessDeniedException("Must be logged in to delete a recipe star.");
|
throw new AccessDeniedException("Must be logged in to delete a recipe star.");
|
||||||
}
|
}
|
||||||
@ -178,7 +166,7 @@ public class RecipesController {
|
|||||||
@PathVariable String slug,
|
@PathVariable String slug,
|
||||||
Pageable pageable,
|
Pageable pageable,
|
||||||
@Nullable @AuthenticationPrincipal User principal
|
@Nullable @AuthenticationPrincipal User principal
|
||||||
) throws RecipeException {
|
) {
|
||||||
final Slice<RecipeCommentView> slice = this.recipeCommentService.getComments(
|
final Slice<RecipeCommentView> slice = this.recipeCommentService.getComments(
|
||||||
username,
|
username,
|
||||||
slug,
|
slug,
|
||||||
@ -194,7 +182,7 @@ public class RecipesController {
|
|||||||
@PathVariable String slug,
|
@PathVariable String slug,
|
||||||
@RequestBody RecipeCommentCreateBody body,
|
@RequestBody RecipeCommentCreateBody body,
|
||||||
@Nullable @AuthenticationPrincipal User principal
|
@Nullable @AuthenticationPrincipal User principal
|
||||||
) throws RecipeException {
|
) {
|
||||||
if (principal == null) {
|
if (principal == null) {
|
||||||
throw new AccessDeniedException("Must be logged in to comment on a recipe.");
|
throw new AccessDeniedException("Must be logged in to comment on a recipe.");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
package app.mealsmadeeasy.api.recipe.comment;
|
package app.mealsmadeeasy.api.recipe.comment;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeException;
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Slice;
|
import org.springframework.data.domain.Slice;
|
||||||
|
|
||||||
public interface RecipeCommentService {
|
public interface RecipeCommentService {
|
||||||
RecipeComment create(String recipeUsername, String recipeSlug, User owner, RecipeCommentCreateBody body)
|
RecipeComment create(String recipeUsername, String recipeSlug, User owner, RecipeCommentCreateBody body);
|
||||||
throws RecipeException;
|
RecipeComment get(Integer commentId, User viewer) ;
|
||||||
RecipeComment get(Integer commentId, User viewer) throws RecipeException;
|
Slice<RecipeCommentView> getComments(String recipeUsername, String recipeSlug, Pageable pageable, User viewer);
|
||||||
Slice<RecipeCommentView> getComments(String recipeUsername, String recipeSlug, Pageable pageable, User viewer)
|
RecipeComment update(Integer commentId, User viewer, RecipeCommentUpdateSpec spec) ;
|
||||||
throws RecipeException;
|
void delete(Integer commentId, User modifier) ;
|
||||||
RecipeComment update(Integer commentId, User viewer, RecipeCommentUpdateSpec spec) throws RecipeException;
|
|
||||||
void delete(Integer commentId, User modifier) throws RecipeException;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,10 @@ package app.mealsmadeeasy.api.recipe.comment;
|
|||||||
|
|
||||||
import app.mealsmadeeasy.api.markdown.MarkdownService;
|
import app.mealsmadeeasy.api.markdown.MarkdownService;
|
||||||
import app.mealsmadeeasy.api.recipe.Recipe;
|
import app.mealsmadeeasy.api.recipe.Recipe;
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeException;
|
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeRepository;
|
import app.mealsmadeeasy.api.recipe.RecipeRepository;
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithIdException;
|
||||||
|
import app.mealsmadeeasy.api.util.NoSuchEntityWithUsernameAndSlugException;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Slice;
|
import org.springframework.data.domain.Slice;
|
||||||
import org.springframework.security.access.prepost.PostAuthorize;
|
import org.springframework.security.access.prepost.PostAuthorize;
|
||||||
@ -39,42 +40,43 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
|
|||||||
String recipeSlug,
|
String recipeSlug,
|
||||||
User commenter,
|
User commenter,
|
||||||
RecipeCommentCreateBody body
|
RecipeCommentCreateBody body
|
||||||
) throws RecipeException {
|
) {
|
||||||
requireNonNull(commenter);
|
requireNonNull(commenter);
|
||||||
final RecipeComment draft = new RecipeComment();
|
final RecipeComment draft = new RecipeComment();
|
||||||
draft.setCreated(OffsetDateTime.now());
|
draft.setCreated(OffsetDateTime.now());
|
||||||
draft.setRawText(body.getText());
|
draft.setRawText(body.getText());
|
||||||
draft.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(body.getText()));
|
draft.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(body.getText()));
|
||||||
draft.setOwner((User) commenter);
|
draft.setOwner(commenter);
|
||||||
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(recipeUsername, recipeSlug)
|
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(recipeUsername, recipeSlug)
|
||||||
.orElseThrow(() -> new RecipeException(
|
.orElseThrow(
|
||||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
() -> new NoSuchEntityWithUsernameAndSlugException(Recipe.class, recipeUsername, recipeSlug)
|
||||||
"Invalid username or slug: " + recipeUsername + "/" + recipeSlug
|
);
|
||||||
));
|
|
||||||
draft.setRecipe(recipe);
|
draft.setRecipe(recipe);
|
||||||
return this.recipeCommentRepository.save(draft);
|
return this.recipeCommentRepository.save(draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject.recipe, #viewer)")
|
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject.recipe, #viewer)")
|
||||||
private RecipeComment loadCommentEntity(Integer commentId, User viewer) throws RecipeException {
|
private RecipeComment loadCommentEntity(Integer commentId, User viewer) {
|
||||||
return this.recipeCommentRepository.findById(commentId).orElseThrow(() -> new RecipeException(
|
return this.recipeCommentRepository.findById(commentId).orElseThrow(
|
||||||
RecipeException.Type.INVALID_COMMENT_ID, "No such RecipeComment for id: " + commentId
|
() -> new NoSuchEntityWithIdException(RecipeComment.class, commentId)
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecipeComment get(Integer commentId, User viewer) throws RecipeException {
|
public RecipeComment get(Integer commentId, User viewer) {
|
||||||
return this.loadCommentEntity(commentId, viewer);
|
return this.loadCommentEntity(commentId, viewer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@PreAuthorize("@recipeSecurity.isViewableBy(#recipeUsername, #recipeSlug, #viewer)")
|
@PreAuthorize("@recipeSecurity.isViewableBy(#recipeUsername, #recipeSlug, #viewer)")
|
||||||
public Slice<RecipeCommentView> getComments(String recipeUsername, String recipeSlug, Pageable pageable, User viewer) throws RecipeException {
|
public Slice<RecipeCommentView> getComments(
|
||||||
|
String recipeUsername,
|
||||||
|
String recipeSlug,
|
||||||
|
Pageable pageable,
|
||||||
|
User viewer
|
||||||
|
) {
|
||||||
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(recipeUsername, recipeSlug).orElseThrow(
|
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(recipeUsername, recipeSlug).orElseThrow(
|
||||||
() -> new RecipeException(
|
() -> new NoSuchEntityWithUsernameAndSlugException(Recipe.class, recipeUsername, recipeSlug)
|
||||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
|
||||||
"No such Recipe for username/slug: " + recipeUsername + "/" + recipeSlug
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
final Slice<RecipeComment> commentEntities = this.recipeCommentRepository.findAllByRecipe(recipe, pageable);
|
final Slice<RecipeComment> commentEntities = this.recipeCommentRepository.findAllByRecipe(recipe, pageable);
|
||||||
return commentEntities.map(commentEntity -> RecipeCommentView.from(
|
return commentEntities.map(commentEntity -> RecipeCommentView.from(
|
||||||
@ -84,21 +86,21 @@ public class RecipeCommentServiceImpl implements RecipeCommentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecipeComment update(Integer commentId, User viewer, RecipeCommentUpdateSpec spec) throws RecipeException {
|
public RecipeComment update(Integer commentId, User viewer, RecipeCommentUpdateSpec spec) {
|
||||||
final RecipeComment entity = this.loadCommentEntity(commentId, viewer);
|
final RecipeComment entity = this.loadCommentEntity(commentId, viewer);
|
||||||
entity.setRawText(spec.getRawText());
|
entity.setRawText(spec.getRawText());
|
||||||
return this.recipeCommentRepository.save(entity);
|
return this.recipeCommentRepository.save(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostAuthorize("@recipeSecurity.isOwner(returnObject.recipe, #modifier)")
|
@PostAuthorize("@recipeSecurity.isOwner(returnObject.recipe, #modifier)")
|
||||||
private RecipeComment loadForDelete(Integer commentId, User modifier) throws RecipeException {
|
private RecipeComment loadForDelete(Integer commentId, User modifier) {
|
||||||
return this.recipeCommentRepository.findById(commentId).orElseThrow(() -> new RecipeException(
|
return this.recipeCommentRepository.findById(commentId).orElseThrow(() ->
|
||||||
RecipeException.Type.INVALID_COMMENT_ID, "No such RecipeComment for id: " + commentId
|
new NoSuchEntityWithIdException(RecipeComment.class, commentId)
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(Integer commentId, User modifier) throws RecipeException {
|
public void delete(Integer commentId, User modifier) {
|
||||||
final RecipeComment entityToDelete = this.loadForDelete(commentId, modifier);
|
final RecipeComment entityToDelete = this.loadForDelete(commentId, modifier);
|
||||||
this.recipeCommentRepository.delete(entityToDelete);
|
this.recipeCommentRepository.delete(entityToDelete);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,39 @@
|
|||||||
|
package app.mealsmadeeasy.api.recipe.converter;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.image.converter.ImageToViewConverter;
|
||||||
|
import app.mealsmadeeasy.api.image.view.ImageView;
|
||||||
|
import app.mealsmadeeasy.api.recipe.RecipeDraft;
|
||||||
|
import app.mealsmadeeasy.api.recipe.view.RecipeDraftView;
|
||||||
|
import app.mealsmadeeasy.api.user.User;
|
||||||
|
import app.mealsmadeeasy.api.user.view.UserInfoView;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RecipeDraftToViewConverter {
|
||||||
|
|
||||||
|
private final ImageToViewConverter imageToViewConverter;
|
||||||
|
|
||||||
|
public RecipeDraftView convert(RecipeDraft recipeDraft, User viewer) {
|
||||||
|
final @Nullable ImageView mainImageView = recipeDraft.getMainImage() != null
|
||||||
|
? this.imageToViewConverter.convert(recipeDraft.getMainImage(), viewer, false)
|
||||||
|
: null;
|
||||||
|
return RecipeDraftView.builder()
|
||||||
|
.id(recipeDraft.getId())
|
||||||
|
.created(recipeDraft.getCreated())
|
||||||
|
.modified(recipeDraft.getModified())
|
||||||
|
.state(recipeDraft.getState())
|
||||||
|
.slug(recipeDraft.getSlug())
|
||||||
|
.preparationTime(recipeDraft.getPreparationTime())
|
||||||
|
.cookingTime(recipeDraft.getCookingTime())
|
||||||
|
.totalTime(recipeDraft.getTotalTime())
|
||||||
|
.rawText(recipeDraft.getRawText())
|
||||||
|
.ingredients(recipeDraft.getIngredients())
|
||||||
|
.owner(UserInfoView.from(recipeDraft.getOwner()))
|
||||||
|
.mainImage(mainImageView)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,7 +1,6 @@
|
|||||||
package app.mealsmadeeasy.api.recipe.converter;
|
package app.mealsmadeeasy.api.recipe.converter;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.image.Image;
|
import app.mealsmadeeasy.api.image.Image;
|
||||||
import app.mealsmadeeasy.api.image.ImageException;
|
|
||||||
import app.mealsmadeeasy.api.image.ImageService;
|
import app.mealsmadeeasy.api.image.ImageService;
|
||||||
import app.mealsmadeeasy.api.recipe.body.RecipeDraftUpdateBody;
|
import app.mealsmadeeasy.api.recipe.body.RecipeDraftUpdateBody;
|
||||||
import app.mealsmadeeasy.api.recipe.spec.RecipeDraftUpdateSpec;
|
import app.mealsmadeeasy.api.recipe.spec.RecipeDraftUpdateSpec;
|
||||||
@ -15,7 +14,7 @@ public class RecipeDraftUpdateBodyToSpecConverter {
|
|||||||
|
|
||||||
private final ImageService imageService;
|
private final ImageService imageService;
|
||||||
|
|
||||||
public RecipeDraftUpdateSpec convert(RecipeDraftUpdateBody body, User viewer) throws ImageException {
|
public RecipeDraftUpdateSpec convert(RecipeDraftUpdateBody body, User viewer) {
|
||||||
final var b = RecipeDraftUpdateSpec.builder()
|
final var b = RecipeDraftUpdateSpec.builder()
|
||||||
.slug(body.slug())
|
.slug(body.slug())
|
||||||
.title(body.title())
|
.title(body.title())
|
||||||
|
|||||||
@ -0,0 +1,44 @@
|
|||||||
|
package app.mealsmadeeasy.api.recipe.converter;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.image.converter.ImageToViewConverter;
|
||||||
|
import app.mealsmadeeasy.api.recipe.Recipe;
|
||||||
|
import app.mealsmadeeasy.api.recipe.RecipeService;
|
||||||
|
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
||||||
|
import app.mealsmadeeasy.api.user.User;
|
||||||
|
import app.mealsmadeeasy.api.user.view.UserInfoView;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RecipeToFullViewConverter {
|
||||||
|
|
||||||
|
private final RecipeService recipeService;
|
||||||
|
private final ImageToViewConverter imageToViewConverter;
|
||||||
|
|
||||||
|
public FullRecipeView convert(Recipe recipe, boolean includeRawText, @Nullable User viewer) {
|
||||||
|
final var b = FullRecipeView.builder()
|
||||||
|
.id(recipe.getId())
|
||||||
|
.created(recipe.getCreated())
|
||||||
|
.modified(recipe.getModified())
|
||||||
|
.slug(recipe.getSlug())
|
||||||
|
.title(recipe.getTitle())
|
||||||
|
.preparationTime(recipe.getPreparationTime())
|
||||||
|
.cookingTime(recipe.getCookingTime())
|
||||||
|
.totalTime(recipe.getTotalTime())
|
||||||
|
.text(this.recipeService.getRenderedMarkdown(recipe))
|
||||||
|
.owner(UserInfoView.from(recipe.getOwner()))
|
||||||
|
.starCount(this.recipeService.getStarCount(recipe))
|
||||||
|
.viewerCount(this.recipeService.getViewerCount(recipe))
|
||||||
|
.isPublic(recipe.getIsPublic());
|
||||||
|
if (recipe.getMainImage() != null) {
|
||||||
|
b.mainImage(this.imageToViewConverter.convert(recipe.getMainImage(), viewer, false));
|
||||||
|
}
|
||||||
|
if (includeRawText) {
|
||||||
|
b.rawText(recipe.getRawText());
|
||||||
|
}
|
||||||
|
return b.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package app.mealsmadeeasy.api.recipe.converter;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.image.converter.ImageToViewConverter;
|
||||||
|
import app.mealsmadeeasy.api.recipe.Recipe;
|
||||||
|
import app.mealsmadeeasy.api.recipe.RecipeService;
|
||||||
|
import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
|
||||||
|
import app.mealsmadeeasy.api.user.User;
|
||||||
|
import app.mealsmadeeasy.api.user.view.UserInfoView;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RecipeToInfoViewConverter {
|
||||||
|
|
||||||
|
private final ImageToViewConverter imageToViewConverter;
|
||||||
|
private final RecipeService recipeService;
|
||||||
|
|
||||||
|
public RecipeInfoView convert(@NotNull Recipe recipe, @Nullable User viewer) {
|
||||||
|
return RecipeInfoView.builder()
|
||||||
|
.id(recipe.getId())
|
||||||
|
.created(recipe.getCreated())
|
||||||
|
.modified(recipe.getModified())
|
||||||
|
.slug(recipe.getSlug())
|
||||||
|
.title(recipe.getTitle())
|
||||||
|
.preparationTime(recipe.getPreparationTime())
|
||||||
|
.cookingTime(recipe.getCookingTime())
|
||||||
|
.totalTime(recipe.getTotalTime())
|
||||||
|
.owner(UserInfoView.from(recipe.getOwner()))
|
||||||
|
.isPublic(recipe.getIsPublic())
|
||||||
|
.starCount(this.recipeService.getStarCount(recipe))
|
||||||
|
.mainImage(this.imageToViewConverter.convert(recipe.getMainImage(), viewer, false))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,16 +1,15 @@
|
|||||||
package app.mealsmadeeasy.api.recipe.star;
|
package app.mealsmadeeasy.api.recipe.star;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeException;
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface RecipeStarService {
|
public interface RecipeStarService {
|
||||||
RecipeStar create(Integer recipeId, Integer ownerId);
|
RecipeStar create(Integer recipeId, Integer ownerId);
|
||||||
RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException;
|
RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer);
|
||||||
|
|
||||||
Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException;
|
Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer);
|
||||||
|
|
||||||
void delete(Integer recipeId, Integer ownerId);
|
void delete(Integer recipeId, Integer ownerId);
|
||||||
void delete(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException;
|
void delete(String recipeOwnerUsername, String recipeSlug, User starer);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package app.mealsmadeeasy.api.recipe.star;
|
package app.mealsmadeeasy.api.recipe.star;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.recipe.Recipe;
|
import app.mealsmadeeasy.api.recipe.Recipe;
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeException;
|
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeService;
|
import app.mealsmadeeasy.api.recipe.RecipeService;
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@ -32,7 +31,7 @@ public class RecipeStarServiceImpl implements RecipeStarService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
|
public RecipeStar create(String recipeOwnerUsername, String recipeSlug, User starer) {
|
||||||
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
|
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
|
||||||
final Optional<RecipeStar> existing = this.recipeStarRepository.findByRecipeIdAndOwnerId(
|
final Optional<RecipeStar> existing = this.recipeStarRepository.findByRecipeIdAndOwnerId(
|
||||||
recipe.getId(),
|
recipe.getId(),
|
||||||
@ -45,7 +44,7 @@ public class RecipeStarServiceImpl implements RecipeStarService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
|
public Optional<RecipeStar> find(String recipeOwnerUsername, String recipeSlug, User starer) {
|
||||||
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
|
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
|
||||||
return this.recipeStarRepository.findByRecipeIdAndOwnerId(recipe.getId(), starer.getId());
|
return this.recipeStarRepository.findByRecipeIdAndOwnerId(recipe.getId(), starer.getId());
|
||||||
}
|
}
|
||||||
@ -56,7 +55,7 @@ public class RecipeStarServiceImpl implements RecipeStarService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void delete(String recipeOwnerUsername, String recipeSlug, User starer) throws RecipeException {
|
public void delete(String recipeOwnerUsername, String recipeSlug, User starer) {
|
||||||
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
|
final Recipe recipe = this.recipeService.getByUsernameAndSlug(recipeOwnerUsername, recipeSlug, starer);
|
||||||
this.delete(recipe.getId(), starer.getId());
|
this.delete(recipe.getId(), starer.getId());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,26 +25,4 @@ public record RecipeDraftView(
|
|||||||
@Nullable List<RecipeDraft.IngredientDraft> ingredients,
|
@Nullable List<RecipeDraft.IngredientDraft> ingredients,
|
||||||
UserInfoView owner,
|
UserInfoView owner,
|
||||||
@Nullable ImageView mainImage
|
@Nullable ImageView mainImage
|
||||||
) {
|
) {}
|
||||||
|
|
||||||
public static RecipeDraftView from(
|
|
||||||
RecipeDraft recipeDraft,
|
|
||||||
@Nullable ImageView mainImageView
|
|
||||||
) {
|
|
||||||
return RecipeDraftView.builder()
|
|
||||||
.id(recipeDraft.getId())
|
|
||||||
.created(recipeDraft.getCreated())
|
|
||||||
.modified(recipeDraft.getModified())
|
|
||||||
.state(recipeDraft.getState())
|
|
||||||
.slug(recipeDraft.getSlug())
|
|
||||||
.preparationTime(recipeDraft.getPreparationTime())
|
|
||||||
.cookingTime(recipeDraft.getCookingTime())
|
|
||||||
.totalTime(recipeDraft.getTotalTime())
|
|
||||||
.rawText(recipeDraft.getRawText())
|
|
||||||
.ingredients(recipeDraft.getIngredients())
|
|
||||||
.owner(UserInfoView.from(recipeDraft.getOwner()))
|
|
||||||
.mainImage(mainImageView)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package app.mealsmadeeasy.api.recipe.view;
|
package app.mealsmadeeasy.api.recipe.view;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.image.view.ImageView;
|
import app.mealsmadeeasy.api.image.view.ImageView;
|
||||||
import app.mealsmadeeasy.api.recipe.Recipe;
|
|
||||||
import app.mealsmadeeasy.api.user.view.UserInfoView;
|
import app.mealsmadeeasy.api.user.view.UserInfoView;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
@ -12,24 +11,6 @@ import java.time.OffsetDateTime;
|
|||||||
@Value
|
@Value
|
||||||
@Builder
|
@Builder
|
||||||
public class RecipeInfoView {
|
public class RecipeInfoView {
|
||||||
|
|
||||||
public static RecipeInfoView from(Recipe recipe, int starCount, @Nullable ImageView mainImage) {
|
|
||||||
return RecipeInfoView.builder()
|
|
||||||
.id(recipe.getId())
|
|
||||||
.created(recipe.getCreated())
|
|
||||||
.modified(recipe.getModified())
|
|
||||||
.slug(recipe.getSlug())
|
|
||||||
.title(recipe.getTitle())
|
|
||||||
.preparationTime(recipe.getPreparationTime())
|
|
||||||
.cookingTime(recipe.getCookingTime())
|
|
||||||
.totalTime(recipe.getTotalTime())
|
|
||||||
.owner(UserInfoView.from(recipe.getOwner()))
|
|
||||||
.isPublic(recipe.getIsPublic())
|
|
||||||
.starCount(starCount)
|
|
||||||
.mainImage(mainImage)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
Integer id;
|
Integer id;
|
||||||
OffsetDateTime created;
|
OffsetDateTime created;
|
||||||
OffsetDateTime modified;
|
OffsetDateTime modified;
|
||||||
@ -42,5 +23,4 @@ public class RecipeInfoView {
|
|||||||
boolean isPublic;
|
boolean isPublic;
|
||||||
int starCount;
|
int starCount;
|
||||||
@Nullable ImageView mainImage;
|
@Nullable ImageView mainImage;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ public class ExceptionHandlers {
|
|||||||
.body(new NoSuchEntityWithIdExceptionView<>(
|
.body(new NoSuchEntityWithIdExceptionView<>(
|
||||||
e.getEntityType().getSimpleName(),
|
e.getEntityType().getSimpleName(),
|
||||||
e.getId(),
|
e.getId(),
|
||||||
"Could not find " + e.getEntityType().getSimpleName() + " with id " + e.getId()
|
String.format("No such entity %s with id %s", e.getEntityType().getSimpleName(), e.getId())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,4 +32,28 @@ public class ExceptionHandlers {
|
|||||||
.body(new MustBeLoggedInExceptionView(e.getMessage()));
|
.body(new MustBeLoggedInExceptionView(e.getMessage()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record NoSuchEntityWithUsernameAndSlugExceptionView(
|
||||||
|
String entityName,
|
||||||
|
String username,
|
||||||
|
String slug,
|
||||||
|
String message
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@ExceptionHandler(NoSuchEntityWithUsernameAndSlugException.class)
|
||||||
|
public ResponseEntity<NoSuchEntityWithUsernameAndSlugExceptionView> handleNoSuchEntityWithUsernameAndSlugException(
|
||||||
|
NoSuchEntityWithUsernameAndSlugException e
|
||||||
|
) {
|
||||||
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new NoSuchEntityWithUsernameAndSlugExceptionView(
|
||||||
|
e.getEntityType().getSimpleName(),
|
||||||
|
e.getUsername(),
|
||||||
|
e.getSlug(),
|
||||||
|
String.format(
|
||||||
|
"No such entity %s for username %s and slug %s",
|
||||||
|
e.getEntityType().getSimpleName(),
|
||||||
|
e.getUsername(),
|
||||||
|
e.getSlug()
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
package app.mealsmadeeasy.api.util;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NoSuchEntityWithUsernameAndFilenameException extends RuntimeException {
|
||||||
|
private final Class<?> entityType;
|
||||||
|
private final String username;
|
||||||
|
private final String filename;
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package app.mealsmadeeasy.api.util;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class NoSuchEntityWithUsernameAndSlugException extends RuntimeException {
|
||||||
|
private final Class<?> entityType;
|
||||||
|
private final String username;
|
||||||
|
private final String slug;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user