Show List

Sample Cloud Application Using Spring Cloud, API Gateway, DynamoDB, JWT Authentication, RabbitMQ - Part3

This is the third part of Food Delivery demonstration Application is an application. We will be creating the addrestaurant,  updatePrice and searchFood,  microservices. Create new maven modules for these microservices under the parent project.

addRestaurant Microservice

Here is the project structure for addRestaurant microservice. This Service is responsible for handling the functionality related to adding new restaurants to the food delivery application. This service provides an API endpoint for the admin to add restaurant details, including restaurant name, menu items, rating, address, and price.


Here are the key classes and configuration under the module:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>searchRestaurant</artifactId>
<groupId>com.food</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>addrestaurant</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.4.7</version>
</dependency>
</dependencies>
</project>
Model Classes are Menu, MenuList, Restaurant and AddRestaurantCommand. 

Menu.java
package addRestaurant.model;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBDocument;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data // Lombok annotation to automatically generate getters, setters, equals, hashCode, and toString methods
@AllArgsConstructor // Lombok annotation to generate a constructor with all arguments
@NoArgsConstructor // Lombok annotation to generate a no-argument constructor
@DynamoDBDocument // Indicates that this class is mapped to a DynamoDB document
public class Menu {

public enum ItemName {
Naan("Naan"),
Pizza("Pizza"),
Burger("Burger"),
Fries("French Fries");

private final String value;

ItemName(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}

@DynamoDBAttribute // Indicates that this field is mapped to a DynamoDB attribute
private String itemName; // Represents the name of the menu item

@DynamoDBAttribute // Indicates that this field is mapped to a DynamoDB attribute
private String ratings; // Represents the ratings of the menu item

@DynamoDBAttribute // Indicates that this field is mapped to a DynamoDB attribute
private String price; // Represents the price of the menu item

}
MenuList.java
package addRestaurant.model;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBDocument;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data // Lombok annotation to automatically generate getters, setters, equals, hashCode, and toString methods
@AllArgsConstructor // Lombok annotation to generate a constructor with all arguments
@NoArgsConstructor // Lombok annotation to generate a no-argument constructor
@DynamoDBDocument // Indicates that this class is mapped to a DynamoDB document
public class MenuList {

@DynamoDBAttribute // Indicates that this field is mapped to a DynamoDB attribute
private List<Menu> items; // Represents a list of Menu objects
}
Restaurant.java

This class maps to DynamoDB table in the database and is used to perform DB operations.
package addRestaurant.model;

import com.amazonaws.services.dynamodbv2.datamodeling.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data // Lombok annotation to generate getters, setters, equals, hashCode, and toString methods
@AllArgsConstructor // Lombok annotation to generate a constructor with all arguments
@NoArgsConstructor // Lombok annotation to generate a no-argument constructor
@DynamoDBTable(tableName = "restaurant") // Specifies the DynamoDB table name for the class
public class Restaurant {

@DynamoDBHashKey // Marks the field as the hash key of the DynamoDB table
@DynamoDBAttribute // Specifies that the field is mapped to a DynamoDB attribute
private String restaurantName; // Represents the name of the restaurant

@DynamoDBAttribute // Specifies that the field is mapped to a DynamoDB attribute
private String address; // Represents the address of the restaurant

@DynamoDBAttribute // Specifies that the field is mapped to a DynamoDB attribute
private MenuList menuList; // Represents the menu list of the restaurant

@DynamoDBAttribute // Specifies that the field is mapped to a DynamoDB attribute
private String createdAt; // Represents the creation timestamp of the restaurant

@DynamoDBAttribute // Specifies that the field is mapped to a DynamoDB attribute
private String updatedAt; // Represents the last update timestamp of the restaurant

}
AddRestaurantCommand

This model class is used to send message to the RabbitMQ when a new restaurant is added. This message will be read by searchFood microservice which will then update its database under Command and Query Responsibility Segregation (CQRS) pattern.
 
package addRestaurant.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data // Lombok annotation to automatically generate getters, setters, equals, hashCode, and toString methods
@AllArgsConstructor // Lombok annotation to generate a constructor with all arguments
@NoArgsConstructor // Lombok annotation to generate a no-argument constructor
public class AddRestaurantCommand {

private String restaurantName; // Represents the name of the restaurant
private String address; // Represents the address of the restaurant
private MenuList menuList; // Represents the menu list of the restaurant

}
RestaurantRepository.java
  • The saveRestaurant method saves a Restaurant entity to the repository. It takes a Restaurant object as a parameter, calls dynamoDBMapper.save(restaurant) to persist the entity in DynamoDB, and logs a success message. If an exception occurs during the save operation, it logs an error message and rethrows the exception.
  • The getRestaurantByName method retrieves a Restaurant entity from the repository based on its name. It takes the name of the restaurant as a parameter, calls dynamoDBMapper.load(Restaurant.class, name) to load the entity from DynamoDB, and returns the retrieved entity. If an exception occurs during the retrieval operation, it logs an error message and rethrows the exception.
package addRestaurant.repository;

import addRestaurant.model.Restaurant;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Repository
public class RestaurantRepository {

private static final Logger LOGGER = LoggerFactory.getLogger(RestaurantRepository.class);

@Autowired
private DynamoDBMapper dynamoDBMapper;

/**
* Saves a restaurant in the repository.
*
* @param restaurant The restaurant to be saved.
* @return The saved restaurant.
*/
public Restaurant saveRestaurant(Restaurant restaurant) {
try {
dynamoDBMapper.save(restaurant);
LOGGER.info("Restaurant saved successfully: {}", restaurant.getRestaurantName());
} catch (Exception e) {
LOGGER.error("Failed to save restaurant: {}", restaurant.getRestaurantName(), e);
throw e; // Rethrow the exception to be handled by the caller
}
return restaurant;
}

/**
* Retrieves a restaurant by its name.
*
* @param name The name of the restaurant.
* @return The restaurant with the given name, or null if not found.
*/
public Restaurant getRestaurantByName(String name) {
try {
return dynamoDBMapper.load(Restaurant.class, name);
} catch (Exception e) {
LOGGER.error("Failed to retrieve restaurant by name: {}", name, e);
throw e; // Rethrow the exception to be handled by the caller
}
}

}
AddRestaurantController.java

The addRestaurant method receives a AddRestaurantCommand object as the request body, which represents the restaurant and its menu details to be added. It performs the following steps:
  • Checks if the restaurant already exists in the repository by calling restaurantRepository.getRestaurantByName(restaurantRequest.getRestaurantName()).
  • If the restaurant already exists, returns a bad request response with an error message.
  • Validates the menu items by checking the item name, price, and ratings for each item. If any validation fails, returns a bad request response with an appropriate error message.
  • Creates a new Restaurant object using the data from the AddRestaurantCommand request object.
  • Calls restaurantRepository.saveRestaurant(restaurant) to save the restaurant to the repository.
  • Converts the restaurantRequest object to JSON using objectMapper.writeValueAsString() and sends it as a message to a RabbitMQ queue named "addrestaurant-command" using the rabbitTemplate.convertAndSend() method.
  • Logs the success message and returns a response entity with a success message.
The isValidValue method is a helper method that checks if a given value is a valid item name. It iterates over the possible item names defined in the Menu.ItemName enum and returns true if a match is found.

package addRestaurant.controller;

import addRestaurant.model.Menu;
import addRestaurant.model.MenuList;
import addRestaurant.model.Restaurant;
import addRestaurant.model.AddRestaurantCommand;
import addRestaurant.repository.RestaurantRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDateTime;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@RestController
@RequestMapping("/food/api/v1/admin")
public class AddRestaurantController {

private static final Logger LOGGER = LoggerFactory.getLogger(AddRestaurantController.class);

@Autowired
private ObjectMapper objectMapper;

@Autowired
private RestaurantRepository restaurantRepository;

public void setObjectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

public void setRestaurantRepository(RestaurantRepository restaurantRepository) {
this.restaurantRepository = restaurantRepository;
}

private final RabbitTemplate rabbitTemplate;

public AddRestaurantController(final RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}

/**
* Adds a new restaurant with menu details.
*
* @param restaurantRequest The Restaurant object to be added.
* @return ResponseEntity with success message if the restaurant is added successfully, or error message if validation or database error occurs.
*/
@PostMapping("/add-restaurant")
public ResponseEntity<String> addRestaurant(@RequestBody AddRestaurantCommand restaurantRequest) {
try {
// Check if the restaurant already exists
Restaurant existingRestaurant = restaurantRepository.getRestaurantByName(restaurantRequest.getRestaurantName());

LOGGER.info("Adding restaurant: {}", restaurantRequest.getRestaurantName());

if (existingRestaurant != null) {
LOGGER.warn("Restaurant already exists: {}", restaurantRequest.getRestaurantName());
return ResponseEntity.badRequest().body("Restaurant already exists");
}

MenuList menuList = restaurantRequest.getMenuList();

List<Menu> items = menuList.getItems();
Pattern pattern = Pattern.compile("\\d+(\\.\\d+)?");

// Validate the menu items
for (Menu menu : items) {
LOGGER.info("Validating item: {}", menu.getItemName());
Matcher matcher = pattern.matcher(menu.getPrice());
// Validate the price
if (!matcher.matches()) {
LOGGER.warn("Non-numeric price: {} for item: {}", menu.getPrice(), menu.getItemName());
return ResponseEntity.badRequest().body("Price " + menu.getPrice() + " of item " + menu.getItemName() + " is non-numeric");
}

if (!isValidValue(String.valueOf(menu.getItemName()))) {
LOGGER.warn("Invalid item name: {}", menu.getItemName());
return ResponseEntity.badRequest().body("Item name " + menu.getItemName() + " is invalid");
}
double price = Double.parseDouble(menu.getPrice());
if (price < 100 || price > 200) {
LOGGER.warn("Invalid price range: {} for item: {}", menu.getPrice(), menu.getItemName());
return ResponseEntity.badRequest().body("Price " + menu.getPrice() + " of item " + menu.getItemName() + " is outside allowed range 100-200");
}

// Validate the ratings
matcher = pattern.matcher(menu.getRatings());
if (!matcher.matches()) {
LOGGER.warn("Non-numeric rating: {} for item: {}", menu.getRatings(), menu.getItemName());
return ResponseEntity.badRequest().body("Rating " + menu.getRatings() + " of item " + menu.getItemName() + " is non-numeric");
}

double rating = Double.parseDouble(menu.getRatings());
if (rating < 1 || rating > 10) {
LOGGER.warn("Invalid rating range: {} for item: {}", menu.getRatings(), menu.getItemName());
return ResponseEntity.badRequest().body("Rating " + menu.getRatings() + " of item " + menu.getItemName() + " is outside allowed range 1-10");
}
}

// Save the restaurant to the database
Restaurant restaurant = new Restaurant();
restaurant.setRestaurantName(restaurantRequest.getRestaurantName());
restaurant.setAddress(restaurantRequest.getAddress());
restaurant.setMenuList(restaurantRequest.getMenuList());
restaurant.setCreatedAt(String.valueOf(LocalDateTime.now()));

restaurantRepository.saveRestaurant(restaurant);


String restaurantJson = objectMapper.writeValueAsString(restaurantRequest);
Message message = MessageBuilder
.withBody(restaurantJson.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON)
.build();
this.rabbitTemplate.convertAndSend("addrestaurant-command", message);

LOGGER.info("Restaurant saved successfully: {}", restaurantRequest.getRestaurantName());

return ResponseEntity.ok("Restaurant saved successfully");
} catch (Exception e) {
LOGGER.error("Error occurred while adding restaurant: {}", restaurantRequest.getRestaurantName(), e);
return ResponseEntity.status(500).body("Internal Server Error");
}
}

/**
* Checks if the given value is a valid item name.
*
* @param value the value to check
* @return true if the value is a valid item name, false otherwise
*/
public static boolean isValidValue(String value) {
// Iterate over all possible item names
for (Menu.ItemName itemName : Menu.ItemName.values()) {
// Check if the value matches the current item name
if (itemName.getValue().equals(value)) {
// If a match is found, return true
return true;
}
}
// If no match is found, return false
return false;
}

}

updatePrice Microservice

The Update-Price Service is responsible for handling the functionality related to updating the price details of menu items in the Food Delivery Application. This service provides an API endpoint for the admin to update the price details for a specific menu item of a restaurant. 

Here are the key classes and configuration under the module:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>searchRestaurant</artifactId>
<groupId>com.food</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>updatePrice</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.4.7</version>
</dependency>
</dependencies>
</project>
PriceUpdateRequest.java
package updatePrice.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data // Lombok annotation to automatically generate getters, setters, equals, hashCode, and toString methods
@AllArgsConstructor // Lombok annotation to generate a constructor with all arguments
@NoArgsConstructor // Lombok annotation to generate a no-argument constructor
public class PriceUpdateRequest {

String menuItemName;
String newPrice;

}
PriceUpdateCommand.java

This model class is used to send the message to RabbitMQ when a request is received to update price. This command would then be read by the searchFood microservice to update its database.
package updatePrice.model;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data // Lombok annotation to automatically generate getters, setters, equals, hashCode, and toString methods
@AllArgsConstructor // Lombok annotation to generate a constructor with all arguments
@NoArgsConstructor // Lombok annotation to generate a no-argument constructor
public class PriceUpdateCommand {
private String restaurantName;
private String address;
private MenuList menuList;
private String createdAt;
private String updatedAt;
}
UpdatePriceController.java

The updatePrice method handles HTTP POST requests to the /update-price/menu/{restaurantName} endpoint. It takes the restaurantName as a path variable and a PriceUpdateRequest object as the request body. It performs the following steps:

