Skip to content

Adds remote-procedure-call via gRPC #3189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -225,6 +225,7 @@
<module>money</module>
<module>table-inheritance</module>
<module>bloc</module>
<module>remote-procedure-call</module>
</modules>
<repositories>
<repository>
147 changes: 147 additions & 0 deletions remote-procedure-call/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README.md needs to follow the specified format. See https://github.com/iluwatar/java-design-patterns/wiki/01.-How-to-contribute

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iluwatar is it fine to attach a link from GeeksForGeeks or any reliable website for the architecture diagram?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer own production. You can also leave it out for now. I've used https://whimsical.com/ myself to describe the architectural patterns.

title: Remote Procedure Call in Java using gRPC
shortTitle: gRPC in Java
description: Remote Procedure Call in Java using gRPC communicating between 2 microservices product and cart application
category: Data Transfer
language: en
tag:
- Behavioral
- Integration
- Messaging
- Client-Server
- Data Transfer
- Microservices
- Remote Procedure Call
- Remote Method Invocation
---
# Remote Method Invocation / Remote Procedure Call
Remote Method Invocation has various different aliases, Remote Procedure Call / Remote Method Invocation or RPC for short. Is a protocol
that one program can use to request a service from a different program located in same or another computer in a network without having to understand network details.

RMI can be implemented in various different languages and frameworks. Technologies like REST, gRPC and Thrift can be used for RMI. In this example we
will be using gRPC to implement Remote Procedure Call in Java.

## Terminologies
- Client: The client is the application that sends a request to the server.
- Server: The server is the application that receives the request from the client and sends a response back to the client.
- Stub: The client-side proxy for the server.
- Skeleton: The server-side proxy for the client.
- Protocol: The set of rules that the client and server follow to communicate with each other.
- Stream: The sequence of messages that are sent between the client and server, understand it as list of objects.

