Edit RecipeServiceImpl.java

This commit is contained in:
Madeleine Stamp
2026-03-06 09:46:51 -07:00
parent fe17089e85
commit 9994b9c746
@@ -1,265 +1,320 @@
package com.example.demo.service.Impl; package com.example.demo.service.Impl;
import com.example.demo.dto.RecipeDto; import java.util.ArrayList;
import com.example.demo.dto.RecipeIngredientDto; import java.util.List;
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 org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal; import com.example.demo.dto.RecipeDto;
import java.time.LocalDateTime; import com.example.demo.dto.UserDto;
import java.util.*; import com.example.demo.dto.StepDto;
import java.util.stream.Collectors; 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 @Service
public class RecipeServiceImpl implements RecipeService { public class RecipeServiceImpl implements RecipeService {
private final RecipeRepo recipeRepo; private RecipeRepo recipeRepository;
private final UserRepo userRepo; private IngredientRepo ingredientRepository;
private final IngredientRepo ingredientRepo; private RecipeIngredientRepo recipeIngredientRepository;
private final StepRepo stepRepo; private UserRepo userRepository;
private final RecipeIngredientRepo recipeIngredientRepo; private StepRepo stepRepository;
private ImageRepo imageRepository;
private TagRepo tagRepository;
// DEV DEFAULT — must exist in DB public RecipeServiceImpl(RecipeRepo recipeRepository, IngredientRepo ingredientRepository,
private static final int DEFAULT_USER_ID = 1; 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( public RecipeDto convertToDto(Recipe recipe) {
RecipeRepo recipeRepo, List<RecipeIngredientDto> ingredientDtos = recipe.getRecipeIngredients().stream()
UserRepo userRepo, .map(ri -> new RecipeIngredientDto(ri.getIngredient().getName(), ri.getQuantity(), ri.getUnit(),
IngredientRepo ingredientRepo, ri.getNotes()))
StepRepo stepRepo, .toList();
RecipeIngredientRepo recipeIngredientRepo
) {
this.recipeRepo = recipeRepo;
this.userRepo = userRepo;
this.ingredientRepo = ingredientRepo;
this.stepRepo = stepRepo;
this.recipeIngredientRepo = recipeIngredientRepo;
}
// -------------------- convertToDto -------------------- List<StepDto> stepDtos = recipe.getSteps().stream()
// Called by controller when it receives Recipe (entity) in request body. .map(ri -> new StepDto(ri.getStepNumber(), ri.getInstruction())).toList();
// MUST be null-safe and MUST never return null lists.
@Override List<ImageDto> imageDtos = recipe.getImages().stream().map(ri -> new ImageDto(ri.getImageUrl())).toList();
public RecipeDto convertToDto(Recipe recipe) {
RecipeDto dto = new RecipeDto();
dto.setTitle(recipe.getTitle()); List<TagDto> tagDtos = recipe.getTags().stream().map(ri -> new TagDto(ri.getName())).toList();
dto.setDescription(recipe.getDescription());
dto.setPrepTimeMinutes(recipe.getPrepTimeMinutes());
dto.setCookTimeMinutes(recipe.getCookTimeMinutes());
dto.setServings(recipe.getServings());
dto.setStatus(recipe.getStatus());
dto.setId(recipe.getId());
// IMPORTANT: prevent null lists (this was your NPE earlier) UserDto userDto = new UserDto(recipe.getUser().getId(), recipe.getUser().getUsername(),
dto.setIngredients(new java.util.ArrayList<>()); recipe.getUser().getEmail());
dto.setSteps(new java.util.ArrayList<>());
dto.setImages(new java.util.ArrayList<>());
dto.setTags(new java.util.ArrayList<>());
// userDto optional return new RecipeDto(recipe.getTitle(), recipe.getDescription(), recipe.getPrepTimeMinutes(),
if (recipe.getUser() != null) { recipe.getCookTimeMinutes(), recipe.getServings(), userDto, recipe.getStatus(), ingredientDtos,
com.example.demo.dto.UserDto ud = new com.example.demo.dto.UserDto(); stepDtos, imageDtos, tagDtos);
ud.setId(recipe.getUser().getId()); }
ud.setUsername(recipe.getUser().getUsername());
ud.setEmail(recipe.getUser().getEmail());
dto.setUserDto(ud);
}
return dto; @Override
} public RecipeDto saveRecipe(RecipeDto dto) {
User user = userRepository.findById(dto.getUserDto().getId())
.orElseThrow(() -> new NotFoundException("User", "id", dto.getUserDto().getId()));
Recipe recipe = new Recipe(dto.getTitle(), dto.getDescription(), dto.getPrepTimeMinutes(),
dto.getCookTimeMinutes(), dto.getServings(), user, dto.getStatus());
// -------------------- CREATE -------------------- for (RecipeIngredientDto riDto : dto.getIngredients()) {
@Override
@Transactional
public RecipeDto saveRecipe(RecipeDto recipeDto) {
List<RecipeIngredientDto> ingredientDtos = Ingredient ingredient = ingredientRepository.findByNameIgnoreCase(riDto.getIngredientName())
recipeDto.getIngredients() == null ? Collections.emptyList() : recipeDto.getIngredients(); .orElseGet(() -> new Ingredient(riDto.getIngredientName()));
List<StepDto> stepDtos = if (ingredient.getId() == null) {
recipeDto.getSteps() == null ? Collections.emptyList() : recipeDto.getSteps(); ingredientRepository.save(ingredient);
}
Recipe recipe = new Recipe(); RecipeIngredient ri = new RecipeIngredient(recipe, ingredient, riDto.getQuantity(), riDto.getUnit(),
recipe.setTitle(recipeDto.getTitle()); riDto.getNotes());
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());
// REQUIRED user_id recipe.getRecipeIngredients().add(ri);
recipe.setUser(resolveUser(recipeDto.getUserDto(), null)); }
// attach children (cascade = ALL on Recipe will persist them) if (dto.getSteps() != null) {
attachSteps(recipe, stepDtos); for (StepDto stepDto : dto.getSteps()) {
attachRecipeIngredients(recipe, ingredientDtos); Step step = new Step(recipe, stepDto.getStepNumber(), stepDto.getInstruction());
recipe.getSteps().add(step);
}
}
Recipe saved = recipeRepo.save(recipe); if (dto.getImages() != null) {
return getRecipeById(saved.getId()); for (ImageDto imageDto : dto.getImages()) {
} Image image = new Image(recipe, imageDto.getImageUrl());
recipe.getImages().add(image);
}
}
// -------------------- READ ALL -------------------- for (TagDto tDto : dto.getTags()) {
@Override
public List<RecipeDto> 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());
}
// -------------------- READ ONE -------------------- Tag tag = tagRepository.findByName(tDto.getName()).orElseGet(() -> new Tag(tDto.getName()));
@Override
@org.springframework.transaction.annotation.Transactional(readOnly = true)
public RecipeDto getRecipeById(Integer recipeId) {
Recipe recipe = recipeRepo.findById(recipeId)
.orElseThrow(() -> new NotFoundException("Recipe", "id", recipeId));
// Start with base fields if (tag.getId() == null) {
RecipeDto dto = convertToDto(recipe); tagRepository.save(tag);
}
recipe.getTags().add(tag);
}
// Overwrite lists using repo results (no mutation of Recipe.steps / recipeIngredients) Recipe saved = recipeRepository.save(recipe);
dto.setSteps(
stepRepo.findById(recipeId).stream()
.map(s -> new com.example.demo.dto.StepDto(s.getStepNumber(), s.getInstruction()))
.collect(java.util.stream.Collectors.toList())
);
dto.setIngredients( return getRecipeById(saved.getId());
recipeIngredientRepo.findByRecipeId(recipeId).stream() //return convertToDto(saved);
.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())
);
return dto; @Override
} @Transactional
// -------------------- UPDATE -------------------- public List<RecipeDto> getAllRecipes() {
@Override
@Transactional
public RecipeDto updateRecipe(RecipeDto recipeDto, Integer id) {
Recipe recipe = recipeRepo.findById(id) List<RecipeDto> list = new ArrayList<>();
.orElseThrow(() -> new NotFoundException("Recipe", "id", recipeDto)); for (Recipe recipe : recipeRepository.findAll()) {
RecipeDto recipeDto = convertToDto(recipe);
list.add(recipeDto);
}
List<RecipeIngredientDto> ingredientDtos = return list;
recipeDto.getIngredients() == null ? Collections.emptyList() : recipeDto.getIngredients(); }
List<StepDto> stepDtos = @Override
recipeDto.getSteps() == null ? Collections.emptyList() : recipeDto.getSteps(); @Transactional
public RecipeDto getRecipeById(Integer Id) {
return convertToDto(recipeRepository.findById(Id).orElseThrow(() -> new NotFoundException("Recipe", "id", Id)));
}
recipe.setTitle(recipeDto.getTitle()); @Override
recipe.setDescription(recipeDto.getDescription()); @Transactional
recipe.setPrepTimeMinutes(recipeDto.getPrepTimeMinutes()); public RecipeDto updateRecipe(RecipeDto recipeDto, Integer id) {
recipe.setCookTimeMinutes(recipeDto.getCookTimeMinutes()); Recipe existingRecipe = recipeRepository.findById(id)
recipe.setServings(recipeDto.getServings()); .orElseThrow(() -> new NotFoundException("Recipe", "id", id));
recipe.setStatus(recipeDto.getStatus());
recipe.setUpdatedAt(LocalDateTime.now());
// keep old user if not provided existingRecipe.setTitle(recipeDto.getTitle());
recipe.setUser(resolveUser(recipeDto.getUserDto(), recipe.getUser())); existingRecipe.setDescription(recipeDto.getDescription());
existingRecipe.setPrepTimeMinutes(recipeDto.getPrepTimeMinutes());
existingRecipe.setCookTimeMinutes(recipeDto.getCookTimeMinutes());
existingRecipe.setServings(recipeDto.getServings());
existingRecipe.setStatus(recipeDto.getStatus());
// Clear old children (orphanRemoval = true will delete) List<RecipeIngredientDto> updatedIngredients = recipeDto.getIngredients();
recipe.getSteps().clear(); List<RecipeIngredient> ingredientsToRemove = new ArrayList<>();
recipe.getRecipeIngredients().clear();
attachSteps(recipe, stepDtos); List<StepDto> updatedSteps = recipeDto.getSteps();
attachRecipeIngredients(recipe, ingredientDtos); List<Step> stepsToRemove = new ArrayList<>();
Recipe saved = recipeRepo.save(recipe); List<ImageDto> updatedImages = recipeDto.getImages();
return getRecipeById(saved.getId()); List<Image> imagesToRemove = new ArrayList<>();
}
// -------------------- DELETE -------------------- List<TagDto> updatedTags = recipeDto.getTags();
@Override List<Tag> tagsToRemove = new ArrayList<>();
@Transactional
public void deleteRecipe(Integer id) {
if (!recipeRepo.existsById(id)) {
throw new NotFoundException("Recipe not found", null, id);
}
recipeRepo.deleteById(id);
}
// -------------------- Helpers -------------------- for (RecipeIngredient ri : existingRecipe.getRecipeIngredients()) {
private User resolveUser(UserDto incoming, User existing) { boolean existsInUpdatedList = false;
if (incoming != null && incoming.getId() != null) { for (RecipeIngredientDto dto : updatedIngredients) {
return userRepo.findById(incoming.getId()) String updatedName = dto.getIngredientName();
.orElseThrow(() -> new NotFoundException("User not found: " + incoming.getId(), null, existing)); String existingName = ri.getIngredient().getName();
}
if (existing != null) return existing;
return userRepo.findById(DEFAULT_USER_ID) if (updatedName.equals(existingName)) {
.orElseThrow(() -> new IllegalStateException( existsInUpdatedList = true;
"DEFAULT_USER_ID=" + DEFAULT_USER_ID + " not found. Insert a user row or change DEFAULT_USER_ID." break;
)); }
} }
private void attachSteps(Recipe recipe, List<StepDto> stepDtos) { if (!existsInUpdatedList) {
for (StepDto sd : stepDtos) { ingredientsToRemove.add(ri);
if (sd == null) continue; }
if (sd.getInstruction() == null || sd.getInstruction().isBlank()) continue; }
Step s = new Step(); existingRecipe.getRecipeIngredients().removeAll(ingredientsToRemove);
s.setRecipe(recipe);
s.setStepNumber(sd.getStepNumber() == null ? 1 : sd.getStepNumber());
s.setInstruction(sd.getInstruction());
recipe.getSteps().add(s); for (RecipeIngredientDto riDto : updatedIngredients) {
}
}
private void attachRecipeIngredients(Recipe recipe, List<RecipeIngredientDto> ingredientDtos) { // go through the old list of ingredients until we find a match with updated
for (RecipeIngredientDto rid : ingredientDtos) { // list
if (rid == null) continue; RecipeIngredient existingRI = existingRecipe.getRecipeIngredients().stream()
if (rid.getIngredientName() == null || rid.getIngredientName().isBlank()) continue; .filter(ri -> ri.getIngredient().getName().equals(riDto.getIngredientName())).findFirst()
.orElse(null);
Ingredient ing = ingredientRepo.findByNameIgnoreCase(rid.getIngredientName().trim()) // if old ingredient just update parameters
.orElseGet(() -> { if (existingRI != null) {
Ingredient i = new Ingredient();
i.setName(rid.getIngredientName().trim());
return ingredientRepo.save(i);
});
RecipeIngredient ri = new RecipeIngredient(); existingRI.setQuantity(riDto.getQuantity());
ri.setRecipe(recipe); existingRI.setUnit(riDto.getUnit());
ri.setIngredient(ing); existingRI.setNotes(riDto.getNotes());
ri.setQuantity(rid.getQuantity() == null ? BigDecimal.ZERO : rid.getQuantity()); }
ri.setUnit(rid.getUnit());
ri.setNotes(rid.getNotes());
recipe.getRecipeIngredients().add(ri); // 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);
}
} }