Update 4 files

- /demo/src/main/java/com/example/demo/service/Impl/RecipeServiceImpl.java
- /demo/src/main/java/com/example/demo/dto/RecipeDto.java
- /demo/src/main/java/com/example/demo/repository/IngredientRepo.java
- /demo/src/main/java/com/example/demo/repository/RecipeIngredientRepo.java
This commit is contained in:
Madeleine Stamp
2026-03-04 13:35:40 -07:00
parent 4bdb449f47
commit 9335314e1f
4 changed files with 247 additions and 269 deletions
@@ -12,6 +12,7 @@ public class RecipeDto {
private Integer servings;
private UserDto userDto;
private String status;
private Integer id;
private List<RecipeIngredientDto> ingredients;
private List<StepDto> steps;
private List<ImageDto> images;
@@ -92,6 +93,10 @@ public class RecipeDto {
return ingredients;
}
public Integer getId() {
return id;
}
public void setIngredients(List<RecipeIngredientDto> ingredients) {
this.ingredients = ingredients;
}
@@ -128,4 +133,7 @@ public class RecipeDto {
this.userDto = userDto;
}
public void setId(Integer id) {
this.id = id;
}
}
@@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
import com.example.demo.entity.Ingredient;
public interface IngredientRepo extends JpaRepository<Ingredient, Integer> {
Optional<Ingredient> findByName(String name);
Optional<Ingredient> findByNameIgnoreCase(String name);
}
@@ -7,5 +7,6 @@ import com.example.demo.entity.RecipeIngredient;
public interface RecipeIngredientRepo extends JpaRepository<RecipeIngredient, Integer> {
// Custom query: find all ingredients for a recipe
List<RecipeIngredient> findByRecipeId(Integer recipeId);
void deleteByRecipe(Recipe recipe);
}
@@ -1,319 +1,288 @@
package com.example.demo.service.Impl;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
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.dto.StepDto;
import com.example.demo.dto.UserDto;
import com.example.demo.entity.*;
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.repository.*;
import com.example.demo.service.RecipeService;
import jakarta.transaction.Transactional;
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;
@Service
public class RecipeServiceImpl implements RecipeService {
private RecipeRepo recipeRepository;
private IngredientRepo ingredientRepository;
private RecipeIngredientRepo recipeIngredientRepository;
private UserRepo userRepository;
private StepRepo stepRepository;
private ImageRepo imageRepository;
private TagRepo tagRepository;
private final RecipeRepo recipeRepo;
private final UserRepo userRepo;
private final IngredientRepo ingredientRepo;
private final StepRepo stepRepo;
private final RecipeIngredientRepo recipeIngredientRepo;
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;
// DEV DEFAULT — must exist in DB
private static final int DEFAULT_USER_ID = 1;
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;
}
// -------------------- 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) {
List<RecipeIngredientDto> ingredientDtos = recipe.getRecipeIngredients().stream()
.map(ri -> new RecipeIngredientDto(ri.getIngredient().getName(), ri.getQuantity(), ri.getUnit(),
ri.getNotes()))
.toList();
RecipeDto dto = new RecipeDto();
List<StepDto> 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<ImageDto> 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<TagDto> tagDtos = recipe.getTags().stream().map(ri -> new TagDto(ri.getName())).toList();
UserDto userDto = new UserDto(recipe.getUser().getId(), recipe.getUser().getUsername(),
recipe.getUser().getEmail());
return new RecipeDto(recipe.getTitle(), recipe.getDescription(), recipe.getPrepTimeMinutes(),
recipe.getCookTimeMinutes(), recipe.getServings(), userDto, recipe.getStatus(), ingredientDtos,
stepDtos, imageDtos, tagDtos);
// 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);
}
@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());
for (RecipeIngredientDto riDto : dto.getIngredients()) {
Ingredient ingredient = ingredientRepository.findByName(riDto.getIngredientName())
.orElseGet(() -> new Ingredient(riDto.getIngredientName()));
if (ingredient.getId() == null) {
ingredientRepository.save(ingredient);
return dto;
}
RecipeIngredient ri = new RecipeIngredient(recipe, ingredient, riDto.getQuantity(), riDto.getUnit(),
riDto.getNotes());
recipe.getRecipeIngredients().add(ri);
}
if (dto.getSteps() != null) {
for (StepDto stepDto : dto.getSteps()) {
Step step = new Step(recipe, stepDto.getStepNumber(), stepDto.getInstruction());
recipe.getSteps().add(step);
}
}
if (dto.getImages() != null) {
for (ImageDto imageDto : dto.getImages()) {
Image image = new Image(recipe, imageDto.getImageUrl());
recipe.getImages().add(image);
}
}
for (TagDto tDto : dto.getTags()) {
Tag tag = tagRepository.findByName(tDto.getName()).orElseGet(() -> new Tag(tDto.getName()));
if (tag.getId() == null) {
tagRepository.save(tag);
}
recipe.getTags().add(tag);
}
Recipe saved = recipeRepository.save(recipe);
return convertToDto(saved);
}
// -------------------- CREATE --------------------
@Override
@Transactional
public RecipeDto saveRecipe(RecipeDto recipeDto) {
List<RecipeIngredientDto> ingredientDtos =
recipeDto.getIngredients() == null ? Collections.emptyList() : recipeDto.getIngredients();
List<StepDto> stepDtos =
recipeDto.getSteps() == null ? Collections.emptyList() : recipeDto.getSteps();
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());
// REQUIRED user_id
recipe.setUser(resolveUser(recipeDto.getUserDto(), null));
// attach children (cascade = ALL on Recipe will persist them)
attachSteps(recipe, stepDtos);
attachRecipeIngredients(recipe, ingredientDtos);
Recipe saved = recipeRepo.save(recipe);
return getRecipeById(saved.getId());
}
// -------------------- READ ALL --------------------
@Override
public List<RecipeDto> getAllRecipes() {
List<RecipeDto> list = new ArrayList<>();
for (Recipe recipe : recipeRepository.findAll()) {
RecipeDto recipeDto = convertToDto(recipe);
list.add(recipeDto);
}
return list;
// 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 --------------------
@Override
@Transactional
public RecipeDto getRecipeById(Integer Id) {
return convertToDto(recipeRepository.findById(Id).orElseThrow(() -> new NotFoundException("Recipe", "id", Id)));
}
@org.springframework.transaction.annotation.Transactional(readOnly = true)
public RecipeDto getRecipeById(Integer recipeId) {
Recipe recipe = recipeRepo.findById(recipeId)
.orElseThrow(() -> new NotFoundException("Recipe not found"));
// Start with base fields
RecipeDto dto = convertToDto(recipe);
// Overwrite lists using repo results (no mutation of Recipe.steps / recipeIngredients)
dto.setSteps(
stepRepo.findByRecipeId(recipeId).stream()
.map(s -> new com.example.demo.dto.StepDto(s.getStepNumber(), s.getInstruction()))
.collect(java.util.stream.Collectors.toList())
);
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())
);
return dto;
}
// -------------------- UPDATE --------------------
@Override
@Transactional
public RecipeDto updateRecipe(RecipeDto recipeDto, Integer id) {
Recipe existingRecipe = recipeRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Recipe", "id", 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());
Recipe recipe = recipeRepo.findById(id)
.orElseThrow(() -> new NotFoundException("Recipe not found"));
List<RecipeIngredientDto> updatedIngredients = recipeDto.getIngredients();
List<RecipeIngredient> ingredientsToRemove = new ArrayList<>();
List<RecipeIngredientDto> ingredientDtos =
recipeDto.getIngredients() == null ? Collections.emptyList() : recipeDto.getIngredients();
List<StepDto> updatedSteps = recipeDto.getSteps();
List<Step> stepsToRemove = new ArrayList<>();
List<StepDto> stepDtos =
recipeDto.getSteps() == null ? Collections.emptyList() : recipeDto.getSteps();
List<ImageDto> updatedImages = recipeDto.getImages();
List<Image> imagesToRemove = new ArrayList<>();
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());
List<TagDto> updatedTags = recipeDto.getTags();
List<Tag> tagsToRemove = new ArrayList<>();
// keep old user if not provided
recipe.setUser(resolveUser(recipeDto.getUserDto(), recipe.getUser()));
for (RecipeIngredient ri : existingRecipe.getRecipeIngredients()) {
// Clear old children (orphanRemoval = true will delete)
recipe.getSteps().clear();
recipe.getRecipeIngredients().clear();
boolean existsInUpdatedList = false;
for (RecipeIngredientDto dto : updatedIngredients) {
String updatedName = dto.getIngredientName();
String existingName = ri.getIngredient().getName();
attachSteps(recipe, stepDtos);
attachRecipeIngredients(recipe, ingredientDtos);
if (updatedName.equals(existingName)) {
existsInUpdatedList = true;
break;
}
}
if (!existsInUpdatedList) {
ingredientsToRemove.add(ri);
}
}
existingRecipe.getRecipeIngredients().removeAll(ingredientsToRemove);
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.findByName(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);
Recipe saved = recipeRepo.save(recipe);
return getRecipeById(saved.getId());
}
// -------------------- DELETE --------------------
@Override
public void deleteRecipe(Integer Id) {
recipeRepository.findById(Id).orElseThrow(() -> new NotFoundException("Recipe", "id", Id));
recipeRepository.deleteById(Id);
@Transactional
public void deleteRecipe(Integer id) {
if (!recipeRepo.existsById(id)) {
throw new NotFoundException("Recipe not found");
}
recipeRepo.deleteById(id);
}
// -------------------- Helpers --------------------
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()));
}
if (existing != null) return existing;
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."
));
}
private void attachSteps(Recipe recipe, List<StepDto> stepDtos) {
for (StepDto sd : stepDtos) {
if (sd == null) continue;
if (sd.getInstruction() == null || sd.getInstruction().isBlank()) continue;
Step s = new Step();
s.setRecipe(recipe);
s.setStepNumber(sd.getStepNumber() == null ? 1 : sd.getStepNumber());
s.setInstruction(sd.getInstruction());
recipe.getSteps().add(s);
}
}
private void attachRecipeIngredients(Recipe recipe, List<RecipeIngredientDto> ingredientDtos) {
for (RecipeIngredientDto rid : ingredientDtos) {
if (rid == null) continue;
if (rid.getIngredientName() == null || rid.getIngredientName().isBlank()) continue;
Ingredient ing = ingredientRepo.findByNameIgnoreCase(rid.getIngredientName().trim())
.orElseGet(() -> {
Ingredient i = new Ingredient();
i.setName(rid.getIngredientName().trim());
return ingredientRepo.save(i);
});
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());
recipe.getRecipeIngredients().add(ri);
}
}
/*
private Ingredient attachRecipeIngredients(Recipe recipe, List<RecipeIngredientDto> ingredientDtos) {
for (RecipeIngredientDto rid : ingredientDtos) {
if (rid == null) continue;
if (rid.getIngredientName() == null || rid.getIngredientName().isBlank()) continue;
Ingredient ing = ingredientRepo.findByNameIgnoreCase(rid.getIngredientName().trim())
.orElseGet(() -> {
Ingredient i = new Ingredient();
i.setName(rid.getIngredientName().trim());
return ingredientRepo.save(i);
});
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());
recipe.getRecipeIngredients().add(ri);
}
return null;
}*/
}