diff --git a/pom.xml b/pom.xml index 5ac889f7f24f..989d5d436f90 100644 --- a/pom.xml +++ b/pom.xml @@ -225,6 +225,7 @@ money table-inheritance bloc + remote-procedure-call diff --git a/remote-procedure-call/README.md b/remote-procedure-call/README.md new file mode 100644 index 000000000000..58c2b7eb3a9d --- /dev/null +++ b/remote-procedure-call/README.md @@ -0,0 +1,147 @@ +--- +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. diff --git a/remote-procedure-call/cart-service/etc/cart-service.urm.png b/remote-procedure-call/cart-service/etc/cart-service.urm.png new file mode 100644 index 000000000000..ab374fa9732b Binary files /dev/null and b/remote-procedure-call/cart-service/etc/cart-service.urm.png differ diff --git a/remote-procedure-call/cart-service/etc/cart-service.urm.puml b/remote-procedure-call/cart-service/etc/cart-service.urm.puml new file mode 100644 index 000000000000..b49796603e31 --- /dev/null +++ b/remote-procedure-call/cart-service/etc/cart-service.urm.puml @@ -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 + - 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) + - reduceProductQuantity(request: ReduceProductRequest, responseStreamObserver : StreamObserver) + } + +} +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 \ No newline at end of file diff --git a/remote-procedure-call/cart-service/pom.xml b/remote-procedure-call/cart-service/pom.xml new file mode 100644 index 000000000000..7fad34a3edb6 --- /dev/null +++ b/remote-procedure-call/cart-service/pom.xml @@ -0,0 +1,42 @@ + + + + + + remote-procedure-call + com.iluwatar + 1.26.0-SNAPSHOT + + + 4.0.0 + cart-service + jar + + \ No newline at end of file diff --git a/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/Main.java b/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/Main.java new file mode 100644 index 000000000000..db8bd1738ee5 --- /dev/null +++ b/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/Main.java @@ -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 + */ +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"); + } +} \ No newline at end of file diff --git a/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/model/ProductInCart.java b/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/model/ProductInCart.java new file mode 100644 index 000000000000..522dce357f5d --- /dev/null +++ b/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/model/ProductInCart.java @@ -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; +} diff --git a/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/service/CartService.java b/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/service/CartService.java new file mode 100644 index 000000000000..e6720f439cd4 --- /dev/null +++ b/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/service/CartService.java @@ -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(); +} diff --git a/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/service/CartServiceImpl.java b/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/service/CartServiceImpl.java new file mode 100644 index 000000000000..c647bde3110d --- /dev/null +++ b/remote-procedure-call/cart-service/src/main/java/com/iluwatar/rpc/cart/service/CartServiceImpl.java @@ -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 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(); + } +} diff --git a/remote-procedure-call/cart-service/src/main/resources/logback.xml b/remote-procedure-call/cart-service/src/main/resources/logback.xml new file mode 100644 index 000000000000..ed930c512e7e --- /dev/null +++ b/remote-procedure-call/cart-service/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/remote-procedure-call/cart-service/src/test/java/com/iluwatar/rpc/cart/service/CartServiceImplIntegrationTest.java b/remote-procedure-call/cart-service/src/test/java/com/iluwatar/rpc/cart/service/CartServiceImplIntegrationTest.java new file mode 100644 index 000000000000..64bdbe46eb20 --- /dev/null +++ b/remote-procedure-call/cart-service/src/test/java/com/iluwatar/rpc/cart/service/CartServiceImplIntegrationTest.java @@ -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 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)); + } +} \ No newline at end of file diff --git a/remote-procedure-call/cart-service/src/test/java/com/iluwatar/rpc/cart/service/CartServiceImplTest.java b/remote-procedure-call/cart-service/src/test/java/com/iluwatar/rpc/cart/service/CartServiceImplTest.java new file mode 100644 index 000000000000..6cd2ccdd627c --- /dev/null +++ b/remote-procedure-call/cart-service/src/test/java/com/iluwatar/rpc/cart/service/CartServiceImplTest.java @@ -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 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 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(); + } +} \ No newline at end of file diff --git a/remote-procedure-call/cart-service/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/remote-procedure-call/cart-service/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000000..ca6ee9cea8ec --- /dev/null +++ b/remote-procedure-call/cart-service/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/remote-procedure-call/etc/remote-procedure-call.urm.puml b/remote-procedure-call/etc/remote-procedure-call.urm.puml new file mode 100644 index 000000000000..02af47ddf261 --- /dev/null +++ b/remote-procedure-call/etc/remote-procedure-call.urm.puml @@ -0,0 +1,2 @@ +@startuml +@enduml \ No newline at end of file diff --git a/remote-procedure-call/pom.xml b/remote-procedure-call/pom.xml new file mode 100644 index 000000000000..a9c8761191a9 --- /dev/null +++ b/remote-procedure-call/pom.xml @@ -0,0 +1,131 @@ + + + + + java-design-patterns + com.iluwatar + 1.26.0-SNAPSHOT + + + + 1.68.0 + 4.28.2 + 3.0.0 + 5.14.2 + 3.11.0 + 0.6.1 + + + 4.0.0 + remote-procedure-call + 1.26.0-SNAPSHOT + pom + + + cart-service + product-service + + + + + io.grpc + grpc-netty-shaded + ${grpc-version} + runtime + + + io.grpc + grpc-protobuf + ${grpc-version} + + + io.grpc + grpc-stub + ${grpc-version} + + + com.google.protobuf + protobuf-java + ${protobuf-version} + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-junit-jupiter + ${mockito-version} + test + + + + + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin-version} + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf-maven-plugin-version} + + ${project.basedir}/../proto + com.google.protobuf:protoc:4.28.2:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:1.68.0:exe:${os.detected.classifier} + + + + + @generated=omit + + + compile + compile-custom + + + + + + + + \ No newline at end of file diff --git a/remote-procedure-call/product-service/etc/product-service.urm.png b/remote-procedure-call/product-service/etc/product-service.urm.png new file mode 100644 index 000000000000..948b3d5273be Binary files /dev/null and b/remote-procedure-call/product-service/etc/product-service.urm.png differ diff --git a/remote-procedure-call/product-service/etc/product-service.urm.puml b/remote-procedure-call/product-service/etc/product-service.urm.puml new file mode 100644 index 000000000000..799192b93d2e --- /dev/null +++ b/remote-procedure-call/product-service/etc/product-service.urm.puml @@ -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 {static} + } +} + +package com.iluwatar.rpc.product.service { + interface ProductService { + + getAllProducts() : List {abstract} + + getProduct(Long) : Optional {abstract} + } + class ProductServiceGrpcImpl { + - log : Logger {static} + - productService : ProductService + + ProductServiceGrpcImpl(productService : ProductService) + + getAllProducts(request : Empty, responseStreamObserver : StreamObserver) + + reduceProductQuantity(request : ReduceProductRequest, responseStreamObserver : StreamObserver) + } + class ProductServiceImpl { + - products : List + + ProductServiceImpl(products : List) + + getAllProducts() : List + + getProduct(id : Long) : Optional + } +} + +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) + - reduceProductQuantity(request: ReduceProductRequest, responseStreamObserver : StreamObserver) + } +} + +ProductServiceImpl --> "-products" Product +ProductServiceGrpcImpl --> "-productService" ProductService +ProductServiceGrpcImpl --|> ShoppingServiceImplBase +ProductServiceImpl ..|> ProductService +@enduml \ No newline at end of file diff --git a/remote-procedure-call/product-service/pom.xml b/remote-procedure-call/product-service/pom.xml new file mode 100644 index 000000000000..bf54b0e75413 --- /dev/null +++ b/remote-procedure-call/product-service/pom.xml @@ -0,0 +1,42 @@ + + + + + + remote-procedure-call + com.iluwatar + 1.26.0-SNAPSHOT + + + 4.0.0 + product-service + jar + + \ No newline at end of file diff --git a/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/Main.java b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/Main.java new file mode 100644 index 000000000000..bd1c958cbb20 --- /dev/null +++ b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/Main.java @@ -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 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(); + } +} \ No newline at end of file diff --git a/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/mocks/ProductMocks.java b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/mocks/ProductMocks.java new file mode 100644 index 000000000000..68b5cd036fa2 --- /dev/null +++ b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/mocks/ProductMocks.java @@ -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 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)); + } + }; + } +} \ No newline at end of file diff --git a/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/model/Product.java b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/model/Product.java new file mode 100644 index 000000000000..f0975877b35b --- /dev/null +++ b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/model/Product.java @@ -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; +} \ No newline at end of file diff --git a/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/service/ProductService.java b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/service/ProductService.java new file mode 100644 index 000000000000..01ef6f2dbb5b --- /dev/null +++ b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/service/ProductService.java @@ -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 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 getAllProducts() throws IllegalStateException; +} diff --git a/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/service/ProductServiceGrpcImpl.java b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/service/ProductServiceGrpcImpl.java new file mode 100644 index 000000000000..2dd5d58e3f2f --- /dev/null +++ b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/service/ProductServiceGrpcImpl.java @@ -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 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 responseStreamObserver) { + log.info("Received request to fetch all products"); + List 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"); + } +} diff --git a/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/service/ProductServiceImpl.java b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/service/ProductServiceImpl.java new file mode 100644 index 000000000000..2ce1749b1927 --- /dev/null +++ b/remote-procedure-call/product-service/src/main/java/com/iluwatar/rpc/product/service/ProductServiceImpl.java @@ -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 products; + + /** + * Constructor to set internal state of Product array. + * + * @param products ArrayList of products + */ + public ProductServiceImpl(List 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 getProduct(Long id) { + return products + .stream() + .filter(product -> product.getId().equals(id)) + .findFirst(); + } + + @Override + public List 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; + } +} diff --git a/remote-procedure-call/product-service/src/main/resources/logback.xml b/remote-procedure-call/product-service/src/main/resources/logback.xml new file mode 100644 index 000000000000..ed930c512e7e --- /dev/null +++ b/remote-procedure-call/product-service/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/remote-procedure-call/product-service/src/test/java/com/iluwatar/rpc/product/service/ProductServiceGrpcImplTest.java b/remote-procedure-call/product-service/src/test/java/com/iluwatar/rpc/product/service/ProductServiceGrpcImplTest.java new file mode 100644 index 000000000000..4a390b9123d1 --- /dev/null +++ b/remote-procedure-call/product-service/src/test/java/com/iluwatar/rpc/product/service/ProductServiceGrpcImplTest.java @@ -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 productsMockData; + + @BeforeEach + public void setUp() { + productsMockData = ProductMocks.getMockProducts(); + } + + @Test + void testReduceProductQuantity_ProductNotFound() { + var request = ReduceProductRequest.newBuilder() + .setProductId(6L) + .setQuantity(5) + .build(); + StreamObserver responseObserver = mock(StreamObserver.class); + + when(productService.getProduct(request.getProductId())).thenReturn(Optional.empty()); + + productServiceGrpcImpl.reduceProductQuantity(request, responseObserver); + + ArgumentCaptor 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 responseObserver = mock(StreamObserver.class); + + when(productService.getProduct(product.getId())).thenReturn(Optional.of(product)); + + productServiceGrpcImpl.reduceProductQuantity(request, responseObserver); + + ArgumentCaptor 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 responseObserver = mock(StreamObserver.class); + + when(productService.getProduct(product.getId())).thenReturn(Optional.of(product)); + + productServiceGrpcImpl.reduceProductQuantity(request, responseObserver); + + ArgumentCaptor 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 responseObserver = mock(StreamObserver.class); + + when(productService.getProduct(product.getId())).thenReturn(Optional.of(product)); + + productServiceGrpcImpl.reduceProductQuantity(request, responseObserver); + + ArgumentCaptor 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 productsLocal = ProductMocks.getMockProducts(); + var product = productsLocal.get(0); + + StreamObserver responseObserver = mock(StreamObserver.class); + + when(productService.getAllProducts()).thenReturn(productsMockData); + productServiceGrpcImpl.getAllProducts(null, responseObserver); + + ArgumentCaptor 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 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(); + } +} diff --git a/remote-procedure-call/product-service/src/test/java/com/iluwatar/rpc/product/service/ProductServiceImplTest.java b/remote-procedure-call/product-service/src/test/java/com/iluwatar/rpc/product/service/ProductServiceImplTest.java new file mode 100644 index 000000000000..9c82c86605d3 --- /dev/null +++ b/remote-procedure-call/product-service/src/test/java/com/iluwatar/rpc/product/service/ProductServiceImplTest.java @@ -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 products; + private ProductServiceImpl productService; + + @BeforeEach + void setUp() { + products = ProductMocks.getMockProducts(); + productService = new ProductServiceImpl(products); + } + + @Test + void testGetProduct() { + Optional 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 = productService.getProduct(6L); + assertFalse(product.isPresent()); + } + + @Test + void testGetAllProducts() { + List 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()); + } +} \ No newline at end of file diff --git a/remote-procedure-call/product-service/src/test/java/com/iluwatar/rpc/product/service/ProductServiceIntegrationTest.java b/remote-procedure-call/product-service/src/test/java/com/iluwatar/rpc/product/service/ProductServiceIntegrationTest.java new file mode 100644 index 000000000000..a9a87463b43b --- /dev/null +++ b/remote-procedure-call/product-service/src/test/java/com/iluwatar/rpc/product/service/ProductServiceIntegrationTest.java @@ -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 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 responses = new ArrayList<>(); + responseIterator.forEachRemaining(responses::add); + + assertEquals(ProductMocks.getMockProducts().size(), responses.size()); + } +} diff --git a/remote-procedure-call/product-service/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/remote-procedure-call/product-service/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000000..ca6ee9cea8ec --- /dev/null +++ b/remote-procedure-call/product-service/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/remote-procedure-call/proto/shopping.proto b/remote-procedure-call/proto/shopping.proto new file mode 100644 index 000000000000..47515f054fda --- /dev/null +++ b/remote-procedure-call/proto/shopping.proto @@ -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 {}