  • Retrieves the menu item name and the new price from the priceUpdateRequest.
  • Retrieves the existing restaurant from the repository based on the provided restaurantName.
  • If the restaurant is not found, returns a bad request response with an error message.
  • Validates the item name and new price. If any validation fails, returns a bad request response with an appropriate error message.
  • Retrieves the menu list from the existing restaurant and updates the price of the specified menu item.
  • If the menu item is not found, returns a bad request response with an error message.
  • Updates the menu list in the existing restaurant, sets the updated timestamp, and saves the restaurant to the repository.
  • Creates a PriceUpdateCommand object containing the updated restaurant data.
  • Converts the priceUpdateCommand object to JSON using objectMapper.writeValueAsString() and sends it as a message to a RabbitMQ queue named "priceupdate-command" using the rabbitTemplate.convertAndSend() method.
  • Logs the success message and returns a response entity with a success message.
package updatePrice.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import updatePrice.model.*;
import updatePrice.repository.RestaurantRepository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@RestController
@RequestMapping("/food/api/v1/admin")
public class UpdatePriceController {

private static final Logger LOGGER = LoggerFactory.getLogger(UpdatePriceController.class);

@Autowired
private RestaurantRepository restaurantRepository;

@Autowired
private ObjectMapper objectMapper;

public RestaurantRepository getRestaurantRepository() {
return restaurantRepository;
}

public void setRestaurantRepository(RestaurantRepository restaurantRepository) {
this.restaurantRepository = restaurantRepository;
}

public ObjectMapper getObjectMapper() {
return objectMapper;
}

public void setObjectMapper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}

public RabbitTemplate getRabbitTemplate() {
return rabbitTemplate;
}

private final RabbitTemplate rabbitTemplate;

public UpdatePriceController(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}

/**
* Updates the price for a specific menu item in a restaurant.
*
* @param restaurantName The name of the restaurant.
* @param priceUpdateRequest The name of the menu item.
* @return ResponseEntity containing the status of the price update or error message.
*/
@PostMapping("/update-price/menu/{restaurantName}")
public ResponseEntity<String> updatePrice(
@PathVariable String restaurantName, @RequestBody PriceUpdateRequest priceUpdateRequest
) {

try {
String menuItemName = priceUpdateRequest.getMenuItemName();
String newPrice = priceUpdateRequest.getNewPrice();

LOGGER.info("Updating price for item: {} in restaurant: {}", menuItemName, restaurantName);

Restaurant existingRestaurant = restaurantRepository.getRestaurantByRestaurantName(restaurantName);
if (existingRestaurant == null) {
LOGGER.warn("Restaurant not found: {}", restaurantName);
return ResponseEntity.badRequest().body("Restaurant not found");
}

if (!isValidValue(menuItemName)) {
LOGGER.warn("Invalid item name: {}", menuItemName);
return ResponseEntity.badRequest().body("Item name " + menuItemName + " is invalid");
}

Pattern pattern = Pattern.compile("\\d+(\\.\\d+)?");
Matcher matcher = pattern.matcher(newPrice);
if (!matcher.matches()) {
LOGGER.warn("Non-numeric price: {} for item: {} in restaurant: {}", newPrice, menuItemName, restaurantName);
return ResponseEntity.badRequest().body("Price " + newPrice + " of item " + menuItemName + " under restaurant " + restaurantName + " is non-numeric");
}

double price = Double.parseDouble(newPrice);
if (price < 100 || price > 200) {
LOGGER.warn("Invalid price range: {} for item: {} in restaurant: {}", newPrice, menuItemName, restaurantName);
return ResponseEntity.badRequest().body("Price " + newPrice + " of item " + menuItemName + " under restaurant " + restaurantName + " is outside allowed range 100-200");
}

MenuList menuList = existingRestaurant.getMenuList();
List<Menu> items = menuList.getItems();

AtomicBoolean itemFound = new AtomicBoolean(false);
items.replaceAll(menu -> {
if (menu.getItemName().equals(menuItemName)) {
menu.setPrice(newPrice);
itemFound.set(true);
}
return menu;
});

if (!itemFound.get()) {
LOGGER.warn("Menu item not found: {} in restaurant: {}", menuItemName, restaurantName);
return ResponseEntity.badRequest().body("Menu item " + menuItemName + " under restaurant " + restaurantName + " is not found");
}

menuList.setItems(items);
existingRestaurant.setMenuList(menuList);

existingRestaurant.setUpdatedAt(String.valueOf(LocalDateTime.now()));
restaurantRepository.saveRestaurant(existingRestaurant);
LOGGER.info("Price updated successfully for item: {} in restaurant: {}", menuItemName, restaurantName);

PriceUpdateCommand priceUpdateCommand = new PriceUpdateCommand();
priceUpdateCommand.setRestaurantName(existingRestaurant.getRestaurantName());
priceUpdateCommand.setAddress(existingRestaurant.getAddress());
priceUpdateCommand.setMenuList(existingRestaurant.getMenuList());
priceUpdateCommand.setUpdatedAt(existingRestaurant.getUpdatedAt());
priceUpdateCommand.setCreatedAt(existingRestaurant.getCreatedAt());


String restaurantJson = objectMapper.writeValueAsString(priceUpdateCommand);
Message message = MessageBuilder
.withBody(restaurantJson.getBytes())
.setContentType(MessageProperties.CONTENT_TYPE_JSON)
.build();
this.rabbitTemplate.convertAndSend("priceupdate-command", message);

return ResponseEntity.ok("Price updated successfully");
} catch (Exception e) {
LOGGER.error("Error occurred while updating price for item: {} in restaurant: {}", priceUpdateRequest.getMenuItemName(), restaurantName, e);
return ResponseEntity.status(500).body("Internal Server Error");
}
}

public static boolean isValidValue(String value) {
for (Menu.ItemName itemName : Menu.ItemName.values()) {
if (itemName.getValue().equals(value)) {
return true;
}
}
return false;
}

}

