Refactored recipe controller to use services

This commit is contained in:
Oskar Rasieński 2026-01-08 01:05:36 +01:00
commit e9b6d81a27
2 changed files with 36 additions and 84 deletions

View file

@ -7,8 +7,6 @@ import commons.ws.Topics;
import commons.ws.messages.CreateRecipeMessage; import commons.ws.messages.CreateRecipeMessage;
import commons.ws.messages.DeleteRecipeMessage; import commons.ws.messages.DeleteRecipeMessage;
import commons.ws.messages.UpdateRecipeMessage; 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.http.ResponseEntity;
import org.springframework.messaging.simp.SimpMessagingTemplate; 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.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import server.database.IngredientRepository; import server.service.RecipeService;
import server.database.RecipeIngredientRepository;
import server.database.RecipeRepository;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -32,19 +28,12 @@ import java.util.Optional;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
public class RecipeController { public class RecipeController {
private final RecipeRepository recipeRepository; // JPA repository used in this controller
private final SimpMessagingTemplate messagingTemplate; private final SimpMessagingTemplate messagingTemplate;
private final RecipeIngredientRepository recipeIngredientRepository; private final RecipeService recipeService;
private final IngredientRepository ingredientRepository;
public RecipeController(RecipeRepository recipeRepository, public RecipeController(RecipeService recipeService, SimpMessagingTemplate messagingTemplate) {
SimpMessagingTemplate messagingTemplate, this.recipeService = recipeService;
IngredientRepository ingredientRepository,
RecipeIngredientRepository recipeIngredientRepository) {
this.recipeRepository = recipeRepository;
this.messagingTemplate = messagingTemplate; this.messagingTemplate = messagingTemplate;
this.recipeIngredientRepository = recipeIngredientRepository;
this.ingredientRepository = ingredientRepository;
} }
/** /**
@ -57,10 +46,9 @@ public class RecipeController {
*/ */
@GetMapping("/recipe/{id}") @GetMapping("/recipe/{id}")
public ResponseEntity<Recipe> getRecipe(@PathVariable Long id) { public ResponseEntity<Recipe> getRecipe(@PathVariable Long id) {
if (!recipeRepository.existsById(id)) { return recipeService.findById(id)
return ResponseEntity.notFound().build(); .map(ResponseEntity::ok)
} .orElseGet(() -> ResponseEntity.notFound().build());
return ResponseEntity.ok(recipeRepository.findById(id).get());
} }
/** /**
@ -72,28 +60,12 @@ public class RecipeController {
*/ */
@GetMapping("/recipes") @GetMapping("/recipes")
public ResponseEntity<List<Recipe>> getRecipes(@RequestParam Optional<Integer> limit) { public ResponseEntity<List<Recipe>> getRecipes(@RequestParam Optional<Integer> limit) {
if (limit.isPresent()) {
return ResponseEntity.ok( return ResponseEntity.ok(
recipeRepository.findAll( // Choose the right overload. One has a limit, other doesn't.
PageRequest.of(0, limit.get()) limit.map(recipeService::findAll).orElseGet(recipeService::findAll)
).toList()); );
} }
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 <code>POST /recipe/{id}</code>. * Mapping for <code>POST /recipe/{id}</code>.
* Also creates the ingredient elements if they do not exist. * Also creates the ingredient elements if they do not exist.
@ -104,13 +76,12 @@ public class RecipeController {
*/ */
@PostMapping("/recipe/{id}") @PostMapping("/recipe/{id}")
public ResponseEntity<Recipe> updateRecipe(@PathVariable Long id, @RequestBody Recipe recipe) { public ResponseEntity<Recipe> updateRecipe(@PathVariable Long id, @RequestBody Recipe recipe) {
if (!recipeRepository.existsById(id)) { return recipeService.update(id, recipe)
return ResponseEntity.badRequest().build(); .map(saved -> {
} messagingTemplate.convertAndSend(Topics.RECIPES, new UpdateRecipeMessage(saved)); // Send to WS.
Recipe saved = saveRecipeAndDependencies(recipe);
messagingTemplate.convertAndSend(Topics.RECIPES, new UpdateRecipeMessage(saved));
return ResponseEntity.ok(saved); return ResponseEntity.ok(saved);
})
.orElseGet(() -> ResponseEntity.notFound().build()); // Recipe with that id not found.
} }
/** /**
@ -125,22 +96,12 @@ public class RecipeController {
*/ */
@PutMapping("/recipe/new") @PutMapping("/recipe/new")
public ResponseEntity<Recipe> createRecipe(@RequestBody Recipe recipe) { public ResponseEntity<Recipe> createRecipe(@RequestBody Recipe recipe) {
return recipeService.create(recipe)
// We initialize a new example recipe with the name of input recipe .map(saved -> {
// This is the only attribute we are concerned about making sure it's unique messagingTemplate.convertAndSend(Topics.RECIPES, new CreateRecipeMessage(saved)); // Send to WS.
Recipe example = new Recipe();
example.setName(recipe.getName());
/* Here we use very funny JPA magic repository.exists(Example<Recipe>)
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 ResponseEntity.ok(saved);
})
.orElseGet(() -> ResponseEntity.badRequest().build()); // That recipe already exists.
} }
/** /**
@ -153,13 +114,10 @@ public class RecipeController {
*/ */
@DeleteMapping("/recipe/{id}") @DeleteMapping("/recipe/{id}")
public ResponseEntity<Boolean> deleteRecipe(@PathVariable Long id) { public ResponseEntity<Boolean> deleteRecipe(@PathVariable Long id) {
if (!recipeRepository.existsById(id)) { if (!recipeService.delete(id)) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
recipeRepository.deleteById(id); messagingTemplate.convertAndSend(Topics.RECIPES, new DeleteRecipeMessage(id)); // Send to WS.
messagingTemplate.convertAndSend(Topics.RECIPES, new DeleteRecipeMessage(id));
return ResponseEntity.ok(true); return ResponseEntity.ok(true);
} }
} }

View file

@ -13,9 +13,8 @@ import org.springframework.http.HttpStatus;
import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import server.WebSocketConfig; import server.WebSocketConfig;
import server.database.IngredientRepository;
import server.database.RecipeIngredientRepository;
import server.database.RecipeRepository; import server.database.RecipeRepository;
import server.service.RecipeService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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 // This is required to enable WebSocket messaging in tests
// //
// Without this line, Spring screams about missing SimpMessagingTemplate bean // Without this line, Spring screams about missing SimpMessagingTemplate bean
@Import(WebSocketConfig.class) @Import({WebSocketConfig.class, RecipeService.class})
public class RecipeControllerTest { public class RecipeControllerTest {
private final RecipeService recipeService;
private final RecipeRepository recipeRepository;
private final SimpMessagingTemplate template; private final SimpMessagingTemplate template;
private RecipeController controller; private RecipeController controller;
private List<Recipe> recipes; private List<Recipe> recipes;
private final RecipeRepository recipeRepository;
private List<Long> recipeIds; private List<Long> recipeIds;
public static final int NUM_RECIPES = 10; public static final int NUM_RECIPES = 10;
private final IngredientRepository ingredientRepository;
private final RecipeIngredientRepository recipeIngredientRepository;
// Injects a test repository into the test class // Injects a test repository into the test class
@Autowired @Autowired
public RecipeControllerTest( public RecipeControllerTest(
RecipeService recipeService,
RecipeRepository recipeRepository, RecipeRepository recipeRepository,
SimpMessagingTemplate template, SimpMessagingTemplate template
IngredientRepository ingredientRepository,
RecipeIngredientRepository recipeIngredientRepository
) { ) {
this.recipeService = recipeService;
this.recipeRepository = recipeRepository; this.recipeRepository = recipeRepository;
this.template = template; this.template = template;
this.ingredientRepository = ingredientRepository;
this.recipeIngredientRepository = recipeIngredientRepository;
} }
@BeforeEach @BeforeEach
@ -71,10 +67,8 @@ public class RecipeControllerTest {
.mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of())) .mapToObj(x -> new Recipe(null, "Recipe " + x, List.of(), List.of()))
.toList(); .toList();
controller = new RecipeController( controller = new RecipeController(
recipeRepository, recipeService,
template, template
ingredientRepository,
recipeIngredientRepository
); );
Set<String> tags = info.getTags(); Set<String> tags = info.getTags();
List<Long> ids = new ArrayList<>(); List<Long> ids = new ArrayList<>();