diff --git a/commons/src/main/java/commons/Ingredient.java b/commons/src/main/java/commons/Ingredient.java index 207d568..dd93aab 100644 --- a/commons/src/main/java/commons/Ingredient.java +++ b/commons/src/main/java/commons/Ingredient.java @@ -72,6 +72,26 @@ public class Ingredient { this.id = id; } + public double getProteinPer100g() { + return proteinPer100g; + } + + public double getFatPer100g() { + return fatPer100g; + } + + public double getCarbsPer100g() { + return carbsPer100g; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; diff --git a/server/src/main/java/server/SomeController.java b/server/src/main/java/server/api/HealthCheckController.java similarity index 61% rename from server/src/main/java/server/SomeController.java rename to server/src/main/java/server/api/HealthCheckController.java index 3e0b88c..734b633 100644 --- a/server/src/main/java/server/SomeController.java +++ b/server/src/main/java/server/api/HealthCheckController.java @@ -1,17 +1,16 @@ -package server; +package server.api; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; -@Controller +@RestController @RequestMapping("/") -public class SomeController { - +public class HealthCheckController { @GetMapping("/") @ResponseBody public String index() { - return "Hello world!"; + return "Server is online!"; } } \ No newline at end of file diff --git a/server/src/main/java/server/api/IngredientController.java b/server/src/main/java/server/api/IngredientController.java index 946a62d..324276b 100644 --- a/server/src/main/java/server/api/IngredientController.java +++ b/server/src/main/java/server/api/IngredientController.java @@ -5,8 +5,6 @@ import commons.ws.Topics; import commons.ws.messages.CreateIngredientMessage; import commons.ws.messages.DeleteIngredientMessage; import commons.ws.messages.UpdateIngredientMessage; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.web.bind.annotation.DeleteMapping; @@ -19,6 +17,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import server.database.IngredientRepository; +import server.service.IngredientService; import java.util.Optional; import java.util.List; @@ -42,12 +41,12 @@ import java.util.List; @RestController @RequestMapping("/api") public class IngredientController { - private final IngredientRepository ingredientRepository; + private final IngredientService ingredientService; private final SimpMessagingTemplate messagingTemplate; - public IngredientController(IngredientRepository ingredientRepository, + public IngredientController(IngredientService ingredientService, SimpMessagingTemplate messagingTemplate) { - this.ingredientRepository = ingredientRepository; + this.ingredientService = ingredientService; this.messagingTemplate = messagingTemplate; } @@ -75,15 +74,9 @@ public class IngredientController { @RequestParam Optional page, @RequestParam Optional limit ) { - List ingredients = limit - .map(l -> { - return ingredientRepository.findAllByOrderByNameAsc( - PageRequest.of(page.orElse(0), l) - ).toList(); - }) - .orElseGet(ingredientRepository::findAllByOrderByNameAsc); - - return ResponseEntity.ok(ingredients); + return limit + .map(integer -> ResponseEntity.ok(ingredientService.findAll(page.orElse(0), integer))) + .orElseGet(() -> ResponseEntity.ok(ingredientService.findAll())); } /** @@ -102,7 +95,7 @@ public class IngredientController { */ @GetMapping("/ingredients/{id}") public ResponseEntity getIngredientById(@PathVariable Long id) { - return ingredientRepository.findById(id) + return ingredientService.findById(id) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } @@ -130,15 +123,13 @@ public class IngredientController { @PathVariable Long id, @RequestBody Ingredient updated ) { - if (!ingredientRepository.existsById(id)) { - return ResponseEntity.notFound().build(); - } - updated.setId(id); - Ingredient savedIngredient = ingredientRepository.save(updated); - messagingTemplate.convertAndSend(Topics.INGREDIENTS, new CreateIngredientMessage(savedIngredient)); - - return ResponseEntity.ok(savedIngredient); + return ingredientService.update(id, updated) + .map(saved -> { + messagingTemplate.convertAndSend(Topics.INGREDIENTS, new CreateIngredientMessage(saved)); + return ResponseEntity.ok(saved); + }) + .orElseGet(() -> ResponseEntity.notFound().build()); } /** @@ -164,17 +155,12 @@ public class IngredientController { return ResponseEntity.badRequest().build(); } - Ingredient example = new Ingredient(); - example.name = ingredient.name; - - if (ingredientRepository.existsById(ingredient.id) || ingredientRepository.exists(Example.of(example))) { - return ResponseEntity.badRequest().build(); - } - - Ingredient saved = ingredientRepository.save(ingredient); - messagingTemplate.convertAndSend(Topics.INGREDIENTS, new UpdateIngredientMessage(saved)); - - return ResponseEntity.ok(saved); + return ingredientService.create(ingredient) + .map(saved -> { + messagingTemplate.convertAndSend(Topics.INGREDIENTS, new UpdateIngredientMessage(saved)); + return ResponseEntity.ok(saved); + }) + .orElseGet(() -> ResponseEntity.badRequest().build()); } /** @@ -193,13 +179,11 @@ public class IngredientController { */ @DeleteMapping("/ingredients/{id}") public ResponseEntity deleteIngredient(@PathVariable Long id) { - if (!ingredientRepository.existsById(id)) { + if (!ingredientService.delete(id)) { return ResponseEntity.notFound().build(); } - ingredientRepository.deleteById(id); messagingTemplate.convertAndSend(Topics.INGREDIENTS, new DeleteIngredientMessage(id)); - return ResponseEntity.ok(true); } } diff --git a/server/src/main/java/server/api/RecipeController.java b/server/src/main/java/server/api/RecipeController.java index 2a56310..37a2a78 100644 --- a/server/src/main/java/server/api/RecipeController.java +++ b/server/src/main/java/server/api/RecipeController.java @@ -7,8 +7,6 @@ import commons.ws.Topics; import commons.ws.messages.CreateRecipeMessage; import commons.ws.messages.DeleteRecipeMessage; import commons.ws.messages.UpdateRecipeMessage; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -22,9 +20,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import server.database.IngredientRepository; -import server.database.RecipeIngredientRepository; -import server.database.RecipeRepository; +import server.service.RecipeService; import java.util.List; import java.util.Optional; @@ -32,19 +28,12 @@ import java.util.Optional; @RestController @RequestMapping("/api") public class RecipeController { - private final RecipeRepository recipeRepository; // JPA repository used in this controller private final SimpMessagingTemplate messagingTemplate; - private final RecipeIngredientRepository recipeIngredientRepository; - private final IngredientRepository ingredientRepository; + private final RecipeService recipeService; - public RecipeController(RecipeRepository recipeRepository, - SimpMessagingTemplate messagingTemplate, - IngredientRepository ingredientRepository, - RecipeIngredientRepository recipeIngredientRepository) { - this.recipeRepository = recipeRepository; + public RecipeController(RecipeService recipeService, SimpMessagingTemplate messagingTemplate) { + this.recipeService = recipeService; this.messagingTemplate = messagingTemplate; - this.recipeIngredientRepository = recipeIngredientRepository; - this.ingredientRepository = ingredientRepository; } /** @@ -57,10 +46,9 @@ public class RecipeController { */ @GetMapping("/recipe/{id}") public ResponseEntity getRecipe(@PathVariable Long id) { - if (!recipeRepository.existsById(id)) { - return ResponseEntity.notFound().build(); - } - return ResponseEntity.ok(recipeRepository.findById(id).get()); + return recipeService.findById(id) + .map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build()); } /** @@ -72,28 +60,12 @@ public class RecipeController { */ @GetMapping("/recipes") public ResponseEntity> getRecipes(@RequestParam Optional limit) { - if (limit.isPresent()) { - return ResponseEntity.ok( - recipeRepository.findAll( - PageRequest.of(0, limit.get()) - ).toList()); - } + return ResponseEntity.ok( + // Choose the right overload. One has a limit, other doesn't. + limit.map(recipeService::findAll).orElseGet(recipeService::findAll) + ); + } - return ResponseEntity.ok(recipeRepository.findAll()); - } - private Recipe saveRecipeAndDependencies(Recipe recipe) { - recipe.getIngredients() - .forEach(recipeIngredient -> - recipeIngredient.setIngredient( - ingredientRepository - .findByName(recipeIngredient.getIngredient().name) - .orElseGet(() -> ingredientRepository.save(recipeIngredient.getIngredient()) - )) - ); - recipeIngredientRepository.saveAll(recipe.getIngredients()); - Recipe saved = recipeRepository.save(recipe); - return saved; - } /** * Mapping for POST /recipe/{id}. * Also creates the ingredient elements if they do not exist. @@ -104,13 +76,12 @@ public class RecipeController { */ @PostMapping("/recipe/{id}") public ResponseEntity updateRecipe(@PathVariable Long id, @RequestBody Recipe recipe) { - if (!recipeRepository.existsById(id)) { - return ResponseEntity.badRequest().build(); - } - Recipe saved = saveRecipeAndDependencies(recipe); - messagingTemplate.convertAndSend(Topics.RECIPES, new UpdateRecipeMessage(saved)); - - return ResponseEntity.ok(saved); + return recipeService.update(id, recipe) + .map(saved -> { + messagingTemplate.convertAndSend(Topics.RECIPES, new UpdateRecipeMessage(saved)); // Send to WS. + return ResponseEntity.ok(saved); + }) + .orElseGet(() -> ResponseEntity.notFound().build()); // Recipe with that id not found. } /** @@ -125,22 +96,12 @@ public class RecipeController { */ @PutMapping("/recipe/new") public ResponseEntity createRecipe(@RequestBody Recipe recipe) { - - // We initialize a new example recipe with the name of input recipe - // This is the only attribute we are concerned about making sure it's unique - Recipe example = new Recipe(); - example.setName(recipe.getName()); - - /* Here we use very funny JPA magic repository.exists(Example) - We check if any recipe in the repository has the same name as the input - */ - if (recipeRepository.exists(Example.of(example))) { - return ResponseEntity.badRequest().build(); - } - Recipe saved = saveRecipeAndDependencies(recipe); - messagingTemplate.convertAndSend(Topics.RECIPES, new CreateRecipeMessage(saved)); - - return ResponseEntity.ok(saved); + return recipeService.create(recipe) + .map(saved -> { + messagingTemplate.convertAndSend(Topics.RECIPES, new CreateRecipeMessage(saved)); // Send to WS. + return ResponseEntity.ok(saved); + }) + .orElseGet(() -> ResponseEntity.badRequest().build()); // That recipe already exists. } /** @@ -153,13 +114,10 @@ public class RecipeController { */ @DeleteMapping("/recipe/{id}") public ResponseEntity deleteRecipe(@PathVariable Long id) { - if (!recipeRepository.existsById(id)) { + if (!recipeService.delete(id)) { return ResponseEntity.badRequest().build(); } - recipeRepository.deleteById(id); - - messagingTemplate.convertAndSend(Topics.RECIPES, new DeleteRecipeMessage(id)); - + messagingTemplate.convertAndSend(Topics.RECIPES, new DeleteRecipeMessage(id)); // Send to WS. return ResponseEntity.ok(true); } } diff --git a/server/src/main/java/server/database/IngredientRepository.java b/server/src/main/java/server/database/IngredientRepository.java index df54bd3..ba90987 100644 --- a/server/src/main/java/server/database/IngredientRepository.java +++ b/server/src/main/java/server/database/IngredientRepository.java @@ -12,5 +12,7 @@ public interface IngredientRepository extends JpaRepository { List findAllByOrderByNameAsc(); Page findAllByOrderByNameAsc(Pageable pageable); Optional findByName(String name); + + boolean existsByName(String name); } diff --git a/server/src/main/java/server/database/RecipeRepository.java b/server/src/main/java/server/database/RecipeRepository.java index 60ec130..8657873 100644 --- a/server/src/main/java/server/database/RecipeRepository.java +++ b/server/src/main/java/server/database/RecipeRepository.java @@ -19,4 +19,6 @@ import org.springframework.data.jpa.repository.JpaRepository; import commons.Recipe; -public interface RecipeRepository extends JpaRepository {} \ No newline at end of file +public interface RecipeRepository extends JpaRepository { + boolean existsByName(String name); +} \ No newline at end of file diff --git a/server/src/main/java/server/service/IngredientService.java b/server/src/main/java/server/service/IngredientService.java new file mode 100644 index 0000000..8a79659 --- /dev/null +++ b/server/src/main/java/server/service/IngredientService.java @@ -0,0 +1,67 @@ +package server.service; + +import commons.Ingredient; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import server.database.IngredientRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class IngredientService { + IngredientRepository ingredientRepository; + + public IngredientService(IngredientRepository ingredientRepository) { + this.ingredientRepository = ingredientRepository; + } + + public Optional findById(Long id) { + return ingredientRepository.findById(id); + } + + public List findAll() { + return ingredientRepository.findAllByOrderByNameAsc(); + } + + public List findAll(int page, int limit) { + return ingredientRepository.findAllByOrderByNameAsc(PageRequest.of(page, limit)).toList(); + } + + /** + * Creates a new ingredient. Returns empty if the recipe with the same name or id already exists. + * @param ingredient Ingredient to be saved in the db. + * @return The created ingredient (the ingredient arg with a new assigned id) or empty if it already exists in db. + */ + public Optional create(Ingredient ingredient) { + if (ingredientRepository.existsByName(ingredient.getName()) || + ingredientRepository.existsById(ingredient.getId())) { + return Optional.empty(); + } + + return Optional.of(ingredientRepository.save(ingredient)); + } + + /** + * Updates an ingredient. The ingredient with the provided id will be replaced (in db) with the provided ingredient. + * @param id id of the ingredient to update. + * @param ingredient Ingredient to be saved in the db. + * @return The created ingredient (the ingredient arg with a new assigned id.) + */ + public Optional update(Long id, Ingredient ingredient) { + assert id.equals(ingredient.getId()) : + "The id of the updated ingredient doesn't match the provided ingredient's id."; + if (!ingredientRepository.existsById(id)) { + return Optional.empty(); + } + + return Optional.of(ingredientRepository.save(ingredient)); + } + + public boolean delete(Long id) { + if (!ingredientRepository.existsById(id)) return false; + ingredientRepository.deleteById(id); + return true; + } + +} diff --git a/server/src/main/java/server/service/RecipeService.java b/server/src/main/java/server/service/RecipeService.java new file mode 100644 index 0000000..ac38e3c --- /dev/null +++ b/server/src/main/java/server/service/RecipeService.java @@ -0,0 +1,89 @@ +package server.service; + +import commons.Recipe; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import server.database.IngredientRepository; +import server.database.RecipeIngredientRepository; +import server.database.RecipeRepository; + +import java.util.List; +import java.util.Optional; + +@Service +public class RecipeService { + RecipeRepository recipeRepository; + IngredientRepository ingredientRepository; + RecipeIngredientRepository recipeIngredientRepository; + + public RecipeService(RecipeRepository recipeRepository, + IngredientRepository ingredientRepository, + RecipeIngredientRepository recipeIngredientRepository) { + this.recipeRepository = recipeRepository; + this.ingredientRepository = ingredientRepository; + this.recipeIngredientRepository = recipeIngredientRepository; + } + + public Optional findById(Long id) { + return recipeRepository.findById(id); + } + + public List findAll() { + return recipeRepository.findAll(); + } + + public List findAll(int limit) { + return recipeRepository.findAll(PageRequest.of(0, limit)).toList(); + } + + /** + * Creates a new recipe. Returns empty if the recipe with the same name already exists. + * @param recipe Recipe to be saved in the db. + * @return The created recipe (the recipe arg with a new assigned id) or empty if it already exists in db. + */ + public Optional create(Recipe recipe) { + if (recipeRepository.existsByName(recipe.getName())) { + return Optional.empty(); + } + + return Optional.of(saveWithDependencies(recipe)); + } + + /** + * Updates a recipe. The recipe with the provided id will be replaced with the provided recipe. + * Automatically updates ingredients and any dependencies of recipe. + * @param id id of the recipe to update. + * @param recipe Recipe to be saved in the db. + * @return The created recipe (the recipe arg with a new assigned id.) + */ + public Optional update(Long id, Recipe recipe) { + assert id.equals(recipe.getId()) : "The id of the updated recipe doesn't match the provided recipes id."; + if (!recipeRepository.existsById(id)) { + return Optional.empty(); + } + + return Optional.of(saveWithDependencies(recipe)); + } + + public boolean delete(Long id) { + // TODO: Propagate deletion to ingredients. + if (!recipeRepository.existsById(id)) return false; + recipeRepository.deleteById(id); + return true; + } + + private Recipe saveWithDependencies(Recipe recipe) { + // TODO: try to automate this with JFX somehow. + recipe.getIngredients() + .forEach(recipeIngredient -> + recipeIngredient.setIngredient( + ingredientRepository + .findByName(recipeIngredient.getIngredient().name) + .orElseGet(() -> ingredientRepository.save(recipeIngredient.getIngredient()) + )) + ); + recipeIngredientRepository.saveAll(recipe.getIngredients()); + return recipeRepository.save(recipe); + } + +} diff --git a/server/src/test/java/server/api/IngredientControllerTest.java b/server/src/test/java/server/api/IngredientControllerTest.java index 57523b9..849269a 100644 --- a/server/src/test/java/server/api/IngredientControllerTest.java +++ b/server/src/test/java/server/api/IngredientControllerTest.java @@ -11,6 +11,7 @@ import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.test.context.ActiveProfiles; import server.database.IngredientRepository; import server.WebSocketConfig; +import server.service.IngredientService; import static org.junit.jupiter.api.Assertions.*; @@ -20,10 +21,11 @@ import java.util.stream.Stream; @DataJpaTest @ActiveProfiles("mock-data-test") -@Import(WebSocketConfig.class) +@Import({WebSocketConfig.class, IngredientService.class}) public class IngredientControllerTest { private final SimpMessagingTemplate template; private final IngredientRepository ingredientRepository; + private final IngredientService ingredientService; private IngredientController controller; private static final double PROTEIN_BASE = 1.0; @@ -42,8 +44,10 @@ public class IngredientControllerTest { @Autowired public IngredientControllerTest(IngredientRepository ingredientRepository, + IngredientService ingredientService, SimpMessagingTemplate template) { this.ingredientRepository = ingredientRepository; + this.ingredientService = ingredientService; this.template = template; } @@ -59,7 +63,7 @@ public class IngredientControllerTest { @BeforeEach public void setup() { - controller = new IngredientController(ingredientRepository, template); + controller = new IngredientController(ingredientService, template); this.createInitialIngredients(); } diff --git a/server/src/test/java/server/api/RecipeControllerTest.java b/server/src/test/java/server/api/RecipeControllerTest.java index c16490c..c052688 100644 --- a/server/src/test/java/server/api/RecipeControllerTest.java +++ b/server/src/test/java/server/api/RecipeControllerTest.java @@ -13,9 +13,8 @@ import org.springframework.http.HttpStatus; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.test.context.ActiveProfiles; import server.WebSocketConfig; -import server.database.IngredientRepository; -import server.database.RecipeIngredientRepository; import server.database.RecipeRepository; +import server.service.RecipeService; import java.util.ArrayList; import java.util.List; @@ -38,30 +37,27 @@ import static org.junit.jupiter.api.Assertions.assertEquals; // This is required to enable WebSocket messaging in tests // // Without this line, Spring screams about missing SimpMessagingTemplate bean -@Import(WebSocketConfig.class) +@Import({WebSocketConfig.class, RecipeService.class}) public class RecipeControllerTest { - + private final RecipeService recipeService; + private final RecipeRepository recipeRepository; private final SimpMessagingTemplate template; private RecipeController controller; + private List recipes; - private final RecipeRepository recipeRepository; private List recipeIds; public static final int NUM_RECIPES = 10; - private final IngredientRepository ingredientRepository; - private final RecipeIngredientRepository recipeIngredientRepository; // Injects a test repository into the test class @Autowired public RecipeControllerTest( + RecipeService recipeService, RecipeRepository recipeRepository, - SimpMessagingTemplate template, - IngredientRepository ingredientRepository, - RecipeIngredientRepository recipeIngredientRepository + SimpMessagingTemplate template ) { + this.recipeService = recipeService; this.recipeRepository = recipeRepository; this.template = template; - this.ingredientRepository = ingredientRepository; - this.recipeIngredientRepository = recipeIngredientRepository; } @BeforeEach @@ -71,10 +67,8 @@ public class RecipeControllerTest { .mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of())) .toList(); controller = new RecipeController( - recipeRepository, - template, - ingredientRepository, - recipeIngredientRepository + recipeService, + template ); Set tags = info.getTags(); List ids = new ArrayList<>();