diff --git a/demo/src/main/java/com/example/demo/dto/RecipeDto.java b/demo/src/main/java/com/example/demo/dto/RecipeDto.java index 8ddae95..ff5cb80 100644 --- a/demo/src/main/java/com/example/demo/dto/RecipeDto.java +++ b/demo/src/main/java/com/example/demo/dto/RecipeDto.java @@ -12,6 +12,7 @@ public class RecipeDto { private Integer servings; private UserDto userDto; private String status; + private Integer id; private List ingredients; private List steps; private List images; @@ -91,6 +92,10 @@ public class RecipeDto { public List getIngredients() { return ingredients; } + + public Integer getId() { + return id; + } public void setIngredients(List ingredients) { this.ingredients = ingredients; @@ -127,5 +132,8 @@ public class RecipeDto { public void setUserDto(UserDto userDto) { this.userDto = userDto; } - + + public void setId(Integer id) { + this.id = id; + } } diff --git a/demo/src/main/java/com/example/demo/repository/IngredientRepo.java b/demo/src/main/java/com/example/demo/repository/IngredientRepo.java index a45192a..0420f12 100644 --- a/demo/src/main/java/com/example/demo/repository/IngredientRepo.java +++ b/demo/src/main/java/com/example/demo/repository/IngredientRepo.java @@ -6,5 +6,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import com.example.demo.entity.Ingredient; public interface IngredientRepo extends JpaRepository { - Optional findByName(String name); + Optional findByNameIgnoreCase(String name); } \ No newline at end of file diff --git a/demo/src/main/java/com/example/demo/repository/RecipeIngredientRepo.java b/demo/src/main/java/com/example/demo/repository/RecipeIngredientRepo.java index e121dc5..e97f96a 100644 --- a/demo/src/main/java/com/example/demo/repository/RecipeIngredientRepo.java +++ b/demo/src/main/java/com/example/demo/repository/RecipeIngredientRepo.java @@ -7,5 +7,6 @@ import com.example.demo.entity.RecipeIngredient; public interface RecipeIngredientRepo extends JpaRepository { // Custom query: find all ingredients for a recipe + List findByRecipeId(Integer recipeId); void deleteByRecipe(Recipe recipe); } \ No newline at end of file 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 57bb65b..6b922d2 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,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 RecipeDto convertToDto(Recipe recipe) { - List ingredientDtos = recipe.getRecipeIngredients().stream() - .map(ri -> new RecipeIngredientDto(ri.getIngredient().getName(), ri.getQuantity(), ri.getUnit(), - ri.getNotes())) - .toList(); + 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; + } - List stepDtos = recipe.getSteps().stream() - .map(ri -> new StepDto(ri.getStepNumber(), ri.getInstruction())).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 imageDtos = recipe.getImages().stream().map(ri -> new ImageDto(ri.getImageUrl())).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 tagDtos = recipe.getTags().stream().map(ri -> new TagDto(ri.getName())).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<>()); - UserDto userDto = new UserDto(recipe.getUser().getId(), recipe.getUser().getUsername(), - recipe.getUser().getEmail()); + // 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); + } - return new RecipeDto(recipe.getTitle(), recipe.getDescription(), recipe.getPrepTimeMinutes(), - recipe.getCookTimeMinutes(), recipe.getServings(), userDto, recipe.getStatus(), ingredientDtos, - stepDtos, imageDtos, tagDtos); - } + return dto; + } + + + + // -------------------- CREATE -------------------- + @Override + @Transactional + public RecipeDto saveRecipe(RecipeDto recipeDto) { - @Override - public RecipeDto saveRecipe(RecipeDto dto) { + List ingredientDtos = + recipeDto.getIngredients() == null ? Collections.emptyList() : recipeDto.getIngredients(); - User user = userRepository.findById(dto.getUserDto().getId()) - .orElseThrow(() -> new NotFoundException("User", "id", dto.getUserDto().getId())); + List stepDtos = + recipeDto.getSteps() == null ? Collections.emptyList() : recipeDto.getSteps(); - Recipe recipe = new Recipe(dto.getTitle(), dto.getDescription(), dto.getPrepTimeMinutes(), - dto.getCookTimeMinutes(), dto.getServings(), user, dto.getStatus()); + 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()); - for (RecipeIngredientDto riDto : dto.getIngredients()) { + // REQUIRED user_id + recipe.setUser(resolveUser(recipeDto.getUserDto(), null)); - Ingredient ingredient = ingredientRepository.findByName(riDto.getIngredientName()) - .orElseGet(() -> new Ingredient(riDto.getIngredientName())); + // attach children (cascade = ALL on Recipe will persist them) + attachSteps(recipe, stepDtos); + attachRecipeIngredients(recipe, ingredientDtos); - if (ingredient.getId() == null) { - ingredientRepository.save(ingredient); - } + Recipe saved = recipeRepo.save(recipe); + return getRecipeById(saved.getId()); + } - RecipeIngredient ri = new RecipeIngredient(recipe, ingredient, riDto.getQuantity(), riDto.getUnit(), - riDto.getNotes()); + // -------------------- 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()); + } - recipe.getRecipeIngredients().add(ri); - } + // -------------------- READ ONE -------------------- + @Override + @org.springframework.transaction.annotation.Transactional(readOnly = true) + public RecipeDto getRecipeById(Integer recipeId) { + Recipe recipe = recipeRepo.findById(recipeId) + .orElseThrow(() -> new NotFoundException("Recipe not found")); - if (dto.getSteps() != null) { - for (StepDto stepDto : dto.getSteps()) { - Step step = new Step(recipe, stepDto.getStepNumber(), stepDto.getInstruction()); - recipe.getSteps().add(step); - } - } + // Start with base fields + RecipeDto dto = convertToDto(recipe); - if (dto.getImages() != null) { - for (ImageDto imageDto : dto.getImages()) { - Image image = new Image(recipe, imageDto.getImageUrl()); - recipe.getImages().add(image); - } - } + // 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()) + ); - for (TagDto tDto : dto.getTags()) { + 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()) + ); - Tag tag = tagRepository.findByName(tDto.getName()).orElseGet(() -> new Tag(tDto.getName())); + return dto; + } + // -------------------- UPDATE -------------------- + @Override + @Transactional + public RecipeDto updateRecipe(RecipeDto recipeDto, Integer id) { - if (tag.getId() == null) { - tagRepository.save(tag); - } - recipe.getTags().add(tag); - } + Recipe recipe = recipeRepo.findById(id) + .orElseThrow(() -> new NotFoundException("Recipe not found")); - Recipe saved = recipeRepository.save(recipe); + List ingredientDtos = + recipeDto.getIngredients() == null ? Collections.emptyList() : recipeDto.getIngredients(); - return convertToDto(saved); - } + List stepDtos = + recipeDto.getSteps() == null ? Collections.emptyList() : recipeDto.getSteps(); - @Override - @Transactional - public List getAllRecipes() { + 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 list = new ArrayList<>(); - for (Recipe recipe : recipeRepository.findAll()) { - RecipeDto recipeDto = convertToDto(recipe); - list.add(recipeDto); - } + // keep old user if not provided + recipe.setUser(resolveUser(recipeDto.getUserDto(), recipe.getUser())); - return list; - } + // Clear old children (orphanRemoval = true will delete) + recipe.getSteps().clear(); + recipe.getRecipeIngredients().clear(); - @Override - @Transactional - public RecipeDto getRecipeById(Integer Id) { - return convertToDto(recipeRepository.findById(Id).orElseThrow(() -> new NotFoundException("Recipe", "id", Id))); - } + attachSteps(recipe, stepDtos); + attachRecipeIngredients(recipe, ingredientDtos); - @Override - @Transactional - public RecipeDto updateRecipe(RecipeDto recipeDto, Integer id) { - Recipe existingRecipe = recipeRepository.findById(id) - .orElseThrow(() -> new NotFoundException("Recipe", "id", id)); + Recipe saved = recipeRepo.save(recipe); + return getRecipeById(saved.getId()); + } - existingRecipe.setTitle(recipeDto.getTitle()); - existingRecipe.setDescription(recipeDto.getDescription()); - existingRecipe.setPrepTimeMinutes(recipeDto.getPrepTimeMinutes()); - existingRecipe.setCookTimeMinutes(recipeDto.getCookTimeMinutes()); - existingRecipe.setServings(recipeDto.getServings()); - existingRecipe.setStatus(recipeDto.getStatus()); + // -------------------- DELETE -------------------- + @Override + @Transactional + public void deleteRecipe(Integer id) { + if (!recipeRepo.existsById(id)) { + throw new NotFoundException("Recipe not found"); + } + recipeRepo.deleteById(id); + } - List updatedIngredients = recipeDto.getIngredients(); - List ingredientsToRemove = new ArrayList<>(); + // -------------------- Helpers -------------------- - List updatedSteps = recipeDto.getSteps(); - List stepsToRemove = 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())); + } + if (existing != null) return existing; - List updatedImages = recipeDto.getImages(); - List imagesToRemove = 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 updatedTags = recipeDto.getTags(); - List tagsToRemove = 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; - for (RecipeIngredient ri : existingRecipe.getRecipeIngredients()) { + Step s = new Step(); + s.setRecipe(recipe); + s.setStepNumber(sd.getStepNumber() == null ? 1 : sd.getStepNumber()); + s.setInstruction(sd.getInstruction()); - boolean existsInUpdatedList = false; - for (RecipeIngredientDto dto : updatedIngredients) { - String updatedName = dto.getIngredientName(); - String existingName = ri.getIngredient().getName(); + recipe.getSteps().add(s); + } + } - if (updatedName.equals(existingName)) { - existsInUpdatedList = true; - break; - } - } + private void attachRecipeIngredients(Recipe recipe, List ingredientDtos) { + for (RecipeIngredientDto rid : ingredientDtos) { + if (rid == null) continue; + if (rid.getIngredientName() == null || rid.getIngredientName().isBlank()) continue; - if (!existsInUpdatedList) { - ingredientsToRemove.add(ri); - } - } + Ingredient ing = ingredientRepo.findByNameIgnoreCase(rid.getIngredientName().trim()) + .orElseGet(() -> { + Ingredient i = new Ingredient(); + i.setName(rid.getIngredientName().trim()); + return ingredientRepo.save(i); + }); - existingRecipe.getRecipeIngredients().removeAll(ingredientsToRemove); + 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()); - for (RecipeIngredientDto riDto : updatedIngredients) { + recipe.getRecipeIngredients().add(ri); + } + } + /* + private Ingredient attachRecipeIngredients(Recipe recipe, List ingredientDtos) { + for (RecipeIngredientDto rid : ingredientDtos) { + if (rid == null) continue; + if (rid.getIngredientName() == null || rid.getIngredientName().isBlank()) continue; - // 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); + Ingredient ing = ingredientRepo.findByNameIgnoreCase(rid.getIngredientName().trim()) + .orElseGet(() -> { + Ingredient i = new Ingredient(); + i.setName(rid.getIngredientName().trim()); + return ingredientRepo.save(i); + }); - // if old ingredient just update parameters - if (existingRI != null) { + 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()); - 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); - } - - @Override - public void deleteRecipe(Integer Id) { - recipeRepository.findById(Id).orElseThrow(() -> new NotFoundException("Recipe", "id", Id)); - recipeRepository.deleteById(Id); - - } -} + recipe.getRecipeIngredients().add(ri); + } + return null; + }*/ +} \ No newline at end of file