searchFood Microservice

The Search Food Service is responsible for handling the functionality related to searching for food items in the Food Delivery Application. This service provides an API endpoint for customers to search for food based on restaurant name or menu item. It reads the messages from RabbitMQ to update its database when add restaurant or update price action is performed by the admin. It also calls review microservice using Feign client to get most up-to-date review for the items. Factory pattern is used to create the result object.

Here are the key classes and configuration under the module:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>searchRestaurant</artifactId>
<groupId>com.food</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>searchFood</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.4.7</version>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
</dependencies>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

</project>
SearchRestaurant.java

This is the model class that maps to the DynamoDB table and is used to perform DB operations.
package searchFood.model;

import com.amazonaws.services.dynamodbv2.datamodeling.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data // Lombok annotation to automatically generate getters, setters, equals, hashCode, and toString methods
@AllArgsConstructor // Lombok annotation to generate a constructor with all arguments
@NoArgsConstructor // Lombok annotation to generate a no-argument constructor
@DynamoDBTable(tableName = "searchrestaurant")
public class SearchRestaurant {

@DynamoDBHashKey
@DynamoDBAttribute
private String restaurantName;

@DynamoDBAttribute
private String address;

@DynamoDBAttribute
private MenuList menuList;

@DynamoDBAttribute
private String createdAt;

@DynamoDBAttribute
private String updatedAt;

}
SearchResult.Java
package searchFood.model;

