From 9994b9c74651e62ea3e7d67a4091c7943a2c222e Mon Sep 17 00:00:00 2001 From: Madeleine Stamp Date: Fri, 6 Mar 2026 09:46:51 -0700 Subject: [PATCH] Edit RecipeServiceImpl.java --- .../demo/service/Impl/RecipeServiceImpl.java | 487 ++++++++++-------- 1 file changed, 271 insertions(+), 216 deletions(-) diff --git a/demo/src/main/java/com/example/demo/service/Impl/RecipeServiceImpl.java b/demo/src/main/java/com/example/demo/service/Impl/RecipeServiceImpl.java index dd549bc..c223bae 100644 --- a/demo/src/main/java/com/example/demo/service/Impl/RecipeServiceImpl.java +++ b/demo/src/main/java/com/example/demo/service/Impl/RecipeServiceImpl.java @@ -1,265 +1,320 @@ package com.example.demo.service.Impl; -import com.example.demo.dto.RecipeDto; -import com.example.demo.dto.RecipeIngredientDto; -import com.example.demo.dto.StepDto; -import com.example.demo.dto.UserDto; -import com.example.demo.entity.*; -import com.example.demo.exception.NotFoundException; -import com.example.demo.repository.*; -import com.example.demo.service.RecipeService; +import java.util.ArrayList; +import java.util.List; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.*; -import java.util.stream.Collectors; +import com.example.demo.dto.RecipeDto; +import com.example.demo.dto.UserDto; +import com.example.demo.dto.StepDto; +import com.example.demo.dto.TagDto; +import com.example.demo.dto.ImageDto; +import com.example.demo.dto.RecipeIngredientDto; +import com.example.demo.entity.Image; +import com.example.demo.entity.Ingredient; +import com.example.demo.entity.Recipe; +import com.example.demo.entity.RecipeIngredient; +import com.example.demo.entity.Step; +import com.example.demo.entity.Tag; +import com.example.demo.entity.User; +import com.example.demo.exception.NotFoundException; +import com.example.demo.repository.ImageRepo; +import com.example.demo.repository.IngredientRepo; +import com.example.demo.repository.RecipeIngredientRepo; +import com.example.demo.repository.RecipeRepo; +import com.example.demo.repository.StepRepo; +import com.example.demo.repository.TagRepo; +import com.example.demo.repository.UserRepo; +import com.example.demo.service.RecipeService; + +import jakarta.transaction.Transactional; @Service public class RecipeServiceImpl implements RecipeService { - private final RecipeRepo recipeRepo; - private final UserRepo userRepo; - private final IngredientRepo ingredientRepo; - private final StepRepo stepRepo; - private final RecipeIngredientRepo recipeIngredientRepo; + private RecipeRepo recipeRepository; + private IngredientRepo ingredientRepository; + private RecipeIngredientRepo recipeIngredientRepository; + private UserRepo userRepository; + private StepRepo stepRepository; + private ImageRepo imageRepository; + private TagRepo tagRepository; - // DEV DEFAULT — must exist in DB - private static final int DEFAULT_USER_ID = 1; + public RecipeServiceImpl(RecipeRepo recipeRepository, IngredientRepo ingredientRepository, + RecipeIngredientRepo recipeIngredientRepository, UserRepo userRepository, StepRepo stepRepository, + ImageRepo imageRepository, TagRepo tagRepository) { + super(); + this.recipeRepository = recipeRepository; + this.ingredientRepository = ingredientRepository; + this.recipeIngredientRepository = recipeIngredientRepository; + this.userRepository = userRepository; + this.stepRepository = stepRepository; + this.imageRepository = imageRepository; + this.tagRepository = tagRepository; + } - public RecipeServiceImpl( - RecipeRepo recipeRepo, - UserRepo userRepo, - IngredientRepo ingredientRepo, - StepRepo stepRepo, - RecipeIngredientRepo recipeIngredientRepo - ) { - this.recipeRepo = recipeRepo; - this.userRepo = userRepo; - this.ingredientRepo = ingredientRepo; - this.stepRepo = stepRepo; - this.recipeIngredientRepo = recipeIngredientRepo; - } + public RecipeDto convertToDto(Recipe recipe) { + List ingredientDtos = recipe.getRecipeIngredients().stream() + .map(ri -> new RecipeIngredientDto(ri.getIngredient().getName(), ri.getQuantity(), ri.getUnit(), + ri.getNotes())) + .toList(); - // -------------------- convertToDto -------------------- - // Called by controller when it receives Recipe (entity) in request body. - // MUST be null-safe and MUST never return null lists. - - @Override - public RecipeDto convertToDto(Recipe recipe) { - RecipeDto dto = new RecipeDto(); + List stepDtos = recipe.getSteps().stream() + .map(ri -> new StepDto(ri.getStepNumber(), ri.getInstruction())).toList(); - dto.setTitle(recipe.getTitle()); - dto.setDescription(recipe.getDescription()); - dto.setPrepTimeMinutes(recipe.getPrepTimeMinutes()); - dto.setCookTimeMinutes(recipe.getCookTimeMinutes()); - dto.setServings(recipe.getServings()); - dto.setStatus(recipe.getStatus()); - dto.setId(recipe.getId()); + List imageDtos = recipe.getImages().stream().map(ri -> new ImageDto(ri.getImageUrl())).toList(); - // IMPORTANT: prevent null lists (this was your NPE earlier) - dto.setIngredients(new java.util.ArrayList<>()); - dto.setSteps(new java.util.ArrayList<>()); - dto.setImages(new java.util.ArrayList<>()); - dto.setTags(new java.util.ArrayList<>()); + List tagDtos = recipe.getTags().stream().map(ri -> new TagDto(ri.getName())).toList(); - // userDto optional - if (recipe.getUser() != null) { - com.example.demo.dto.UserDto ud = new com.example.demo.dto.UserDto(); - ud.setId(recipe.getUser().getId()); - ud.setUsername(recipe.getUser().getUsername()); - ud.setEmail(recipe.getUser().getEmail()); - dto.setUserDto(ud); - } + UserDto userDto = new UserDto(recipe.getUser().getId(), recipe.getUser().getUsername(), + recipe.getUser().getEmail()); - return dto; - } - - - - // -------------------- CREATE -------------------- - @Override - @Transactional - public RecipeDto saveRecipe(RecipeDto recipeDto) { + return new RecipeDto(recipe.getTitle(), recipe.getDescription(), recipe.getPrepTimeMinutes(), + recipe.getCookTimeMinutes(), recipe.getServings(), userDto, recipe.getStatus(), ingredientDtos, + stepDtos, imageDtos, tagDtos); + } - List ingredientDtos = - recipeDto.getIngredients() == null ? Collections.emptyList() : recipeDto.getIngredients(); + @Override + public RecipeDto saveRecipe(RecipeDto dto) { - List stepDtos = - recipeDto.getSteps() == null ? Collections.emptyList() : recipeDto.getSteps(); + User user = userRepository.findById(dto.getUserDto().getId()) + .orElseThrow(() -> new NotFoundException("User", "id", dto.getUserDto().getId())); - Recipe recipe = new Recipe(); - recipe.setTitle(recipeDto.getTitle()); - recipe.setDescription(recipeDto.getDescription()); - recipe.setPrepTimeMinutes(recipeDto.getPrepTimeMinutes()); - recipe.setCookTimeMinutes(recipeDto.getCookTimeMinutes()); - recipe.setServings(recipeDto.getServings()); - recipe.setStatus(recipeDto.getStatus()); - recipe.setCreatedAt(LocalDateTime.now()); - recipe.setUpdatedAt(LocalDateTime.now()); + Recipe recipe = new Recipe(dto.getTitle(), dto.getDescription(), dto.getPrepTimeMinutes(), + dto.getCookTimeMinutes(), dto.getServings(), user, dto.getStatus()); - // REQUIRED user_id - recipe.setUser(resolveUser(recipeDto.getUserDto(), null)); + for (RecipeIngredientDto riDto : dto.getIngredients()) { - // attach children (cascade = ALL on Recipe will persist them) - attachSteps(recipe, stepDtos); - attachRecipeIngredients(recipe, ingredientDtos); + Ingredient ingredient = ingredientRepository.findByNameIgnoreCase(riDto.getIngredientName()) + .orElseGet(() -> new Ingredient(riDto.getIngredientName())); - Recipe saved = recipeRepo.save(recipe); - return getRecipeById(saved.getId()); - } + if (ingredient.getId() == null) { + ingredientRepository.save(ingredient); + } - // -------------------- READ ALL -------------------- - @Override - public List getAllRecipes() { - // Return shallow for list to avoid lazy-loading issues - return recipeRepo.findAll().stream() - .map(r -> { - RecipeDto dto = new RecipeDto(); - dto.setTitle(r.getTitle()); - dto.setDescription(r.getDescription()); - dto.setPrepTimeMinutes(r.getPrepTimeMinutes()); - dto.setCookTimeMinutes(r.getCookTimeMinutes()); - dto.setServings(r.getServings()); - dto.setStatus(r.getStatus()); - dto.setIngredients(new ArrayList<>()); - dto.setSteps(new ArrayList<>()); - dto.setImages(new ArrayList<>()); - dto.setTags(new ArrayList<>()); - dto.setId(r.getId()); - return dto; - }) - .collect(Collectors.toList()); - } + RecipeIngredient ri = new RecipeIngredient(recipe, ingredient, riDto.getQuantity(), riDto.getUnit(), + riDto.getNotes()); - // -------------------- READ ONE -------------------- - @Override - @org.springframework.transaction.annotation.Transactional(readOnly = true) - public RecipeDto getRecipeById(Integer recipeId) { - Recipe recipe = recipeRepo.findById(recipeId) - .orElseThrow(() -> new NotFoundException("Recipe", "id", recipeId)); + recipe.getRecipeIngredients().add(ri); + } - // Start with base fields - RecipeDto dto = convertToDto(recipe); + if (dto.getSteps() != null) { + for (StepDto stepDto : dto.getSteps()) { + Step step = new Step(recipe, stepDto.getStepNumber(), stepDto.getInstruction()); + recipe.getSteps().add(step); + } + } - // Overwrite lists using repo results (no mutation of Recipe.steps / recipeIngredients) - dto.setSteps( - stepRepo.findById(recipeId).stream() - .map(s -> new com.example.demo.dto.StepDto(s.getStepNumber(), s.getInstruction())) - .collect(java.util.stream.Collectors.toList()) - ); + if (dto.getImages() != null) { + for (ImageDto imageDto : dto.getImages()) { + Image image = new Image(recipe, imageDto.getImageUrl()); + recipe.getImages().add(image); + } + } - dto.setIngredients( - recipeIngredientRepo.findByRecipeId(recipeId).stream() - .map(ri -> new com.example.demo.dto.RecipeIngredientDto( - ri.getIngredient() != null ? ri.getIngredient().getName() : null, - ri.getQuantity(), - ri.getUnit(), - ri.getNotes() - )) - .collect(java.util.stream.Collectors.toList()) - ); + for (TagDto tDto : dto.getTags()) { - return dto; - } - // -------------------- UPDATE -------------------- - @Override - @Transactional - public RecipeDto updateRecipe(RecipeDto recipeDto, Integer id) { + Tag tag = tagRepository.findByName(tDto.getName()).orElseGet(() -> new Tag(tDto.getName())); - Recipe recipe = recipeRepo.findById(id) - .orElseThrow(() -> new NotFoundException("Recipe", "id", recipeDto)); + if (tag.getId() == null) { + tagRepository.save(tag); + } + recipe.getTags().add(tag); + } - List ingredientDtos = - recipeDto.getIngredients() == null ? Collections.emptyList() : recipeDto.getIngredients(); + Recipe saved = recipeRepository.save(recipe); - List stepDtos = - recipeDto.getSteps() == null ? Collections.emptyList() : recipeDto.getSteps(); + return getRecipeById(saved.getId()); + //return convertToDto(saved); + } - recipe.setTitle(recipeDto.getTitle()); - recipe.setDescription(recipeDto.getDescription()); - recipe.setPrepTimeMinutes(recipeDto.getPrepTimeMinutes()); - recipe.setCookTimeMinutes(recipeDto.getCookTimeMinutes()); - recipe.setServings(recipeDto.getServings()); - recipe.setStatus(recipeDto.getStatus()); - recipe.setUpdatedAt(LocalDateTime.now()); + @Override + @Transactional + public List getAllRecipes() { - // keep old user if not provided - recipe.setUser(resolveUser(recipeDto.getUserDto(), recipe.getUser())); + List list = new ArrayList<>(); + for (Recipe recipe : recipeRepository.findAll()) { + RecipeDto recipeDto = convertToDto(recipe); + list.add(recipeDto); + } - // Clear old children (orphanRemoval = true will delete) - recipe.getSteps().clear(); - recipe.getRecipeIngredients().clear(); + return list; + } - attachSteps(recipe, stepDtos); - attachRecipeIngredients(recipe, ingredientDtos); + @Override + @Transactional + public RecipeDto getRecipeById(Integer Id) { + return convertToDto(recipeRepository.findById(Id).orElseThrow(() -> new NotFoundException("Recipe", "id", Id))); + } - Recipe saved = recipeRepo.save(recipe); - return getRecipeById(saved.getId()); - } + @Override + @Transactional + public RecipeDto updateRecipe(RecipeDto recipeDto, Integer id) { + Recipe existingRecipe = recipeRepository.findById(id) + .orElseThrow(() -> new NotFoundException("Recipe", "id", id)); - // -------------------- DELETE -------------------- - @Override - @Transactional - public void deleteRecipe(Integer id) { - if (!recipeRepo.existsById(id)) { - throw new NotFoundException("Recipe not found", null, id); - } - recipeRepo.deleteById(id); - } + existingRecipe.setTitle(recipeDto.getTitle()); + existingRecipe.setDescription(recipeDto.getDescription()); + existingRecipe.setPrepTimeMinutes(recipeDto.getPrepTimeMinutes()); + existingRecipe.setCookTimeMinutes(recipeDto.getCookTimeMinutes()); + existingRecipe.setServings(recipeDto.getServings()); + existingRecipe.setStatus(recipeDto.getStatus()); - // -------------------- Helpers -------------------- + List updatedIngredients = recipeDto.getIngredients(); + List ingredientsToRemove = new ArrayList<>(); - private User resolveUser(UserDto incoming, User existing) { - if (incoming != null && incoming.getId() != null) { - return userRepo.findById(incoming.getId()) - .orElseThrow(() -> new NotFoundException("User not found: " + incoming.getId(), null, existing)); - } - if (existing != null) return existing; + List updatedSteps = recipeDto.getSteps(); + List stepsToRemove = new ArrayList<>(); - return userRepo.findById(DEFAULT_USER_ID) - .orElseThrow(() -> new IllegalStateException( - "DEFAULT_USER_ID=" + DEFAULT_USER_ID + " not found. Insert a user row or change DEFAULT_USER_ID." - )); - } + List updatedImages = recipeDto.getImages(); + List imagesToRemove = new ArrayList<>(); - private void attachSteps(Recipe recipe, List stepDtos) { - for (StepDto sd : stepDtos) { - if (sd == null) continue; - if (sd.getInstruction() == null || sd.getInstruction().isBlank()) continue; + List updatedTags = recipeDto.getTags(); + List tagsToRemove = new ArrayList<>(); - Step s = new Step(); - s.setRecipe(recipe); - s.setStepNumber(sd.getStepNumber() == null ? 1 : sd.getStepNumber()); - s.setInstruction(sd.getInstruction()); + for (RecipeIngredient ri : existingRecipe.getRecipeIngredients()) { - recipe.getSteps().add(s); - } - } + boolean existsInUpdatedList = false; + for (RecipeIngredientDto dto : updatedIngredients) { + String updatedName = dto.getIngredientName(); + String existingName = ri.getIngredient().getName(); - private void attachRecipeIngredients(Recipe recipe, List ingredientDtos) { - for (RecipeIngredientDto rid : ingredientDtos) { - if (rid == null) continue; - if (rid.getIngredientName() == null || rid.getIngredientName().isBlank()) continue; + if (updatedName.equals(existingName)) { + existsInUpdatedList = true; + break; + } + } - Ingredient ing = ingredientRepo.findByNameIgnoreCase(rid.getIngredientName().trim()) - .orElseGet(() -> { - Ingredient i = new Ingredient(); - i.setName(rid.getIngredientName().trim()); - return ingredientRepo.save(i); - }); + if (!existsInUpdatedList) { + ingredientsToRemove.add(ri); + } + } - RecipeIngredient ri = new RecipeIngredient(); - ri.setRecipe(recipe); - ri.setIngredient(ing); - ri.setQuantity(rid.getQuantity() == null ? BigDecimal.ZERO : rid.getQuantity()); - ri.setUnit(rid.getUnit()); - ri.setNotes(rid.getNotes()); + existingRecipe.getRecipeIngredients().removeAll(ingredientsToRemove); - recipe.getRecipeIngredients().add(ri); - } - } + for (RecipeIngredientDto riDto : updatedIngredients) { + // go through the old list of ingredients until we find a match with updated + // list + RecipeIngredient existingRI = existingRecipe.getRecipeIngredients().stream() + .filter(ri -> ri.getIngredient().getName().equals(riDto.getIngredientName())).findFirst() + .orElse(null); + + // if old ingredient just update parameters + if (existingRI != null) { + + existingRI.setQuantity(riDto.getQuantity()); + existingRI.setUnit(riDto.getUnit()); + existingRI.setNotes(riDto.getNotes()); + } + + // if new ingredient, have to make a whole new thing + else { + + Ingredient ingredient = ingredientRepository.findByNameIgnoreCase(riDto.getIngredientName()) + .orElseGet(() -> new Ingredient(riDto.getIngredientName())); + + if (ingredient.getId() == null) { + ingredientRepository.save(ingredient); + } + + RecipeIngredient newRI = new RecipeIngredient(existingRecipe, ingredient, riDto.getQuantity(), + riDto.getUnit(), riDto.getNotes()); + + existingRecipe.getRecipeIngredients().add(newRI); + } + } + + if (updatedSteps != null) { + // find steps that weren't included + for (Step step : existingRecipe.getSteps()) { + boolean existsInUpdatedList = updatedSteps.stream() + .anyMatch(dto -> dto.getStepNumber().equals(step.getStepNumber())); + + if (!existsInUpdatedList) + stepsToRemove.add(step); + } + // delete those steps + existingRecipe.getSteps().removeAll(stepsToRemove); + + // go through updated steps + for (StepDto stepDto : updatedSteps) { + + // find matching step by step number + Step existingStep = existingRecipe.getSteps().stream() + .filter(s -> s.getStepNumber().equals(stepDto.getStepNumber())).findFirst().orElse(null); + + // if there's a match update the instruction string + if (existingStep != null) { + existingStep.setInstruction(stepDto.getInstruction()); + } + + // if no match then make a whole new step + else { + Step newStep = new Step(existingRecipe, stepDto.getStepNumber(), stepDto.getInstruction()); + existingRecipe.getSteps().add(newStep); + } + } + } + + // same process as above just with images instead + if (updatedImages != null) { + for (Image image : existingRecipe.getImages()) { + boolean existsInUpdatedList = updatedImages.stream() + .anyMatch(dto -> dto.getImageUrl().equals(image.getImageUrl())); + if (!existsInUpdatedList) + imagesToRemove.add(image); + } + + existingRecipe.getImages().removeAll(imagesToRemove); + + for (ImageDto imageDto : updatedImages) { + Image existingImage = existingRecipe.getImages().stream() + .filter(img -> img.getImageUrl().equals(imageDto.getImageUrl())).findFirst().orElse(null); + + if (existingImage != null) { + existingImage.setImageUrl(imageDto.getImageUrl()); + } else { + Image newImage = new Image(existingRecipe, imageDto.getImageUrl()); + existingRecipe.getImages().add(newImage); + } + } + } + + // same process as above just with tags instead, except for saving the tag + // since the relationship for this one was slightly different + if (updatedTags != null) { + for (Tag tag : existingRecipe.getTags()) { + boolean existsInUpdatedList = updatedTags.stream().anyMatch(dto -> dto.getName().equals(tag.getName())); + if (!existsInUpdatedList) + tagsToRemove.add(tag); + } + + existingRecipe.getTags().removeAll(tagsToRemove); + + for (TagDto tagDto : updatedTags) { + Tag existingTag = existingRecipe.getTags().stream() + .filter(tag -> tag.getName().equals(tagDto.getName())).findFirst().orElse(null); + + if (existingTag != null) { + existingTag.setName(tagDto.getName()); + } else { + Tag newTag = tagRepository.findByName(tagDto.getName()) + .orElseGet(() -> tagRepository.save(new Tag(tagDto.getName()))); + + existingRecipe.getTags().add(newTag); + } + } + } + recipeRepository.save(existingRecipe); + return convertToDto(existingRecipe); + } + + @Override + public void deleteRecipe(Integer Id) { + recipeRepository.findById(Id).orElseThrow(() -> new NotFoundException("Recipe", "id", Id)); + recipeRepository.deleteById(Id); + + } } \ No newline at end of file