Merge branch 'fix/search-bar-wiring' into 'main'

Refined search code and wired up the client side search with server

Closes #68

See merge request cse1105/2025-2026/teams/csep-team-76!66
This commit is contained in:
Zhongheng Liu 2026-01-18 18:30:48 +01:00
commit 75e9fc1e62
3 changed files with 94 additions and 55 deletions

View file

@ -56,9 +56,28 @@ public class ServerUtils {
return list; // JSON string-> List<Recipe> (Jackson) return list; // JSON string-> List<Recipe> (Jackson)
} }
/**
* The method used by the search bar to get filtered recipes.
* @param filter - filter string
* @param locales - locales of the user
* @return filtered recipe list
*/
public List<Recipe> getRecipesFiltered(String filter, List<String> locales) throws IOException, InterruptedException { public List<Recipe> getRecipesFiltered(String filter, List<String> locales) throws IOException, InterruptedException {
// TODO: implement filtering on server side //TODO add limit integration
return this.getRecipes(locales); String uri = SERVER + "/recipes?search=" + filter + "&locales=" + String.join(",", locales);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if(response.statusCode() != statusOK){
throw new IOException("Failed to get filtered recipes. Server responds with " + response.body());
}
List<Recipe> list = objectMapper.readValue(response.body(), new TypeReference<List<Recipe>>() {});
logger.info("Received filtered recipes from server: " + list);
return list;
} }
/** /**

View file

@ -23,9 +23,11 @@ import org.springframework.web.bind.annotation.RestController;
import server.database.RecipeRepository; import server.database.RecipeRepository;
import server.service.RecipeService; import server.service.RecipeService;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Logger; import java.util.logging.Logger;
import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
@ -60,26 +62,6 @@ public class RecipeController {
.orElseGet(() -> ResponseEntity.notFound().build()); .orElseGet(() -> ResponseEntity.notFound().build());
} }
/**
* Mapping for <code>GET /recipes(?limit=)(&locales=)</code>
* <p>
* If the limit parameter is unspecified, return all recipes in the repository.
* @param limit Integer limit of items you want to get
* @return The list of recipes
*/
@GetMapping("/recipes")
public ResponseEntity<List<Recipe>> getRecipes(
@RequestParam Optional<List<String>> locales,
@RequestParam Optional<Integer> limit
) {
logger.info("GET /recipes called.");
List<Recipe> recipes = locales
.map(loc -> findWithLocales(loc, limit))
.orElseGet(() -> findAll(limit));
return ResponseEntity.ok(recipes);
}
private List<Recipe> findWithLocales(List<String> locales, Optional<Integer> limit) { private List<Recipe> findWithLocales(List<String> locales, Optional<Integer> limit) {
return limit return limit
@ -155,36 +137,68 @@ public class RecipeController {
/** /**
* Performs a search based on a case-insensitive partial match on * Performs a search based on a case-insensitive partial match on
* Recipe name and limits the result to a set amount of results. * Recipe name, ingredients and preparations steps
* and limits the result to a set amount of results.
* @param search - name of the recipe to be searched for. * @param search - name of the recipe to be searched for.
* @param limit - limit of the results queried for. * @param limit - limit of the results queried for.
* @param lang - stores the info of the language of the user to provide server support/ * @param locales - stores the info of the language of the user to provide server support/
* @return - returns a ResponseEntity with a List of Recipes and an HTTP 200 ok status. * @return - returns a ResponseEntity with a List of Recipes and an HTTP 200 ok status.
*/ */
@GetMapping("/recipes")
public ResponseEntity<List<Recipe>> getRecipes( public ResponseEntity<List<Recipe>> getRecipes(
@RequestParam Optional<String> search, @RequestParam Optional<String> search,
@RequestParam Optional<Integer> limit, @RequestParam Optional<Integer> limit,
@RequestParam Optional<String> lang){ @RequestParam Optional<List<String>> locales){
List<Recipe> recipes = recipeRepository.findAll(); logger.info("GET /recipes called ith search: " + search.orElse("none"));
List<Recipe> finalRecipes = recipes; //Start the search with all the recipes (locale filtered)
recipes = search List<Recipe> recipeList = locales.map(loc->findWithLocales(loc, Optional.empty()))
.filter(s -> !s.trim().isEmpty()) // filters recipes if the string is not empty by doing a lowercase search .orElseGet(()->recipeService.findAll());
.map(s -> {
String lowercaseSearch = s.toLowerCase(); //check if limit is zero (and also below 0 for anomalous cases) and end the search early
return finalRecipes.stream() if(limit.isPresent() && limit.get() <= 0){
.filter(recipe -> recipeList.clear();
recipe.getName().toLowerCase().contains(lowercaseSearch) return ResponseEntity.ok(recipeList);
) }
.toList();
}) //Apply Search filter
.orElse(recipes);
recipes = limit // filters based on limit if provided if(search.isPresent() && !search.get().trim().isEmpty()) {
.filter(l -> l < finalRecipes.size()) recipeList = recipeList.stream()
.map(l -> finalRecipes.stream().limit(l).toList()) .filter(recipe ->
.orElse(recipes); matchesSearchTerms(recipe, Arrays.stream(search.get().split(" ")).toList()))
return ResponseEntity.ok(recipes); .collect(Collectors.toList());
}
//Apply limit
if(limit.isPresent() && limit.get()< recipeList.size()){
recipeList = recipeList.stream()
.limit(limit.get())
.collect(Collectors.toList());
}
return ResponseEntity.ok(recipeList);
}
public boolean matchesSearchTerms(Recipe recipe, List<String> searchTerms){
List<String> lowerCased = searchTerms.stream()
.map(String::toLowerCase)
.map(String::trim)
.toList();
for (String s : lowerCased) {
boolean foundInRecipeName = recipe.getName().trim().toLowerCase().contains(s); //searches recipe names
boolean foundInIngredients = recipe.getIngredients().stream().anyMatch(ing -> ing.getIngredient()
.getName().toLowerCase().contains(s)); // searches in ingredients
boolean foundInPrepSteps = recipe.getPreparationSteps().stream()
.anyMatch(step -> step.toLowerCase().contains(s)); //searches preparation steps
if(!foundInRecipeName && !foundInIngredients && !foundInPrepSteps){
return false; // returns false if not met all criteria. this meets the backlog criteria
}
}
return true; // all terms found
} }

View file

@ -115,19 +115,21 @@ public class RecipeControllerTest {
// The number of recipes returned is the same as the entire input list // The number of recipes returned is the same as the entire input list
assertEquals(recipes.size(), assertEquals(recipes.size(),
controller.getRecipes( controller.getRecipes(
Optional.empty(),
Optional.empty(), Optional.empty(),
Optional.empty()).getBody().size()); Optional.of(List.of("en", "nl"))).getBody().size());;
} }
@Test @Test
@Tag("test-from-init-data") @Tag("test-from-init-data")
public void getSomeRecipes() { public void getSomeRecipes() {
final int LIMIT = 5; final int LIMIT = 5;
// The number of recipes returned is the same as the entire input list // The number of recipes returned is the same as the limit
assertEquals(LIMIT, assertEquals(LIMIT,
controller.getRecipes( controller.getRecipes(
Optional.empty(), Optional.empty(),
Optional.of(LIMIT)).getBody().size()); Optional.of(LIMIT),
Optional.of(List.of("en"))).getBody().size());
} }
@Test @Test
@ -135,8 +137,10 @@ public class RecipeControllerTest {
public void getManyRecipesWithLocale() { public void getManyRecipesWithLocale() {
// The number of recipes returned is the same as the entire input list // The number of recipes returned is the same as the entire input list
assertEquals(recipes.size(), assertEquals(recipes.size(),
controller.getRecipes(Optional.of(List.of("en", "nl")), controller.getRecipes(
Optional.empty()).getBody().size()); Optional.empty(),
Optional.empty(),
Optional.of(List.of("en", "nl"))).getBody().size());
} }
@Test @Test
@ -144,18 +148,20 @@ public class RecipeControllerTest {
public void getNoRecipesWithLocale() { public void getNoRecipesWithLocale() {
// should have NO Dutch recipes (thank god) // should have NO Dutch recipes (thank god)
assertEquals(0, assertEquals(0,
controller.getRecipes(Optional.of(List.of("nl")), controller.getRecipes(
Optional.empty()).getBody().size()); Optional.empty(),
Optional.empty(),
Optional.of(List.of("nl"))).getBody().size());
} }
@Test @Test
@Tag("test-from-init-data") @Tag("test-from-init-data")
public void getSomeRecipesWithLocale() { public void getSomeRecipesWithLocale() {
final int LIMIT = 5; final int LIMIT = 5;
// The number of recipes returned is the same as the entire input list
assertEquals(LIMIT, assertEquals(LIMIT,
controller.getRecipes(Optional.of(List.of("en", "nl")), controller.getRecipes(
Optional.of(LIMIT)).getBody().size()); Optional.empty(),
Optional.of(LIMIT),
Optional.of(List.of("en", "nl"))).getBody().size());
} }
@Test @Test