import lombok.Data;

@Data // Lombok annotation to automatically generate getters, setters, equals, hashCode, and toString methods
public abstract class SearchResult {
private String name;
private String address;
private String itemName;
private String Ratings;
private String price;
}
SearchResultFactory.java
package searchFood.model;

/**
* Factory class for creating search results based on the type.
*/
public class SearchResultFactory {

/**
* Creates a search result based on the given type.
*
* @param type The type of search result to create.
* @return The created search result object.
*/
public static SearchResult getSearchResult(String type){

// Check the type and create the corresponding search result
if (type.equalsIgnoreCase("SearchRestaurant")){
return new RestaurantSearchResult();
}

// Return null if the type is not recognized
return null;
}
}
ReviewRequestItem.java

This is the model class used to get the item review objects from review microservice.
package searchFood.model;

import lombok.Data;

@Data // Lombok annotation to automatically generate getters, setters, equals, hashCode, and toString methods
public class ReviewRequestItem {

private String restaurantName;
private String itemName;
}
ReviewRequest.java
package searchFood.model;

import lombok.Data;

import java.util.List;

@Data // Lombok annotation to automatically generate getters, setters, equals, hashCode, and toString methods
public class ReviewRequest {
private List<ReviewRequestItem> items;
} 
ReviewResponseItem.java
package searchFood.model;

