More pushing down interface methods for recipes.
This commit is contained in:
parent
fc19361ab6
commit
012bf743a1
@ -1,7 +1,7 @@
|
|||||||
package app.mealsmadeeasy.api;
|
package app.mealsmadeeasy.api;
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.recipe.Recipe;
|
import app.mealsmadeeasy.api.recipe.Recipe;
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeEmbeddingEntity;
|
import app.mealsmadeeasy.api.recipe.RecipeEmbedding;
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeRepository;
|
import app.mealsmadeeasy.api.recipe.RecipeRepository;
|
||||||
import app.mealsmadeeasy.api.recipe.RecipeService;
|
import app.mealsmadeeasy.api.recipe.RecipeService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@ -44,7 +44,7 @@ public class BackfillRecipeEmbeddings implements ApplicationRunner {
|
|||||||
final String toEmbed = "<h1>" + recipe.getTitle() + "</h1>" + renderedMarkdown;
|
final String toEmbed = "<h1>" + recipe.getTitle() + "</h1>" + renderedMarkdown;
|
||||||
final float[] embedding = this.embeddingModel.embed(toEmbed);
|
final float[] embedding = this.embeddingModel.embed(toEmbed);
|
||||||
|
|
||||||
final RecipeEmbeddingEntity recipeEmbedding = new RecipeEmbeddingEntity();
|
final RecipeEmbedding recipeEmbedding = new RecipeEmbedding();
|
||||||
recipeEmbedding.setRecipe(recipe);
|
recipeEmbedding.setRecipe(recipe);
|
||||||
recipeEmbedding.setEmbedding(embedding);
|
recipeEmbedding.setEmbedding(embedding);
|
||||||
recipeEmbedding.setTimestamp(OffsetDateTime.now());
|
recipeEmbedding.setTimestamp(OffsetDateTime.now());
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import java.util.Set;
|
|||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Data
|
@Data
|
||||||
public final class Recipe {
|
public class Recipe {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
@ -79,6 +79,6 @@ public final class Recipe {
|
|||||||
private Image mainImage;
|
private Image mainImage;
|
||||||
|
|
||||||
@OneToOne(mappedBy = "recipe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToOne(mappedBy = "recipe", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private RecipeEmbeddingEntity embedding;
|
private RecipeEmbedding embedding;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
package app.mealsmadeeasy.api.recipe;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.hibernate.annotations.Array;
|
||||||
|
import org.hibernate.annotations.JdbcTypeCode;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "recipe_embedding")
|
||||||
|
@Data
|
||||||
|
public class RecipeEmbedding {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
@OneToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@MapsId
|
||||||
|
@JoinColumn(name = "recipe_id")
|
||||||
|
private Recipe recipe;
|
||||||
|
|
||||||
|
@JdbcTypeCode(SqlTypes.VECTOR)
|
||||||
|
@Array(length = 1024)
|
||||||
|
@Nullable
|
||||||
|
private float[] embedding;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private OffsetDateTime timestamp;
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,63 +0,0 @@
|
|||||||
package app.mealsmadeeasy.api.recipe;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import org.hibernate.annotations.Array;
|
|
||||||
import org.hibernate.annotations.JdbcTypeCode;
|
|
||||||
import org.hibernate.type.SqlTypes;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
|
|
||||||
@Entity
|
|
||||||
@Table(name = "recipe_embedding")
|
|
||||||
public class RecipeEmbeddingEntity {
|
|
||||||
|
|
||||||
@Id
|
|
||||||
private Integer id;
|
|
||||||
|
|
||||||
@OneToOne(fetch = FetchType.LAZY, optional = false)
|
|
||||||
@MapsId
|
|
||||||
@JoinColumn(name = "recipe_id")
|
|
||||||
private Recipe recipe;
|
|
||||||
|
|
||||||
@JdbcTypeCode(SqlTypes.VECTOR)
|
|
||||||
@Array(length = 1024)
|
|
||||||
@Nullable
|
|
||||||
private float[] embedding;
|
|
||||||
|
|
||||||
@Column(nullable = false)
|
|
||||||
private OffsetDateTime timestamp;
|
|
||||||
|
|
||||||
public Integer getId() {
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setId(Integer id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Recipe getRecipe() {
|
|
||||||
return this.recipe;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRecipe(Recipe recipe) {
|
|
||||||
this.recipe = recipe;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float[] getEmbedding() {
|
|
||||||
return this.embedding;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEmbedding(float[] embedding) {
|
|
||||||
this.embedding = embedding;
|
|
||||||
}
|
|
||||||
|
|
||||||
public OffsetDateTime getTimestamp() {
|
|
||||||
return this.timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimestamp(OffsetDateTime timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -2,12 +2,82 @@ package app.mealsmadeeasy.api.recipe;
|
|||||||
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class RecipeSecurity {
|
||||||
|
|
||||||
|
private final RecipeRepository recipeRepository;
|
||||||
|
|
||||||
|
public RecipeSecurity(RecipeRepository recipeRepository) {
|
||||||
|
this.recipeRepository = recipeRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOwner(Recipe recipe, User user) {
|
||||||
|
return recipe.getOwner() != null && recipe.getOwner().getId().equals(user.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOwner(Integer recipeId, User user) throws RecipeException {
|
||||||
|
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
|
||||||
|
RecipeException.Type.INVALID_ID,
|
||||||
|
"No such Recipe with id " + recipeId
|
||||||
|
));
|
||||||
|
return this.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException {
|
||||||
|
if (recipe.getIsPublic()) {
|
||||||
|
// public recipe
|
||||||
|
return true;
|
||||||
|
} else if (user == null) {
|
||||||
|
// a non-public recipe with no principal
|
||||||
|
return false;
|
||||||
|
} else if (Objects.equals(recipe.getOwner().getId(), user.getId())) {
|
||||||
|
// is owner
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// check if viewer
|
||||||
|
final Recipe withViewers = this.recipeRepository.findByIdWithViewers(recipe.getId())
|
||||||
|
.orElseThrow(() -> new RecipeException(
|
||||||
|
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + recipe.getId()
|
||||||
|
));
|
||||||
|
for (final User viewer : withViewers.getViewers()) {
|
||||||
|
if (viewer.getId() != null && viewer.getId().equals(user.getId())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// non-public recipe and not viewer
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) throws RecipeException {
|
||||||
|
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(ownerUsername, slug)
|
||||||
|
.orElseThrow(() -> new RecipeException(
|
||||||
|
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
||||||
|
"No such Recipe for username " + ownerUsername + " and slug: " + slug
|
||||||
|
));
|
||||||
|
return this.isViewableBy(recipe, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isViewableBy(Integer recipeId, @Nullable User user) throws RecipeException {
|
||||||
|
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
|
||||||
|
RecipeException.Type.INVALID_ID,
|
||||||
|
"No such Recipe with id: " + recipeId
|
||||||
|
));
|
||||||
|
return this.isViewableBy(recipe, user);
|
||||||
|
}
|
||||||
|
|
||||||
public interface RecipeSecurity {
|
|
||||||
boolean isOwner(Recipe recipe, User user);
|
|
||||||
boolean isOwner(Integer recipeId, User user) throws RecipeException;
|
|
||||||
boolean isOwner(String username, String slug, @Nullable User user) throws RecipeException;
|
|
||||||
boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException;
|
|
||||||
boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) throws RecipeException;
|
|
||||||
boolean isViewableBy(Integer recipeId, @Nullable User user) throws RecipeException;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
package app.mealsmadeeasy.api.recipe;
|
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.user.User;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@Component("recipeSecurity")
|
|
||||||
public class RecipeSecurityImpl implements RecipeSecurity {
|
|
||||||
|
|
||||||
private final RecipeRepository recipeRepository;
|
|
||||||
|
|
||||||
public RecipeSecurityImpl(RecipeRepository recipeRepository) {
|
|
||||||
this.recipeRepository = recipeRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOwner(Recipe recipe, User user) {
|
|
||||||
return recipe.getOwner() != null && recipe.getOwner().getId().equals(user.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isOwner(Integer recipeId, User user) throws RecipeException {
|
|
||||||
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_ID,
|
|
||||||
"No such Recipe with id " + recipeId
|
|
||||||
));
|
|
||||||
return this.isOwner(recipe, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isViewableBy(Recipe recipe, @Nullable User user) throws RecipeException {
|
|
||||||
if (recipe.getIsPublic()) {
|
|
||||||
// public recipe
|
|
||||||
return true;
|
|
||||||
} else if (user == null) {
|
|
||||||
// a non-public recipe with no principal
|
|
||||||
return false;
|
|
||||||
} else if (Objects.equals(recipe.getOwner().getId(), user.getId())) {
|
|
||||||
// is owner
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// check if viewer
|
|
||||||
final Recipe withViewers = this.recipeRepository.findByIdWithViewers(recipe.getId())
|
|
||||||
.orElseThrow(() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + recipe.getId()
|
|
||||||
));
|
|
||||||
for (final User viewer : withViewers.getViewers()) {
|
|
||||||
if (viewer.getId() != null && viewer.getId().equals(user.getId())) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// non-public recipe and not viewer
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isViewableBy(String ownerUsername, String slug, @Nullable User user) throws RecipeException {
|
|
||||||
final Recipe recipe = this.recipeRepository.findByOwnerUsernameAndSlug(ownerUsername, slug)
|
|
||||||
.orElseThrow(() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
|
||||||
"No such Recipe for username " + ownerUsername + " and slug: " + slug
|
|
||||||
));
|
|
||||||
return this.isViewableBy(recipe, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isViewableBy(Integer recipeId, @Nullable User user) throws RecipeException {
|
|
||||||
final Recipe recipe = this.recipeRepository.findById(recipeId).orElseThrow(() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_ID,
|
|
||||||
"No such Recipe with id: " + recipeId
|
|
||||||
));
|
|
||||||
return this.isViewableBy(recipe, user);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,63 +1,317 @@
|
|||||||
package app.mealsmadeeasy.api.recipe;
|
package app.mealsmadeeasy.api.recipe;
|
||||||
|
|
||||||
|
import app.mealsmadeeasy.api.image.Image;
|
||||||
import app.mealsmadeeasy.api.image.ImageException;
|
import app.mealsmadeeasy.api.image.ImageException;
|
||||||
|
import app.mealsmadeeasy.api.image.ImageService;
|
||||||
|
import app.mealsmadeeasy.api.image.view.ImageView;
|
||||||
|
import app.mealsmadeeasy.api.markdown.MarkdownService;
|
||||||
import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody;
|
import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody;
|
||||||
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.RecipeStarRepository;
|
||||||
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
import app.mealsmadeeasy.api.recipe.view.FullRecipeView;
|
||||||
import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
|
import app.mealsmadeeasy.api.recipe.view.RecipeInfoView;
|
||||||
import app.mealsmadeeasy.api.user.User;
|
import app.mealsmadeeasy.api.user.User;
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
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.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.PreAuthorize;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public interface RecipeService {
|
@Service
|
||||||
|
public class RecipeService {
|
||||||
|
|
||||||
Recipe create(@Nullable User owner, RecipeCreateSpec spec);
|
private final RecipeRepository recipeRepository;
|
||||||
|
private final RecipeStarRepository recipeStarRepository;
|
||||||
|
private final ImageService imageService;
|
||||||
|
private final MarkdownService markdownService;
|
||||||
|
private final EmbeddingModel embeddingModel;
|
||||||
|
|
||||||
Recipe getById(Integer id, @Nullable User viewer) throws RecipeException;
|
public RecipeService(
|
||||||
Recipe getByIdWithStars(Integer id, @Nullable User viewer) throws RecipeException;
|
RecipeRepository recipeRepository,
|
||||||
Recipe getByUsernameAndSlug(String username, String slug, @Nullable User viewer) throws RecipeException;
|
RecipeStarRepository recipeStarRepository,
|
||||||
|
ImageService imageService,
|
||||||
|
MarkdownService markdownService,
|
||||||
|
EmbeddingModel embeddingModel
|
||||||
|
) {
|
||||||
|
this.recipeRepository = recipeRepository;
|
||||||
|
this.recipeStarRepository = recipeStarRepository;
|
||||||
|
this.imageService = imageService;
|
||||||
|
this.markdownService = markdownService;
|
||||||
|
this.embeddingModel = embeddingModel;
|
||||||
|
}
|
||||||
|
|
||||||
FullRecipeView getFullViewById(Integer id, @Nullable User viewer) throws RecipeException;
|
public Recipe create(@Nullable User owner, RecipeCreateSpec spec) {
|
||||||
FullRecipeView getFullViewByUsernameAndSlug(
|
if (owner == null) {
|
||||||
|
throw new AccessDeniedException("Must be logged in.");
|
||||||
|
}
|
||||||
|
final Recipe draft = new Recipe();
|
||||||
|
draft.setCreated(OffsetDateTime.now());
|
||||||
|
draft.setOwner(owner);
|
||||||
|
draft.setSlug(spec.getSlug());
|
||||||
|
draft.setTitle(spec.getTitle());
|
||||||
|
draft.setRawText(spec.getRawText());
|
||||||
|
draft.setMainImage(spec.getMainImage());
|
||||||
|
draft.setIsPublic(spec.isPublic());
|
||||||
|
return this.recipeRepository.save(draft);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recipe findRecipeEntity(Integer id) throws RecipeException {
|
||||||
|
return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
|
||||||
|
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
||||||
|
public Recipe getById(Integer id, @Nullable User viewer) throws RecipeException {
|
||||||
|
return this.findRecipeEntity(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
||||||
|
public Recipe getByIdWithStars(Integer id, @Nullable User viewer) throws RecipeException {
|
||||||
|
return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException(
|
||||||
|
RecipeException.Type.INVALID_ID,
|
||||||
|
"No such Recipe with id: " + id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
||||||
|
public Recipe getByUsernameAndSlug(String username, String slug, @Nullable User viewer) throws RecipeException {
|
||||||
|
return this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(() -> new RecipeException(
|
||||||
|
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
||||||
|
"No such Recipe for username " + username + " and slug " + slug
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiStatus.Internal
|
||||||
|
public String getRenderedMarkdown(Recipe entity) {
|
||||||
|
if (entity.getCachedRenderedText() == null) {
|
||||||
|
entity.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(entity.getRawText()));
|
||||||
|
entity = this.recipeRepository.save(entity);
|
||||||
|
}
|
||||||
|
return entity.getCachedRenderedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getStarCount(Recipe recipe) {
|
||||||
|
return this.recipeRepository.getStarCount(recipe.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getViewerCount(long recipeId) {
|
||||||
|
return this.recipeRepository.getViewerCount(recipeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract("null, _ -> null")
|
||||||
|
private @Nullable ImageView getImageView(@Nullable Image image, @Nullable User viewer) {
|
||||||
|
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 username,
|
||||||
String slug,
|
String slug,
|
||||||
boolean includeRawText,
|
boolean includeRawText,
|
||||||
@Nullable User viewer
|
@Nullable User viewer
|
||||||
) throws RecipeException;
|
) 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);
|
||||||
|
}
|
||||||
|
|
||||||
Slice<RecipeInfoView> getInfoViewsViewableBy(Pageable pageable, @Nullable User viewer);
|
public Slice<RecipeInfoView> getInfoViewsViewableBy(Pageable pageable, @Nullable User viewer) {
|
||||||
List<Recipe> getByMinimumStars(long minimumStars, @Nullable User viewer);
|
return this.recipeRepository.findAllViewableBy(viewer, pageable).map(recipe ->
|
||||||
List<Recipe> getPublicRecipes();
|
this.getInfoView(recipe, viewer)
|
||||||
List<Recipe> getRecipesViewableBy(User viewer);
|
);
|
||||||
List<Recipe> getRecipesOwnedBy(User owner);
|
}
|
||||||
|
|
||||||
List<RecipeInfoView> aiSearch(RecipeAiSearchBody searchSpec, @Nullable User viewer);
|
public List<Recipe> getByMinimumStars(long minimumStars, @Nullable User viewer) {
|
||||||
|
return List.copyOf(
|
||||||
|
this.recipeRepository.findAllViewableByStarsGreaterThanEqual(minimumStars, viewer)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier)
|
public List<Recipe> getPublicRecipes() {
|
||||||
throws RecipeException, ImageException;
|
return List.copyOf(this.recipeRepository.findAllByIsPublicIsTrue());
|
||||||
|
}
|
||||||
|
|
||||||
Recipe addViewer(Integer id, User modifier, User viewer) throws RecipeException;
|
public List<Recipe> getRecipesViewableBy(User viewer) {
|
||||||
Recipe removeViewer(Integer id, User modifier, User viewer) throws RecipeException;
|
return List.copyOf(this.recipeRepository.findAllByViewersContaining(viewer));
|
||||||
Recipe clearAllViewers(Integer id, User modifier) throws RecipeException;
|
}
|
||||||
|
|
||||||
void deleteRecipe(Integer id, User modifier);
|
public List<Recipe> getRecipesOwnedBy(User owner) {
|
||||||
|
return List.copyOf(this.recipeRepository.findAllByOwner(owner));
|
||||||
|
}
|
||||||
|
|
||||||
FullRecipeView toFullRecipeView(Recipe recipe, boolean includeRawText, @Nullable User viewer);
|
public List<RecipeInfoView> aiSearch(RecipeAiSearchBody searchSpec, @Nullable User viewer) {
|
||||||
RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer);
|
final float[] queryEmbedding = this.embeddingModel.embed(searchSpec.getPrompt());
|
||||||
|
final List<Recipe> results;
|
||||||
|
if (viewer == null) {
|
||||||
|
results = this.recipeRepository.searchByEmbeddingAndIsPublic(queryEmbedding, 0.5f);
|
||||||
|
} else {
|
||||||
|
results = this.recipeRepository.searchByEmbeddingAndViewableBy(queryEmbedding, 0.5f, viewer.getId());
|
||||||
|
}
|
||||||
|
return results.stream()
|
||||||
|
.map(recipeEntity -> this.getInfoView(recipeEntity, viewer))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareForUpdate(RecipeUpdateSpec spec, Recipe recipe, User modifier) throws ImageException {
|
||||||
|
boolean didUpdate = false;
|
||||||
|
if (spec.getTitle() != null) {
|
||||||
|
recipe.setTitle(spec.getTitle());
|
||||||
|
didUpdate = true;
|
||||||
|
}
|
||||||
|
if (spec.getPreparationTime() != null) {
|
||||||
|
recipe.setPreparationTime(spec.getPreparationTime());
|
||||||
|
didUpdate = true;
|
||||||
|
}
|
||||||
|
if (spec.getCookingTime() != null) {
|
||||||
|
recipe.setCookingTime(spec.getCookingTime());
|
||||||
|
didUpdate = true;
|
||||||
|
}
|
||||||
|
if (spec.getTotalTime() != null) {
|
||||||
|
recipe.setTotalTime(spec.getTotalTime());
|
||||||
|
didUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spec.getRawText() != null) {
|
||||||
|
recipe.setRawText(spec.getRawText());
|
||||||
|
recipe.setCachedRenderedText(null);
|
||||||
|
didUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spec.getIsPublic() != null) {
|
||||||
|
recipe.setIsPublic(spec.getIsPublic());
|
||||||
|
didUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: we have to think about how to unset the main image vs. just leaving it out of the request
|
||||||
|
if (spec.getMainImage() != null) {
|
||||||
|
final Image mainImage = this.imageService.getByUsernameAndFilename(
|
||||||
|
spec.getMainImage().getUsername(),
|
||||||
|
spec.getMainImage().getFilename(),
|
||||||
|
modifier
|
||||||
|
);
|
||||||
|
recipe.setMainImage(mainImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (didUpdate) {
|
||||||
|
recipe.setModified(OffsetDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@recipeSecurity.isOwner(#username, #slug, #modifier)")
|
||||||
|
public Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier)
|
||||||
|
throws RecipeException, ImageException {
|
||||||
|
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);
|
||||||
|
return this.recipeRepository.save(recipe);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
||||||
|
public Recipe addViewer(Integer id, User modifier, User viewer) throws RecipeException {
|
||||||
|
final Recipe entity = this.recipeRepository.findByIdWithViewers(id).orElseThrow(() -> new RecipeException(
|
||||||
|
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id
|
||||||
|
));
|
||||||
|
final Set<User> viewers = new HashSet<>(entity.getViewers());
|
||||||
|
viewers.add(viewer);
|
||||||
|
entity.setViewers(viewers);
|
||||||
|
return this.recipeRepository.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
||||||
|
public Recipe removeViewer(Integer id, User modifier, User viewer) throws RecipeException {
|
||||||
|
final Recipe entity = this.findRecipeEntity(id);
|
||||||
|
final Set<User> viewers = new HashSet<>(entity.getViewers());
|
||||||
|
viewers.remove(viewer);
|
||||||
|
entity.setViewers(viewers);
|
||||||
|
return this.recipeRepository.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
||||||
|
public Recipe clearAllViewers(Integer id, User modifier) throws RecipeException {
|
||||||
|
final Recipe entity = this.findRecipeEntity(id);
|
||||||
|
entity.setViewers(new HashSet<>());
|
||||||
|
return this.recipeRepository.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
||||||
|
public void deleteRecipe(Integer id, User modifier) {
|
||||||
|
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)")
|
||||||
@Contract("_, _, null -> null")
|
@Contract("_, _, null -> null")
|
||||||
@Nullable Boolean isStarer(String username, String slug, @Nullable User viewer);
|
public @Nullable Boolean isStarer(String username, String slug, @Nullable User viewer) {
|
||||||
|
if (viewer == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.recipeStarRepository.isStarer(username, slug, viewer.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@recipeSecurity.isViewableBy(#username, #slug, #viewer)")
|
||||||
@Contract("_, _, null -> null")
|
@Contract("_, _, null -> null")
|
||||||
@Nullable Boolean isOwner(String username, String slug, @Nullable User viewer);
|
public @Nullable Boolean isOwner(String username, String slug, @Nullable User viewer) {
|
||||||
|
if (viewer == null) {
|
||||||
@ApiStatus.Internal
|
return null;
|
||||||
String getRenderedMarkdown(Recipe entity);
|
}
|
||||||
|
return viewer.getUsername().equals(username);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,339 +0,0 @@
|
|||||||
package app.mealsmadeeasy.api.recipe;
|
|
||||||
|
|
||||||
import app.mealsmadeeasy.api.image.Image;
|
|
||||||
import app.mealsmadeeasy.api.image.ImageException;
|
|
||||||
import app.mealsmadeeasy.api.image.ImageService;
|
|
||||||
import app.mealsmadeeasy.api.image.view.ImageView;
|
|
||||||
import app.mealsmadeeasy.api.markdown.MarkdownService;
|
|
||||||
import app.mealsmadeeasy.api.recipe.body.RecipeAiSearchBody;
|
|
||||||
import app.mealsmadeeasy.api.recipe.spec.RecipeCreateSpec;
|
|
||||||
import app.mealsmadeeasy.api.recipe.spec.RecipeUpdateSpec;
|
|
||||||
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 org.jetbrains.annotations.ApiStatus;
|
|
||||||
import org.jetbrains.annotations.Contract;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.springframework.ai.embedding.EmbeddingModel;
|
|
||||||
import org.springframework.data.domain.Pageable;
|
|
||||||
import org.springframework.data.domain.Slice;
|
|
||||||
import org.springframework.security.access.AccessDeniedException;
|
|
||||||
import org.springframework.security.access.prepost.PostAuthorize;
|
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class RecipeServiceImpl implements RecipeService {
|
|
||||||
|
|
||||||
private final RecipeRepository recipeRepository;
|
|
||||||
private final RecipeStarRepository recipeStarRepository;
|
|
||||||
private final ImageService imageService;
|
|
||||||
private final MarkdownService markdownService;
|
|
||||||
private final EmbeddingModel embeddingModel;
|
|
||||||
|
|
||||||
public RecipeServiceImpl(
|
|
||||||
RecipeRepository recipeRepository,
|
|
||||||
RecipeStarRepository recipeStarRepository,
|
|
||||||
ImageService imageService,
|
|
||||||
MarkdownService markdownService,
|
|
||||||
EmbeddingModel embeddingModel
|
|
||||||
) {
|
|
||||||
this.recipeRepository = recipeRepository;
|
|
||||||
this.recipeStarRepository = recipeStarRepository;
|
|
||||||
this.imageService = imageService;
|
|
||||||
this.markdownService = markdownService;
|
|
||||||
this.embeddingModel = embeddingModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Recipe create(@Nullable User owner, RecipeCreateSpec spec) {
|
|
||||||
if (owner == null) {
|
|
||||||
throw new AccessDeniedException("Must be logged in.");
|
|
||||||
}
|
|
||||||
final Recipe draft = new Recipe();
|
|
||||||
draft.setCreated(OffsetDateTime.now());
|
|
||||||
draft.setOwner(owner);
|
|
||||||
draft.setSlug(spec.getSlug());
|
|
||||||
draft.setTitle(spec.getTitle());
|
|
||||||
draft.setRawText(spec.getRawText());
|
|
||||||
draft.setMainImage(spec.getMainImage());
|
|
||||||
draft.setIsPublic(spec.isPublic());
|
|
||||||
return this.recipeRepository.save(draft);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Recipe findRecipeEntity(Integer id) throws RecipeException {
|
|
||||||
return this.recipeRepository.findById(id).orElseThrow(() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
|
||||||
public Recipe getById(Integer id, User viewer) throws RecipeException {
|
|
||||||
return this.findRecipeEntity(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
|
||||||
public Recipe getByIdWithStars(Integer id, @Nullable User viewer) throws RecipeException {
|
|
||||||
return this.recipeRepository.findByIdWithStars(id).orElseThrow(() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_ID,
|
|
||||||
"No such Recipe with id: " + id
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@PostAuthorize("@recipeSecurity.isViewableBy(returnObject, #viewer)")
|
|
||||||
public Recipe getByUsernameAndSlug(String username, String slug, @Nullable User viewer) throws RecipeException {
|
|
||||||
return this.recipeRepository.findByOwnerUsernameAndSlug(username, slug).orElseThrow(() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_USERNAME_OR_SLUG,
|
|
||||||
"No such Recipe for username " + username + " and slug " + slug
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@ApiStatus.Internal
|
|
||||||
public String getRenderedMarkdown(Recipe entity) {
|
|
||||||
if (entity.getCachedRenderedText() == null) {
|
|
||||||
entity.setCachedRenderedText(this.markdownService.renderAndCleanMarkdown(entity.getRawText()));
|
|
||||||
entity = this.recipeRepository.save(entity);
|
|
||||||
}
|
|
||||||
return entity.getCachedRenderedText();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getStarCount(Recipe recipe) {
|
|
||||||
return this.recipeRepository.getStarCount(recipe.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getViewerCount(long recipeId) {
|
|
||||||
return this.recipeRepository.getViewerCount(recipeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Contract("null, _ -> null")
|
|
||||||
private @Nullable ImageView getImageView(@Nullable Image image, @Nullable User viewer) {
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Slice<RecipeInfoView> getInfoViewsViewableBy(Pageable pageable, @Nullable User viewer) {
|
|
||||||
return this.recipeRepository.findAllViewableBy(viewer, pageable).map(recipe ->
|
|
||||||
this.getInfoView(recipe, viewer)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Recipe> getByMinimumStars(long minimumStars, User viewer) {
|
|
||||||
return List.copyOf(
|
|
||||||
this.recipeRepository.findAllViewableByStarsGreaterThanEqual(minimumStars, viewer)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Recipe> getPublicRecipes() {
|
|
||||||
return List.copyOf(this.recipeRepository.findAllByIsPublicIsTrue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Recipe> getRecipesViewableBy(User viewer) {
|
|
||||||
return List.copyOf(this.recipeRepository.findAllByViewersContaining(viewer));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Recipe> getRecipesOwnedBy(User owner) {
|
|
||||||
return List.copyOf(this.recipeRepository.findAllByOwner(owner));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<RecipeInfoView> aiSearch(RecipeAiSearchBody searchSpec, @Nullable User viewer) {
|
|
||||||
final float[] queryEmbedding = this.embeddingModel.embed(searchSpec.getPrompt());
|
|
||||||
final List<Recipe> results;
|
|
||||||
if (viewer == null) {
|
|
||||||
results = this.recipeRepository.searchByEmbeddingAndIsPublic(queryEmbedding, 0.5f);
|
|
||||||
} else {
|
|
||||||
results = this.recipeRepository.searchByEmbeddingAndViewableBy(queryEmbedding, 0.5f, viewer.getId());
|
|
||||||
}
|
|
||||||
return results.stream()
|
|
||||||
.map(recipeEntity -> this.getInfoView(recipeEntity, viewer))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void prepareForUpdate(RecipeUpdateSpec spec, Recipe recipe, User modifier) throws ImageException {
|
|
||||||
boolean didUpdate = false;
|
|
||||||
if (spec.getTitle() != null) {
|
|
||||||
recipe.setTitle(spec.getTitle());
|
|
||||||
didUpdate = true;
|
|
||||||
}
|
|
||||||
if (spec.getPreparationTime() != null) {
|
|
||||||
recipe.setPreparationTime(spec.getPreparationTime());
|
|
||||||
didUpdate = true;
|
|
||||||
}
|
|
||||||
if (spec.getCookingTime() != null) {
|
|
||||||
recipe.setCookingTime(spec.getCookingTime());
|
|
||||||
didUpdate = true;
|
|
||||||
}
|
|
||||||
if (spec.getTotalTime() != null) {
|
|
||||||
recipe.setTotalTime(spec.getTotalTime());
|
|
||||||
didUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spec.getRawText() != null) {
|
|
||||||
recipe.setRawText(spec.getRawText());
|
|
||||||
recipe.setCachedRenderedText(null);
|
|
||||||
didUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spec.getIsPublic() != null) {
|
|
||||||
recipe.setIsPublic(spec.getIsPublic());
|
|
||||||
didUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: we have to think about how to unset the main image vs. just leaving it out of the request
|
|
||||||
if (spec.getMainImage() != null) {
|
|
||||||
final Image mainImage = this.imageService.getByUsernameAndFilename(
|
|
||||||
spec.getMainImage().getUsername(),
|
|
||||||
spec.getMainImage().getFilename(),
|
|
||||||
modifier
|
|
||||||
);
|
|
||||||
recipe.setMainImage(mainImage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (didUpdate) {
|
|
||||||
recipe.setModified(OffsetDateTime.now());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@PreAuthorize("@recipeSecurity.isOwner(#username, #slug, #modifier)")
|
|
||||||
public Recipe update(String username, String slug, RecipeUpdateSpec spec, User modifier)
|
|
||||||
throws RecipeException, ImageException {
|
|
||||||
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);
|
|
||||||
return this.recipeRepository.save(recipe);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
|
||||||
public Recipe addViewer(Integer id, User modifier, User viewer) throws RecipeException {
|
|
||||||
final Recipe entity = this.recipeRepository.findByIdWithViewers(id).orElseThrow(() -> new RecipeException(
|
|
||||||
RecipeException.Type.INVALID_ID, "No such Recipe with id: " + id
|
|
||||||
));
|
|
||||||
final Set<User> viewers = new HashSet<>(entity.getViewers());
|
|
||||||
viewers.add(viewer);
|
|
||||||
entity.setViewers(viewers);
|
|
||||||
return this.recipeRepository.save(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
|
||||||
public Recipe removeViewer(Integer id, User modifier, User viewer) throws RecipeException {
|
|
||||||
final Recipe entity = this.findRecipeEntity(id);
|
|
||||||
final Set<User> viewers = new HashSet<>(entity.getViewers());
|
|
||||||
viewers.remove(viewer);
|
|
||||||
entity.setViewers(viewers);
|
|
||||||
return this.recipeRepository.save(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
|
||||||
public Recipe clearAllViewers(Integer id, User modifier) throws RecipeException {
|
|
||||||
final Recipe entity = this.findRecipeEntity(id);
|
|
||||||
entity.setViewers(new HashSet<>());
|
|
||||||
return this.recipeRepository.save(entity);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@PreAuthorize("@recipeSecurity.isOwner(#id, #modifier)")
|
|
||||||
public void deleteRecipe(Integer id, User modifier) {
|
|
||||||
this.recipeRepository.deleteById(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FullRecipeView toFullRecipeView(Recipe recipe, boolean includeRawText, @Nullable User viewer) {
|
|
||||||
return this.getFullView(recipe, includeRawText, viewer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RecipeInfoView toRecipeInfoView(Recipe recipe, @Nullable User viewer) {
|
|
||||||
return this.getInfoView(recipe, viewer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@PreAuthorize("@recipeSecurity.isViewableBy(#username, #slug, #viewer)")
|
|
||||||
@Contract("_, _, null -> null")
|
|
||||||
public @Nullable Boolean isStarer(String username, String slug, @Nullable User viewer) {
|
|
||||||
if (viewer == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.recipeStarRepository.isStarer(username, slug, viewer.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@PreAuthorize("@recipeSecurity.isViewableBy(#username, #slug, #viewer)")
|
|
||||||
@Contract("_, _, null -> null")
|
|
||||||
public @Nullable Boolean isOwner(String username, String slug, @Nullable User viewer) {
|
|
||||||
if (viewer == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return viewer.getUsername().equals(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user