## What is gRPC?
[gRPC](https://grpc.io/docs/what-is-grpc/introduction/) is a high-performance, open-source and universal RPC framework. gRPC was developed by Google but is now open-source
and is based on the HTTP/2 protocol.

A gRPC client can call a method on a gRPC server similar to if it was calling a method on a local object.
This allows client and server to communicate with each other by just using method calls. gRPC internally uses [protobuf](https://protobuf.dev/) to serialize the data for communication.

## When to use gRPC?
gRPC should be used when you need high performance communication between client and server. It is mostly used in micro-service architecture when one
service needs to communicate with another service.

For communication you need to define contract / interfaces denoting the method signature, data types and parameters along with return type.
These methods can be called by client service just like a method call, when using gRPC, a gRPC service is created which will in-turn call the
implementation of the RPC method in the server and return the response (if any).

Start by creating a .proto file which should have all the methods and data types you need for communication between the services.
When you compile your code `maven/gradle gRPC` plugin will in return create objects and interfaces which you need to implement / extend in your
server side. The client will then call the method defined in .proto file using the generated stubs by gPRC. In return inside the server the
implementation of the method will be called and the response will be sent back to the client.

### In this example
We will be using 2 different micro-services
- product-service
- cart-service

Along with a shopping.proto file to define the contract between the services.
- ShoppingService

This is a basic e-commerce simulation.

In this simple example the `product-service` has data related to products and is used a source of truth. The `cart-service`
needs the product data that is available in `product-service`. Certain number of products in turn may be bought by a customer,
inside the cart service at which point the product quantity needs to be decreased in `product-service`, hence the need for bidirectional
communication from `product-service` -> `cart-service` and vice versa they both communicate via gRPC.

- getAllProducts() - gets all the product from state in `product-service` and stores it in `cart-service`
- reduceProductQuantity() - reduces the quantity of a product by `id` fetched by `getAllProducts` and stored in `cart-service`
when the method is hit, it reduces the quantity of product with same `id` in `product-service`

## How to implement gRPC in Java?
### .proto file
- Create a [.proto](https://protobuf.dev/programming-guides/proto2/) file [example](./proto/shopping.proto) defining the service and message contracts
- Define service interfaces, method signatures and data types in your .proto file
### At the server end
- Add gRPC and protobuf dependencies in your `pom.xml`
- Include gRPC and protobuf plugins in `mvn build plugins`, for it to generate interfaces from your `.proto` during compilation
- Include the .proto file directory in mvn build plugins to generate the interfaces
- Build the project via `mvn clean install`
- gRPC will generate the stubs and skeletons for you
- Implement the service logic for the generated methods of skeleton in your service classes
- Start the gRPC server at server's side on a specific port and attach the gRPC Implementation service to it
### At the client end
- Add gRPC and protobuf dependencies in your `pom.xml`
- Include gRPC and protobuf plugins in `mvn build plugins`, for it to generate interfaces from your `.proto` during compilation
- Include the .proto file directory in mvn build plugins to generate the interfaces
- Build the project via `mvn clean install`
- gRPC will generate the stubs and skeletons for you
- A stub will expose the available methods to be called by the client, call the methods you need on server via the stub
- Create Channel with server's host and port at client's end to communicate between server and client
- Start client, and you are good to go

## gRPC in action
### Product Service
#### Service
- ProductService - API Interface for Internal logic in `product-service`
- ProductServiceImpl - Implementation of ProductService, saves product data in memory for simplicity, exposes getter(s) for the same.
Houses Composition of ProductService to store state.
- ProductServiceGrpcImpl - gRPC contract implementation, methods to retrieve all products and reduce quantity of a product.
This file implements the logic that should be executed when gRPC methods are called

#### Model
- Product - Product POJO Model
#### Mocks
- ProductMocks - Mock data of Product for testing and state initialization.

### Cart Service
#### Service
- CartService - API Interface for Internal logic in `cart-service`,
- CartServiceImpl - Implementation of CartService, methods to call the stub to populate data in cart and reduce quantities.
This file calls the gRPC method to communicate with `product-service`.
#### Model
- ProductInCart - Cut from Product POJO in `product-service`

### proto
Proto folder contains all the proto files which define contract for the services.
proto files end with .proto and contain the types, methods and services that are to be used in gRPC communication.

### Good practise
- Keep types / method names in PascalCase in .proto file

### How to run this project
- Clone the project
- navigate to 'remote-procedure-call' directory via
```shell
cd java-design-patterns/remote-procedure-call
```
- build the project with, this will download dependencies, compile .proto to java interface and classes and create final jar
```shell
mvn clean install
```
- Start the `product-service` before `cart-service` as `cart-service` depends on product-service
```shell
mvn exec:java -Dexec.mainClass="com.iluwatar.rpc.product.Main" -pl product-service
```
- Start a new terminal session
- navigate to 'remote-procedure-call' directory
- Start the `cart-service`
```shell
mvn exec:java -Dexec.mainClass="com.iluwatar.rpc.cart.Main" -pl cart-service
```
- `cart-service` on startup will hit a gRPC call to `product-service` to get all products and populate the cart.
- `cart-service` will then reduce the quantity of a product in `product-service` when a product is bought.
- `cart-service` will then shut-down gracefully.
- all the operations will be logged in the console.
- `product-service` will continue to run and can be used for further operations by running `cart-service` again.
- To stop the services, press `ctrl+c` in the terminal.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 70 additions & 0 deletions remote-procedure-call/cart-service/etc/cart-service.urm.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
@startuml
skinparam dpi 300
scale 0.3

package com.iluwatar.rpc.cart.model {
class ProductInCart {
- id : Long
- name : String
- price : double
- quantityToReduce : int
- type : String
}
}

package com.iluwatar.rpc.cart.service {
interface CartService {
+ getAllProducts() {abstract}
+ getRandomProductFromCart() : ProductInCart {abstract}
+ reduceCartQuantityFromProduct(ProductInCart) {abstract}
}

class CartServiceImpl {
- log : Logger {static}
- productsInCart : List<ProductInCart>
- shoppingServiceBlockingStub : ShoppingServiceBlockingStub
+ CartServiceImpl(shoppingStub : ShoppingServiceBlockingStub)
+ getAllProducts()
+ getRandomProductFromCart() : ProductInCart
+ reduceCartQuantityFromProduct(product : ProductInCart)
}
}

package com.iluwatar.rpc.proto {
class Empty {}

class ProductResponse {
- id : long
- name : String
- price : double
- type : String
}

class ReduceProductRequest {
- productId : long
- quantity : int
}

class ReduceProductResponse {
- message : String
- status : boolean
}

class ShoppingServiceImplBase {
- getAllProducts(request: Empty, responseStreamObserver: StreamObserver<ProductResponse>)
- reduceProductQuantity(request: ReduceProductRequest, responseStreamObserver : StreamObserver<ReduceProductResponse>)
}

}
package com.iluwatar.rpc.cart {
class Main {
- HOST : String {static}
- SERVER_PORT : int {static}
- log : Logger {static}
+ main(args : String[]) {static}
}
}

CartServiceImpl --> "-productsInCart" ProductInCart
CartServiceImpl ..|> CartService
@enduml
42 changes: 42 additions & 0 deletions remote-procedure-call/cart-service/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
The MIT License
Copyright © 2014-2022 Ilkka Seppälä
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<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>remote-procedure-call</artifactId>
<groupId>com.iluwatar</groupId>
<version>1.26.0-SNAPSHOT</version>
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>cart-service</artifactId>
<packaging>jar</packaging>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.cart;

import com.iluwatar.rpc.cart.service.CartServiceImpl;
import com.iluwatar.rpc.proto.ShoppingServiceGrpc;
import com.iluwatar.rpc.proto.ShoppingServiceGrpc.ShoppingServiceBlockingStub;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Main class for the cart service.
* Initializes the shopping channel and the cart service.
*
* @author CoderSleek
* @version 1.0
*/
Comment on lines +36 to +42
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, above the class, please explain the pattern briefly and describe how the example implements it

public class Main {
private static final int SERVER_PORT = 8080;
private static final String HOST = "localhost";
private static final Logger log = LoggerFactory.getLogger(Main.class);
/**
* Main method to initialize the cart service and channel for RPC connection
* initializes blocking stub and passes it to CartServiceImpl for constructor initialization.
* Initializes data fetching from product-service.
* shuts down after 1 request
*/
public static void main(String[] args) {
ManagedChannel productChannel = ManagedChannelBuilder
.forAddress(HOST, SERVER_PORT)
.usePlaintext()
.enableRetry()
.keepAliveTime(10, TimeUnit.SECONDS)
.build();

ShoppingServiceBlockingStub blockingStub = ShoppingServiceGrpc.newBlockingStub(productChannel);
log.info("cart-service started");

var cartService = new CartServiceImpl(blockingStub);
cartService.getAllProducts();

var productInCart = cartService.getRandomProductFromCart();
productInCart.setQuantityToReduce(10);

cartService.reduceCartQuantityFromProduct(productInCart);
productChannel.shutdown();
log.info("cart-service execution successful, shutting down");
Comment on lines +54 to +72
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comments. Remember, this is material for studying the design pattern.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.cart.model;

import lombok.Builder;
import lombok.Data;

/**
* ProductInCart is a POJO model class for product in cart.
* ProductInCart is a cut from Product class in product-service
*
* @link com.iluwatar.rpc.product.model.Product
* @author CoderSleek
* @version 1.0
*/
@Data
@Builder(toBuilder = true)
public class ProductInCart {
private Long id;
private String name;
private String type;
private double price;
private int quantityToReduce;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.cart.service;

import com.iluwatar.rpc.cart.model.ProductInCart;

/**
* API Contract for Cart service, which can be exposed.
* Includes contract method for grpc stub as well.
* Thread safety is not guaranteed.
* @link com.iluwatar.rpc.cart.model.ProductInCart
* @author CoderSleek
* @version 1.0
*/
public interface CartService {
/**
* returns a random product from cart, if cart is empty throws IllegalStateException.
* randomized just for demonstration purposes.
*
* @return randomly chosen ProductInCart from List of ProductInCart
* @throws IllegalStateException if cart is empty
*/
ProductInCart getRandomProductFromCart() throws IllegalStateException;

/**
* reduces the quantity of a product from cart by doing a RPC call to product service.
*
* @param product product whose quantity needs to be reduced in product-service
* @throws IllegalArgumentException if product is null or id invalid
* @throws IllegalStateException if product is not found in cart or cart is null
*/
void reduceCartQuantityFromProduct(ProductInCart product)
throws IllegalStateException, IllegalArgumentException;

/**
* RPC call to get all the products from product-service and add them to cart.
*/
void getAllProducts();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.cart.service;

import com.iluwatar.rpc.cart.model.ProductInCart;
import com.iluwatar.rpc.proto.Shopping.ReduceProductRequest;
import com.iluwatar.rpc.proto.Shopping.ReduceProductResponse;
import com.iluwatar.rpc.proto.ShoppingServiceGrpc.ShoppingServiceBlockingStub;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Implementation of cart-service Contract exposed by gRPC proto.
* Thread safety is not guaranteed.
* @link com.iluwatar.rpc.cart.model.ProductInCart
* @link com.iluwatar.rpc.cart.service.CartService
* @author CoderSleek
* @version 1.0
*/
public class CartServiceImpl implements CartService {
private final ShoppingServiceBlockingStub shoppingServiceBlockingStub;
private final List<ProductInCart> productsInCart = new ArrayList<>();
private static final Logger log = LoggerFactory.getLogger(CartServiceImpl.class);

/**
* Constructor to initialize CartServiceImpl with ProductServiceBlockingStub.
* blocking-stub is initialized in Main
* @param shoppingStub ProductServiceBlockingStub
* @throws IllegalArgumentException if ProductServiceBlockingStub is null
*/
public CartServiceImpl(ShoppingServiceBlockingStub shoppingStub) {
if (shoppingStub == null) {
throw new IllegalArgumentException("ShoppingServiceBlockingStub is null");
}
this.shoppingServiceBlockingStub = shoppingStub;
}

@Override
public void reduceCartQuantityFromProduct(ProductInCart product)
throws IllegalArgumentException, IllegalStateException {
log.info("Started Request for reducing product quantity from cart in product-service");
if (product == null) {
throw new IllegalArgumentException("Product state is null");
}
if (product.getId() <= 0) {
throw new IllegalArgumentException("Invalid Product id");
}
if (productsInCart.isEmpty()) {
throw new IllegalStateException("Products In cart array empty, populate cart first");
}

// type exposed and maintained in gRPC proto
ReduceProductRequest request = ReduceProductRequest
.newBuilder()
.setProductId(product.getId())
.setQuantity(product.getQuantityToReduce())
.build();

log.info("Initiating RPC call to reduce quantity of product with id: {}", product.getId());
ReduceProductResponse response = shoppingServiceBlockingStub.reduceProductQuantity(request);
log.info("Completed RPC call reduce quantity with status: {}", response.getStatus());
log.info("RPC call response message: {}", response.getMessage());
log.info("Completed Request for reducing product quantity from cart in product-service");
}

@Override
public void getAllProducts() {
log.info("Started request to fetch all products from product-service");
// stream of products / multiple products
try {
shoppingServiceBlockingStub.getAllProducts(null)
.forEachRemaining(product ->
productsInCart.add(ProductInCart.builder()
.name(product.getName())
.id(product.getId())
.price(product.getPrice())
.type(product.getType())
.quantityToReduce(0)
.build())
);
} catch (Exception e) {
log.error("Error occurred while fetching products: ", e);
throw new IllegalStateException("Failed to fetch products from ProductService", e);
}
log.info("Fetching of products completed");
}

/**
* returns a random product from cart, if cart is empty throws IllegalStateException.
* randomized just for demonstration purposes.
* returns a new instance of ProductInCart to avoid mutation of quantityToReduce in original object.
*
* @return randomly chosen ProductInCart from List of ProductInCart
* @throws IllegalStateException if cart is empty
*/
@Override
public ProductInCart getRandomProductFromCart() throws IllegalStateException {
if (productsInCart.isEmpty()) {
throw new IllegalStateException("Products In cart array empty");
}

// randomly choose a product for dynamic results
int randInt = new Random().nextInt(productsInCart.size());
// to builder is used to return a copy of the object to avoid mutation of original object
return productsInCart.get(randInt).toBuilder().build();
}
}
12 changes: 12 additions & 0 deletions remote-procedure-call/cart-service/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!-- to ensure only info and error logging -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.cart.service;

import com.iluwatar.rpc.cart.model.ProductInCart;
import com.iluwatar.rpc.proto.ShoppingServiceGrpc.ShoppingServiceBlockingStub;
import com.iluwatar.rpc.proto.Shopping.ProductResponse;
import com.iluwatar.rpc.proto.Shopping.ReduceProductRequest;
import com.iluwatar.rpc.proto.Shopping.ReduceProductResponse;
import java.util.ArrayList;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class CartServiceImplIntegrationTest {

@Mock
private ShoppingServiceBlockingStub productServiceBlockingStub;

@InjectMocks
private CartServiceImpl cartService;

private ProductResponse product;

@BeforeEach
public void setUp() {
product = ProductResponse.newBuilder()
.setId(1)
.setName("Test")
.setPrice(100)
.setType("Test")
.build();

ArrayList<ProductResponse> productList = new ArrayList<>();

productList.add(product);
when(productServiceBlockingStub.getAllProducts(null)).thenReturn(productList.iterator());
cartService.getAllProducts();
}

@Test
void testGetRandomProductFromCartAndReduceQuantity_Success() {
ProductInCart randomProduct = cartService.getRandomProductFromCart();

assertNotNull(randomProduct);
assertEquals(product.getId(), randomProduct.getId());
assertEquals(product.getName(), randomProduct.getName());
randomProduct.setQuantityToReduce(100);

ReduceProductResponse response = ReduceProductResponse.newBuilder()
.setStatus(true)
.setMessage("Success")
.build();

when(productServiceBlockingStub.reduceProductQuantity(
any(ReduceProductRequest.class)
)).thenReturn(response);
cartService.reduceCartQuantityFromProduct(randomProduct);

verify(productServiceBlockingStub, times(1)).reduceProductQuantity(
any(ReduceProductRequest.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.cart.service;

import com.iluwatar.rpc.cart.model.ProductInCart;
import com.iluwatar.rpc.proto.ShoppingServiceGrpc.ShoppingServiceBlockingStub;
import com.iluwatar.rpc.proto.Shopping;
import com.iluwatar.rpc.proto.Shopping.ReduceProductRequest;
import com.iluwatar.rpc.proto.Shopping.ReduceProductResponse;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class CartServiceImplTest {
@Mock
private ShoppingServiceBlockingStub productServiceBlockingStub;
@InjectMocks
private CartServiceImpl cartService;

private static List<Shopping.ProductResponse> products;

@BeforeEach
public void setUp() {
products = new ArrayList<>();
var product = productInCartProvider();
products.add(Shopping.ProductResponse.newBuilder()
.setId(product.getId())
.setPrice(product.getPrice())
.setType(product.getType())
.setName(product.getName())
.build());
}

@Test
void testReduceCartQuantityFromProduct_NullProduct() {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
cartService.reduceCartQuantityFromProduct(null);
});
assertEquals("Product state is null", exception.getMessage());
}

@Test
void testReduceCartQuantityFromProduct_InvalidProductProperties() {
ProductInCart invalidProduct = productInCartProvider();
invalidProduct.setId(-1L);
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
cartService.reduceCartQuantityFromProduct(invalidProduct);
});
assertEquals("Invalid Product id", exception.getMessage());
}

@Test
void testReduceCartQuantityFromProduct_EmptyCart() {
ProductInCart product = productInCartProvider();

IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
cartService.reduceCartQuantityFromProduct(product);
});
assertEquals("Products In cart array empty, populate cart first", exception.getMessage());
}

@Test
void testReduceCartQuantityFromProduct_Success() {
when(productServiceBlockingStub.getAllProducts(null)).thenReturn(products.iterator());

ProductInCart product = productInCartProvider();
product.setId(3L);

ReduceProductResponse response =
ReduceProductResponse.newBuilder().setStatus(true).setMessage("Success").build();
when(productServiceBlockingStub.reduceProductQuantity(
any(ReduceProductRequest.class))).thenReturn(response);

cartService.getAllProducts();
cartService.reduceCartQuantityFromProduct(product);
verify(productServiceBlockingStub, times(1)).reduceProductQuantity(
any(ReduceProductRequest.class));
}

@Test
void testGetAllProducts_Success() {
when(productServiceBlockingStub.getAllProducts(null)).thenReturn(products.iterator());

List<Shopping.ProductResponse> products = new ArrayList<>();
var productInCart = productInCartProvider();
products.add(Shopping.ProductResponse.newBuilder()
.setId(productInCart.getId())
.setName(productInCart.getName())
.setType(productInCart.getType())
.setPrice(productInCart.getPrice() + 10)
.build());

when(productServiceBlockingStub.getAllProducts(null)).thenReturn(products.iterator());

cartService.getAllProducts();
assertDoesNotThrow(() -> cartService.getRandomProductFromCart());
}

@Test
void testGetAllProducts_Exception() {
when(productServiceBlockingStub.getAllProducts(null)).thenThrow(
new RuntimeException("Service error"));

IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
cartService.getAllProducts();
});
assertEquals("Failed to fetch products from ProductService", exception.getMessage());
}

@Test
void testGetRandomProductFromCart_EmptyCart() {
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
cartService.getRandomProductFromCart();
});
assertEquals("Products In cart array empty", exception.getMessage());
}

@Test
void testGetRandomProductFromCart_Success() {
when(productServiceBlockingStub.getAllProducts(null)).thenReturn(products.iterator());

cartService.getAllProducts();
ProductInCart randomProduct = cartService.getRandomProductFromCart();
assertNotNull(randomProduct);
}

private ProductInCart productInCartProvider() {
return ProductInCart.builder()
.id(1L)
.name("Test")
.type("Test type")
.price(0.0)
.quantityToReduce(10)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mock-maker-inline
2 changes: 2 additions & 0 deletions remote-procedure-call/etc/remote-procedure-call.urm.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@startuml
@enduml
131 changes: 131 additions & 0 deletions remote-procedure-call/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
The MIT License
Copyright © 2014-2022 Ilkka Seppälä
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<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>java-design-patterns</artifactId>
<groupId>com.iluwatar</groupId>
<version>1.26.0-SNAPSHOT</version>
</parent>

<properties>
<grpc-version>1.68.0</grpc-version>
<protobuf-version>4.28.2</protobuf-version>
<jakarta-annotation-version>3.0.0</jakarta-annotation-version>
<mockito-version>5.14.2</mockito-version>
<maven-compiler-plugin-version>3.11.0</maven-compiler-plugin-version>
<protobuf-maven-plugin-version>0.6.1</protobuf-maven-plugin-version>
</properties>

<modelVersion>4.0.0</modelVersion>
<artifactId>remote-procedure-call</artifactId>
<version>1.26.0-SNAPSHOT</version>
<packaging>pom</packaging>

<modules>
<module>cart-service</module>
<module>product-service</module>
</modules>

<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>${grpc-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc-version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc-version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf-version}</version>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito-version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin-version}</version>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>${protobuf-maven-plugin-version}</version>
<configuration>
<protoSourceRoot>${project.basedir}/../proto</protoSourceRoot>
<protocArtifact>com.google.protobuf:protoc:4.28.2:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.68.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<configuration>
<pluginParameter>@generated=omit</pluginParameter>
</configuration>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 80 additions & 0 deletions remote-procedure-call/product-service/etc/product-service.urm.puml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
@startuml
skinparam dpi 300
scale 0.3

package com.iluwatar.rpc.product {
class Main {
- SERVER_PORT : int {static}
- log : Logger {static}
+ main(args : String[]) {static}
}
}

package com.iluwatar.rpc.product.model {
class Product {
- id : Long
- name : String
- price : double
- quantity : int
- type : String
}
}

package com.iluwatar.rpc.product.mocks {
class ProductMocks {
+ ProductMocks()
+ getMockProducts() : List<Product> {static}
}
}

package com.iluwatar.rpc.product.service {
interface ProductService {
+ getAllProducts() : List<Product> {abstract}
+ getProduct(Long) : Optional<Product> {abstract}
}
class ProductServiceGrpcImpl {
- log : Logger {static}
- productService : ProductService
+ ProductServiceGrpcImpl(productService : ProductService)
+ getAllProducts(request : Empty, responseStreamObserver : StreamObserver<ProductResponse>)
+ reduceProductQuantity(request : ReduceProductRequest, responseStreamObserver : StreamObserver<ReduceProductResponse>)
}
class ProductServiceImpl {
- products : List<Product>
+ ProductServiceImpl(products : List<Product>)
+ getAllProducts() : List<Product>
+ getProduct(id : Long) : Optional<Product>
}
}

package com.iluwatar.rpc.proto {
class Empty {}

class ProductResponse {
- id : long
- name : String
- price : double
- type : String
}

class ReduceProductRequest {
- productId : long
- quantity : int
}

class ReduceProductResponse {
- message : String
- status : boolean
}

class ShoppingServiceImplBase {
- getAllProducts(request: Empty, responseStreamObserver: StreamObserver<ProductResponse>)
- reduceProductQuantity(request: ReduceProductRequest, responseStreamObserver : StreamObserver<ReduceProductResponse>)
}
}

ProductServiceImpl --> "-products" Product
ProductServiceGrpcImpl --> "-productService" ProductService
ProductServiceGrpcImpl --|> ShoppingServiceImplBase
ProductServiceImpl ..|> ProductService
@enduml
42 changes: 42 additions & 0 deletions remote-procedure-call/product-service/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
The MIT License
Copyright © 2014-2022 Ilkka Seppälä
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-->
<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>remote-procedure-call</artifactId>
<groupId>com.iluwatar</groupId>
<version>1.26.0-SNAPSHOT</version>
</parent>

<modelVersion>4.0.0</modelVersion>
<artifactId>product-service</artifactId>
<packaging>jar</packaging>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.product;

import com.iluwatar.rpc.product.mocks.ProductMocks;
import com.iluwatar.rpc.product.model.Product;
import com.iluwatar.rpc.product.service.ProductServiceGrpcImpl;
import com.iluwatar.rpc.product.service.ProductServiceImpl;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Main class to start the product service.
*
* @author CoderSleek
* @version 1.0
*/
public class Main {
private static final int SERVER_PORT = 8080;
private static final Logger log = LoggerFactory.getLogger(Main.class);
/**
* Main method to start the product service.
* instantiates product mock data and starts the gRPC server.
* constructor injects ProductService into ProductServiceGrpcImpl
* listens on default server port 8080
*
* @param args the input arguments
* @throws IOException the io exception
* @throws InterruptedException the interrupted exception
*/
public static void main(String[] args) throws IOException, InterruptedException {
List<Product> products = ProductMocks.getMockProducts();
var productServiceImpl = new ProductServiceImpl(products);
var productServiceGrpcImpl = new ProductServiceGrpcImpl(productServiceImpl);

Server server = ServerBuilder
.forPort(SERVER_PORT)
.addService(productServiceGrpcImpl)
.build();

log.info("Starting server on port: " + SERVER_PORT);
log.info("Waiting for request---------");
server.start();
server.awaitTermination();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.product.mocks;

import com.iluwatar.rpc.product.model.Product;
import java.util.ArrayList;
import java.util.List;

/**
* Mocks data for product POJO, used for testing and in memory store.
* Thread safety is not guaranteed.
* @link com.iluwatar.rpc.product.model.Product
* @author CoderSleek
* @version 1.0
*/
public class ProductMocks {
/**
* Returns new Mock product ArrayList on each call, to reset state.
*
* @return the mock products
*/
public static List<Product> getMockProducts() {
return new ArrayList<>() {{
add(new Product(1L, "Product 1", "Type 1", 50.0, 10));
add(new Product(2L, "Product 2", "Type 2", 40.0, 20));
add(new Product(3L, "Product 3", "Type 3", 30.0, 30));
add(new Product(4L, "Product 4", "Type 4", 20.0, 40));
add(new Product(5L, "Product 5", "Type 5", 10.0, 50));
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.product.model;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
* Product POJO class.
*
* @author CoderSleek
* @version 1.0
*/
@Data
@AllArgsConstructor
public class Product {
private Long id;
private String name;
private String type;
private double price;
private int quantity;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.product.service;

import com.iluwatar.rpc.product.model.Product;
import java.util.List;
import java.util.Optional;

/**
* API Contract for Product service, which can be exposed.
* Includes getter methods for product by id and all products.
* Thread safety is not guaranteed.
*
* @link com.iluwatar.rpc.product.model.Product
* @author CoderSleek
* @version 1.0
*/
public interface ProductService {
/**
* Get optional of product by id.
*
* @param id id of product
* @return product
*/
Optional<Product> getProduct(Long id);

/**
* Get all products.
* @return ArrayList of all products
* @throws IllegalStateException thrown when products array is null or empty, denotes improper initialization
*/
List<Product> getAllProducts() throws IllegalStateException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.product.service;

import com.iluwatar.rpc.product.model.Product;
import com.iluwatar.rpc.proto.Shopping.Empty;
import com.iluwatar.rpc.proto.Shopping.ProductResponse;
import com.iluwatar.rpc.proto.Shopping.ReduceProductRequest;
import com.iluwatar.rpc.proto.Shopping.ReduceProductResponse;
import com.iluwatar.rpc.proto.ShoppingServiceGrpc.ShoppingServiceImplBase;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Implementation of product-service Contract exposed by gRPC proto.
* Thread safety is not guaranteed.
*
* @link com.iluwatar.rpc.product.service.ProductService
* @link com.iluwatar.rpc.product.model.Product
* @author CoderSleek
* @version 1.0
*/
public class ProductServiceGrpcImpl extends ShoppingServiceImplBase {
private final ProductService productService;
private static final Logger log = LoggerFactory.getLogger(ProductServiceGrpcImpl.class);

/**
* Constructor for initializing Implementation of ProductService Contract internally.
* ProductService Implementation stores Product data in internal state for simplicity.
*
* @param productService ProductService
*/
public ProductServiceGrpcImpl(ProductService productService) {
this.productService = productService;
}

/**
* Reduces the quantity of a specified product via product id by the requested quantity.
* To simulate a customer buying an item, once bought the quantity of the product is reduced from stock.
* @param request contains product id and quantity to reduce
* @param responseStreamObserver for iterating over response
*/
@Override
public void reduceProductQuantity(ReduceProductRequest request,
StreamObserver<ReduceProductResponse> responseStreamObserver) {
log.info("Received request to reduce product quantity");
boolean status;
String message;
var product = this.productService.getProduct(request.getProductId());

if (product.isEmpty()) {
status = false;
message = "Product with ID does not exist";
} else {
int productQuantity = product.get().getQuantity();
int requestedQuantity = request.getQuantity();

if (requestedQuantity <= 0) {
status = false;
message = "Invalid Quantity";
} else if (requestedQuantity > productQuantity) {
status = false;
message = "Product has less quantity in stock than requested";
} else {
log.info("Before reducing quantity: {}", productQuantity);
product.get().setQuantity(productQuantity - requestedQuantity);
log.info("After reducing quantity: {}", product.get().getQuantity());
status = true;
message = "Success";
}
}

var response = ReduceProductResponse
.newBuilder()
.setMessage(message)
.setStatus(status)
.build();

responseStreamObserver.onNext(response);
responseStreamObserver.onCompleted();
log.info("Request to Reduce Product Quantity Execution Completed");
}

/**
* Fetches all products from ProductService and streams them to the client.
* Throws NOT_FOUND status exception if products are not found.
* @param request empty placeholder request
* @param responseStreamObserver for iterating over responses
*/
@Override
public void getAllProducts(Empty request,
StreamObserver<ProductResponse> responseStreamObserver) {
log.info("Received request to fetch all products");
List<Product> products;

try {
products = this.productService.getAllProducts();
} catch (IllegalStateException e) {
log.error("Failed to fetch products from ProductService", e);
responseStreamObserver.onError(
Status.NOT_FOUND
.withDescription(e.getMessage())
.asException());
responseStreamObserver.onCompleted();
return;
}

for (var product : products) {
responseStreamObserver.onNext(
ProductResponse.newBuilder()
.setId(product.getId())
.setName(product.getName())
.setPrice(product.getPrice())
.setType(product.getType())
.build()
);
}

responseStreamObserver.onCompleted();
log.info("Request to fetch all products Execution Completed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.product.service;

import com.iluwatar.rpc.product.model.Product;
import java.util.List;
import java.util.Optional;

/**
* Implementation of ProductService Contract, maintains Product state for simplicity.
* Provides getter methods for product by id and all products.
* Thread safety is not guaranteed.
*
* @link com.iluwatar.rpc.product.service.ProductService
* @link com.iluwatar.rpc.product.model.Product
* @author CoderSleek
* @version 1.0
*/
public class ProductServiceImpl implements ProductService {
private final List<Product> products;

/**
* Constructor to set internal state of Product array.
*
* @param products ArrayList of products
*/
public ProductServiceImpl(List<Product> products) {
this.products = products;
}

/**
* Get optional of product by id.
* @param id id of product
* @return Product first item in the list with matching id, if not found returns empty optional
*/
@Override
public Optional<Product> getProduct(Long id) {
return products
.stream()
.filter(product -> product.getId().equals(id))
.findFirst();
}

@Override
public List<Product> getAllProducts() throws IllegalStateException {
if (products == null) {
throw new IllegalStateException("Products array is not initialized properly");
}

if (products.isEmpty()) {
throw new IllegalStateException("Products array does not have any data");
}

return products;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!-- to ensure only info and error logging -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.product.service;

import com.iluwatar.rpc.product.mocks.ProductMocks;
import com.iluwatar.rpc.product.model.Product;
import com.iluwatar.rpc.proto.Shopping.Empty;
import com.iluwatar.rpc.proto.Shopping.ProductResponse;
import com.iluwatar.rpc.proto.Shopping.ReduceProductRequest;
import com.iluwatar.rpc.proto.Shopping.ReduceProductResponse;
import io.grpc.StatusException;
import io.grpc.stub.StreamObserver;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;
import static org.mockito.ArgumentMatchers.any;

@ExtendWith(MockitoExtension.class)
class ProductServiceGrpcImplTest {
@Mock
private ProductService productService;

@InjectMocks
private ProductServiceGrpcImpl productServiceGrpcImpl;

private List<Product> productsMockData;

@BeforeEach
public void setUp() {
productsMockData = ProductMocks.getMockProducts();
}

@Test
void testReduceProductQuantity_ProductNotFound() {
var request = ReduceProductRequest.newBuilder()
.setProductId(6L)
.setQuantity(5)
.build();
StreamObserver<ReduceProductResponse> responseObserver = mock(StreamObserver.class);

when(productService.getProduct(request.getProductId())).thenReturn(Optional.empty());

productServiceGrpcImpl.reduceProductQuantity(request, responseObserver);

ArgumentCaptor<ReduceProductResponse> responseCaptor =
ArgumentCaptor.forClass(ReduceProductResponse.class);
verify(responseObserver).onNext(responseCaptor.capture());
verify(responseObserver).onCompleted();

ReduceProductResponse response = responseCaptor.getValue();
assertFalse(response.getStatus());
assertEquals("Product with ID does not exist", response.getMessage());
}

@Test
void testReduceProductQuantity_InvalidQuantity() {
Product product = productsMockData.get(0);
ReduceProductRequest request = ReduceProductRequest.newBuilder()
.setProductId(product.getId())
.setQuantity(-5)
.build();
StreamObserver<ReduceProductResponse> responseObserver = mock(StreamObserver.class);

when(productService.getProduct(product.getId())).thenReturn(Optional.of(product));

productServiceGrpcImpl.reduceProductQuantity(request, responseObserver);

ArgumentCaptor<ReduceProductResponse> responseCaptor =
ArgumentCaptor.forClass(ReduceProductResponse.class);
verify(responseObserver).onNext(responseCaptor.capture());
verify(responseObserver).onCompleted();

ReduceProductResponse response = responseCaptor.getValue();
assertFalse(response.getStatus());
assertEquals("Invalid Quantity", response.getMessage());
}

@Test
void testReduceProductQuantity_InsufficientQuantity() {
Product product = productsMockData.get(0);

ReduceProductRequest request = ReduceProductRequest.newBuilder()
.setProductId(product.getId())
.setQuantity(1000)
.build();
StreamObserver<ReduceProductResponse> responseObserver = mock(StreamObserver.class);

when(productService.getProduct(product.getId())).thenReturn(Optional.of(product));

productServiceGrpcImpl.reduceProductQuantity(request, responseObserver);

ArgumentCaptor<ReduceProductResponse> responseCaptor =
ArgumentCaptor.forClass(ReduceProductResponse.class);
verify(responseObserver).onNext(responseCaptor.capture());
verify(responseObserver).onCompleted();

ReduceProductResponse response = responseCaptor.getValue();
assertFalse(response.getStatus());
assertEquals("Product has less quantity in stock than requested", response.getMessage());
}

@Test
void testReduceProductQuantity_Success() {
Product product = productsMockData.get(0);
ReduceProductRequest request = ReduceProductRequest.newBuilder()
.setProductId(product.getId())
.setQuantity(5)
.build();
StreamObserver<ReduceProductResponse> responseObserver = mock(StreamObserver.class);

when(productService.getProduct(product.getId())).thenReturn(Optional.of(product));

productServiceGrpcImpl.reduceProductQuantity(request, responseObserver);

ArgumentCaptor<ReduceProductResponse> responseCaptor =
ArgumentCaptor.forClass(ReduceProductResponse.class);
verify(responseObserver).onNext(responseCaptor.capture());
verify(responseObserver).onCompleted();

ReduceProductResponse response = responseCaptor.getValue();
assertTrue(response.getStatus());
assertEquals("Success", response.getMessage());
assertEquals(5, product.getQuantity());
}

@Test
void testGetAllProducts_Success() {
List<Product> productsLocal = ProductMocks.getMockProducts();
var product = productsLocal.get(0);

StreamObserver<ProductResponse> responseObserver = mock(StreamObserver.class);

when(productService.getAllProducts()).thenReturn(productsMockData);
productServiceGrpcImpl.getAllProducts(null, responseObserver);

ArgumentCaptor<ProductResponse> responseCaptor = ArgumentCaptor.forClass(ProductResponse.class);
verify(responseObserver, times(5)).onNext(responseCaptor.capture());
verify(responseObserver).onCompleted();

assertEquals(productsLocal.size(), responseCaptor.getAllValues().size());
assertEquals(Long.compare(product.getId(), responseCaptor.getAllValues().get(0).getId()), 0);
assertEquals(product.getName(), responseCaptor.getAllValues().get(0).getName());
assertEquals(product.getType(), responseCaptor.getAllValues().get(0).getType());
assertEquals(
Double.compare(product.getPrice(), responseCaptor.getAllValues().get(0).getPrice()), 0);
}

@Test
void testGetAllProducts_Failure() {
StreamObserver<ProductResponse> responseObserver = mock(StreamObserver.class);

when(productService.getAllProducts()).thenThrow(new IllegalStateException("Database error"));

productServiceGrpcImpl.getAllProducts(Empty.newBuilder().build(), responseObserver);

verify(responseObserver).onError(any(StatusException.class));
verify(responseObserver).onCompleted();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.product.service;

import com.iluwatar.rpc.product.mocks.ProductMocks;
import com.iluwatar.rpc.product.model.Product;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

@ExtendWith(MockitoExtension.class)
class ProductServiceImplTest {
private static List<Product> products;
private ProductServiceImpl productService;

@BeforeEach
void setUp() {
products = ProductMocks.getMockProducts();
productService = new ProductServiceImpl(products);
}

@Test
void testGetProduct() {
Optional<Product> product = productService.getProduct(1L);
assertTrue(product.isPresent());
assertEquals(1L, product.get().getId());
assertEquals("Product 1", product.get().getName());
assertEquals(50.0, product.get().getPrice());
}

@Test
void testGetProductNotFound() {
Optional<Product> product = productService.getProduct(6L);
assertFalse(product.isPresent());
}

@Test
void testGetAllProducts() {
List<Product> allProducts = productService.getAllProducts();
assertNotNull(allProducts);
assertEquals(5, allProducts.size());
}

@Test
void testGetAllProductsEmpty() {
products.clear();
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
productService.getAllProducts();
});
assertEquals("Products array does not have any data", exception.getMessage());
}

@Test
void testGetAllProductNull() {
productService = new ProductServiceImpl(null);
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
productService.getAllProducts();
});
assertEquals("Products array is not initialized properly", exception.getMessage());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
*
* The MIT License
* Copyright © 2014-2022 Ilkka Seppälä
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.iluwatar.rpc.product.service;

import com.iluwatar.rpc.product.mocks.ProductMocks;
import com.iluwatar.rpc.product.model.Product;
import com.iluwatar.rpc.proto.ShoppingServiceGrpc;
import com.iluwatar.rpc.proto.ShoppingServiceGrpc.ShoppingServiceBlockingStub;
import com.iluwatar.rpc.proto.Shopping.ProductResponse;
import com.iluwatar.rpc.proto.Shopping.ReduceProductRequest;
import com.iluwatar.rpc.proto.Shopping.ReduceProductResponse;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

@ExtendWith(MockitoExtension.class)
class ProductServiceIntegrationTest {
private static Server server;
private static ManagedChannel channel;
private static ShoppingServiceBlockingStub blockingStub;

@BeforeAll
public static void setup() throws IOException {
List<Product> products = ProductMocks.getMockProducts();
var productServiceMain = new ProductServiceImpl(products);

server = ServerBuilder
.forPort(8080)
.addService(new ProductServiceGrpcImpl(productServiceMain))
.build()
.start();

channel = ManagedChannelBuilder.forAddress("localhost", 8080)
.usePlaintext()
.build();

blockingStub = ShoppingServiceGrpc.newBlockingStub(channel);
}

@AfterAll
public static void teardown() {
channel.shutdownNow();
server.shutdownNow();
}

@Test
void testReduceProductQuantity() {
ReduceProductRequest request = ReduceProductRequest.newBuilder()
.setProductId(1L)
.setQuantity(5)
.build();

ReduceProductResponse response = blockingStub.reduceProductQuantity(request);

assertTrue(response.getStatus());
assertEquals("Success", response.getMessage());
}

@Test
void testGetAllProducts() {
var responseIterator = blockingStub.getAllProducts(null);

ArrayList<ProductResponse> responses = new ArrayList<>();
responseIterator.forEachRemaining(responses::add);

assertEquals(ProductMocks.getMockProducts().size(), responses.size());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mock-maker-inline
52 changes: 52 additions & 0 deletions remote-procedure-call/proto/shopping.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
//
// The MIT License
// Copyright © 2014-2022 Ilkka Seppälä
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

syntax = "proto3";

package com.iluwatar.rpc.proto;

service ShoppingService {
rpc ReduceProductQuantity (ReduceProductRequest) returns (ReduceProductResponse);
rpc GetAllProducts (Empty) returns (stream ProductResponse);
}

message ReduceProductRequest {
uint64 product_id = 1;
uint32 quantity = 2;
}

message ReduceProductResponse {
bool status = 1;
string message = 2;
}

message ProductResponse {
uint64 id = 1;
string name = 2;
string type = 3;
double price = 4;
}

message Empty {}