import lombok.Data;

@Data // Lombok annotation to automatically generate getters, setters, equals, hashCode, and toString methods
public class ReviewResponseItem {

private String restaurantName;
private String itemName;
private String ratings;

}
Utility class ReviewFallback.java
package searchFood.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import searchFood.model.ReviewRequest;
import searchFood.model.ReviewResponseItem;
import java.util.ArrayList;
import java.util.List;

@Component
public class ReviewsFallback implements ReviewsFeignClient {

private static final Logger LOGGER = LoggerFactory.getLogger(ReviewsFallback.class);

@Override
public List<ReviewResponseItem> fetchReviews(ReviewRequest request) {
// Return an empty list as a fallback response for fetching reviews
return new ArrayList<>();
}
}
ReviewsFeignClass.java
package searchFood.util;

import feign.Headers;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import searchFood.model.ReviewRequest;
import searchFood.model.ReviewRequestItem;
import searchFood.model.ReviewResponseItem;

import java.util.List;

@FeignClient(name = "review", url = "localhost:9005/review", fallback = ReviewsFallback.class)
public interface ReviewsFeignClient {

// Define a Feign client for making HTTP requests to the review service
// The client is named "review" and communicates with the URL "localhost:9005/review"
// If the request fails or the service is unavailable, it falls back to the ReviewsFallback class
// to handle the request
@RequestMapping(method = RequestMethod.POST, value = "/restaurantitem", consumes = "application/json")
@Headers("Content-Type: application/json")
List<ReviewResponseItem> fetchReviews(@RequestBody ReviewRequest request);
}
RestaurantRepository.java
  • The saveRestaurant method is responsible for saving a SearchRestaurant object to the DynamoDB table. It uses the dynamoDBMapper to perform the save operation and returns the saved SearchRestaurant object.
  • The findItemsUnderRestaurant method retrieves a list of SearchResult objects for items under a specific restaurant. It takes several parameters such as criteria, restaurantName, filter, sort, page, and size to filter, sort, and paginate the search results.
  • Inside the method, it loads the SearchRestaurant object from DynamoDB based on the restaurantName.
  • It then transforms the menu items of the SearchRestaurant into SearchResult objects using a stream operation and mapping function. This creates a list of SearchResult objects representing the menu items of the restaurant.
  • The method makes use of a ReviewsFeignClient to fetch reviews for the menu items. It creates a ReviewRequest object containing the necessary data for fetching reviews and sends the request to the feign client. The fetched reviews are then associated with the corresponding SearchResult objects.
  • Filtering and sorting operations are performed on the results list based on the provided filter and sort parameters.
  • Pagination is applied to the results list using the page and size parameters.
  • Finally, the method returns the resulting list of SearchResult objects.
  • The containsKeyword method is a utility method used for filtering the search results based on a keyword. It checks if the keyword is present in any of the fields (name, address, itemName, ratings, price) of a SearchResult object.
  • The sortResultsByField method is another utility method used for sorting the search results based on a field. It takes a list of SearchResult objects and a field parameter (restaurantName, address, itemName, ratings, price). It uses a Comparator to define the sorting logic and sorts the searchResults list accordingly.
  • The findAllItemsbyName method retrieves a list of SearchResult objects for items across all restaurants based on the item name. It is similar to the findItemsUnderRestaurant method but operates on all restaurants instead of a specific one.
  • The mapToSearchResultByItem method is a utility method used to map a Menu object and its associated SearchRestaurant object to a SearchResult object. It creates a RestaurantSearchResult object, sets the corresponding fields, and returns it.
  • The getAllRestaurants method retrieves all the restaurants from DynamoDB by performing a scan operation using DynamoDBMapper and returns a list of SearchRestaurant objects.
