From 1b8513715dd3212ab69c9c4bee5895390d7384c3 Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Mon, 13 May 2024 00:29:16 +0300 Subject: [PATCH 01/18] GET /recipes endpoint implemented and entities structure rearranged for cuisine-dish relation. --- .../controllers/RecipeController.java | 14 +++++++--- .../com/group1/cuisines/dto/NewRecipeDto.java | 2 +- .../com/group1/cuisines/dto/RecipeDto.java | 23 ++++++++++++++++ .../com/group1/cuisines/entities/Cuisine.java | 16 +++++++++++- .../com/group1/cuisines/entities/Dish.java | 13 ++++++++++ .../com/group1/cuisines/entities/Recipe.java | 6 +++++ .../repositories/RecipeRepository.java | 26 ++++++++++++++++++- .../cuisines/services/RecipeService.java | 24 ++++++++++++++--- .../src/main/resources/application.properties | 8 ++++-- 9 files changed, 119 insertions(+), 13 deletions(-) create mode 100644 backend/src/main/java/com/group1/cuisines/dto/RecipeDto.java diff --git a/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java b/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java index 71804121..7623f0b8 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java @@ -1,8 +1,5 @@ package com.group1.cuisines.controllers; -import com.group1.cuisines.dto.CommentsDto; -import com.group1.cuisines.dto.NewRecipeDto; -import com.group1.cuisines.dto.RatingDto; -import com.group1.cuisines.dto.RecipeDetailDto; +import com.group1.cuisines.dto.*; import com.group1.cuisines.entities.Comment; import com.group1.cuisines.entities.User; import com.group1.cuisines.services.RecipeService; @@ -23,6 +20,15 @@ public class RecipeController { @Autowired private RecipeService recipeService; + + @GetMapping("/recipes") + public ResponseEntity> getRecipes(@RequestParam(required = false) String sort, + @RequestParam(required = false) String dishId, + @RequestParam(required = false) String cuisineId) { + List recipes = recipeService.findRecipes(sort, dishId, cuisineId); + return ResponseEntity.ok(recipes); + } + @PostMapping("/recipes") public ResponseEntity createRecipe(@RequestBody NewRecipeDto newRecipe) throws Exception{ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); diff --git a/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java b/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java index 1bc7faec..cc4c853a 100644 --- a/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java +++ b/backend/src/main/java/com/group1/cuisines/dto/NewRecipeDto.java @@ -14,5 +14,5 @@ public class NewRecipeDto { private int cookingTime; private int servingSize; private String dishId; - private List ingredients; // Nested DTOs for ingredients + private List ingredients; } diff --git a/backend/src/main/java/com/group1/cuisines/dto/RecipeDto.java b/backend/src/main/java/com/group1/cuisines/dto/RecipeDto.java new file mode 100644 index 00000000..bca92e79 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/RecipeDto.java @@ -0,0 +1,23 @@ +package com.group1.cuisines.dto; + +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RecipeDto { + private Integer id; + private String title; + private String instructions; + private int preparationTime; + private int cookingTime; + private int servingSize; + private double averageRating; + private String dishName; + + + + +} diff --git a/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java b/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java index 020aa4c3..6adc193a 100644 --- a/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java +++ b/backend/src/main/java/com/group1/cuisines/entities/Cuisine.java @@ -9,7 +9,9 @@ import java.net.URL; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Data @@ -32,8 +34,20 @@ public class Cuisine { inverseJoinColumns = @JoinColumn(name = "dish_id") ) - private List dishes = new ArrayList<>(); + private Set dishes = new HashSet<>(); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Cuisine cuisine = (Cuisine) o; + return id != null && id.equals(cuisine.id); + } + + @Override + public int hashCode() { + return 31; + } @Override public String toString() { return "Cuisine{" + diff --git a/backend/src/main/java/com/group1/cuisines/entities/Dish.java b/backend/src/main/java/com/group1/cuisines/entities/Dish.java index 71acad01..6e1790c9 100644 --- a/backend/src/main/java/com/group1/cuisines/entities/Dish.java +++ b/backend/src/main/java/com/group1/cuisines/entities/Dish.java @@ -34,6 +34,19 @@ public class Dish { @ManyToMany(mappedBy = "dishes") private List cuisines = new ArrayList<>(); + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Dish dish = (Dish) o; + return id != null && id.equals(dish.id); + } + + @Override + public int hashCode() { + return 31; + } + @Override public String toString() { return "Dish{" + diff --git a/backend/src/main/java/com/group1/cuisines/entities/Recipe.java b/backend/src/main/java/com/group1/cuisines/entities/Recipe.java index 10e33734..eadeb35c 100644 --- a/backend/src/main/java/com/group1/cuisines/entities/Recipe.java +++ b/backend/src/main/java/com/group1/cuisines/entities/Recipe.java @@ -4,6 +4,7 @@ import lombok.*; import java.util.ArrayList; +import java.util.Date; import java.util.List; @Data @@ -31,6 +32,11 @@ public class Recipe { @Builder.Default private List ratings = new ArrayList<>(); + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_at", nullable = false, updatable = false) + @org.hibernate.annotations.CreationTimestamp + private Date createdAt; + @ManyToOne @JoinColumn(name = "user_id", nullable = false) private User user; diff --git a/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java b/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java index 9d027dd9..c34d8c78 100644 --- a/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java +++ b/backend/src/main/java/com/group1/cuisines/repositories/RecipeRepository.java @@ -3,10 +3,34 @@ import com.group1.cuisines.entities.Recipe; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository -public interface RecipeRepository extends JpaRepository { +public interface RecipeRepository extends JpaRepository { + + + + + @Query("SELECT r FROM Recipe r WHERE " + + "(:dishId IS NULL OR r.dish.id = :dishId) AND " + + "(:cuisineId IS NULL OR EXISTS (SELECT d FROM Dish d JOIN d.cuisines c WHERE d = r.dish AND c.id = :cuisineId)) " + + "ORDER BY CASE WHEN :sort = 'recent' THEN r.createdAt ELSE r.averageRating END DESC") + List findByDishIdAndCuisineIdWithSort( + @Param("dishId") String dishId, + @Param("cuisineId") String cuisineId, + @Param("sort") String sort); + + // Fetch recipes by dishId and cuisineId + @Query("SELECT r FROM Recipe r JOIN r.dish d JOIN d.cuisines c WHERE d.id = :dishId AND c.id = :cuisineId ORDER BY r.createdAt DESC") + List findByDishIdAndCuisineIdOrderByCreatedAtDesc(String dishId, String cuisineId); + + @Query("SELECT r FROM Recipe r JOIN r.dish d JOIN d.cuisines c WHERE d.id = :dishId AND c.id = :cuisineId ORDER BY r.averageRating DESC") + List findByDishIdAndCuisineIdOrderByAverageRatingDesc(String dishId, String cuisineId); + @Query("SELECT r FROM Recipe r JOIN r.dish d JOIN d.cuisines c WHERE d.id = :dishId AND c.id = :cuisineId") + List findByDishIdAndCuisineId(String dishId, String cuisineId); @Query("SELECT AVG(r.ratingValue) FROM Rating r WHERE r.recipe.id = :recipeId") Double findAverageByRecipeId(Integer recipeId); } diff --git a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java index cc811ffe..316d7b1c 100644 --- a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java +++ b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java @@ -1,9 +1,6 @@ package com.group1.cuisines.services; -import com.group1.cuisines.dto.CommentsDto; -import com.group1.cuisines.dto.IngredientsDto; -import com.group1.cuisines.dto.NewRecipeDto; -import com.group1.cuisines.dto.RecipeDetailDto; +import com.group1.cuisines.dto.*; import com.group1.cuisines.entities.*; import com.group1.cuisines.repositories.*; import org.springframework.beans.factory.annotation.Autowired; @@ -35,6 +32,25 @@ public class RecipeService { @Autowired private CommentRepository commentRepository; + + + + public List findRecipes(String sort, String dishId, String cuisineId) { + List recipes = recipeRepository.findByDishIdAndCuisineIdWithSort(dishId, cuisineId, sort); + + return recipes.stream() + .map(recipe -> new RecipeDto( + recipe.getId(), + recipe.getTitle(), + recipe.getInstructions(), + recipe.getPreparationTime(), + recipe.getCookingTime(), + recipe.getServingSize(), + recipe.getAverageRating(), + recipe.getTitle())) + .collect(Collectors.toList()); + } + @Transactional public RecipeDetailDto createRecipe(NewRecipeDto newRecipe, String username) throws Exception { Optional user = userRepository.findByUsername(username); diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 14a044e0..a88f9b2c 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -5,11 +5,15 @@ jwt.signing.key=413F4428472B4B6250655368566D5970337336763979244226452948404D6351 spring.application.name=cuisines spring.datasource.url=jdbc:mysql://localhost:3306/cuisines-test?createDatabaseIfNotExist=true spring.datasource.username=root -spring.datasource.password=adminadmin +spring.datasource.password=admin spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true logging.level.com.group1.cuisines=DEBUG +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.use_sql_comments=true +logging.level.org.hibernate.type.descriptor.sql=trace + spring.banner.location=./banner.txt From 6b04d5be2e9d978d57ffefe3c8fef22c5527d62a Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Mon, 13 May 2024 12:11:36 +0300 Subject: [PATCH 02/18] GET /recipes/{recieId} endpoint implemented. --- .../controllers/RecipeController.java | 14 +++++++ .../dao/response/SuccessResponse.java | 9 +++++ .../com/group1/cuisines/dto/AuthorDto.java | 16 ++++++++ .../com/group1/cuisines/dto/CuisineDto.java | 13 +++++++ .../java/com/group1/cuisines/dto/DishDto.java | 10 +++++ .../group1/cuisines/dto/RecipeDetailsDto.java | 29 ++++++++++++++ .../cuisines/services/RecipeService.java | 39 +++++++++++++++++++ 7 files changed, 130 insertions(+) create mode 100644 backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java create mode 100644 backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java create mode 100644 backend/src/main/java/com/group1/cuisines/dto/CuisineDto.java create mode 100644 backend/src/main/java/com/group1/cuisines/dto/DishDto.java create mode 100644 backend/src/main/java/com/group1/cuisines/dto/RecipeDetailsDto.java diff --git a/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java b/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java index 7623f0b8..7f12b83b 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java @@ -1,4 +1,5 @@ package com.group1.cuisines.controllers; +import com.group1.cuisines.dao.response.SuccessResponse; import com.group1.cuisines.dto.*; import com.group1.cuisines.entities.Comment; import com.group1.cuisines.entities.User; @@ -21,6 +22,17 @@ public class RecipeController { private RecipeService recipeService; + @GetMapping("/recipes/{recipeId}") + public ResponseEntity getRecipeById(@PathVariable Integer recipeId) { + RecipeDetailsDto recipeDetails = recipeService.getRecipeById(recipeId); + if (recipeDetails != null) { + return ResponseEntity.ok(new SuccessResponse<>(recipeDetails, "Recipe fetched successfully")); + } else { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Recipe not found"); + } + } + + @GetMapping("/recipes") public ResponseEntity> getRecipes(@RequestParam(required = false) String sort, @RequestParam(required = false) String dishId, @@ -29,6 +41,8 @@ public ResponseEntity> getRecipes(@RequestParam(required = false return ResponseEntity.ok(recipes); } + + @PostMapping("/recipes") public ResponseEntity createRecipe(@RequestBody NewRecipeDto newRecipe) throws Exception{ Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); diff --git a/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java b/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java new file mode 100644 index 00000000..be965ff0 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java @@ -0,0 +1,9 @@ +package com.group1.cuisines.dao.response; +import lombok.*; +@Data +@AllArgsConstructor +public class SuccessResponse { + private T data; + private String message; + +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java b/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java new file mode 100644 index 00000000..d208f44e --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java @@ -0,0 +1,16 @@ +package com.group1.cuisines.dto; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AuthorDto { + private Integer id; + private String name; + private String username; + private Integer followersCount; + private Integer recipesCount; + +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/CuisineDto.java b/backend/src/main/java/com/group1/cuisines/dto/CuisineDto.java new file mode 100644 index 00000000..efdd9272 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/CuisineDto.java @@ -0,0 +1,13 @@ +package com.group1.cuisines.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CuisineDto { + private String id; + private String name; +} \ No newline at end of file diff --git a/backend/src/main/java/com/group1/cuisines/dto/DishDto.java b/backend/src/main/java/com/group1/cuisines/dto/DishDto.java new file mode 100644 index 00000000..93879540 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/DishDto.java @@ -0,0 +1,10 @@ +package com.group1.cuisines.dto; +import lombok.*; +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DishDto { + private String id; + private String name; + private String image; +} \ No newline at end of file diff --git a/backend/src/main/java/com/group1/cuisines/dto/RecipeDetailsDto.java b/backend/src/main/java/com/group1/cuisines/dto/RecipeDetailsDto.java new file mode 100644 index 00000000..5f5ec185 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/RecipeDetailsDto.java @@ -0,0 +1,29 @@ +package com.group1.cuisines.dto; + +import java.util.List; + +import com.group1.cuisines.entities.Ingredient; +import lombok.*; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RecipeDetailsDto { + private Integer id; + private String name; + // private String description; + private String instructions; + private List ingredients; + //private List images; + private Integer cookTime; + private Integer servingSize; + // private List allergens; + private CuisineDto cuisine; + private DishDto dish; + private Double avgRating; + + private AuthorDto author; + + + +} diff --git a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java index 316d7b1c..71ba3672 100644 --- a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java +++ b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java @@ -178,4 +178,43 @@ public List getCommentsByRecipeId(Integer recipeId) { .build()) .collect(Collectors.toList()); } + + public RecipeDetailsDto getRecipeById(Integer recipeId) { + Optional recipe = recipeRepository.findById(recipeId); + + + if (recipe.isPresent()) { + CuisineDto cuisineDto = new CuisineDto(); + Recipe r = recipe.get(); + if (r.getDish() != null && !r.getDish().getCuisines().isEmpty()) { + + cuisineDto.setId(r.getDish().getCuisines().get(0).getId()); + cuisineDto.setName(r.getDish().getCuisines().get(0).getName()); + + } + else if(r.getDish() != null && r.getDish().getCuisines().isEmpty()){ + cuisineDto.setId("No cuisine Id from wikidata"); + cuisineDto.setName("No cuisine name from wikidata"); + } + // Conversion from Recipe entity to RecipeDetailsDto + return new RecipeDetailsDto( + r.getId(), + r.getTitle(), + + r.getInstructions(), + r.getIngredients().stream().map(ingredient -> new IngredientsDto( ingredient.getName())).collect(Collectors.toList()), + + r.getCookingTime(), + r.getServingSize(), + cuisineDto, + + new DishDto(r.getDish().getId(), r.getDish().getName(), r.getDish().getImage()), + r.getAverageRating(), + new AuthorDto(r.getUser().getId(), r.getUser().getFirstName() , r.getUser().getUsername(), r.getUser().getFollowers().size(), r.getUser().getRecipeCount()) + + ); + } + return null; + } + } From 2c63f0c110e4f356f7db8ab771432576f232dde3 Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Mon, 13 May 2024 12:44:45 +0300 Subject: [PATCH 03/18] GET /feed endpoint implemented --- .../config/SecurityConfiguration.java | 1 + .../cuisines/controllers/FeedController.java | 45 +++++++++++++++++ .../com/group1/cuisines/entities/User.java | 12 +++-- .../cuisines/services/RecipeService.java | 49 +++++++++++++++++++ 4 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 backend/src/main/java/com/group1/cuisines/controllers/FeedController.java diff --git a/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java b/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java index 653b2358..7ee00e81 100644 --- a/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java +++ b/backend/src/main/java/com/group1/cuisines/config/SecurityConfiguration.java @@ -45,6 +45,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) .hasRole("ADMIN") // Require ADMIN role for "/api/v1/resources" .requestMatchers(HttpMethod.POST,"/**") .authenticated() + .requestMatchers("/feed?type=following").authenticated() .requestMatchers(HttpMethod.DELETE,"/**").authenticated()) // Require authentication for all other requests .sessionManagement( manager -> diff --git a/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java b/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java new file mode 100644 index 00000000..ff3fd957 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java @@ -0,0 +1,45 @@ +package com.group1.cuisines.controllers; + +import com.group1.cuisines.dao.response.SuccessResponse; +import com.group1.cuisines.dto.RecipeDetailsDto; +import com.group1.cuisines.services.RecipeService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; + +@RestController +@RequestMapping("/api/v1") +public class FeedController { + + @Autowired + private RecipeService recipeService; + + @GetMapping("/feed") + public ResponseEntity getFeed(@RequestParam String type, Authentication authentication) { + if (!"explore".equals(type) && !"following".equals(type)) { + return ResponseEntity.badRequest().body("Invalid type parameter."); + } + + if ("following".equals(type)) { + if (authentication == null || !authentication.isAuthenticated()) { + // Return an empty set and a message for unauthenticated users + return ResponseEntity.ok(new SuccessResponse<>(Collections.emptyList(), "No content available. Please log in and follow other users !.")); + } + // Fetch following users' recipes for authenticated users + String username = authentication.getName(); + List recipes = recipeService.getRecipesByType(type, username); + return ResponseEntity.ok(new SuccessResponse<>(recipes, "Recipes fetched successfully from followed users.")); + } + + // For 'explore', accessible to everyone + List recipes = recipeService.getRecipesByType(type, null); + return ResponseEntity.ok(new SuccessResponse<>(recipes, "Recipes fetched successfully.")); + } +} diff --git a/backend/src/main/java/com/group1/cuisines/entities/User.java b/backend/src/main/java/com/group1/cuisines/entities/User.java index 5e7d5923..53c31bdf 100644 --- a/backend/src/main/java/com/group1/cuisines/entities/User.java +++ b/backend/src/main/java/com/group1/cuisines/entities/User.java @@ -1,10 +1,7 @@ package com.group1.cuisines.entities; import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -18,6 +15,8 @@ @NoArgsConstructor @AllArgsConstructor @Entity +@Getter +@Setter @Table(name = "users") public class User implements UserDetails { @@ -53,6 +52,11 @@ public class User implements UserDetails { @Builder.Default private int recipeCount = 0; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private Set recipes = new HashSet<>(); + + + diff --git a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java index 71ba3672..01d2c3e4 100644 --- a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java +++ b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java @@ -3,14 +3,17 @@ import com.group1.cuisines.dto.*; import com.group1.cuisines.entities.*; import com.group1.cuisines.repositories.*; +import jakarta.annotation.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; @Service @@ -217,4 +220,50 @@ else if(r.getDish() != null && r.getDish().getCuisines().isEmpty()){ return null; } + public List getRecipesByType(String type, @Nullable String username) { + if ("explore".equals(type)) { + return recipeRepository.findAll().stream() + .map(this::convertToRecipeDto) + .collect(Collectors.toList()); + } else if ("following".equals(type) && username != null) { + User user = userRepository.findByUsername(username) + .orElseThrow(() -> new RuntimeException("User not found")); + return user.getFollowing().stream() + .flatMap(followingUser -> followingUser.getRecipes().stream()) + .map(this::convertToRecipeDto) + .collect(Collectors.toList()); + } + return new ArrayList<>(); // Return an empty list if username is null or other conditions are not met + } + + private RecipeDetailsDto convertToRecipeDto(Recipe r) { + CuisineDto cuisineDto = new CuisineDto(); + if (r.getDish() != null && !r.getDish().getCuisines().isEmpty()) { + + cuisineDto.setId(r.getDish().getCuisines().get(0).getId()); + cuisineDto.setName(r.getDish().getCuisines().get(0).getName()); + + } + else if(r.getDish() != null && r.getDish().getCuisines().isEmpty()){ + cuisineDto.setId("No cuisine Id from wikidata"); + cuisineDto.setName("No cuisine name from wikidata"); + } + // Conversion logic here + return new RecipeDetailsDto( + r.getId(), + r.getTitle(), + + r.getInstructions(), + r.getIngredients().stream().map(ingredient -> new IngredientsDto( ingredient.getName())).collect(Collectors.toList()), + + r.getCookingTime(), + r.getServingSize(), + cuisineDto, + + new DishDto(r.getDish().getId(), r.getDish().getName(), r.getDish().getImage()), + r.getAverageRating(), + new AuthorDto(r.getUser().getId(), r.getUser().getFirstName() , r.getUser().getUsername(), r.getUser().getFollowers().size(), r.getUser().getRecipeCount()) + + ); + } } From 6e2d31d699d3beebf368b010fe48c4447df78d5b Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Mon, 13 May 2024 15:03:41 +0300 Subject: [PATCH 04/18] GET users/{userId} endpoint implemented.(User Profile & bookmark recipe related infos.) --- .../cuisines/controllers/UserController.java | 17 ++++ .../cuisines/dao/response/ErrorResponse.java | 11 +++ .../com/group1/cuisines/dto/BookmarkDto.java | 21 ++++ .../group1/cuisines/dto/UserProfileDto.java | 24 +++++ .../com/group1/cuisines/entities/User.java | 2 + .../cuisines/services/RecipeService.java | 1 + .../group1/cuisines/services/UserService.java | 95 +++++++++++++++++++ 7 files changed, 171 insertions(+) create mode 100644 backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java create mode 100644 backend/src/main/java/com/group1/cuisines/dto/BookmarkDto.java create mode 100644 backend/src/main/java/com/group1/cuisines/dto/UserProfileDto.java diff --git a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java index c5a9d711..50dd7478 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java @@ -1,12 +1,17 @@ package com.group1.cuisines.controllers; +import com.group1.cuisines.dao.response.ErrorResponse; +import com.group1.cuisines.dao.response.SuccessResponse; import com.group1.cuisines.dto.UserDto; +import com.group1.cuisines.dto.UserProfileDto; import com.group1.cuisines.entities.User; import com.group1.cuisines.repositories.UserRepository; import com.group1.cuisines.services.UserService; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; @@ -25,6 +30,18 @@ public class UserController { private final UserService userService; private final UserRepository userRepository; + + @GetMapping("/{userId}") + public ResponseEntity getUserById(@PathVariable Integer userId, Authentication authentication) { + String currentUsername = authentication != null ? authentication.getName() : null; + try { + UserProfileDto userProfile = userService.getUserProfileById(userId, currentUsername); + return ResponseEntity.ok(new SuccessResponse<>(userProfile, "User profile fetched successfully")); + } catch (EntityNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + } + } + @GetMapping("/me") public ResponseEntity getUserDetails(@AuthenticationPrincipal UserDetails userDetails) { if (userDetails instanceof com.group1.cuisines.entities.User) { diff --git a/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java b/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java new file mode 100644 index 00000000..2450f365 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java @@ -0,0 +1,11 @@ +package com.group1.cuisines.dao.response; +import lombok.*; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ErrorResponse { + private String message; +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/BookmarkDto.java b/backend/src/main/java/com/group1/cuisines/dto/BookmarkDto.java new file mode 100644 index 00000000..2f920dae --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/BookmarkDto.java @@ -0,0 +1,21 @@ +package com.group1.cuisines.dto; +import lombok.*; +import java.util.List; +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BookmarkDto { + + private Integer id; + private String name; + private String instructions; + private List ingredients; + private int servingSize; + private Integer cookTime; + //private List images; + private CuisineDto cuisine; + private DishDto dish; + private Double avgRating; + + private AuthorDto author; +} diff --git a/backend/src/main/java/com/group1/cuisines/dto/UserProfileDto.java b/backend/src/main/java/com/group1/cuisines/dto/UserProfileDto.java new file mode 100644 index 00000000..447263f8 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/UserProfileDto.java @@ -0,0 +1,24 @@ +package com.group1.cuisines.dto; +import lombok.*; + +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor + + +public class UserProfileDto { + private Integer id; + private String username; + private String name; + private String bio; + private Integer followersCount; + private Integer followingCount; + //private String gender; + // private String profilePicture; + // private List diets; + private Integer recipeCount; + private List bookmarks; + private List recipes; +} diff --git a/backend/src/main/java/com/group1/cuisines/entities/User.java b/backend/src/main/java/com/group1/cuisines/entities/User.java index 53c31bdf..16a2e4b7 100644 --- a/backend/src/main/java/com/group1/cuisines/entities/User.java +++ b/backend/src/main/java/com/group1/cuisines/entities/User.java @@ -54,6 +54,8 @@ public class User implements UserDetails { private int recipeCount = 0; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private Set recipes = new HashSet<>(); + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private Set bookmarks = new HashSet<>(); diff --git a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java index 01d2c3e4..35e9fb31 100644 --- a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java +++ b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java @@ -169,6 +169,7 @@ public List getWhoBookmarked(Integer recipeId) { return bookmarkRepository.findByRecipeId(recipeId).stream().map(Bookmark::getUser).toList(); } + public List getCommentsByRecipeId(Integer recipeId) { return commentRepository.findByRecipeId(recipeId).stream() .map(comment -> CommentsDto.builder() diff --git a/backend/src/main/java/com/group1/cuisines/services/UserService.java b/backend/src/main/java/com/group1/cuisines/services/UserService.java index 0332882e..b6bd20e5 100644 --- a/backend/src/main/java/com/group1/cuisines/services/UserService.java +++ b/backend/src/main/java/com/group1/cuisines/services/UserService.java @@ -1,7 +1,11 @@ package com.group1.cuisines.services; +import com.group1.cuisines.dto.*; +import com.group1.cuisines.entities.Bookmark; +import com.group1.cuisines.entities.Recipe; import com.group1.cuisines.entities.User; import com.group1.cuisines.repositories.UserRepository; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -14,6 +18,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -108,4 +113,94 @@ public Set getUserFollower(Integer userId) { } return Collections.emptySet(); } + + public UserProfileDto getUserProfileById(Integer userId, String currentUsername) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("User not found")); + + boolean isSelf = user.getUsername().equals(currentUsername); + + UserProfileDto profile = new UserProfileDto(); + profile.setId(user.getId()); + profile.setUsername(user.getUsername()); + profile.setName(user.getFirstName() + " " + user.getLastName()); + profile.setBio(user.getBio()); + profile.setFollowersCount(user.getFollowers().size()); + profile.setFollowingCount(user.getFollowing().size()); + profile.setRecipeCount(user.getRecipes().size()); + profile.setRecipes(user.getRecipes().stream() + .map(this::convertToRecipeDetailsDto) + .collect(Collectors.toList())); + + if (isSelf) { + profile.setBookmarks(user.getBookmarks().stream() + .map(this::convertToBookmarkDto) + .collect(Collectors.toList())); + } else { + profile.setBookmarks(Collections.emptyList()); + } + + return profile; + } + + private RecipeDetailsDto convertToRecipeDetailsDto(Recipe recipe) { + CuisineDto cuisineDto = new CuisineDto(); + if (recipe.getDish() != null && !recipe.getDish().getCuisines().isEmpty()) { + + cuisineDto.setId(recipe.getDish().getCuisines().get(0).getId()); + cuisineDto.setName(recipe.getDish().getCuisines().get(0).getName()); + + } + else if(recipe.getDish() != null && recipe.getDish().getCuisines().isEmpty()){ + cuisineDto.setId("No cuisine Id from wikidata"); + cuisineDto.setName("No cuisine name from wikidata"); + } + return new RecipeDetailsDto( + recipe.getId(), + recipe.getTitle(), + recipe.getInstructions(), + recipe.getIngredients().stream() + .map(ingredient -> new IngredientsDto(ingredient.getName())) + .collect(Collectors.toList()), + recipe.getCookingTime(), + recipe.getServingSize(), + cuisineDto, + new DishDto(recipe.getDish().getId(), recipe.getDish().getName(), recipe.getDish().getImage()), + recipe.getAverageRating(), + new AuthorDto(recipe.getUser().getId(), recipe.getUser().getUsername(), recipe.getUser().getFirstName(), + recipe.getUser().getFollowerCount(),recipe.getUser().getRecipeCount()) + ); + } + + private BookmarkDto convertToBookmarkDto(Bookmark bookmark) { + + Recipe recipe = bookmark.getRecipe(); + CuisineDto cuisineDto = new CuisineDto(); + if (recipe.getDish() != null && !recipe.getDish().getCuisines().isEmpty()) { + + cuisineDto.setId(recipe.getDish().getCuisines().get(0).getId()); + cuisineDto.setName(recipe.getDish().getCuisines().get(0).getName()); + + } + else if(recipe.getDish() != null && recipe.getDish().getCuisines().isEmpty()){ + cuisineDto.setId("No cuisine Id from wikidata"); + cuisineDto.setName("No cuisine name from wikidata"); + } + return new BookmarkDto( + recipe.getId(), + recipe.getTitle(), + recipe.getInstructions(), + recipe.getIngredients().stream() + .map(ingredient -> new IngredientsDto(ingredient.getName())) + .collect(Collectors.toList()), + recipe.getServingSize(), + recipe.getCookingTime(), + //recipe.getImages(), + cuisineDto, + new DishDto(recipe.getDish().getId(), recipe.getDish().getName(), recipe.getDish().getImage()), + recipe.getAverageRating(), + new AuthorDto(recipe.getUser().getId(), recipe.getUser().getUsername(), recipe.getUser().getFirstName(), + recipe.getUser().getFollowerCount(),recipe.getUser().getRecipeCount()) + ); + } } From 9b15a89c0aae4ef3763d3a3dc9355ec5c8f51eb9 Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Mon, 13 May 2024 15:36:48 +0300 Subject: [PATCH 05/18] GET users/{userId} endpoint implemented.(User Profile & bookmark recipe related infos.) --- .../controllers/CuisineController.java | 19 ++++++---- .../cuisines/dto/CuisineDetailsDto.java | 15 ++++++++ .../cuisines/services/CuisineService.java | 37 ++++++++++++++----- .../src/main/resources/application.properties | 2 +- 4 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 backend/src/main/java/com/group1/cuisines/dto/CuisineDetailsDto.java diff --git a/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java b/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java index 996df87f..bca83b3f 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java @@ -1,11 +1,16 @@ package com.group1.cuisines.controllers; import com.group1.cuisines.dao.response.ApiResponse; +import com.group1.cuisines.dao.response.ErrorResponse; +import com.group1.cuisines.dao.response.SuccessResponse; +import com.group1.cuisines.dto.CuisineDetailsDto; import com.group1.cuisines.entities.Cuisine; import com.group1.cuisines.repositories.CuisineRepository; import com.group1.cuisines.services.CuisineService; +import jakarta.persistence.EntityNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -21,14 +26,12 @@ public class CuisineController { private final CuisineRepository cuisineRepository; @GetMapping("/{cuisineId}") - public ResponseEntity getCuisineDetails( - @PathVariable String cuisineId, - @RequestParam(defaultValue = "false") boolean includeDishes) { - - Cuisine cuisine = cuisineService.getCuisineById(cuisineId, includeDishes); - if (cuisine == null) { - return ResponseEntity.notFound().build(); + public ResponseEntity getCuisineById(@PathVariable String cuisineId, @RequestParam(required = false) Boolean includeDishes) { + try { + CuisineDetailsDto cuisineDetails = cuisineService.getCuisineById(cuisineId, Boolean.TRUE.equals(includeDishes)); + return ResponseEntity.ok(new SuccessResponse<>(cuisineDetails, "Cuisine details fetched successfully")); + } catch (EntityNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Cuisine not found")); } - return ResponseEntity.ok(cuisine); } } diff --git a/backend/src/main/java/com/group1/cuisines/dto/CuisineDetailsDto.java b/backend/src/main/java/com/group1/cuisines/dto/CuisineDetailsDto.java new file mode 100644 index 00000000..e06a70d0 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/CuisineDetailsDto.java @@ -0,0 +1,15 @@ +package com.group1.cuisines.dto; +import lombok.*; + +import java.util.List; +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CuisineDetailsDto { + private String id; + private String name; + // private String description; + //private String image; + // private Boolean isSelfFollowing; + private List dishes; +} diff --git a/backend/src/main/java/com/group1/cuisines/services/CuisineService.java b/backend/src/main/java/com/group1/cuisines/services/CuisineService.java index e65a84ca..e0211558 100644 --- a/backend/src/main/java/com/group1/cuisines/services/CuisineService.java +++ b/backend/src/main/java/com/group1/cuisines/services/CuisineService.java @@ -1,24 +1,43 @@ package com.group1.cuisines.services; +import com.group1.cuisines.dto.CuisineDetailsDto; +import com.group1.cuisines.dto.DishDto; import com.group1.cuisines.entities.Cuisine; +import com.group1.cuisines.entities.Dish; import com.group1.cuisines.repositories.CuisineRepository; +import jakarta.persistence.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + @Service public class CuisineService { @Autowired private CuisineRepository cuisineRepository; - public Cuisine getCuisineById(String cuisineId, boolean includeDishes) { - return cuisineRepository.findById(cuisineId) - .map(cuisine -> { - if (!includeDishes) { - cuisine.setDishes(null); // Clear the dishes if not required - } - return cuisine; - }) - .orElse(null); + public CuisineDetailsDto getCuisineById(String cuisineId, boolean includeDishes) { + Cuisine cuisine = cuisineRepository.findById(cuisineId) + .orElseThrow(() -> new EntityNotFoundException("Cuisine not found")); + + CuisineDetailsDto detailsDto = new CuisineDetailsDto( + cuisine.getId(), + cuisine.getName(), + // cuisine.getDescription(), + + + includeDishes ? convertDishes(cuisine.getDishes()) : new ArrayList<>() + ); + return detailsDto; + } + + private List convertDishes(Set dishes) { + return dishes.stream() + .map(dish -> new DishDto(dish.getId(), dish.getName(),dish.getImage())) + .collect(Collectors.toList()); } } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index a88f9b2c..14898104 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -7,7 +7,7 @@ spring.datasource.url=jdbc:mysql://localhost:3306/cuisines-test?createDatabaseIf spring.datasource.username=root spring.datasource.password=admin spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.jpa.hibernate.ddl-auto=update +spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.show-sql=true logging.level.com.group1.cuisines=DEBUG From a9921243a29e9eacff7bd0616b6daf559bdf7b0f Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Mon, 13 May 2024 23:07:52 +0300 Subject: [PATCH 06/18] Formatting responses for /search (dishes) --- .../controllers/AuthenticationController.java | 13 +++++++-- .../controllers/SearchController.java | 7 +++-- .../group1/cuisines/dto/DishResponseDto.java | 19 ++++++++++++ .../exceptions/GlobalExceptionHandler.java | 26 +++++++++++++++++ .../exceptions/ResourceNotFoundException.java | 7 +++++ .../services/AuthenticationService.java | 2 +- .../cuisines/services/SearchService.java | 29 +++++++++++++++++-- 7 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 backend/src/main/java/com/group1/cuisines/dto/DishResponseDto.java create mode 100644 backend/src/main/java/com/group1/cuisines/exceptions/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/com/group1/cuisines/exceptions/ResourceNotFoundException.java diff --git a/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java b/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java index 0ffe909b..9efcd63c 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java @@ -6,6 +6,7 @@ import com.group1.cuisines.dao.response.AuthenticationTokenResponse; import com.group1.cuisines.services.AuthenticationService; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -20,10 +21,18 @@ public class AuthenticationController { private final AuthenticationService authenticationService; // Authentication service @PostMapping("/signup") // Sign up endpoint - public ResponseEntity> signup( + public ResponseEntity signup( @RequestBody SignUpRequest request ) { - return ResponseEntity.ok(authenticationService.signup(request)); // Return response + ApiResponse response = authenticationService.signup(request); + + if (response.getStatus() == 409) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(response); + } else if (response.getStatus() == 400) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } else { + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } } @PostMapping("/login") // Sign in endpoint diff --git a/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java b/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java index 05799f48..77bb7669 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java @@ -1,6 +1,7 @@ package com.group1.cuisines.controllers; import com.group1.cuisines.dao.response.ApiResponse; +import com.group1.cuisines.dto.DishResponseDto; import com.group1.cuisines.entities.Dish; import com.group1.cuisines.entities.User; import com.group1.cuisines.services.SearchService; @@ -34,9 +35,9 @@ public ResponseEntity searchUsers(@RequestParam(required = false) String q) { } @GetMapping("/dishes") - public ApiResponse> searchDishes(@RequestParam(required = false) String q, - @RequestParam(required = false) String cuisine, - @RequestParam(required = false) String foodType) { + public ApiResponse> searchDishes(@RequestParam(required = false) String q, + @RequestParam(required = false) String cuisine, + @RequestParam(required = false) String foodType) { return new ApiResponse<>( 200, "Search completed", diff --git a/backend/src/main/java/com/group1/cuisines/dto/DishResponseDto.java b/backend/src/main/java/com/group1/cuisines/dto/DishResponseDto.java new file mode 100644 index 00000000..6eb4915e --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/dto/DishResponseDto.java @@ -0,0 +1,19 @@ +package com.group1.cuisines.dto; + + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DishResponseDto { + private String id; + private String name; + private String image; + private String description; + private String countries; + private String ingredients; + private String foodTypes; + private String cuisines; +} diff --git a/backend/src/main/java/com/group1/cuisines/exceptions/GlobalExceptionHandler.java b/backend/src/main/java/com/group1/cuisines/exceptions/GlobalExceptionHandler.java new file mode 100644 index 00000000..bff8d299 --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/exceptions/GlobalExceptionHandler.java @@ -0,0 +1,26 @@ +package com.group1.cuisines.exceptions; + +import com.group1.cuisines.dao.response.ApiResponse; +import org.springframework.boot.context.config.ConfigDataResourceNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception e) { + ApiResponse response = new ApiResponse<>(500, "An unexpected error occurred: " + e.getMessage(), null); + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + } + + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity> handleResourceNotFoundException(ResourceNotFoundException e) { + ApiResponse response = new ApiResponse<>(400, e.getMessage(), null); + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + + +} \ No newline at end of file diff --git a/backend/src/main/java/com/group1/cuisines/exceptions/ResourceNotFoundException.java b/backend/src/main/java/com/group1/cuisines/exceptions/ResourceNotFoundException.java new file mode 100644 index 00000000..d1ab87bf --- /dev/null +++ b/backend/src/main/java/com/group1/cuisines/exceptions/ResourceNotFoundException.java @@ -0,0 +1,7 @@ +package com.group1.cuisines.exceptions; + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java b/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java index 06bb7823..1221f008 100644 --- a/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java +++ b/backend/src/main/java/com/group1/cuisines/services/AuthenticationService.java @@ -52,7 +52,7 @@ public ApiResponse signup( String token = jwtService.generateToken(user); return new ApiResponse<>( - 200, + 201, "User registered successfully.", AuthenticationTokenResponse.builder().token(token).build() ); diff --git a/backend/src/main/java/com/group1/cuisines/services/SearchService.java b/backend/src/main/java/com/group1/cuisines/services/SearchService.java index ee33a83e..abbe6422 100644 --- a/backend/src/main/java/com/group1/cuisines/services/SearchService.java +++ b/backend/src/main/java/com/group1/cuisines/services/SearchService.java @@ -1,6 +1,9 @@ package com.group1.cuisines.services; +import com.group1.cuisines.dto.DishDto; +import com.group1.cuisines.dto.DishResponseDto; import com.group1.cuisines.entities.Dish; +import com.group1.cuisines.exceptions.ResourceNotFoundException; import com.group1.cuisines.repositories.DishRepository; import com.group1.cuisines.repositories.UserRepository; import lombok.RequiredArgsConstructor; @@ -16,17 +19,23 @@ public class SearchService { private final DishRepository dishRepository; - public List searchDishes(String query, String cuisine, String foodType) { + public List searchDishes(String query, String cuisine, String foodType) { List dishes = dishRepository.findAll(); // Filter by dish name if (query != null && !query.isEmpty()) { dishes = dishRepository.findByNameContainingIgnoreCase(query); + if (dishes.isEmpty()) { + throw new ResourceNotFoundException("No dishes found with the given name query."); + } } // Filter by cuisine name if (cuisine != null && !cuisine.isEmpty()) { List dishesByCuisine = dishRepository.findByCuisinesName(cuisine); + if (dishesByCuisine.isEmpty()) { + throw new ResourceNotFoundException("No dishes found with the given cuisine."); + } dishes = dishes.stream() .filter(dishesByCuisine::contains) .collect(Collectors.toList()); @@ -37,9 +46,25 @@ public List searchDishes(String query, String cuisine, String foodType) { dishes = dishes.stream() .filter(d -> d.getFoodTypes() != null && d.getFoodTypes().contains(foodType)) .collect(Collectors.toList()); + + if (dishes.isEmpty()) { + throw new ResourceNotFoundException("No dishes found with the given food type."); + } } - return dishes; + // Map to DishResponseDto + return dishes.stream() + .map(d -> new DishResponseDto( + d.getId(), + d.getName(), + d.getImage(), + d.getDescription(), + d.getCountries(), + d.getIngredients(), + d.getFoodTypes(), + d.getCuisines().isEmpty() ? null : d.getCuisines().get(0).getName() + )) + .collect(Collectors.toList()); } From 98abe2a9737297cfa68f14be4e543563506bdbe3 Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Mon, 13 May 2024 23:25:18 +0300 Subject: [PATCH 07/18] Test cases updated. --- backend/pom.xml | 12 ++ .../controllers/AuthenticationController.java | 10 +- .../AuthenticationControllerTest.java | 151 ++++++------------ 3 files changed, 67 insertions(+), 106 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index 470caae3..81791029 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -21,6 +21,18 @@ org.springframework.boot spring-boot-starter-security + + org.mockito + mockito-core + 5.2.0 + test + + + org.mockito + mockito-junit-jupiter + 5.2.0 + test + org.apache.jena apache-jena-libs diff --git a/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java b/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java index 9efcd63c..777b70ad 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/AuthenticationController.java @@ -21,7 +21,7 @@ public class AuthenticationController { private final AuthenticationService authenticationService; // Authentication service @PostMapping("/signup") // Sign up endpoint - public ResponseEntity signup( + public ResponseEntity> signup( @RequestBody SignUpRequest request ) { ApiResponse response = authenticationService.signup(request); @@ -39,6 +39,12 @@ public ResponseEntity signup( public ResponseEntity> signin( @RequestBody SignInRequest request ) { - return ResponseEntity.ok(authenticationService.signin(request)); // Return response + ApiResponse response = authenticationService.signin(request); + + if (response.getStatus() == 401) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); + } else { + return ResponseEntity.ok(response); + } } } diff --git a/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java b/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java index d3d6218b..4aa77e4c 100644 --- a/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java +++ b/backend/src/test/java/com/group1/cuisines/AuthenticationControllerTest.java @@ -14,6 +14,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; public class AuthenticationControllerTest { @@ -31,123 +32,65 @@ public void setUp() { @Test public void shouldReturnSuccessOnValidSignin() { - SignInRequest signInRequest = new SignInRequest( - "testUser", - "testPassword" - ); - ApiResponse apiResponse = - new ApiResponse<>( - 200, - "Success", - new AuthenticationTokenResponse("token") - ); - when(authenticationService.signin(signInRequest)).thenReturn( - apiResponse - ); - ResponseEntity< - ApiResponse - > responseEntity = authenticationController.signin(signInRequest); - assertEquals( - 200, - responseEntity.getBody().getStatus(), - "Status code does not match expected value on successful signin" - ); - assertEquals( - apiResponse, - responseEntity.getBody(), - "Response body does not match expected value on successful signin" - ); + SignInRequest signInRequest = new SignInRequest("testUser", "testPassword"); + ApiResponse apiResponse = new ApiResponse<>(200, "Success", new AuthenticationTokenResponse("token")); + when(authenticationService.signin(signInRequest)).thenReturn(apiResponse); + + ResponseEntity> responseEntity = authenticationController.signin(signInRequest); + + assertEquals(HttpStatus.OK.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on successful signin"); + assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on successful signin"); } @Test public void shouldReturnFailureOnInvalidSignin() { - SignInRequest signInRequest = new SignInRequest( - "testUser", - "wrongPassword" - ); - ApiResponse apiResponse = - new ApiResponse<>(401, "Failure", null); - when(authenticationService.signin(signInRequest)).thenReturn( - apiResponse - ); - ResponseEntity< - ApiResponse - > responseEntity = authenticationController.signin(signInRequest); - assertEquals( - 401, - responseEntity.getBody().getStatus(), - "Status code does not match expected value on failed signin" - ); - assertEquals( - apiResponse, - responseEntity.getBody(), - "Response body does not match expected value on failed signin" - ); + SignInRequest signInRequest = new SignInRequest("testUser", "wrongPassword"); + ApiResponse apiResponse = new ApiResponse<>(401, "Invalid email/username or password.", null); + when(authenticationService.signin(signInRequest)).thenReturn(apiResponse); + + ResponseEntity> responseEntity = authenticationController.signin(signInRequest); + + assertEquals(HttpStatus.UNAUTHORIZED.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on failed signin"); + assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on failed signin"); } @Test public void shouldReturnSuccessOnValidSignup() { SignUpRequest signUpRequest = SignUpRequest.builder() - .email("newUser@gmail.com") - .username("newUser") - .country("USA") - .bio("Bio of the new user") - .password("newPassword") - .firstName("New") - .lastName("User") - .build(); - ApiResponse apiResponse = - new ApiResponse<>( - 200, - "Success", - new AuthenticationTokenResponse("token") - ); - when(authenticationService.signup(signUpRequest)).thenReturn( - apiResponse - ); - ResponseEntity< - ApiResponse - > responseEntity = authenticationController.signup(signUpRequest); - assertEquals( - 200, - responseEntity.getBody().getStatus(), - "Status code does not match expected value on successful signup" - ); - assertEquals( - apiResponse, - responseEntity.getBody(), - "Response body does not match expected value on successful signup" - ); + .email("newUser@gmail.com") + .username("newUser") + .country("USA") + .bio("Bio of the new user") + .password("newPassword") + .firstName("New") + .lastName("User") + .build(); + ApiResponse apiResponse = new ApiResponse<>(201, "User registered successfully.", new AuthenticationTokenResponse("token")); + when(authenticationService.signup(signUpRequest)).thenReturn(apiResponse); + + ResponseEntity> responseEntity = authenticationController.signup(signUpRequest); + + assertEquals(HttpStatus.CREATED.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on successful signup"); + assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on successful signup"); } @Test public void shouldReturnFailureOnInvalidSignup() { SignUpRequest signUpRequest = SignUpRequest.builder() - .email("newUser@gmail.com") - .username("newUser") - .country("USA") - .bio("Bio of the new user") - .password("newPassword") - .firstName("New") - .lastName("User") - .build(); - ApiResponse apiResponse = - new ApiResponse<>(409, "Failure", null); - when(authenticationService.signup(signUpRequest)).thenReturn( - apiResponse - ); - ResponseEntity< - ApiResponse - > responseEntity = authenticationController.signup(signUpRequest); - assertEquals( - 409, - responseEntity.getBody().getStatus(), - "Status code does not match expected value on failed signup" - ); - assertEquals( - apiResponse, - responseEntity.getBody(), - "Response body does not match expected value on failed signup" - ); + .email("newUser@gmail.com") + .username("newUser") + .country("USA") + .bio("Bio of the new user") + .password("newPassword") + .firstName("New") + .lastName("User") + .build(); + ApiResponse apiResponse = new ApiResponse<>(409, "Email or username already exists.", null); + when(authenticationService.signup(signUpRequest)).thenReturn(apiResponse); + + ResponseEntity> responseEntity = authenticationController.signup(signUpRequest); + + assertEquals(HttpStatus.CONFLICT.value(), responseEntity.getStatusCodeValue(), "Status code does not match expected value on failed signup"); + assertEquals(apiResponse, responseEntity.getBody(), "Response body does not match expected value on failed signup"); } } From c7d3997ee44140b6746607a64d2fab4d29f3123c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Tue, 14 May 2024 10:38:01 +0300 Subject: [PATCH 08/18] feat(backend): adjust query to search other fields --- .../cuisines/repositories/DishRepository.java | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java b/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java index a1b94f43..7eedc5ac 100644 --- a/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java +++ b/backend/src/main/java/com/group1/cuisines/repositories/DishRepository.java @@ -1,24 +1,28 @@ package com.group1.cuisines.repositories; import com.group1.cuisines.entities.Dish; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.List; -import java.util.Optional; - @Repository -public interface DishRepository extends JpaRepository { - @Query("SELECT d FROM Dish d WHERE LOWER(d.name) LIKE LOWER(CONCAT('%', :name, '%'))") +public interface DishRepository extends JpaRepository { + @Query( + "SELECT d FROM Dish d WHERE LOWER(d.name) LIKE LOWER(CONCAT('%', :name, '%')) OR LOWER(d.countries) LIKE LOWER(CONCAT('%', :name, '%')) OR LOWER(d.ingredients) LIKE LOWER(CONCAT('%', :name, '%'))" + ) List findByNameContainingIgnoreCase(String name); - @Query("SELECT d FROM Dish d JOIN d.cuisines c WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :cuisineName, '%'))") + + @Query( + "SELECT d FROM Dish d JOIN d.cuisines c WHERE LOWER(c.name) LIKE LOWER(CONCAT('%', :cuisineName, '%'))" + ) List findByCuisinesName(@Param("cuisineName") String cuisineName); + List findByFoodTypesContainingIgnoreCase(String foodType); Optional findById(String id); @Query("SELECT d FROM Dish d JOIN d.cuisines c WHERE c.id = :cuisineId") List findByCuisineId(@Param("cuisineId") String cuisineId); - } From bafa11cbe3b153505837d3caf968c7396035d201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Tue, 14 May 2024 10:43:47 +0300 Subject: [PATCH 09/18] chore(devops): remove redundant trigger --- .github/workflows/backend_test.yml | 15 ++++----------- .github/workflows/frontend_test.yml | 14 +++++++++++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/backend_test.yml b/.github/workflows/backend_test.yml index 6d33851e..7fbcc1f8 100644 --- a/.github/workflows/backend_test.yml +++ b/.github/workflows/backend_test.yml @@ -3,30 +3,23 @@ name: backend-test on: push: paths: - - 'backend/**' + - "backend/**" - compose.yml - dev.yml - - '.github/workflows/backend_test.yml' - + - ".github/workflows/backend_test.yml" pull_request: branches: - main - staging - - develop - jobs: test: runs-on: ubuntu-latest env: - working-directory: - backend + working-directory: backend steps: - name: Checkout uses: actions/checkout@v4 - + - name: Test with Maven run: docker compose -f dev.yml run --rm backend mvn test - - - diff --git a/.github/workflows/frontend_test.yml b/.github/workflows/frontend_test.yml index b57d72a0..330f7bd0 100644 --- a/.github/workflows/frontend_test.yml +++ b/.github/workflows/frontend_test.yml @@ -1,6 +1,14 @@ name: Front-End CI -on: [push, pull_request] +on: + push: + paths: + - "frontend/**" + - ".github/**" + pull_request: + branches: + - main + - staging jobs: build: @@ -15,8 +23,8 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: '20' - cache: 'yarn' + node-version: "20" + cache: "yarn" - name: Enable Corepack run: corepack enable From 425f602603fd79f73c963ee32f424b5e813653d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Tue, 14 May 2024 10:45:09 +0300 Subject: [PATCH 10/18] chore(devops): further limit push trigger --- .github/workflows/frontend_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/frontend_test.yml b/.github/workflows/frontend_test.yml index 330f7bd0..79b24a57 100644 --- a/.github/workflows/frontend_test.yml +++ b/.github/workflows/frontend_test.yml @@ -4,7 +4,7 @@ on: push: paths: - "frontend/**" - - ".github/**" + - ".github/workflows/frontend_test.yml" pull_request: branches: - main From 4ebb8199a75ca24986306a3b0146f2dbd24fa091 Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Tue, 14 May 2024 19:41:29 +0300 Subject: [PATCH 11/18] /cuisines/{cuisineId} response format reimplemented. --- .../com/group1/cuisines/controllers/CuisineController.java | 4 ++-- .../com/group1/cuisines/controllers/FeedController.java | 6 +++--- .../com/group1/cuisines/controllers/RecipeController.java | 2 +- .../com/group1/cuisines/controllers/UserController.java | 4 ++-- .../com/group1/cuisines/dao/response/ErrorResponse.java | 1 + .../com/group1/cuisines/dao/response/SuccessResponse.java | 1 + 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java b/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java index bca83b3f..d902e924 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java @@ -29,9 +29,9 @@ public class CuisineController { public ResponseEntity getCuisineById(@PathVariable String cuisineId, @RequestParam(required = false) Boolean includeDishes) { try { CuisineDetailsDto cuisineDetails = cuisineService.getCuisineById(cuisineId, Boolean.TRUE.equals(includeDishes)); - return ResponseEntity.ok(new SuccessResponse<>(cuisineDetails, "Cuisine details fetched successfully")); + return ResponseEntity.ok(new SuccessResponse<>(200,cuisineDetails, "Cuisine details fetched successfully")); } catch (EntityNotFoundException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("Cuisine not found")); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(204,"Cuisine not found")); } } } diff --git a/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java b/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java index ff3fd957..acea433c 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java @@ -30,16 +30,16 @@ public ResponseEntity getFeed(@RequestParam String type, Authentication authe if ("following".equals(type)) { if (authentication == null || !authentication.isAuthenticated()) { // Return an empty set and a message for unauthenticated users - return ResponseEntity.ok(new SuccessResponse<>(Collections.emptyList(), "No content available. Please log in and follow other users !.")); + return ResponseEntity.ok(new SuccessResponse<>(200,Collections.emptyList(), "No content available. Please log in and follow other users !.")); } // Fetch following users' recipes for authenticated users String username = authentication.getName(); List recipes = recipeService.getRecipesByType(type, username); - return ResponseEntity.ok(new SuccessResponse<>(recipes, "Recipes fetched successfully from followed users.")); + return ResponseEntity.ok(new SuccessResponse<>(200,recipes, "Recipes fetched successfully from followed users.")); } // For 'explore', accessible to everyone List recipes = recipeService.getRecipesByType(type, null); - return ResponseEntity.ok(new SuccessResponse<>(recipes, "Recipes fetched successfully.")); + return ResponseEntity.ok(new SuccessResponse<>(200,recipes, "Recipes fetched successfully.")); } } diff --git a/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java b/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java index 7f12b83b..814ec4d0 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/RecipeController.java @@ -26,7 +26,7 @@ public class RecipeController { public ResponseEntity getRecipeById(@PathVariable Integer recipeId) { RecipeDetailsDto recipeDetails = recipeService.getRecipeById(recipeId); if (recipeDetails != null) { - return ResponseEntity.ok(new SuccessResponse<>(recipeDetails, "Recipe fetched successfully")); + return ResponseEntity.ok(new SuccessResponse<>(200,recipeDetails, "Recipe fetched successfully")); } else { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Recipe not found"); } diff --git a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java index 50dd7478..de506bfc 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java @@ -36,9 +36,9 @@ public ResponseEntity getUserById(@PathVariable Integer userId, Authenticatio String currentUsername = authentication != null ? authentication.getName() : null; try { UserProfileDto userProfile = userService.getUserProfileById(userId, currentUsername); - return ResponseEntity.ok(new SuccessResponse<>(userProfile, "User profile fetched successfully")); + return ResponseEntity.ok(new SuccessResponse<>(200,userProfile, "User profile fetched successfully")); } catch (EntityNotFoundException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse("User not found")); + return ResponseEntity.ok(new ErrorResponse(204,"User not found")); } } diff --git a/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java b/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java index 2450f365..df3e3fca 100644 --- a/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java +++ b/backend/src/main/java/com/group1/cuisines/dao/response/ErrorResponse.java @@ -7,5 +7,6 @@ @Data @AllArgsConstructor public class ErrorResponse { + private int status; private String message; } diff --git a/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java b/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java index be965ff0..b4ab374e 100644 --- a/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java +++ b/backend/src/main/java/com/group1/cuisines/dao/response/SuccessResponse.java @@ -3,6 +3,7 @@ @Data @AllArgsConstructor public class SuccessResponse { + private int status; private T data; private String message; From dee242453e6d8b224d2c58a59449f4d47c584eef Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Tue, 14 May 2024 19:43:30 +0300 Subject: [PATCH 12/18] /cuisines/{cuisineId} response format reimplemented. --- .../java/com/group1/cuisines/controllers/CuisineController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java b/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java index d902e924..eadf6d76 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/CuisineController.java @@ -31,7 +31,7 @@ public ResponseEntity getCuisineById(@PathVariable String cuisineId, @Request CuisineDetailsDto cuisineDetails = cuisineService.getCuisineById(cuisineId, Boolean.TRUE.equals(includeDishes)); return ResponseEntity.ok(new SuccessResponse<>(200,cuisineDetails, "Cuisine details fetched successfully")); } catch (EntityNotFoundException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(204,"Cuisine not found")); + return ResponseEntity.ok(new ErrorResponse(204,"Cuisine not found")); } } } From 3cd6155e160d8fc2915eb31861defe930b61a3c1 Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Tue, 14 May 2024 20:06:39 +0300 Subject: [PATCH 13/18] GET /users/{userId} response format reimplemented. --- backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java | 1 + .../main/java/com/group1/cuisines/services/RecipeService.java | 4 ++-- .../main/java/com/group1/cuisines/services/UserService.java | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java b/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java index d208f44e..e22aeb60 100644 --- a/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java +++ b/backend/src/main/java/com/group1/cuisines/dto/AuthorDto.java @@ -11,6 +11,7 @@ public class AuthorDto { private String name; private String username; private Integer followersCount; + private Integer followingCount; private Integer recipesCount; } diff --git a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java index 35e9fb31..51efe39f 100644 --- a/backend/src/main/java/com/group1/cuisines/services/RecipeService.java +++ b/backend/src/main/java/com/group1/cuisines/services/RecipeService.java @@ -214,7 +214,7 @@ else if(r.getDish() != null && r.getDish().getCuisines().isEmpty()){ new DishDto(r.getDish().getId(), r.getDish().getName(), r.getDish().getImage()), r.getAverageRating(), - new AuthorDto(r.getUser().getId(), r.getUser().getFirstName() , r.getUser().getUsername(), r.getUser().getFollowers().size(), r.getUser().getRecipeCount()) + new AuthorDto(r.getUser().getId(), r.getUser().getFirstName() , r.getUser().getUsername(), r.getUser().getFollowing().size(),r.getUser().getFollowers().size(), r.getUser().getRecipeCount()) ); } @@ -263,7 +263,7 @@ else if(r.getDish() != null && r.getDish().getCuisines().isEmpty()){ new DishDto(r.getDish().getId(), r.getDish().getName(), r.getDish().getImage()), r.getAverageRating(), - new AuthorDto(r.getUser().getId(), r.getUser().getFirstName() , r.getUser().getUsername(), r.getUser().getFollowers().size(), r.getUser().getRecipeCount()) + new AuthorDto(r.getUser().getId(), r.getUser().getFirstName() , r.getUser().getUsername(),r.getUser().getFollowing().size(), r.getUser().getFollowers().size(), r.getUser().getRecipeCount()) ); } diff --git a/backend/src/main/java/com/group1/cuisines/services/UserService.java b/backend/src/main/java/com/group1/cuisines/services/UserService.java index b6bd20e5..7dacf859 100644 --- a/backend/src/main/java/com/group1/cuisines/services/UserService.java +++ b/backend/src/main/java/com/group1/cuisines/services/UserService.java @@ -168,7 +168,7 @@ else if(recipe.getDish() != null && recipe.getDish().getCuisines().isEmpty()){ new DishDto(recipe.getDish().getId(), recipe.getDish().getName(), recipe.getDish().getImage()), recipe.getAverageRating(), new AuthorDto(recipe.getUser().getId(), recipe.getUser().getUsername(), recipe.getUser().getFirstName(), - recipe.getUser().getFollowerCount(),recipe.getUser().getRecipeCount()) + recipe.getUser().getFollowingCount(), recipe.getUser().getFollowerCount(),recipe.getUser().getRecipeCount()) ); } @@ -200,7 +200,7 @@ else if(recipe.getDish() != null && recipe.getDish().getCuisines().isEmpty()){ new DishDto(recipe.getDish().getId(), recipe.getDish().getName(), recipe.getDish().getImage()), recipe.getAverageRating(), new AuthorDto(recipe.getUser().getId(), recipe.getUser().getUsername(), recipe.getUser().getFirstName(), - recipe.getUser().getFollowerCount(),recipe.getUser().getRecipeCount()) + recipe.getUser().getFollowingCount(), recipe.getUser().getFollowerCount(),recipe.getUser().getRecipeCount()) ); } } From 3646bf726165827ea7a3c369f90f4d83d233c287 Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Tue, 14 May 2024 20:22:41 +0300 Subject: [PATCH 14/18] Format GET /users/{userId}/following response --- .../group1/cuisines/controllers/UserController.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java index de506bfc..6eedc554 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java @@ -84,12 +84,16 @@ public ResponseEntity followUser(@PathVariable Integer userId) { public ResponseEntity getUserFollowing(@PathVariable Integer userId) { // Validate the provided user ID if (userId == null) { - return ResponseEntity.badRequest().body("Invalid user ID provided"); + + return ResponseEntity.ok(new ErrorResponse(204,"Invalid user ID provided") ); + } + if(userRepository.findById(userId).isEmpty()){ + return ResponseEntity.ok(new ErrorResponse(204,"User not found") ); } Set following = userService.getUserFollowing(userId); if (following.isEmpty()) { - return ResponseEntity.ok().body("User is not following anyone"); + return ResponseEntity.ok(new ErrorResponse(204,"User is not following anyone")); } else { Set followingDto = following.stream() .map(user -> UserDto.builder() @@ -102,7 +106,7 @@ public ResponseEntity getUserFollowing(@PathVariable Integer userId) { .recipeCount(user.getRecipeCount()) .build()) .collect(Collectors.toSet()); - return ResponseEntity.ok().body(followingDto); + return ResponseEntity.ok(new SuccessResponse<>(200,followingDto, "User following fetched successfully")); } } @GetMapping("/{userId}/followers") From 2e648564a92d8dfce97d4db3721c2db3b87b3e6e Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Tue, 14 May 2024 20:34:31 +0300 Subject: [PATCH 15/18] Format GET /users/{userI}/followers response --- .../group1/cuisines/controllers/UserController.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java index 6eedc554..35c9f5f5 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java @@ -113,11 +113,15 @@ public ResponseEntity getUserFollowing(@PathVariable Integer userId) { public ResponseEntity getUserFollowers(@PathVariable Integer userId) { // Validate the provided user ID if (userId == null) { - return ResponseEntity.badRequest().body("Invalid user ID provided"); + + return ResponseEntity.ok(new ErrorResponse(204,"Invalid user ID provided") ); + } + if(userRepository.findById(userId).isEmpty()){ + return ResponseEntity.ok(new ErrorResponse(204,"User not found") ); } Set followers = userService.getUserFollower(userId); if (followers.isEmpty()) { - return ResponseEntity.ok().body("User is not followed by anyone"); + return ResponseEntity.ok(new ErrorResponse(204,"User is not followed by anyone")); } else { Set followingDto = followers.stream() .map(user -> UserDto.builder() @@ -130,7 +134,7 @@ public ResponseEntity getUserFollowers(@PathVariable Integer userId) { .recipeCount(user.getRecipeCount()) .build()) .collect(Collectors.toSet()); - return ResponseEntity.ok().body(followingDto); + return ResponseEntity.ok(new SuccessResponse<>(200,followingDto, "User followers fetched successfully")); } } @PostMapping("/{userId}/unfollow") From 09b7b4572d424a8830f59ec8c250f3d03764bc10 Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Tue, 14 May 2024 21:00:22 +0300 Subject: [PATCH 16/18] Format GET /users/follow and /users/unfollow response. --- .../cuisines/controllers/UserController.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java index 35c9f5f5..a7b6a138 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/UserController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/UserController.java @@ -58,27 +58,32 @@ public ResponseEntity getUserDetails(@AuthenticationPrincipal UserDetails use return ResponseEntity.status(HttpStatus.FORBIDDEN).body("User not authenticated"); } - @PostMapping("/{userId}/follow") - public ResponseEntity followUser(@PathVariable Integer userId) { + @PostMapping("/follow") + public ResponseEntity followUser(@RequestBody Map requestBody) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(authentication.getPrincipal()=="anonymousUser"){ - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authentication required."); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(401,"Authentication required") ); + } String username = authentication.getName(); Integer followerId = userRepository.findUserIdByUsername(username); + Integer userId = requestBody.get("userId"); if (followerId == null || userId == null) { - return ResponseEntity.badRequest().body("Invalid user data"); + + return ResponseEntity.ok(new ErrorResponse(204,"Invalid user data")); } boolean result = userService.followUser(userId, followerId); if (!result) { - return ResponseEntity.status(HttpStatus.ALREADY_REPORTED).body("Already following"); + + return ResponseEntity.ok(new ErrorResponse(209,"Already following")); } - return ResponseEntity.ok().body("Followed successfully"); + + return ResponseEntity.ok(new SuccessResponse<>(200,null,"Followed successfully")); } @GetMapping("/{userId}/following") public ResponseEntity getUserFollowing(@PathVariable Integer userId) { @@ -137,26 +142,28 @@ public ResponseEntity getUserFollowers(@PathVariable Integer userId) { return ResponseEntity.ok(new SuccessResponse<>(200,followingDto, "User followers fetched successfully")); } } - @PostMapping("/{userId}/unfollow") - public ResponseEntity unfollowUser(@PathVariable Integer userId) { + @PostMapping("/unfollow") + public ResponseEntity unfollowUser(@RequestBody Map requestBody) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if(authentication.getPrincipal()=="anonymousUser"){ - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authentication required."); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new ErrorResponse(401,"Authentication required") ); } String username = authentication.getName(); Integer followerId = userRepository.findUserIdByUsername(username); + Integer userId = requestBody.get("userId"); if (followerId == null || userId == null) { - return ResponseEntity.badRequest().body("Invalid user data."); + return ResponseEntity.ok(new ErrorResponse(204,"Invalid user data")); } boolean result = userService.unfollowUser(userId, followerId); if (!result) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Follow relationship does not exist."); + + return ResponseEntity.ok(new ErrorResponse(209,"Follow relationship does not exist")); } - return ResponseEntity.ok().body("Unfollowed successfully."); + return ResponseEntity.ok(new SuccessResponse<>(200,null,"Unfollowed successfully")); } } From 3264ff63d4bdbd36a30a0abed8d0c0b527eb4bf3 Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Tue, 14 May 2024 21:09:57 +0300 Subject: [PATCH 17/18] Format GET /search/users response. --- .../cuisines/controllers/SearchController.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java b/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java index 77bb7669..1d53a486 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/SearchController.java @@ -1,7 +1,10 @@ package com.group1.cuisines.controllers; import com.group1.cuisines.dao.response.ApiResponse; +import com.group1.cuisines.dao.response.ErrorResponse; +import com.group1.cuisines.dao.response.SuccessResponse; import com.group1.cuisines.dto.DishResponseDto; +import com.group1.cuisines.dto.UserDto; import com.group1.cuisines.entities.Dish; import com.group1.cuisines.entities.User; import com.group1.cuisines.services.SearchService; @@ -9,6 +12,7 @@ import com.group1.cuisines.services.WikidataService; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -29,9 +33,18 @@ public ResponseEntity searchUsers(@RequestParam(required = false) String q) { List users = userService.searchUsers(q); if (users.isEmpty()) { // Return a custom message with a "No Content" status when no users are found - return ResponseEntity.status(HttpStatus.NO_CONTENT).body("No users found"); + return ResponseEntity.ok(new ErrorResponse(204,"No users found")); } - return ResponseEntity.ok(users); // Return the list of users when found + return ResponseEntity.ok(new SuccessResponse<>(200, users.stream().map(user -> new UserDto( + user.getId(), + user.getUsername(), + user.getFirstName(), + user.getLastName(), + user.getFollowerCount(), + user.getFollowingCount(), + user.getRecipeCount() + )).collect(Collectors.toList()), "Users fetched successfully")); + } @GetMapping("/dishes") From aff78d00d569eb93a5b33f6efee91aa0bba03b16 Mon Sep 17 00:00:00 2001 From: EnesBaserr Date: Tue, 14 May 2024 21:22:51 +0300 Subject: [PATCH 18/18] Format response for /feed . --- .../java/com/group1/cuisines/controllers/FeedController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java b/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java index acea433c..d4fdc09b 100644 --- a/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java +++ b/backend/src/main/java/com/group1/cuisines/controllers/FeedController.java @@ -30,7 +30,7 @@ public ResponseEntity getFeed(@RequestParam String type, Authentication authe if ("following".equals(type)) { if (authentication == null || !authentication.isAuthenticated()) { // Return an empty set and a message for unauthenticated users - return ResponseEntity.ok(new SuccessResponse<>(200,Collections.emptyList(), "No content available. Please log in and follow other users !.")); + return ResponseEntity.ok(new SuccessResponse<>(400,Collections.emptyList(), "No content available. Please log in and follow other users !.")); } // Fetch following users' recipes for authenticated users String username = authentication.getName();