Update 17 files

- /src/main/resources/application.properties
- /src/main/resources/templates/view-recipe.html
- /src/main/resources/templates/create-recipe.html
- /src/main/resources/templates/home.html
- /src/main/resources/templates/explore.html
- /src/main/resources/templates/public-profile.html
- /src/main/resources/templates/my-profile.html
- /src/main/resources/templates/update-recipe.html
- /src/main/resources/static/css/view-recipe.css
- /src/main/java/com/example/demo/controller/SiteController.java
- /src/main/java/com/example/demo/controller/RecipeUploadController.java
- /src/main/java/com/example/demo/controller/RecipeUploadForm.java
- /src/main/java/com/example/demo/config/WebConfig
- /src/main/java/com/example/demo/config/SecurityConfig.java
- /src/main/java/com/example/demo/config/WebConfig.java
- /src/main/java/com/example/demo/service/Impl/RecipeServiceImpl.java
- /src/main/java/com/example/demo/service/RecipeService.java
This commit is contained in:
Madeleine Stamp
2026-04-21 13:37:02 -06:00
parent ce3cea01c3
commit 267d5c7bdf
17 changed files with 924 additions and 327 deletions
@@ -19,15 +19,23 @@ public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/register", "/css/**", "/images/**").permitAll()
// public pages and static files
.requestMatchers("/", "/login", "/register", "/css/**", "/images/**").permitAll()
.requestMatchers("/api/users").permitAll()
.requestMatchers("/users/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
// protected create recipe routes
.requestMatchers("/create", "/api/recipes/upload").authenticated()
// protected my profile routes
.requestMatchers("/my-profile", "/my-profile/update").authenticated()
// everything else public
.anyRequest().permitAll()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/", true)
.defaultSuccessUrl("/")
.permitAll()
)
.logout(logout -> logout.permitAll());
@@ -0,0 +1,20 @@
package com.example.demo.config;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
Path uploadPath = Paths.get("uploads").toAbsolutePath().normalize();
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:///" + uploadPath.toString().replace("\\", "/") + "/");
}
}
@@ -0,0 +1,212 @@
package com.example.demo.controller;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.Principal;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.dto.ImageDto;
import com.example.demo.dto.RecipeDto;
import com.example.demo.dto.RecipeIngredientDto;
import com.example.demo.dto.StepDto;
import com.example.demo.dto.TagDto;
import com.example.demo.service.RecipeService;
@RestController
public class RecipeUploadController {
private final RecipeService recipeService;
public RecipeUploadController(RecipeService recipeService) {
this.recipeService = recipeService;
}
@PostMapping(value = "/api/recipes/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<?> createRecipeWithUpload(@ModelAttribute RecipeUploadForm form, Principal principal) {
try {
System.out.println("UPLOAD ENDPOINT HIT");
System.out.println("Title: " + form.getTitle());
System.out.println("Image present: " + (form.getImage() != null && !form.getImage().isEmpty()));
RecipeDto dto = buildRecipeDto(form, true);
System.out.println("Image DTO count: " + (dto.getImages() == null ? 0 : dto.getImages().size()));
String currentUsername = principal.getName();
RecipeDto saved = recipeService.saveRecipe(dto, currentUsername);
System.out.println("Saved recipe id: " + saved.getId());
return new ResponseEntity<>(saved, HttpStatus.CREATED);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Recipe creation failed: " + e.getMessage());
}
}
@PostMapping(value = "/api/recipes/{id}/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<?> updateRecipeWithUpload(
@PathVariable Integer id,
@ModelAttribute RecipeUploadForm form,
Principal principal) {
try {
System.out.println("UPDATE UPLOAD ENDPOINT HIT");
System.out.println("Recipe id: " + id);
System.out.println("Title: " + form.getTitle());
System.out.println("Image present: " + (form.getImage() != null && !form.getImage().isEmpty()));
System.out.println("Remove image: " + form.getRemoveImage());
RecipeDto dto = buildRecipeDto(form, false);
String currentUsername = principal.getName();
RecipeDto updated = recipeService.updateRecipe(dto, id, currentUsername);
System.out.println("Updated recipe id: " + updated.getId());
return new ResponseEntity<>(updated, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Recipe update failed: " + e.getMessage());
}
}
private RecipeDto buildRecipeDto(RecipeUploadForm form, boolean isCreate) throws IOException {
RecipeDto dto = new RecipeDto();
dto.setTitle(safeTrim(form.getTitle()));
dto.setDescription(safeTrim(form.getDescription()));
dto.setPrepTimeMinutes(parseInteger(form.getPrepTimeMinutes()));
dto.setCookTimeMinutes(parseInteger(form.getCookTimeMinutes()));
dto.setServings(parseInteger(form.getServings()));
dto.setStatus("DRAFT");
dto.setCost(parseInteger(form.getCost()));
List<RecipeIngredientDto> ingredientDtos = new ArrayList<>();
if (form.getIngredientName() != null) {
for (int i = 0; i < form.getIngredientName().size(); i++) {
String ingredientName = getListValue(form.getIngredientName(), i);
if (ingredientName == null || ingredientName.isBlank()) {
continue;
}
String quantityString = getListValue(form.getIngredientQuantity(), i);
BigDecimal quantity = null;
if (quantityString != null && !quantityString.isBlank()) {
quantity = new BigDecimal(quantityString.trim());
}
String unit = getListValue(form.getIngredientUnit(), i);
String notes = getListValue(form.getIngredientNotes(), i);
ingredientDtos.add(new RecipeIngredientDto(
ingredientName.trim(),
quantity,
unit != null ? unit.trim() : "",
notes != null ? notes.trim() : ""));
}
}
dto.setIngredients(ingredientDtos);
List<StepDto> stepDtos = new ArrayList<>();
if (form.getStepInstruction() != null) {
for (int i = 0; i < form.getStepInstruction().size(); i++) {
String instruction = form.getStepInstruction().get(i);
if (instruction == null || instruction.isBlank()) {
continue;
}
stepDtos.add(new StepDto(i + 1, instruction.trim()));
}
}
dto.setSteps(stepDtos);
List<TagDto> tagDtos = new ArrayList<>();
if (form.getTags() != null) {
for (String tag : form.getTags()) {
if (tag != null && !tag.isBlank()) {
tagDtos.add(new TagDto(tag.trim()));
}
}
}
dto.setTags(tagDtos);
MultipartFile imageFile = form.getImage();
boolean removeImage = Boolean.TRUE.equals(form.getRemoveImage());
if (imageFile != null && !imageFile.isEmpty()) {
List<ImageDto> imageDtos = new ArrayList<>();
String imageUrl = saveUploadedFile(imageFile);
imageDtos.add(new ImageDto(imageUrl));
dto.setImages(imageDtos);
System.out.println("Saved file path: " + imageUrl);
} else if (removeImage) {
// Empty list means remove all images on update
dto.setImages(new ArrayList<>());
} else if (isCreate) {
dto.setImages(new ArrayList<>());
} else {
// Null on update means keep the existing image as-is
dto.setImages(null);
}
return dto;
}
private Integer parseInteger(String value) {
if (value == null || value.isBlank()) {
return null;
}
return Integer.valueOf(value.trim());
}
private String getListValue(List<String> list, int index) {
if (list == null || index >= list.size()) {
return null;
}
return list.get(index);
}
private String safeTrim(String value) {
return value == null ? null : value.trim();
}
private String saveUploadedFile(MultipartFile file) throws IOException {
String originalFilename = StringUtils.cleanPath(file.getOriginalFilename());
String extension = "";
int dotIndex = originalFilename.lastIndexOf('.');
if (dotIndex >= 0) {
extension = originalFilename.substring(dotIndex);
}
String storedFilename = UUID.randomUUID() + extension;
Path uploadDir = Paths.get("uploads");
if (!Files.exists(uploadDir)) {
Files.createDirectories(uploadDir);
}
Path destination = uploadDir.resolve(storedFilename);
Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
return "/uploads/" + storedFilename;
}
}
@@ -0,0 +1,141 @@
package com.example.demo.controller;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
public class RecipeUploadForm {
private String title;
private String description;
private String prepTimeMinutes;
private String cookTimeMinutes;
private String servings;
private String cost;
private List<String> ingredientName;
private List<String> ingredientQuantity;
private List<String> ingredientUnit;
private List<String> ingredientNotes;
private List<String> stepInstruction;
private List<String> tags;
private MultipartFile image;
private Boolean removeImage;
public RecipeUploadForm() {
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getPrepTimeMinutes() {
return prepTimeMinutes;
}
public void setPrepTimeMinutes(String prepTimeMinutes) {
this.prepTimeMinutes = prepTimeMinutes;
}
public String getCookTimeMinutes() {
return cookTimeMinutes;
}
public void setCookTimeMinutes(String cookTimeMinutes) {
this.cookTimeMinutes = cookTimeMinutes;
}
public String getServings() {
return servings;
}
public void setServings(String servings) {
this.servings = servings;
}
public String getCost() {
return cost;
}
public void setCost(String cost) {
this.cost = cost;
}
public List<String> getIngredientName() {
return ingredientName;
}
public void setIngredientName(List<String> ingredientName) {
this.ingredientName = ingredientName;
}
public List<String> getIngredientQuantity() {
return ingredientQuantity;
}
public void setIngredientQuantity(List<String> ingredientQuantity) {
this.ingredientQuantity = ingredientQuantity;
}
public List<String> getIngredientUnit() {
return ingredientUnit;
}
public void setIngredientUnit(List<String> ingredientUnit) {
this.ingredientUnit = ingredientUnit;
}
public List<String> getIngredientNotes() {
return ingredientNotes;
}
public void setIngredientNotes(List<String> ingredientNotes) {
this.ingredientNotes = ingredientNotes;
}
public List<String> getStepInstruction() {
return stepInstruction;
}
public void setStepInstruction(List<String> stepInstruction) {
this.stepInstruction = stepInstruction;
}
public List<String> getTags() {
return tags;
}
public void setTags(List<String> tags) {
this.tags = tags;
}
public MultipartFile getImage() {
return image;
}
public void setImage(MultipartFile image) {
this.image = image;
}
public Boolean getRemoveImage() {
return removeImage;
}
public void setRemoveImage(Boolean removeImage) {
this.removeImage = removeImage;
}
}
@@ -4,11 +4,16 @@ import java.util.List;
import com.example.demo.service.RecipeService;
import com.example.demo.dto.RecipeDto;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
@Controller
public class SiteController {
@@ -55,6 +60,13 @@ public class SiteController {
return "update-recipe";
}
@GetMapping("/update-recipe/{id}")
public String showUpdateRecipePage(@PathVariable Integer id, Model model) {
RecipeDto recipe = recipeService.getRecipeById(id);
model.addAttribute("recipe", recipe);
return "update-recipe";
}
@GetMapping("/explore")
public String explore(
@RequestParam(required = false) String q,
@@ -70,4 +82,11 @@ public class SiteController {
model.addAttribute("tags", tags);
return "explore";
}
}
@PostMapping("/api/recipes/{id}/image")
@ResponseBody
public ResponseEntity<?> updateRecipeImage(@PathVariable Integer id,
@RequestParam("image") MultipartFile image) {
recipeService.updateRecipeImage(id, image);
return ResponseEntity.ok().build();
}}
@@ -7,6 +7,7 @@ import java.util.stream.Collectors;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.dto.RecipeDto;
import com.example.demo.dto.UserDto;
@@ -57,6 +58,13 @@ public class RecipeServiceImpl implements RecipeService {
this.tagRepository = tagRepository;
}
@Override
public void updateRecipeImage(Integer id, MultipartFile image) {
if (image == null || image.isEmpty()) {
return;
}
}
@Override
public RecipeDto convertToDto(Recipe recipe) {
List<RecipeIngredientDto> ingredientDtos = recipe.getRecipeIngredients().stream()
@@ -236,52 +244,55 @@ public class RecipeServiceImpl implements RecipeService {
List<TagDto> updatedTags = recipeDto.getTags();
List<Tag> tagsToRemove = new ArrayList<>();
for (RecipeIngredient ri : existingRecipe.getRecipeIngredients()) {
if (updatedIngredients != null) {
for (RecipeIngredient ri : existingRecipe.getRecipeIngredients()) {
boolean existsInUpdatedList = false;
for (RecipeIngredientDto dto : updatedIngredients) {
String updatedName = dto.getIngredientName();
String existingName = ri.getIngredient().getName();
boolean existsInUpdatedList = false;
for (RecipeIngredientDto dto : updatedIngredients) {
String updatedName = dto.getIngredientName();
String existingName = ri.getIngredient().getName();
if (updatedName.equals(existingName)) {
existsInUpdatedList = true;
break;
if (java.util.Objects.equals(updatedName, existingName)) {
existsInUpdatedList = true;
break;
}
}
if (!existsInUpdatedList) {
ingredientsToRemove.add(ri);
}
}
if (!existsInUpdatedList) {
ingredientsToRemove.add(ri);
}
}
existingRecipe.getRecipeIngredients().removeAll(ingredientsToRemove);
existingRecipe.getRecipeIngredients().removeAll(ingredientsToRemove);
for (RecipeIngredientDto riDto : updatedIngredients) {
for (RecipeIngredientDto riDto : updatedIngredients) {
RecipeIngredient existingRI = existingRecipe.getRecipeIngredients().stream()
.filter(ri -> java.util.Objects.equals(ri.getIngredient().getName(), riDto.getIngredientName()))
.findFirst()
.orElse(null);
RecipeIngredient existingRI = existingRecipe.getRecipeIngredients().stream()
.filter(ri -> ri.getIngredient().getName().equals(riDto.getIngredientName())).findFirst()
.orElse(null);
if (existingRI != null) {
if (existingRI != null) {
existingRI.setQuantity(riDto.getQuantity());
existingRI.setUnit(riDto.getUnit());
existingRI.setNotes(riDto.getNotes());
}
else {
Ingredient ingredient = ingredientRepository.findByNameIgnoreCase(riDto.getIngredientName())
.orElseGet(() -> new Ingredient(riDto.getIngredientName()));
if (ingredient.getId() == null) {
ingredientRepository.save(ingredient);
existingRI.setQuantity(riDto.getQuantity());
existingRI.setUnit(riDto.getUnit());
existingRI.setNotes(riDto.getNotes());
}
RecipeIngredient newRI = new RecipeIngredient(existingRecipe, ingredient, riDto.getQuantity(),
riDto.getUnit(), riDto.getNotes());
else {
existingRecipe.getRecipeIngredients().add(newRI);
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);
}
}
}
@@ -314,7 +325,7 @@ public class RecipeServiceImpl implements RecipeService {
if (updatedImages != null) {
for (Image image : existingRecipe.getImages()) {
boolean existsInUpdatedList = updatedImages.stream()
.anyMatch(dto -> dto.getImageUrl().equals(image.getImageUrl()));
.anyMatch(dto -> java.util.Objects.equals(dto.getImageUrl(), image.getImageUrl()));
if (!existsInUpdatedList)
imagesToRemove.add(image);
}
@@ -323,7 +334,9 @@ public class RecipeServiceImpl implements RecipeService {
for (ImageDto imageDto : updatedImages) {
Image existingImage = existingRecipe.getImages().stream()
.filter(img -> img.getImageUrl().equals(imageDto.getImageUrl())).findFirst().orElse(null);
.filter(img -> java.util.Objects.equals(img.getImageUrl(), imageDto.getImageUrl()))
.findFirst()
.orElse(null);
if (existingImage != null) {
existingImage.setImageUrl(imageDto.getImageUrl());
@@ -336,7 +349,8 @@ public class RecipeServiceImpl implements RecipeService {
if (updatedTags != null) {
for (Tag tag : existingRecipe.getTags()) {
boolean existsInUpdatedList = updatedTags.stream().anyMatch(dto -> dto.getName().equals(tag.getName()));
boolean existsInUpdatedList = updatedTags.stream()
.anyMatch(dto -> java.util.Objects.equals(dto.getName(), tag.getName()));
if (!existsInUpdatedList)
tagsToRemove.add(tag);
}
@@ -345,7 +359,9 @@ public class RecipeServiceImpl implements RecipeService {
for (TagDto tagDto : updatedTags) {
Tag existingTag = existingRecipe.getTags().stream()
.filter(tag -> tag.getName().equals(tagDto.getName())).findFirst().orElse(null);
.filter(tag -> java.util.Objects.equals(tag.getName(), tagDto.getName()))
.findFirst()
.orElse(null);
if (existingTag != null) {
existingTag.setName(tagDto.getName());
@@ -356,7 +372,7 @@ public class RecipeServiceImpl implements RecipeService {
existingRecipe.getTags().add(newTag);
}
}
}
}
recipeRepository.save(existingRecipe);
return convertToDto(existingRecipe);
}
@@ -3,6 +3,7 @@ package com.example.demo.service;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.web.multipart.MultipartFile;
import com.example.demo.dto.RecipeDto;
import com.example.demo.entity.Recipe;
@@ -23,5 +24,6 @@ public interface RecipeService {
void deleteRecipe(Integer Id, String currentUsername);
}
void updateRecipeImage(Integer id, MultipartFile image);
}