package searchFood.repository;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBScanExpression;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import searchFood.model.*;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import searchFood.util.ReviewsFeignClient;

@Repository
public class RestaurantRepository {

private static final Logger LOGGER = LoggerFactory.getLogger(RestaurantRepository.class);

@Autowired
private DynamoDBMapper dynamoDBMapper;

@Autowired
private ReviewsFeignClient feignClient;

public void setFeignClient(ReviewsFeignClient feignClient) {
this.feignClient = feignClient;
}

public void setDynamoDBMapper(DynamoDBMapper dynamoDBMapper) {
this.dynamoDBMapper = dynamoDBMapper;
}

/**
* Saves a searchRestaurant to the DynamoDB table.
*
* @param searchRestaurant The searchRestaurant object to be saved.
* @return The saved searchRestaurant.
*/
public SearchRestaurant saveRestaurant(SearchRestaurant searchRestaurant) {
dynamoDBMapper.save(searchRestaurant);
LOGGER.info("Saved searchRestaurant: {}", searchRestaurant.getRestaurantName());
return searchRestaurant;
}


/**
* Finds all items under a specific restaurant by name.
*
* @param restaurantName The name of the restaurant.
* @return The list of search results.
*/
public List<SearchResult> findItemsUnderRestaurant(String criteria, String restaurantName, String filter,
String sort,
int page,
int size) {

LOGGER.info("Finding items under searchRestaurant: {}", restaurantName);
SearchRestaurant searchRestaurant = dynamoDBMapper.load(SearchRestaurant.class, restaurantName);
if (searchRestaurant != null) {
List<SearchResult> results = searchRestaurant.getMenuList().getItems().stream()
.map(menu -> {
RestaurantSearchResult searchItem = (RestaurantSearchResult) SearchResultFactory.getSearchResult("SearchRestaurant");
searchItem.setName(searchRestaurant.getRestaurantName());
searchItem.setAddress(searchRestaurant.getAddress());
searchItem.setItemName(menu.getItemName());
searchItem.setRatings(menu.getRatings());
searchItem.setPrice(menu.getPrice());
return searchItem;
})
.collect(Collectors.toList());

List<ReviewRequestItem> reviewRequestItems = new ArrayList<>();

for (SearchResult result : results) {
ReviewRequestItem reviewRequestItem = new ReviewRequestItem();
reviewRequestItem.setRestaurantName(result.getName());
reviewRequestItem.setItemName(result.getItemName());
reviewRequestItems.add(reviewRequestItem);
}

ReviewRequest reviewRequest = new ReviewRequest();
reviewRequest.setItems(reviewRequestItems);
try {
List<ReviewResponseItem> fetchedReviews = feignClient.fetchReviews(reviewRequest);

for (SearchResult result : results) {
for (ReviewResponseItem review : fetchedReviews) {
if (result.getItemName().equals(review.getItemName()) && result.getName().equals(review.getRestaurantName())) {
result.setRatings(review.getRatings());
break; // Break the inner loop once a match is found
} else {
LOGGER.info("result.getItemName() = " + result.getItemName() + ", review.getItemName() = " + review.getItemName() + ", result.getName() = " + result.getName() + ", review.getRestaurantName() = " + review.getRestaurantName());
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
if (filter != null && !filter.isEmpty()) {
results = results.stream()
.filter(result -> containsKeyword(result, filter))
.collect(Collectors.toList());
}

if (sort != null && !sort.isEmpty()) {
results = sortResultsByField(results, sort);
}

// Apply pagination
int start = page * size;
int end = Math.min(start + size, results.size());
results = results.subList(start, end);

return results;


} else {
LOGGER.warn("SearchRestaurant not found: {}", restaurantName);
return new ArrayList<>();
}
}

private static boolean containsKeyword(SearchResult result, String keyword) {
return result.getName().contains(keyword) ||
result.getAddress().contains(keyword) ||
result.getItemName().contains(keyword) ||
result.getRatings().contains(keyword) ||
result.getPrice().contains(keyword);
}


public static List<SearchResult> sortResultsByField(List<SearchResult> searchResults, String field) {
Comparator<SearchResult> comparator;

switch (field) {
case "restaurantName":
comparator = Comparator.comparing(SearchResult::getName);
break;
case "address":
comparator = Comparator.comparing(SearchResult::getAddress);
break;
case "itemName":
comparator = Comparator.comparing(SearchResult::getItemName);
break;
case "ratings":
comparator = Comparator.comparing(SearchResult::getRatings);
break;
case "price":
comparator = Comparator.comparing(SearchResult::getPrice);
break;
default:
throw new IllegalArgumentException("Invalid field for sorting: " + field);
}

searchResults.sort(comparator);
return searchResults;
}

/**
* Finds all items by name across all restaurants.
*
* @param itemName The name of the item to search.
* @return The list of search results.
*/
public List<SearchResult> findAllItemsbyName(String criteria, String itemName, String filter,
String sort,
int page,
int size) {
LOGGER.info("Finding items by name: {}", itemName);
List<SearchRestaurant> searchRestaurantList = getAllRestaurants();
List<SearchResult> results = new ArrayList<>();

try {
results = searchRestaurantList.stream()
.flatMap(searchRestaurant -> searchRestaurant.getMenuList().getItems().stream()
.filter(menu -> menu.getItemName().equals(itemName))
.map(menu -> mapToSearchResultByItem(menu, searchRestaurant)))
.collect(Collectors.toList());
if (results.size() == 0) {
LOGGER.warn("Item not found: {}", itemName);
} else {
if (filter != null && !filter.isEmpty()) {
results = results.stream()
.filter(result -> containsKeyword(result, filter))
.collect(Collectors.toList());
}

if (sort != null && !sort.isEmpty()) {
results = sortResultsByField(results, sort);
}

// Apply pagination
int start = page * size;
int end = Math.min(start + size, results.size());
results = results.subList(start, end);

}
} catch (Exception e) {
LOGGER.error("Error occurred while finding items by name", e);
}
return results;
}

/**
* Maps a menu item and its associated searchRestaurant to a search result.
*
* @param menu The menu item.
* @param searchRestaurant The associated searchRestaurant.
* @return The search result.
*/
private SearchResult mapToSearchResultByItem(Menu menu, SearchRestaurant searchRestaurant) {

RestaurantSearchResult result = (RestaurantSearchResult) SearchResultFactory.getSearchResult("SearchRestaurant");

result.setName(searchRestaurant.getRestaurantName());
result.setAddress(searchRestaurant.getAddress());
result.setItemName(menu.getItemName());
result.setRatings(menu.getRatings());
result.setPrice(menu.getPrice());

return result;
}

public List<SearchRestaurant> getAllRestaurants(){
DynamoDBScanExpression scanExpression = new DynamoDBScanExpression();
return dynamoDBMapper.scan(SearchRestaurant.class, scanExpression);
}
}
Service class AddRestaurantCommandHandler.java
  • The class defines a bean of type Jackson2JsonMessageConverter using the jackson2JsonMessageConverter method. This bean is used for converting messages to JSON format.
  • The handleCommand method is annotated with @RabbitListener and specifies the "addrestaurant-command" queue as the target queue for listening to messages.
  • The method receives an AddRestaurantCommand object as a parameter, representing the received message.
  • Inside the method, a new SearchRestaurant object is created and populated with data from the received command. The data includes the restaurant name, address, menu list, and the current timestamp as the creation time.
  • The restaurantRepository object's saveRestaurant method is called to save the SearchRestaurant object in the repository.
  • When a message is received in the "addrestaurant-command" queue, the handleCommand method is triggered, and the restaurant data is extracted from the message and saved in the repository.
package searchFood.service;

import searchFood.model.AddRestaurantCommand;
import searchFood.model.SearchRestaurant;
import searchFood.repository.RestaurantRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@Service
public class AddRestaurantCommandHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(AddRestaurantCommandHandler.class);

@Autowired
RestaurantRepository restaurantRepository;

// Configure the Jackson2JsonMessageConverter for converting messages
@Bean
public Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}

// Define a RabbitMQ listener for the "addrestaurant-command" queue
@RabbitListener(queues = "addrestaurant-command")
public void handleCommand(AddRestaurantCommand restaurantRequest) {
LOGGER.info("AddRestaurantCommandHandler: Message received in queue addrestaurant-command");

// Create a new SearchRestaurant object and populate it with data from the received command
SearchRestaurant searchRestaurant = new SearchRestaurant();
searchRestaurant.setRestaurantName(restaurantRequest.getRestaurantName());
searchRestaurant.setAddress(restaurantRequest.getAddress());
searchRestaurant.setMenuList(restaurantRequest.getMenuList());
searchRestaurant.setCreatedAt(String.valueOf(LocalDateTime.now()));

// Save the restaurant data in the repository
restaurantRepository.saveRestaurant(searchRestaurant);
}
}
PriceUpdateCommandHandler.java

  • The handlePriceUpdateCommand method is annotated with @RabbitListener and specifies the "priceupdate-command" queue as the target queue for listening to messages.
  • The method receives a SearchRestaurant object as a parameter, representing the received message.
  • Inside the method, the received SearchRestaurant object is directly passed to the restaurantRepository object's saveRestaurant method to update the restaurant data in the repository.
  • When a message is received in the "priceupdate-command" queue, the handlePriceUpdateCommand method is triggered, and the updated restaurant data is extracted from the message and saved in the repository.
package searchFood.service;

import searchFood.model.SearchRestaurant;
import searchFood.repository.RestaurantRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PriceUpdateCommandHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(PriceUpdateCommandHandler.class);

@Autowired
RestaurantRepository restaurantRepository;

// Define a RabbitMQ listener for the "priceupdate-command" queue
@RabbitListener(queues = "priceupdate-command")
public void handlePriceUpdateCommand(SearchRestaurant searchRestaurant) {
LOGGER.info("PriceUpdateCommandHandler: Message received in queue priceupdate-command");

// Save the updated restaurant data in the repository
restaurantRepository.saveRestaurant(searchRestaurant);
}
}
Controller SearchFoodController.java
  • The searchFood method is annotated with @GetMapping and specifies the URL path "/{criteria}/{criteriaValue}" for handling GET requests to search for food.
  • The method parameters include @PathVariable annotations for extracting the criteria and criteriaValue from the URL path, and @RequestParam annotations for extracting optional query parameters such as filter, sort, page, and size.
  • Inside the method, a list of SearchResult objects named myList is declared.
  • Based on the value of the criteria parameter, the method calls different methods of the restaurantRepository to perform the food search: If criteria is "restaurantname", the findItemsUnderRestaurant method of restaurantRepository is called to search for items under a restaurant name. If criteria is "menuitem", the findAllItemsbyName method of restaurantRepository is called to search for items by name.
  • If the criteria parameter doesn't match any of the expected values, a bad request response is returned with an "Invalid search criteria" message.
  • If the search operation is successful, a success response (ResponseEntity.ok()) is returned with the myList containing the search results.
package searchFood.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import searchFood.model.SearchResult;
import searchFood.repository.RestaurantRepository;
import java.util.List;

@CrossOrigin(origins = "http://localhost:3001") //allows cross-origin requests from the specified origin.
@RestController //class is a REST controller
@RequestMapping("/food/api/v1/user") //Mapping the controller to the specified base URL path.
public class SearchFoodController {

private static final Logger LOGGER = LoggerFactory.getLogger(SearchFoodController.class);

@Autowired
private RestaurantRepository restaurantRepository;

@GetMapping("/{criteria}/{criteriaValue}")
public ResponseEntity<Object> searchFood(
@PathVariable String criteria,
@PathVariable String criteriaValue,
@RequestParam(value = "filter", required = false) String filter,
@RequestParam(value = "sort", required = false) String sort,
@RequestParam(value = "page", required = false, defaultValue = "0") int page,
@RequestParam(value = "size", required = false, defaultValue = "10") int size

) {
try {
List<SearchResult> myList;
if (criteria.equalsIgnoreCase("restaurantname")) {
myList = restaurantRepository.findItemsUnderRestaurant(criteria, criteriaValue, filter,
sort, page, size); // Calls the repository method to search for items under a restaurant name
} else if (criteria.equalsIgnoreCase("menuitem")) {
myList = restaurantRepository.findAllItemsbyName(criteria, criteriaValue, filter,
sort, page, size); // Calls the repository method to search for items by name
} else {
return ResponseEntity.badRequest().body("Invalid search criteria");
}
LOGGER.info("Search request processed successfully.");
return ResponseEntity.ok(myList);// Returns a success response with the search results
}catch (Exception e) {
LOGGER.error("An error occurred while processing the search request.", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred while processing the search request.");
}
}

}
Full source code:
https://github.com/it-code-lab/restaurant

    Leave a Comment


  • captcha text